@@ -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 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+
105127type RequestBodyType = 'post' | 'patch'
106128
107129const requestBodySchema = ( fields : Array < Field > , schema : JSONSchema4 ) : JSONSchema4 => ( {
@@ -155,6 +177,48 @@ const generateRequestBodySchema = (
155177 }
156178}
157179
180+ const getRequestBodySchema = (
181+ description : string ,
182+ schema : OpenAPIV3_1 . SchemaObject ,
183+ ) : OpenAPIV3_1 . RequestBodyObject => {
184+ return {
185+ description,
186+ content : {
187+ 'application/json' : {
188+ schema : {
189+ type : 'object' ,
190+ additionalProperties : false ,
191+ ...schema ,
192+ } ,
193+ } ,
194+ } ,
195+ }
196+ }
197+
198+ const generateRequestBodyCustomEndpointSchemas = ( collection : Collection ) => {
199+ const requestBodies : Record < string , OpenAPIV3_1 . RequestBodyObject > = { }
200+ const { singular } = collectionName ( collection )
201+
202+ for ( const endpoint of collection . config . endpoints || [ ] ) {
203+ if ( endpoint . custom ?. openapi ) {
204+ const documentation : CustomEndpointDocumentation = endpoint . custom . openapi
205+
206+ if ( documentation . requestBody ) {
207+ const key = componentName ( 'requestBodies' , singular , {
208+ suffix : `CustomEndpoint${ upperFirst ( endpoint . method ) } ` ,
209+ } )
210+
211+ requestBodies [ key ] = getRequestBodySchema (
212+ `Custom endpoint request body for ${ singular } with method ${ endpoint . method } ` ,
213+ documentation . requestBody ,
214+ )
215+ }
216+ }
217+ }
218+
219+ return requestBodies
220+ }
221+
158222const generateQueryOperationSchemas = ( collection : Collection ) : Record < string , JSONSchema4 > => {
159223 const { singular } = collectionName ( collection )
160224
@@ -353,9 +417,42 @@ const generateCollectionResponses = (
353417 } ,
354418 } ,
355419 } ,
420+ ...generateCollectionCustomEndpointResponses ( collection ) ,
356421 }
357422}
358423
424+ const generateCollectionCustomEndpointResponses = (
425+ collection : Collection ,
426+ ) : Record < string , OpenAPIV3_1 . ResponseObject & OpenAPIV3 . ResponseObject > => {
427+ const responses : Record < string , OpenAPIV3_1 . ResponseObject & OpenAPIV3 . ResponseObject > = { }
428+ const { singular } = collectionName ( collection )
429+
430+ for ( const endpoint of collection . config . endpoints || [ ] ) {
431+ if ( endpoint . custom ?. openapi ) {
432+ const documentation : CustomEndpointDocumentation = endpoint . custom . openapi
433+
434+ for ( const [ statusCode ] of Object . entries ( documentation . responses || { } ) ) {
435+ const key = componentName ( 'responses' , singular , {
436+ suffix : `CustomEndpoint${ upperFirst ( endpoint . method ) } ${ statusCode } ` ,
437+ } )
438+
439+ responses [ key ] = {
440+ description : `Custom endpoint response for ${ singular } with method ${ endpoint . method } and status code ${ statusCode } ` ,
441+ content : {
442+ 'application/json' : {
443+ schema : composeRef ( 'schemas' , singular , {
444+ suffix : `CustomEndpoint${ upperFirst ( endpoint . method ) } ${ statusCode } ` ,
445+ } ) ,
446+ } ,
447+ } ,
448+ }
449+ }
450+ }
451+ }
452+
453+ return responses
454+ }
455+
359456const isOpenToPublic = async ( checker : Access ) : Promise < boolean > => {
360457 try {
361458 const result = await checker (
@@ -488,9 +585,48 @@ const generateCollectionOperations = async (
488585 security : ( await isOpenToPublic ( collection . config . access . delete ) ) ? [ ] : [ apiKeySecurity ] ,
489586 } ,
490587 } ,
588+ ...( await generateCollectionCustomEndpointOperations ( collection ) ) ,
491589 }
492590}
493591
592+ const generateCollectionCustomEndpointOperations = async (
593+ collection : Collection ,
594+ ) : Promise < Record < string , OpenAPIV3 . PathItemObject & OpenAPIV3_1 . PathItemObject > > => {
595+ const { slug } = collection . config
596+ const { singular, plural } = collectionName ( collection )
597+ const tags = [ plural ]
598+ const operations : Record < string , OpenAPIV3 . PathItemObject & OpenAPIV3_1 . PathItemObject > = { }
599+
600+ for ( const endpoint of collection . config . endpoints || [ ] ) {
601+ if ( endpoint . custom ?. openapi ) {
602+ const path = endpoint . path . replace ( / \/ : ( [ ^ / ] + ) / g, '/{$1}' )
603+ const key = `/api/${ slug } ${ path } `
604+ const { parameters, ...documentation } : CustomEndpointDocumentation = endpoint . custom . openapi
605+
606+ operations [ key ] = {
607+ ...operations [ key ] ,
608+ parameters,
609+ [ endpoint . method ] : {
610+ ...documentation ,
611+ tags,
612+ requestBody : composeRef ( 'requestBodies' , singular , {
613+ suffix : `CustomEndpoint${ upperFirst ( endpoint . method ) } ` ,
614+ } ) ,
615+ responses : Object . entries ( documentation . responses || { } ) . reduce ( ( acc , [ statusCode ] ) => {
616+ acc [ statusCode ] = composeRef ( 'responses' , singular , {
617+ suffix : `CustomEndpoint${ upperFirst ( endpoint . method ) } ${ statusCode } ` ,
618+ } )
619+
620+ return acc
621+ } , { } as OpenAPIV3_1 . ResponsesObject ) ,
622+ } ,
623+ }
624+ }
625+ }
626+
627+ return operations
628+ }
629+
494630const generateGlobalResponse = (
495631 global : SanitizedGlobalConfig ,
496632) : OpenAPIV3_1 . ResponseObject & OpenAPIV3 . ResponseObject => {
@@ -587,6 +723,8 @@ const generateComponents = (req: Pick<PayloadRequest, 'payload'>) => {
587723 req . payload . config ,
588724 collection ,
589725 )
726+
727+ Object . assign ( schemas , generateCustomEndpointSchemaObjects ( collection ) )
590728 }
591729
592730 for ( const collection of Object . values ( req . payload . collections ) ) {
@@ -608,6 +746,8 @@ const generateComponents = (req: Pick<PayloadRequest, 'payload'>) => {
608746 )
609747 requestBodies [ componentName ( 'requestBodies' , singular , { suffix : 'Patch' } ) ] =
610748 generateRequestBodySchema ( req . payload . config , collection , 'patch' )
749+
750+ Object . assign ( requestBodies , generateRequestBodyCustomEndpointSchemas ( collection ) )
611751 }
612752
613753 for ( const global of req . payload . globals . config ) {
0 commit comments