@@ -16,8 +16,9 @@ import type {
1616 SelectField ,
1717} from 'payload'
1818import { entityToJSONSchema } from 'payload'
19- import type { SanitizedPluginOptions } from '../types.js'
19+ import type { CustomEndpointDocumentation , SanitizedPluginOptions } from '../types.js'
2020import { mapValuesAsync , visitObjectNodes } from '../utils/objects.js'
21+ import { upperFirst } from '../utils/strings.js'
2122import { type ComponentType , collectionName , componentName , globalName } from './naming.js'
2223import { apiKeySecurity , generateSecuritySchemes } from './securitySchemes.js'
2324
@@ -102,6 +103,27 @@ const generateSchemaObject = (config: SanitizedConfig, collection: Collection):
102103 }
103104}
104105
106+ const generateCustomEndpointSchemaObjects = ( collection : Collection ) => {
107+ const schemas : Record < string , JSONSchema4 > = { }
108+ const { singular } = collectionName ( collection )
109+
110+ for ( const endpoint of collection . config . endpoints || [ ] ) {
111+ if ( endpoint . custom ?. openapi ) {
112+ const { parameters, ...documentation } : CustomEndpointDocumentation = endpoint . custom . openapi
113+
114+ for ( const [ statusCode , response ] of Object . entries ( documentation . responses || { } ) ) {
115+ const key = componentName ( 'schemas' , singular , {
116+ suffix : `CustomEndpoint${ upperFirst ( endpoint . method ) } ${ statusCode } ` ,
117+ } )
118+
119+ schemas [ key ] = response
120+ }
121+ }
122+ }
123+
124+ return schemas
125+ }
126+
105127const requestBodySchema = ( fields : Array < Field > , schema : JSONSchema4 ) : JSONSchema4 => ( {
106128 ...schema ,
107129 properties : Object . fromEntries (
@@ -140,6 +162,48 @@ const generateRequestBodySchema = (
140162 }
141163}
142164
165+ const getRequestBodySchema = (
166+ description : string ,
167+ schema : OpenAPIV3_1 . SchemaObject ,
168+ ) : OpenAPIV3_1 . RequestBodyObject => {
169+ return {
170+ description,
171+ content : {
172+ 'application/json' : {
173+ schema : {
174+ type : 'object' ,
175+ additionalProperties : false ,
176+ ...schema ,
177+ } ,
178+ } ,
179+ } ,
180+ }
181+ }
182+
183+ const generateRequestBodyCustomEndpointSchemas = ( collection : Collection ) => {
184+ const requestBodies : Record < string , OpenAPIV3_1 . RequestBodyObject > = { }
185+ const { singular } = collectionName ( collection )
186+
187+ for ( const endpoint of collection . config . endpoints || [ ] ) {
188+ if ( endpoint . custom ?. openapi ) {
189+ const { parameters, ...documentation } : CustomEndpointDocumentation = endpoint . custom . openapi
190+
191+ if ( documentation . requestBody ) {
192+ const key = componentName ( 'requestBodies' , singular , {
193+ suffix : `CustomEndpoint${ upperFirst ( endpoint . method ) } ` ,
194+ } )
195+
196+ requestBodies [ key ] = getRequestBodySchema (
197+ `Custom endpoint request body for ${ singular } with method ${ endpoint . method } ` ,
198+ documentation . requestBody ,
199+ )
200+ }
201+ }
202+ }
203+
204+ return requestBodies
205+ }
206+
143207const generateQueryOperationSchemas = ( collection : Collection ) : Record < string , JSONSchema4 > => {
144208 const { singular } = collectionName ( collection )
145209
@@ -338,9 +402,42 @@ const generateCollectionResponses = (
338402 } ,
339403 } ,
340404 } ,
405+ ...generateCollectionCustomEndpointResponses ( collection ) ,
341406 }
342407}
343408
409+ const generateCollectionCustomEndpointResponses = (
410+ collection : Collection ,
411+ ) : Record < string , OpenAPIV3_1 . ResponseObject & OpenAPIV3 . ResponseObject > => {
412+ const responses : Record < string , OpenAPIV3_1 . ResponseObject & OpenAPIV3 . ResponseObject > = { }
413+ const { singular } = collectionName ( collection )
414+
415+ for ( const endpoint of collection . config . endpoints || [ ] ) {
416+ if ( endpoint . custom ?. openapi ) {
417+ const documentation : CustomEndpointDocumentation = endpoint . custom . openapi
418+
419+ for ( const [ statusCode ] of Object . entries ( documentation . responses || { } ) ) {
420+ const key = componentName ( 'responses' , singular , {
421+ suffix : `CustomEndpoint${ upperFirst ( endpoint . method ) } ${ statusCode } ` ,
422+ } )
423+
424+ responses [ key ] = {
425+ description : `Custom endpoint response for ${ singular } with method ${ endpoint . method } and status code ${ statusCode } ` ,
426+ content : {
427+ 'application/json' : {
428+ schema : composeRef ( 'schemas' , singular , {
429+ suffix : `CustomEndpoint${ upperFirst ( endpoint . method ) } ${ statusCode } ` ,
430+ } ) ,
431+ } ,
432+ } ,
433+ }
434+ }
435+ }
436+ }
437+
438+ return responses
439+ }
440+
344441const isOpenToPublic = async ( checker : Access ) : Promise < boolean > => {
345442 try {
346443 const result = await checker (
@@ -464,9 +561,48 @@ const generateCollectionOperations = async (
464561 security : ( await isOpenToPublic ( collection . config . access . delete ) ) ? [ ] : [ apiKeySecurity ] ,
465562 } ,
466563 } ,
564+ ...( await generateCollectionCustomEndpointOperations ( collection ) ) ,
467565 }
468566}
469567
568+ const generateCollectionCustomEndpointOperations = async (
569+ collection : Collection ,
570+ ) : Promise < Record < string , OpenAPIV3 . PathItemObject & OpenAPIV3_1 . PathItemObject > > => {
571+ const { slug } = collection . config
572+ const { singular, plural } = collectionName ( collection )
573+ const tags = [ plural ]
574+ const operations : Record < string , OpenAPIV3 . PathItemObject & OpenAPIV3_1 . PathItemObject > = { }
575+
576+ for ( const endpoint of collection . config . endpoints || [ ] ) {
577+ if ( endpoint . custom ?. openapi ) {
578+ const path = endpoint . path . replace ( / \/ : ( [ ^ / ] + ) / g, '/{$1}' )
579+ const key = `/api/${ slug } ${ path } `
580+ const { parameters, ...documentation } : CustomEndpointDocumentation = endpoint . custom . openapi
581+
582+ operations [ key ] = {
583+ ...operations [ key ] ,
584+ parameters,
585+ [ endpoint . method ] : {
586+ ...documentation ,
587+ tags,
588+ requestBody : composeRef ( 'requestBodies' , singular , {
589+ suffix : `CustomEndpoint${ upperFirst ( endpoint . method ) } ` ,
590+ } ) ,
591+ responses : Object . entries ( documentation . responses || { } ) . reduce ( ( acc , [ statusCode ] ) => {
592+ acc [ statusCode ] = composeRef ( 'responses' , singular , {
593+ suffix : `CustomEndpoint${ upperFirst ( endpoint . method ) } ${ statusCode } ` ,
594+ } )
595+
596+ return acc
597+ } , { } as OpenAPIV3_1 . ResponsesObject ) ,
598+ } ,
599+ }
600+ }
601+ }
602+
603+ return operations
604+ }
605+
470606const generateGlobalResponse = (
471607 global : SanitizedGlobalConfig ,
472608) : OpenAPIV3_1 . ResponseObject & OpenAPIV3 . ResponseObject => {
@@ -563,6 +699,8 @@ const generateComponents = (req: Pick<PayloadRequest, 'payload'>) => {
563699 req . payload . config ,
564700 collection ,
565701 )
702+
703+ Object . assign ( schemas , generateCustomEndpointSchemaObjects ( collection ) )
566704 }
567705
568706 for ( const collection of Object . values ( req . payload . collections ) ) {
@@ -581,6 +719,8 @@ const generateComponents = (req: Pick<PayloadRequest, 'payload'>) => {
581719 req . payload . config ,
582720 collection ,
583721 )
722+
723+ Object . assign ( requestBodies , generateRequestBodyCustomEndpointSchemas ( collection ) )
584724 }
585725
586726 for ( const global of req . payload . globals . config ) {
0 commit comments