diff --git a/.gitignore b/.gitignore index 6023549..54b08d3 100644 --- a/.gitignore +++ b/.gitignore @@ -137,6 +137,9 @@ GitHub.sublime-settings !.vscode/extensions.json .history +### Zed ### +.zed/* + ### WebStorm ### # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 diff --git a/package.json b/package.json index 52970ee..376b0f3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "payload-oapi", "description": "An OpenAPI plugin for Payload CMS", - "version": "0.2.5", + "version": "0.2.6", "homepage:": "https://github.com/janbuchar/payload-oapi", "repository:": "https://github.com/janbuchar/payload-oapi", "main": "dist/index.js", diff --git a/src/openapi/generators.ts b/src/openapi/generators.ts index 49e8f21..e332edd 100644 --- a/src/openapi/generators.ts +++ b/src/openapi/generators.ts @@ -389,6 +389,7 @@ const isOpenToPublic = async (checker: Access): Promise => { const generateCollectionOperations = async ( collection: Collection, + apiBasePath: string, ): Promise> => { const { slug } = collection.config const { singular, plural } = collectionName(collection) @@ -400,7 +401,7 @@ const generateCollectionOperations = async ( } satisfies OpenAPIV3_1.ResponsesObject & OpenAPIV3.ResponsesObject return { - [`/api/${slug}`]: { + [`${apiBasePath}/${slug}`]: { get: { operationId: componentName('schemas', plural, { prefix: 'list' }), summary: `Retrieve a list of ${plural}`, @@ -462,7 +463,7 @@ const generateCollectionOperations = async ( security: (await isOpenToPublic(collection.config.access.create)) ? [] : [apiKeySecurity], }, }, - [`/api/${slug}/{id}`]: { + [`${apiBasePath}/${slug}/{id}`]: { parameters: [ ...baseQueryParams, { @@ -563,13 +564,14 @@ const generateGlobalSchemas = ( const generateGlobalOperations = async ( global: SanitizedGlobalConfig, + apiBasePath: string, ): Promise> => { const slug = global.slug const singular = globalName(global) const tags = [singular] return { - [`/api/globals/${slug}`]: { + [`${apiBasePath}/globals/${slug}`]: { get: { summary: `Get the ${singular}`, tags, @@ -652,6 +654,15 @@ const generateComponents = ( return { schemas, requestBodies, responses } } +/** + * Prefer the `apiBasePath` from plugin options (not null), + * otherwise use the currently configured api route from payload. + */ +const getApiBasePath = ( + options: SanitizedPluginOptions, + req: Pick, +): string => (options.apiBasePath !== null ? options.apiBasePath : req.payload.config.routes.api) + export const generateV30Spec = async ( req: Pick, options: SanitizedPluginOptions, @@ -663,6 +674,7 @@ export const generateV30Spec = async ( shouldIncludeCollection(collection, filters), ) const globals = req.payload.globals.config.filter(global => shouldIncludeGlobal(global, filters)) + const apiBasePath = getApiBasePath(options, req) const spec = { openapi: '3.0.3', @@ -670,8 +682,10 @@ export const generateV30Spec = async ( servers: [{ url: `${req.protocol}//${req.headers.get('host')}` }], paths: Object.assign( {}, - ...(await Promise.all(collections.map(generateCollectionOperations))), - ...(await Promise.all(globals.map(generateGlobalOperations))), + ...(await Promise.all( + collections.map(collection => generateCollectionOperations(collection, apiBasePath)), + )), + ...(await Promise.all(globals.map(global => generateGlobalOperations(global, apiBasePath)))), ), components: { securitySchemes: generateSecuritySchemes(options.authEndpoint), @@ -727,6 +741,7 @@ export const generateV31Spec = async ( shouldIncludeCollection(collection, filters), ) const globals = req.payload.globals.config.filter(global => shouldIncludeGlobal(global, filters)) + const apiBasePath = getApiBasePath(options, req) const spec = { openapi: '3.1.0', @@ -734,8 +749,10 @@ export const generateV31Spec = async ( servers: [{ url: `${req.protocol}//${req.headers.get('host')}` }], paths: Object.assign( {}, - ...(await Promise.all(collections.map(generateCollectionOperations))), - ...(await Promise.all(globals.map(generateGlobalOperations))), + ...(await Promise.all( + collections.map(collection => generateCollectionOperations(collection, apiBasePath)), + )), + ...(await Promise.all(globals.map(global => generateGlobalOperations(global, apiBasePath)))), ), components: { securitySchemes: generateSecuritySchemes(options.authEndpoint), diff --git a/src/openapiPlugin.ts b/src/openapiPlugin.ts index 1d85d47..1aaf7ab 100644 --- a/src/openapiPlugin.ts +++ b/src/openapiPlugin.ts @@ -10,6 +10,7 @@ const openapi = metadata, enabled = true, filters = {}, + apiBasePath = null, }: PluginOptions): Plugin => ({ endpoints = [], ...config }) => { if (!enabled) { @@ -28,6 +29,7 @@ const openapi = metadata, authEndpoint, filters, + apiBasePath, }), }, { diff --git a/src/types.ts b/src/types.ts index a2c1244..982a877 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,6 +21,7 @@ export interface PluginOptions { authEndpoint?: string metadata: OpenAPIMetadata filters?: FilterOptions + apiBasePath?: string | null } export type SanitizedPluginOptions = Required> diff --git a/test/openapi-generators.test.ts b/test/openapi-generators.test.ts index f73c040..ea6d663 100644 --- a/test/openapi-generators.test.ts +++ b/test/openapi-generators.test.ts @@ -64,6 +64,7 @@ describe('openapi generators', () => { authEndpoint: '/api/auth', metadata: { title: 'Test API', version: '1.0' }, filters: {}, + apiBasePath: null, }, ) @@ -95,6 +96,7 @@ describe('openapi generators', () => { authEndpoint: '/api/auth', metadata: { title: 'Test API', version: '1.0' }, filters: {}, + apiBasePath: null, }, ) @@ -145,6 +147,7 @@ describe('openapi generators', () => { authEndpoint: '/api/auth', metadata: { title: 'Test API', version: '1.0' }, filters: {}, + apiBasePath: null, }, ) @@ -179,6 +182,7 @@ describe('openapi generators', () => { authEndpoint: '/api/auth', metadata: { title: 'Test API', version: '1.0' }, filters: {}, + apiBasePath: null, }, ) @@ -207,6 +211,7 @@ describe('openapi generators', () => { authEndpoint: '/api/auth', metadata: { title: 'Test API', version: '1.0' }, filters: {}, + apiBasePath: null, }, ) @@ -230,6 +235,7 @@ describe('openapi generators', () => { authEndpoint: '/api/auth', metadata: { title: 'Test API', version: '1.0' }, filters: { includeCollections: ['posts'] }, + apiBasePath: null, }, ) @@ -249,6 +255,7 @@ describe('openapi generators', () => { authEndpoint: '/api/auth', metadata: { title: 'Test API', version: '1.0' }, filters: { excludeCollections: ['users'] }, + apiBasePath: null, }, ) @@ -268,6 +275,7 @@ describe('openapi generators', () => { authEndpoint: '/api/auth', metadata: { title: 'Test API', version: '1.0' }, filters: { hideInternalCollections: true }, + apiBasePath: null, }, ) @@ -289,6 +297,7 @@ describe('openapi generators', () => { authEndpoint: '/api/auth', metadata: { title: 'Test API', version: '1.0' }, filters: { includeCollections: [] }, + apiBasePath: null, }, ) @@ -317,6 +326,7 @@ describe('openapi generators', () => { authEndpoint: '/api/auth', metadata: { title: 'Test API', version: '1.0' }, filters: { includeGlobals: ['settings'] }, + apiBasePath: null, }, ) @@ -344,6 +354,7 @@ describe('openapi generators', () => { authEndpoint: '/api/auth', metadata: { title: 'Test API', version: '1.0' }, filters: { excludeGlobals: ['footer'] }, + apiBasePath: null, }, ) @@ -351,4 +362,51 @@ describe('openapi generators', () => { expect(spec.paths['/api/globals/footer']).toBeUndefined() }) }) + + test('apiBasePath option changes operation paths', async () => { + const payload = await buildPayload({ + collections: [Posts], + }) + + const spec = await generateV30Spec( + { protocol: 'https', headers: new Headers({ host: 'localhost' }), payload }, + { + openapiVersion: '3.0', + authEndpoint: '/api/auth', + metadata: { title: 'Test API', version: '1.0' }, + filters: { hideInternalCollections: true }, + apiBasePath: '/api/custom', + }, + ) + + expect(spec.paths['/api/custom/posts']).toBeDefined() + expect(spec.paths['/api/custom/payload-preferences']).toBeUndefined() + expect(spec.paths['/api/custom/payload-migrations']).toBeUndefined() + expect(spec.paths['/api/custom/payload-locked-documents']).toBeUndefined() + }) + + test('payload routes api config changes operation paths', async () => { + const payload = await buildPayload({ + collections: [Posts], + routes: { + api: '/api/custom', + }, + }) + + const spec = await generateV30Spec( + { protocol: 'https', headers: new Headers({ host: 'localhost' }), payload }, + { + openapiVersion: '3.0', + authEndpoint: '/api/auth', + metadata: { title: 'Test API', version: '1.0' }, + filters: { hideInternalCollections: true }, + apiBasePath: null, + }, + ) + + expect(spec.paths['/api/custom/posts']).toBeDefined() + expect(spec.paths['/api/custom/payload-preferences']).toBeUndefined() + expect(spec.paths['/api/custom/payload-migrations']).toBeUndefined() + expect(spec.paths['/api/custom/payload-locked-documents']).toBeUndefined() + }) })