From cf676348fcc46a1bac7a7b88831ced32ebdf3edc Mon Sep 17 00:00:00 2001 From: Yoann Poupart <66315201+Xmaster6y@users.noreply.github.com> Date: Fri, 16 Jan 2026 19:25:15 +0100 Subject: [PATCH 1/7] initial filter --- src/openapi/generators.ts | 71 ++++++++++----- src/openapiPlugin.ts | 10 +++ src/requestHandlers.ts | 4 +- src/types.ts | 7 +- src/utils/filters.ts | 40 +++++++++ test/openapi-generators.test.ts | 151 +++++++++++++++++++++++++++++++- 6 files changed, 255 insertions(+), 28 deletions(-) create mode 100644 src/utils/filters.ts diff --git a/src/openapi/generators.ts b/src/openapi/generators.ts index 2a89e58..8718cbd 100644 --- a/src/openapi/generators.ts +++ b/src/openapi/generators.ts @@ -16,8 +16,12 @@ import type { SelectField, } from 'payload' import { entityToJSONSchema } from 'payload' -import type { SanitizedPluginOptions } from '../types.js' +import type { PluginOptions } from '../types.js' import { isHiddenField } from '../utils/fields.js' +import { + shouldIncludeCollection, + shouldIncludeGlobal, +} from '../utils/filters.js' import { mapValuesAsync, visitObjectNodes } from '../utils/objects.js' import { type ComponentType, collectionName, componentName, globalName } from './naming.js' import { apiKeySecurity, generateSecuritySchemes } from './securitySchemes.js' @@ -585,7 +589,10 @@ const generateGlobalOperations = async ( } } -const generateComponents = (req: Pick) => { +const generateComponents = ( + req: Pick, + options: PluginOptions, +) => { const schemas: Record = { supportedTimezones: { type: 'string', @@ -593,7 +600,15 @@ const generateComponents = (req: Pick) => { }, } - for (const collection of Object.values(req.payload.collections)) { + const collections = Object.values(req.payload.collections).filter(collection => + shouldIncludeCollection(collection, options), + ) + + const globals = req.payload.globals.config.filter(global => + shouldIncludeGlobal(global, options), + ) + + for (const collection of collections) { const { singular } = collectionName(collection) schemas[componentName('schemas', singular)] = generateSchemaObject( req.payload.config, @@ -601,17 +616,17 @@ const generateComponents = (req: Pick) => { ) } - for (const collection of Object.values(req.payload.collections)) { + for (const collection of collections) { Object.assign(schemas, generateQueryOperationSchemas(collection)) } - for (const global of req.payload.globals.config) { + for (const global of globals) { Object.assign(schemas, generateGlobalSchemas(req.payload.config, global)) } const requestBodies: Record = {} - for (const collection of Object.values(req.payload.collections)) { + for (const collection of collections) { const { singular } = collectionName(collection) requestBodies[componentName('requestBodies', singular)] = generateRequestBodySchema( req.payload.config, @@ -622,15 +637,15 @@ const generateComponents = (req: Pick) => { generateRequestBodySchema(req.payload.config, collection, 'patch') } - for (const global of req.payload.globals.config) { + for (const global of globals) { requestBodies[componentName('requestBodies', globalName(global))] = generateGlobalRequestBody(global) } const responses: Record = Object.assign( {}, - ...Object.values(req.payload.collections).map(generateCollectionResponses), - ...req.payload.globals.config.map(global => ({ + ...collections.map(generateCollectionResponses), + ...globals.map(global => ({ [componentName('responses', globalName(global))]: generateGlobalResponse(global), })), ) @@ -640,9 +655,16 @@ const generateComponents = (req: Pick) => { export const generateV30Spec = async ( req: Pick, - options: SanitizedPluginOptions, + options: PluginOptions, ): Promise => { - const { schemas, requestBodies, responses } = generateComponents(req) + const { schemas, requestBodies, responses } = generateComponents(req, options) + + const collections = Object.values(req.payload.collections).filter(collection => + shouldIncludeCollection(collection, options), + ) + const globals = req.payload.globals.config.filter(global => + shouldIncludeGlobal(global, options), + ) const spec = { openapi: '3.0.3', @@ -650,13 +672,11 @@ export const generateV30Spec = async ( servers: [{ url: `${req.protocol}//${req.headers.get('host')}` }], paths: Object.assign( {}, - ...(await Promise.all( - Object.values(req.payload.collections).map(generateCollectionOperations), - )), - ...(await Promise.all(req.payload.globals.config.map(generateGlobalOperations))), + ...(await Promise.all(collections.map(generateCollectionOperations))), + ...(await Promise.all(globals.map(generateGlobalOperations))), ), components: { - securitySchemes: generateSecuritySchemes(options.authEndpoint), + securitySchemes: generateSecuritySchemes(options.authEndpoint ?? '/openapi-auth'), schemas: await mapValuesAsync(jsonSchemaToOpenapiSchema, schemas), requestBodies: await mapValuesAsync( async requestBody => ({ @@ -700,9 +720,16 @@ export const generateV30Spec = async ( export const generateV31Spec = async ( req: Pick, - options: SanitizedPluginOptions, + options: PluginOptions, ): Promise => { - const { schemas, requestBodies, responses } = generateComponents(req) + const { schemas, requestBodies, responses } = generateComponents(req, options) + + const collections = Object.values(req.payload.collections).filter(collection => + shouldIncludeCollection(collection, options), + ) + const globals = req.payload.globals.config.filter(global => + shouldIncludeGlobal(global, options), + ) const spec = { openapi: '3.1.0', @@ -710,13 +737,11 @@ export const generateV31Spec = async ( servers: [{ url: `${req.protocol}//${req.headers.get('host')}` }], paths: Object.assign( {}, - ...(await Promise.all( - Object.values(req.payload.collections).map(generateCollectionOperations), - )), - ...(await Promise.all(req.payload.globals.config.map(generateGlobalOperations))), + ...(await Promise.all(collections.map(generateCollectionOperations))), + ...(await Promise.all(globals.map(generateGlobalOperations))), ), components: { - securitySchemes: generateSecuritySchemes(options.authEndpoint), + securitySchemes: generateSecuritySchemes(options.authEndpoint ?? '/openapi-auth'), schemas: schemas as Record, requestBodies, responses, diff --git a/src/openapiPlugin.ts b/src/openapiPlugin.ts index d8dd9dd..9b84e4c 100644 --- a/src/openapiPlugin.ts +++ b/src/openapiPlugin.ts @@ -9,6 +9,11 @@ const openapi = openapiVersion = '3.0', metadata, enabled = true, + includeCollections, + excludeCollections, + hideInternalCollections, + includeGlobals, + excludeGlobals, }: PluginOptions): Plugin => ({ endpoints = [], ...config }) => { if (!enabled) { @@ -26,6 +31,11 @@ const openapi = openapiVersion, metadata, authEndpoint, + includeCollections, + excludeCollections, + hideInternalCollections, + includeGlobals, + excludeGlobals, }), }, { diff --git a/src/requestHandlers.ts b/src/requestHandlers.ts index 578be66..88df1d1 100644 --- a/src/requestHandlers.ts +++ b/src/requestHandlers.ts @@ -1,9 +1,9 @@ import type { PayloadRequest } from 'payload' import { generateV30Spec, generateV31Spec } from './openapi/generators.js' -import type { SanitizedPluginOptions } from './types.js' +import type { PluginOptions } from './types.js' export const createOpenAPIRequestHandler = - (options: SanitizedPluginOptions) => + (options: PluginOptions) => async (req: PayloadRequest): Promise => { switch (options.openapiVersion) { case '3.0': diff --git a/src/types.ts b/src/types.ts index 7ebbf0c..0bf9b8d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,6 +12,9 @@ export interface PluginOptions { specEndpoint?: string authEndpoint?: string metadata: OpenAPIMetadata + includeCollections?: string[] + excludeCollections?: string[] + hideInternalCollections?: boolean + includeGlobals?: string[] + excludeGlobals?: string[] } - -export type SanitizedPluginOptions = Required> diff --git a/src/utils/filters.ts b/src/utils/filters.ts new file mode 100644 index 0000000..c47781f --- /dev/null +++ b/src/utils/filters.ts @@ -0,0 +1,40 @@ +import type { Collection, SanitizedGlobalConfig } from 'payload' +import type { PluginOptions } from '../types.js' + +export const shouldIncludeCollection = ( + collection: Collection, + options: PluginOptions, +): boolean => { + const { slug } = collection.config + + if (options.hideInternalCollections && slug.startsWith('payload-')) { + return false + } + + if (options.excludeCollections?.includes(slug)) { + return false + } + + if (options.includeCollections !== undefined) { + return options.includeCollections.includes(slug) + } + + return true +} + +export const shouldIncludeGlobal = ( + global: SanitizedGlobalConfig, + options: PluginOptions, +): boolean => { + const { slug } = global + + if (options.excludeGlobals?.includes(slug)) { + return false + } + + if (options.includeGlobals !== undefined) { + return options.includeGlobals.includes(slug) + } + + return true +} diff --git a/test/openapi-generators.test.ts b/test/openapi-generators.test.ts index 6f0298a..bb5d6fd 100644 --- a/test/openapi-generators.test.ts +++ b/test/openapi-generators.test.ts @@ -2,8 +2,16 @@ import { mongooseAdapter } from '@payloadcms/db-mongodb' import { lexicalEditor } from '@payloadcms/richtext-lexical' import { MongoMemoryServer } from 'mongodb-memory-server' import mongoose from 'mongoose' -import { BasePayload, buildConfig, type CollectionConfig, type Config, type Payload } from 'payload' +import { + BasePayload, + buildConfig, + type CollectionConfig, + type Config, + type GlobalConfig, + type Payload, +} from 'payload' import { afterEach, beforeAll, beforeEach, describe, expect, test } from 'vitest' + import { generateV30Spec } from '../src/openapi/generators' const Posts: CollectionConfig = { @@ -199,4 +207,145 @@ describe('openapi generators', () => { expect(spec).toMatchSnapshot() }) + + describe('collection filtering', () => { + test('includeCollections filters to specified collections only', async () => { + const Categories: CollectionConfig = { + slug: 'categories', + fields: [{ type: 'text', name: 'name' }], + } + const payload = await buildPayload({ + collections: [Posts, Categories], + }) + + 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' }, + includeCollections: ['posts'], + }, + ) + + expect(new Set(Object.keys(spec.paths))).toEqual( + new Set(['/api/posts', '/api/posts/{id}']), + ) + expect(spec.paths['/api/categories']).toBeUndefined() + }) + + test('excludeCollections excludes specified collections', 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' }, + excludeCollections: ['users'], + }, + ) + + expect(spec.paths['/api/posts']).toBeDefined() + expect(spec.paths['/api/users']).toBeUndefined() + }) + + test('hideInternalCollections removes payload-* collections from spec', 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' }, + hideInternalCollections: true, + }, + ) + + expect(spec.paths['/api/posts']).toBeDefined() + expect(spec.paths['/api/payload-preferences']).toBeUndefined() + expect(spec.paths['/api/payload-migrations']).toBeUndefined() + expect(spec.paths['/api/payload-locked-documents']).toBeUndefined() + }) + + test('empty includeCollections array results in no collections', 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' }, + includeCollections: [], + }, + ) + + expect(Object.keys(spec.paths).filter(path => path.startsWith('/api/'))).toEqual([]) + }) + }) + + describe('global filtering', () => { + test('includeGlobals filters to specified globals only', async () => { + const Settings: GlobalConfig = { + slug: 'settings', + fields: [{ type: 'text', name: 'siteName' }], + } + const Footer: GlobalConfig = { + slug: 'footer', + fields: [{ type: 'text', name: 'copyright' }], + } + const payload = await buildPayload({ + globals: [Settings, Footer], + }) + + 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' }, + includeGlobals: ['settings'], + }, + ) + + expect(spec.paths['/api/globals/settings']).toBeDefined() + expect(spec.paths['/api/globals/footer']).toBeUndefined() + }) + + test('excludeGlobals excludes specified globals', async () => { + const Settings: GlobalConfig = { + slug: 'settings', + fields: [{ type: 'text', name: 'siteName' }], + } + const Footer: GlobalConfig = { + slug: 'footer', + fields: [{ type: 'text', name: 'copyright' }], + } + const payload = await buildPayload({ + globals: [Settings, Footer], + }) + + 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' }, + excludeGlobals: ['footer'], + }, + ) + + expect(spec.paths['/api/globals/settings']).toBeDefined() + expect(spec.paths['/api/globals/footer']).toBeUndefined() + }) + }) }) From eaa714d34f044856313725cca12aaef78f385e84 Mon Sep 17 00:00:00 2001 From: Yoann Poupart <66315201+Xmaster6y@users.noreply.github.com> Date: Fri, 16 Jan 2026 19:26:04 +0100 Subject: [PATCH 2/7] format --- src/openapi/generators.ts | 22 +++++----------------- test/openapi-generators.test.ts | 4 +--- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/src/openapi/generators.ts b/src/openapi/generators.ts index 8718cbd..fee6a21 100644 --- a/src/openapi/generators.ts +++ b/src/openapi/generators.ts @@ -18,10 +18,7 @@ import type { import { entityToJSONSchema } from 'payload' import type { PluginOptions } from '../types.js' import { isHiddenField } from '../utils/fields.js' -import { - shouldIncludeCollection, - shouldIncludeGlobal, -} from '../utils/filters.js' +import { shouldIncludeCollection, shouldIncludeGlobal } from '../utils/filters.js' import { mapValuesAsync, visitObjectNodes } from '../utils/objects.js' import { type ComponentType, collectionName, componentName, globalName } from './naming.js' import { apiKeySecurity, generateSecuritySchemes } from './securitySchemes.js' @@ -589,10 +586,7 @@ const generateGlobalOperations = async ( } } -const generateComponents = ( - req: Pick, - options: PluginOptions, -) => { +const generateComponents = (req: Pick, options: PluginOptions) => { const schemas: Record = { supportedTimezones: { type: 'string', @@ -604,9 +598,7 @@ const generateComponents = ( shouldIncludeCollection(collection, options), ) - const globals = req.payload.globals.config.filter(global => - shouldIncludeGlobal(global, options), - ) + const globals = req.payload.globals.config.filter(global => shouldIncludeGlobal(global, options)) for (const collection of collections) { const { singular } = collectionName(collection) @@ -662,9 +654,7 @@ export const generateV30Spec = async ( const collections = Object.values(req.payload.collections).filter(collection => shouldIncludeCollection(collection, options), ) - const globals = req.payload.globals.config.filter(global => - shouldIncludeGlobal(global, options), - ) + const globals = req.payload.globals.config.filter(global => shouldIncludeGlobal(global, options)) const spec = { openapi: '3.0.3', @@ -727,9 +717,7 @@ export const generateV31Spec = async ( const collections = Object.values(req.payload.collections).filter(collection => shouldIncludeCollection(collection, options), ) - const globals = req.payload.globals.config.filter(global => - shouldIncludeGlobal(global, options), - ) + const globals = req.payload.globals.config.filter(global => shouldIncludeGlobal(global, options)) const spec = { openapi: '3.1.0', diff --git a/test/openapi-generators.test.ts b/test/openapi-generators.test.ts index bb5d6fd..af5971d 100644 --- a/test/openapi-generators.test.ts +++ b/test/openapi-generators.test.ts @@ -228,9 +228,7 @@ describe('openapi generators', () => { }, ) - expect(new Set(Object.keys(spec.paths))).toEqual( - new Set(['/api/posts', '/api/posts/{id}']), - ) + expect(new Set(Object.keys(spec.paths))).toEqual(new Set(['/api/posts', '/api/posts/{id}'])) expect(spec.paths['/api/categories']).toBeUndefined() }) From bf1f0b631638db91ab86c8349823781c5c601e9e Mon Sep 17 00:00:00 2001 From: Yoann Poupart <66315201+Xmaster6y@users.noreply.github.com> Date: Tue, 17 Feb 2026 00:26:00 +0100 Subject: [PATCH 3/7] SanitizedPluginOptions --- src/openapi/generators.ts | 11 +++++++---- src/requestHandlers.ts | 4 ++-- src/types.ts | 2 ++ 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/openapi/generators.ts b/src/openapi/generators.ts index fee6a21..6d66a54 100644 --- a/src/openapi/generators.ts +++ b/src/openapi/generators.ts @@ -16,7 +16,7 @@ import type { SelectField, } from 'payload' import { entityToJSONSchema } from 'payload' -import type { PluginOptions } from '../types.js' +import type { SanitizedPluginOptions } from '../types.js' import { isHiddenField } from '../utils/fields.js' import { shouldIncludeCollection, shouldIncludeGlobal } from '../utils/filters.js' import { mapValuesAsync, visitObjectNodes } from '../utils/objects.js' @@ -586,7 +586,10 @@ const generateGlobalOperations = async ( } } -const generateComponents = (req: Pick, options: PluginOptions) => { +const generateComponents = ( + req: Pick, + options: SanitizedPluginOptions, +) => { const schemas: Record = { supportedTimezones: { type: 'string', @@ -647,7 +650,7 @@ const generateComponents = (req: Pick, options: Plugi export const generateV30Spec = async ( req: Pick, - options: PluginOptions, + options: SanitizedPluginOptions, ): Promise => { const { schemas, requestBodies, responses } = generateComponents(req, options) @@ -710,7 +713,7 @@ export const generateV30Spec = async ( export const generateV31Spec = async ( req: Pick, - options: PluginOptions, + options: SanitizedPluginOptions, ): Promise => { const { schemas, requestBodies, responses } = generateComponents(req, options) diff --git a/src/requestHandlers.ts b/src/requestHandlers.ts index 88df1d1..578be66 100644 --- a/src/requestHandlers.ts +++ b/src/requestHandlers.ts @@ -1,9 +1,9 @@ import type { PayloadRequest } from 'payload' import { generateV30Spec, generateV31Spec } from './openapi/generators.js' -import type { PluginOptions } from './types.js' +import type { SanitizedPluginOptions } from './types.js' export const createOpenAPIRequestHandler = - (options: PluginOptions) => + (options: SanitizedPluginOptions) => async (req: PayloadRequest): Promise => { switch (options.openapiVersion) { case '3.0': diff --git a/src/types.ts b/src/types.ts index 0bf9b8d..802f24a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,3 +18,5 @@ export interface PluginOptions { includeGlobals?: string[] excludeGlobals?: string[] } + +export type SanitizedPluginOptions = Required> From e8b281395982fad7d5e7b99d9ff54697515f7376 Mon Sep 17 00:00:00 2001 From: Yoann Poupart <66315201+Xmaster6y@users.noreply.github.com> Date: Tue, 17 Feb 2026 00:31:21 +0100 Subject: [PATCH 4/7] readme update --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 28915de..9e24e52 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Autogenerate an OpenAPI specification from your Payload CMS instance and use it - [x] Preferences endpoints - [x] Support Payload CMS 3.x - [x] Support generating both OpenAPI 3.0 and 3.1 +- [x] Collection and global filtering - [ ] Custom endpoints # Installation @@ -68,6 +69,26 @@ buildConfig({ }) ``` +## 3. Filter collections and globals (optional) + +Control which collections and globals appear in the OpenAPI spec: + +- `includeCollections` / `excludeCollections` — filter collections by slug +- `includeGlobals` / `excludeGlobals` — filter globals by slug +- `hideInternalCollections` — exclude `payload-*` collections + +Example: + +```typescript +openapi({ + openapiVersion: '3.0', + metadata: { title: 'Dev API', version: '0.0.1' }, + includeCollections: ['posts', 'categories'], + excludeGlobals: ['footer'], + hideInternalCollections: true, +}) +``` + # Usage Unless you configured it otherwise, your spec will be accessible via . If you From 96c907018d884730f61c0bc9d56e0fd829e36e16 Mon Sep 17 00:00:00 2001 From: Yoann Poupart <66315201+Xmaster6y@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:14:23 +0100 Subject: [PATCH 5/7] better typing --- README.md | 10 ++++++---- src/openapi/generators.ts | 20 ++++++++++++-------- src/openapiPlugin.ts | 12 ++---------- src/types.ts | 14 +++++++++----- src/utils/filters.ts | 20 ++++++++++---------- test/openapi-generators.test.ts | 17 +++++++++++------ 6 files changed, 50 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 9e24e52..be98fc5 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ buildConfig({ ## 3. Filter collections and globals (optional) -Control which collections and globals appear in the OpenAPI spec: +Control which collections and globals appear in the OpenAPI spec using the `filters` option: - `includeCollections` / `excludeCollections` — filter collections by slug - `includeGlobals` / `excludeGlobals` — filter globals by slug @@ -83,9 +83,11 @@ Example: openapi({ openapiVersion: '3.0', metadata: { title: 'Dev API', version: '0.0.1' }, - includeCollections: ['posts', 'categories'], - excludeGlobals: ['footer'], - hideInternalCollections: true, + filters: { + includeCollections: ['posts', 'categories'], + excludeGlobals: ['footer'], + hideInternalCollections: true, + }, }) ``` diff --git a/src/openapi/generators.ts b/src/openapi/generators.ts index 6d66a54..e2325bb 100644 --- a/src/openapi/generators.ts +++ b/src/openapi/generators.ts @@ -598,10 +598,12 @@ const generateComponents = ( } const collections = Object.values(req.payload.collections).filter(collection => - shouldIncludeCollection(collection, options), + shouldIncludeCollection(collection, options.filters), ) - const globals = req.payload.globals.config.filter(global => shouldIncludeGlobal(global, options)) + const globals = req.payload.globals.config.filter(global => + shouldIncludeGlobal(global, options.filters), + ) for (const collection of collections) { const { singular } = collectionName(collection) @@ -654,10 +656,11 @@ export const generateV30Spec = async ( ): Promise => { const { schemas, requestBodies, responses } = generateComponents(req, options) + const filters = options.filters ?? {} const collections = Object.values(req.payload.collections).filter(collection => - shouldIncludeCollection(collection, options), + shouldIncludeCollection(collection, filters), ) - const globals = req.payload.globals.config.filter(global => shouldIncludeGlobal(global, options)) + const globals = req.payload.globals.config.filter(global => shouldIncludeGlobal(global, filters)) const spec = { openapi: '3.0.3', @@ -669,7 +672,7 @@ export const generateV30Spec = async ( ...(await Promise.all(globals.map(generateGlobalOperations))), ), components: { - securitySchemes: generateSecuritySchemes(options.authEndpoint ?? '/openapi-auth'), + securitySchemes: generateSecuritySchemes(options.authEndpoint), schemas: await mapValuesAsync(jsonSchemaToOpenapiSchema, schemas), requestBodies: await mapValuesAsync( async requestBody => ({ @@ -717,10 +720,11 @@ export const generateV31Spec = async ( ): Promise => { const { schemas, requestBodies, responses } = generateComponents(req, options) + const filters = options.filters ?? {} const collections = Object.values(req.payload.collections).filter(collection => - shouldIncludeCollection(collection, options), + shouldIncludeCollection(collection, filters), ) - const globals = req.payload.globals.config.filter(global => shouldIncludeGlobal(global, options)) + const globals = req.payload.globals.config.filter(global => shouldIncludeGlobal(global, filters)) const spec = { openapi: '3.1.0', @@ -732,7 +736,7 @@ export const generateV31Spec = async ( ...(await Promise.all(globals.map(generateGlobalOperations))), ), components: { - securitySchemes: generateSecuritySchemes(options.authEndpoint ?? '/openapi-auth'), + securitySchemes: generateSecuritySchemes(options.authEndpoint), schemas: schemas as Record, requestBodies, responses, diff --git a/src/openapiPlugin.ts b/src/openapiPlugin.ts index 9b84e4c..1d85d47 100644 --- a/src/openapiPlugin.ts +++ b/src/openapiPlugin.ts @@ -9,11 +9,7 @@ const openapi = openapiVersion = '3.0', metadata, enabled = true, - includeCollections, - excludeCollections, - hideInternalCollections, - includeGlobals, - excludeGlobals, + filters = {}, }: PluginOptions): Plugin => ({ endpoints = [], ...config }) => { if (!enabled) { @@ -31,11 +27,7 @@ const openapi = openapiVersion, metadata, authEndpoint, - includeCollections, - excludeCollections, - hideInternalCollections, - includeGlobals, - excludeGlobals, + filters, }), }, { diff --git a/src/types.ts b/src/types.ts index 802f24a..a2c1244 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,17 +6,21 @@ export interface OpenAPIMetadata { description?: string } +export interface FilterOptions { + includeCollections?: string[] + excludeCollections?: string[] + hideInternalCollections?: boolean + includeGlobals?: string[] + excludeGlobals?: string[] +} + export interface PluginOptions { enabled?: boolean openapiVersion?: OpenAPIVersion specEndpoint?: string authEndpoint?: string metadata: OpenAPIMetadata - includeCollections?: string[] - excludeCollections?: string[] - hideInternalCollections?: boolean - includeGlobals?: string[] - excludeGlobals?: string[] + filters?: FilterOptions } export type SanitizedPluginOptions = Required> diff --git a/src/utils/filters.ts b/src/utils/filters.ts index c47781f..5c31587 100644 --- a/src/utils/filters.ts +++ b/src/utils/filters.ts @@ -1,22 +1,22 @@ import type { Collection, SanitizedGlobalConfig } from 'payload' -import type { PluginOptions } from '../types.js' +import type { FilterOptions } from '../types.js' export const shouldIncludeCollection = ( collection: Collection, - options: PluginOptions, + filters: FilterOptions, ): boolean => { const { slug } = collection.config - if (options.hideInternalCollections && slug.startsWith('payload-')) { + if (filters.hideInternalCollections && slug.startsWith('payload-')) { return false } - if (options.excludeCollections?.includes(slug)) { + if (filters.excludeCollections?.includes(slug)) { return false } - if (options.includeCollections !== undefined) { - return options.includeCollections.includes(slug) + if (filters.includeCollections !== undefined) { + return filters.includeCollections.includes(slug) } return true @@ -24,16 +24,16 @@ export const shouldIncludeCollection = ( export const shouldIncludeGlobal = ( global: SanitizedGlobalConfig, - options: PluginOptions, + filters: FilterOptions, ): boolean => { const { slug } = global - if (options.excludeGlobals?.includes(slug)) { + if (filters.excludeGlobals?.includes(slug)) { return false } - if (options.includeGlobals !== undefined) { - return options.includeGlobals.includes(slug) + if (filters.includeGlobals !== undefined) { + return filters.includeGlobals.includes(slug) } return true diff --git a/test/openapi-generators.test.ts b/test/openapi-generators.test.ts index af5971d..f73c040 100644 --- a/test/openapi-generators.test.ts +++ b/test/openapi-generators.test.ts @@ -63,6 +63,7 @@ describe('openapi generators', () => { openapiVersion: '3.0', authEndpoint: '/api/auth', metadata: { title: 'Test API', version: '1.0' }, + filters: {}, }, ) @@ -93,6 +94,7 @@ describe('openapi generators', () => { openapiVersion: '3.0', authEndpoint: '/api/auth', metadata: { title: 'Test API', version: '1.0' }, + filters: {}, }, ) @@ -142,6 +144,7 @@ describe('openapi generators', () => { openapiVersion: '3.0', authEndpoint: '/api/auth', metadata: { title: 'Test API', version: '1.0' }, + filters: {}, }, ) @@ -175,6 +178,7 @@ describe('openapi generators', () => { openapiVersion: '3.0', authEndpoint: '/api/auth', metadata: { title: 'Test API', version: '1.0' }, + filters: {}, }, ) @@ -202,6 +206,7 @@ describe('openapi generators', () => { openapiVersion: '3.0', authEndpoint: '/api/auth', metadata: { title: 'Test API', version: '1.0' }, + filters: {}, }, ) @@ -224,7 +229,7 @@ describe('openapi generators', () => { openapiVersion: '3.0', authEndpoint: '/api/auth', metadata: { title: 'Test API', version: '1.0' }, - includeCollections: ['posts'], + filters: { includeCollections: ['posts'] }, }, ) @@ -243,7 +248,7 @@ describe('openapi generators', () => { openapiVersion: '3.0', authEndpoint: '/api/auth', metadata: { title: 'Test API', version: '1.0' }, - excludeCollections: ['users'], + filters: { excludeCollections: ['users'] }, }, ) @@ -262,7 +267,7 @@ describe('openapi generators', () => { openapiVersion: '3.0', authEndpoint: '/api/auth', metadata: { title: 'Test API', version: '1.0' }, - hideInternalCollections: true, + filters: { hideInternalCollections: true }, }, ) @@ -283,7 +288,7 @@ describe('openapi generators', () => { openapiVersion: '3.0', authEndpoint: '/api/auth', metadata: { title: 'Test API', version: '1.0' }, - includeCollections: [], + filters: { includeCollections: [] }, }, ) @@ -311,7 +316,7 @@ describe('openapi generators', () => { openapiVersion: '3.0', authEndpoint: '/api/auth', metadata: { title: 'Test API', version: '1.0' }, - includeGlobals: ['settings'], + filters: { includeGlobals: ['settings'] }, }, ) @@ -338,7 +343,7 @@ describe('openapi generators', () => { openapiVersion: '3.0', authEndpoint: '/api/auth', metadata: { title: 'Test API', version: '1.0' }, - excludeGlobals: ['footer'], + filters: { excludeGlobals: ['footer'] }, }, ) From ac621676b1923cc0e5e5d43994d6f0d38fbc194c Mon Sep 17 00:00:00 2001 From: Yoann Poupart <66315201+Xmaster6y@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:40:23 +0100 Subject: [PATCH 6/7] Update src/utils/filters.ts Co-authored-by: Jan Buchar --- src/utils/filters.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/filters.ts b/src/utils/filters.ts index 5c31587..469fb0f 100644 --- a/src/utils/filters.ts +++ b/src/utils/filters.ts @@ -15,11 +15,11 @@ export const shouldIncludeCollection = ( return false } - if (filters.includeCollections !== undefined) { - return filters.includeCollections.includes(slug) + if (filters.includeCollections === undefined) { + return true } - return true + return filters.includeCollections.includes(slug) } export const shouldIncludeGlobal = ( From f5e8ed851da94b2bbadd49439e6a3001db56249c Mon Sep 17 00:00:00 2001 From: Yoann Poupart <66315201+Xmaster6y@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:40:30 +0100 Subject: [PATCH 7/7] Update src/utils/filters.ts Co-authored-by: Jan Buchar --- src/utils/filters.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/filters.ts b/src/utils/filters.ts index 469fb0f..d775a99 100644 --- a/src/utils/filters.ts +++ b/src/utils/filters.ts @@ -32,9 +32,9 @@ export const shouldIncludeGlobal = ( return false } - if (filters.includeGlobals !== undefined) { - return filters.includeGlobals.includes(slug) + if (filters.includeGlobals === undefined) { + return true } - return true + return filters.includeGlobals.includes(slug) }