Skip to content

Commit 48a3edb

Browse files
Xmaster6yjanbuchar
andauthored
feat: Add simple filters for globals, collections and internal collections (#75)
* initial filter * format * SanitizedPluginOptions * readme update * better typing * Update src/utils/filters.ts Co-authored-by: Jan Buchar <Teyras@gmail.com> * Update src/utils/filters.ts Co-authored-by: Jan Buchar <Teyras@gmail.com> --------- Co-authored-by: Jan Buchar <Teyras@gmail.com>
1 parent 176cdaa commit 48a3edb

6 files changed

Lines changed: 265 additions & 19 deletions

File tree

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Autogenerate an OpenAPI specification from your Payload CMS instance and use it
1313
- [x] Preferences endpoints
1414
- [x] Support Payload CMS 3.x
1515
- [x] Support generating both OpenAPI 3.0 and 3.1
16+
- [x] Collection and global filtering
1617
- [ ] Custom endpoints
1718

1819
# Installation
@@ -68,6 +69,28 @@ buildConfig({
6869
})
6970
```
7071

72+
## 3. Filter collections and globals (optional)
73+
74+
Control which collections and globals appear in the OpenAPI spec using the `filters` option:
75+
76+
- `includeCollections` / `excludeCollections` — filter collections by slug
77+
- `includeGlobals` / `excludeGlobals` — filter globals by slug
78+
- `hideInternalCollections` — exclude `payload-*` collections
79+
80+
Example:
81+
82+
```typescript
83+
openapi({
84+
openapiVersion: '3.0',
85+
metadata: { title: 'Dev API', version: '0.0.1' },
86+
filters: {
87+
includeCollections: ['posts', 'categories'],
88+
excludeGlobals: ['footer'],
89+
hideInternalCollections: true,
90+
},
91+
})
92+
```
93+
7194
# Usage
7295

7396
Unless you configured it otherwise, your spec will be accessible via <https://your-payload.com/api/openapi.json>. If you

src/openapi/generators.ts

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
import { entityToJSONSchema } from 'payload'
1919
import type { SanitizedPluginOptions } from '../types.js'
2020
import { isHiddenField } from '../utils/fields.js'
21+
import { shouldIncludeCollection, shouldIncludeGlobal } from '../utils/filters.js'
2122
import { mapValuesAsync, visitObjectNodes } from '../utils/objects.js'
2223
import { type ComponentType, collectionName, componentName, globalName } from './naming.js'
2324
import { apiKeySecurity, generateSecuritySchemes } from './securitySchemes.js'
@@ -587,33 +588,44 @@ const generateGlobalOperations = async (
587588
}
588589
}
589590

590-
const generateComponents = (req: Pick<PayloadRequest, 'payload'>) => {
591+
const generateComponents = (
592+
req: Pick<PayloadRequest, 'payload'>,
593+
options: SanitizedPluginOptions,
594+
) => {
591595
const schemas: Record<string, JSONSchema4> = {
592596
supportedTimezones: {
593597
type: 'string',
594598
example: 'Europe/Prague',
595599
},
596600
}
597601

598-
for (const collection of Object.values(req.payload.collections)) {
602+
const collections = Object.values(req.payload.collections).filter(collection =>
603+
shouldIncludeCollection(collection, options.filters),
604+
)
605+
606+
const globals = req.payload.globals.config.filter(global =>
607+
shouldIncludeGlobal(global, options.filters),
608+
)
609+
610+
for (const collection of collections) {
599611
const { singular } = collectionName(collection)
600612
schemas[componentName('schemas', singular)] = generateSchemaObject(
601613
req.payload.config,
602614
collection,
603615
)
604616
}
605617

606-
for (const collection of Object.values(req.payload.collections)) {
618+
for (const collection of collections) {
607619
Object.assign(schemas, generateQueryOperationSchemas(collection))
608620
}
609621

610-
for (const global of req.payload.globals.config) {
622+
for (const global of globals) {
611623
Object.assign(schemas, generateGlobalSchemas(req.payload.config, global))
612624
}
613625

614626
const requestBodies: Record<string, OpenAPIV3_1.RequestBodyObject> = {}
615627

616-
for (const collection of Object.values(req.payload.collections)) {
628+
for (const collection of collections) {
617629
const { singular } = collectionName(collection)
618630
requestBodies[componentName('requestBodies', singular)] = generateRequestBodySchema(
619631
req.payload.config,
@@ -624,15 +636,15 @@ const generateComponents = (req: Pick<PayloadRequest, 'payload'>) => {
624636
generateRequestBodySchema(req.payload.config, collection, 'patch')
625637
}
626638

627-
for (const global of req.payload.globals.config) {
639+
for (const global of globals) {
628640
requestBodies[componentName('requestBodies', globalName(global))] =
629641
generateGlobalRequestBody(global)
630642
}
631643

632644
const responses: Record<string, OpenAPIV3_1.ResponseObject> = Object.assign(
633645
{},
634-
...Object.values(req.payload.collections).map(generateCollectionResponses),
635-
...req.payload.globals.config.map(global => ({
646+
...collections.map(generateCollectionResponses),
647+
...globals.map(global => ({
636648
[componentName('responses', globalName(global))]: generateGlobalResponse(global),
637649
})),
638650
)
@@ -644,18 +656,22 @@ export const generateV30Spec = async (
644656
req: Pick<PayloadRequest, 'payload' | 'protocol' | 'headers'>,
645657
options: SanitizedPluginOptions,
646658
): Promise<OpenAPIV3.Document> => {
647-
const { schemas, requestBodies, responses } = generateComponents(req)
659+
const { schemas, requestBodies, responses } = generateComponents(req, options)
660+
661+
const filters = options.filters ?? {}
662+
const collections = Object.values(req.payload.collections).filter(collection =>
663+
shouldIncludeCollection(collection, filters),
664+
)
665+
const globals = req.payload.globals.config.filter(global => shouldIncludeGlobal(global, filters))
648666

649667
const spec = {
650668
openapi: '3.0.3',
651669
info: options.metadata,
652670
servers: [{ url: `${req.protocol}//${req.headers.get('host')}` }],
653671
paths: Object.assign(
654672
{},
655-
...(await Promise.all(
656-
Object.values(req.payload.collections).map(generateCollectionOperations),
657-
)),
658-
...(await Promise.all(req.payload.globals.config.map(generateGlobalOperations))),
673+
...(await Promise.all(collections.map(generateCollectionOperations))),
674+
...(await Promise.all(globals.map(generateGlobalOperations))),
659675
),
660676
components: {
661677
securitySchemes: generateSecuritySchemes(options.authEndpoint),
@@ -704,18 +720,22 @@ export const generateV31Spec = async (
704720
req: Pick<PayloadRequest, 'payload' | 'protocol' | 'headers'>,
705721
options: SanitizedPluginOptions,
706722
): Promise<OpenAPIV3_1.Document> => {
707-
const { schemas, requestBodies, responses } = generateComponents(req)
723+
const { schemas, requestBodies, responses } = generateComponents(req, options)
724+
725+
const filters = options.filters ?? {}
726+
const collections = Object.values(req.payload.collections).filter(collection =>
727+
shouldIncludeCollection(collection, filters),
728+
)
729+
const globals = req.payload.globals.config.filter(global => shouldIncludeGlobal(global, filters))
708730

709731
const spec = {
710732
openapi: '3.1.0',
711733
info: options.metadata,
712734
servers: [{ url: `${req.protocol}//${req.headers.get('host')}` }],
713735
paths: Object.assign(
714736
{},
715-
...(await Promise.all(
716-
Object.values(req.payload.collections).map(generateCollectionOperations),
717-
)),
718-
...(await Promise.all(req.payload.globals.config.map(generateGlobalOperations))),
737+
...(await Promise.all(collections.map(generateCollectionOperations))),
738+
...(await Promise.all(globals.map(generateGlobalOperations))),
719739
),
720740
components: {
721741
securitySchemes: generateSecuritySchemes(options.authEndpoint),

src/openapiPlugin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const openapi =
99
openapiVersion = '3.0',
1010
metadata,
1111
enabled = true,
12+
filters = {},
1213
}: PluginOptions): Plugin =>
1314
({ endpoints = [], ...config }) => {
1415
if (!enabled) {
@@ -26,6 +27,7 @@ const openapi =
2627
openapiVersion,
2728
metadata,
2829
authEndpoint,
30+
filters,
2931
}),
3032
},
3133
{

src/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,21 @@ export interface OpenAPIMetadata {
66
description?: string
77
}
88

9+
export interface FilterOptions {
10+
includeCollections?: string[]
11+
excludeCollections?: string[]
12+
hideInternalCollections?: boolean
13+
includeGlobals?: string[]
14+
excludeGlobals?: string[]
15+
}
16+
917
export interface PluginOptions {
1018
enabled?: boolean
1119
openapiVersion?: OpenAPIVersion
1220
specEndpoint?: string
1321
authEndpoint?: string
1422
metadata: OpenAPIMetadata
23+
filters?: FilterOptions
1524
}
1625

1726
export type SanitizedPluginOptions = Required<Omit<PluginOptions, 'enabled' | 'specEndpoint'>>

src/utils/filters.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { Collection, SanitizedGlobalConfig } from 'payload'
2+
import type { FilterOptions } from '../types.js'
3+
4+
export const shouldIncludeCollection = (
5+
collection: Collection,
6+
filters: FilterOptions,
7+
): boolean => {
8+
const { slug } = collection.config
9+
10+
if (filters.hideInternalCollections && slug.startsWith('payload-')) {
11+
return false
12+
}
13+
14+
if (filters.excludeCollections?.includes(slug)) {
15+
return false
16+
}
17+
18+
if (filters.includeCollections === undefined) {
19+
return true
20+
}
21+
22+
return filters.includeCollections.includes(slug)
23+
}
24+
25+
export const shouldIncludeGlobal = (
26+
global: SanitizedGlobalConfig,
27+
filters: FilterOptions,
28+
): boolean => {
29+
const { slug } = global
30+
31+
if (filters.excludeGlobals?.includes(slug)) {
32+
return false
33+
}
34+
35+
if (filters.includeGlobals === undefined) {
36+
return true
37+
}
38+
39+
return filters.includeGlobals.includes(slug)
40+
}

0 commit comments

Comments
 (0)