@seshuk/payload-plugin-openapi 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -582
- package/dist/spec/buildDocument.js +2 -1
- package/dist/spec/downconvert.d.ts +1 -1
- package/dist/spec/downconvert.js +27 -8
- package/dist/spec/entitySchemas.js +36 -32
- package/dist/spec/filters.js +3 -1
- package/dist/spec/params.d.ts +9 -1
- package/dist/spec/params.js +93 -1
- package/dist/spec/paths/auth.js +52 -63
- package/dist/spec/paths/collections.js +31 -10
- package/dist/spec/paths/globals.js +18 -4
- package/dist/spec/paths/versions.d.ts +2 -1
- package/dist/spec/paths/versions.js +14 -5
- package/dist/translations/locales/ar.js +5 -0
- package/dist/translations/locales/az.js +5 -0
- package/dist/translations/locales/bg.js +5 -0
- package/dist/translations/locales/bnBd.js +5 -0
- package/dist/translations/locales/bnIn.js +5 -0
- package/dist/translations/locales/ca.js +5 -0
- package/dist/translations/locales/cs.js +5 -0
- package/dist/translations/locales/da.js +5 -0
- package/dist/translations/locales/de.js +5 -0
- package/dist/translations/locales/en.js +5 -0
- package/dist/translations/locales/es.js +5 -0
- package/dist/translations/locales/et.js +5 -0
- package/dist/translations/locales/fa.js +5 -0
- package/dist/translations/locales/fr.js +5 -0
- package/dist/translations/locales/he.js +5 -0
- package/dist/translations/locales/hr.js +5 -0
- package/dist/translations/locales/hu.js +5 -0
- package/dist/translations/locales/hy.js +5 -0
- package/dist/translations/locales/id.js +5 -0
- package/dist/translations/locales/is.js +5 -0
- package/dist/translations/locales/it.js +5 -0
- package/dist/translations/locales/ja.js +5 -0
- package/dist/translations/locales/ko.js +5 -0
- package/dist/translations/locales/lt.js +5 -0
- package/dist/translations/locales/lv.js +5 -0
- package/dist/translations/locales/my.js +5 -0
- package/dist/translations/locales/nb.js +5 -0
- package/dist/translations/locales/nl.js +5 -0
- package/dist/translations/locales/pl.js +5 -0
- package/dist/translations/locales/pt.js +5 -0
- package/dist/translations/locales/ro.js +5 -0
- package/dist/translations/locales/rs.js +5 -0
- package/dist/translations/locales/rsLatin.js +5 -0
- package/dist/translations/locales/ru.js +5 -0
- package/dist/translations/locales/sk.js +5 -0
- package/dist/translations/locales/sl.js +5 -0
- package/dist/translations/locales/sv.js +5 -0
- package/dist/translations/locales/ta.js +5 -0
- package/dist/translations/locales/th.js +5 -0
- package/dist/translations/locales/tr.js +5 -0
- package/dist/translations/locales/uk.js +5 -0
- package/dist/translations/locales/vi.js +5 -0
- package/dist/translations/locales/zh.js +5 -0
- package/dist/translations/locales/zhTw.js +5 -0
- package/dist/translations/types.d.ts +5 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
+
<picture>
|
|
4
|
+
<img src="docs/logo.svg" alt="OpenAPI Plugin for Payload CMS" height="80" />
|
|
5
|
+
</picture>
|
|
6
|
+
|
|
3
7
|
<h1>OpenAPI Plugin for Payload CMS</h1>
|
|
4
8
|
|
|
5
|
-
<p>OpenAPI 3.0/3.1/3.2
|
|
9
|
+
<p>Generate an OpenAPI 3.0/3.1/3.2 specification from your Payload config and serve it with Scalar or Swagger UI.</p>
|
|
6
10
|
|
|
7
|
-
<a href="https://www.npmjs.com/package/@seshuk/payload-plugin-openapi"><img src="https://img.shields.io/npm/v/@seshuk/payload-plugin-openapi?style=flat-square&logo=npm" alt="npm version" /></a>
|
|
8
|
-
<a href="https://www.npmjs.com/package/@seshuk/payload-plugin-openapi"><img src="https://img.shields.io/npm/dm/@seshuk/payload-plugin-openapi?style=flat-square" alt="npm downloads" /></a>
|
|
9
11
|
<a href="https://github.com/maximseshuk/payload-plugin-openapi/releases/"><img src="https://img.shields.io/github/v/release/maximseshuk/payload-plugin-openapi?style=flat-square&logo=github" alt="GitHub release" /></a>
|
|
12
|
+
<a href="https://www.npmjs.com/package/@seshuk/payload-plugin-openapi"><img src="https://img.shields.io/npm/v/@seshuk/payload-plugin-openapi?style=flat-square&logo=npm" alt="npm version" /></a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/@seshuk/payload-plugin-openapi"><img src="https://img.shields.io/npm/dm/@seshuk/payload-plugin-openapi?style=flat-square&logo=npm" alt="npm downloads" /></a>
|
|
14
|
+
<a href="https://github.com/maximseshuk/payload-plugin-openapi/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/maximseshuk/payload-plugin-openapi/ci.yml?style=flat-square&logo=github" alt="CI" /></a>
|
|
15
|
+
<a href="https://payload-plugin-openapi.seshuk.im/"><img src="https://img.shields.io/badge/docs-payload--plugin--openapi.seshuk.im-blue?style=flat-square&logo=readthedocs&logoColor=white" alt="Documentation" /></a>
|
|
10
16
|
<a href="https://github.com/maximseshuk/payload-plugin-openapi/blob/main/LICENSE"><img src="https://img.shields.io/github/license/maximseshuk/payload-plugin-openapi?style=flat-square" alt="license" /></a>
|
|
11
17
|
<a href="https://ko-fi.com/V7V61UCT39"><img src="https://img.shields.io/badge/Ko--fi-Buy_me_a_coffee-ff5f5f?style=flat-square&logo=ko-fi&logoColor=white" alt="Ko-fi" /></a>
|
|
12
18
|
|
|
@@ -14,64 +20,36 @@
|
|
|
14
20
|
|
|
15
21
|
## Features
|
|
16
22
|
|
|
17
|
-
- Full spec from your config — collections, globals, auth, versions, and jobs
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- [Plugin Options](#plugin-options)
|
|
32
|
-
- [Metadata](#metadata)
|
|
33
|
-
- [OpenAPI Version](#openapi-version)
|
|
34
|
-
- [Filters](#filters)
|
|
35
|
-
- [Interactive Auth](#interactive-auth)
|
|
36
|
-
- [Security Marking](#security-marking)
|
|
37
|
-
- [Caching](#caching)
|
|
38
|
-
- [Docs UI](#docs-ui)
|
|
39
|
-
- [Documenting Custom Endpoints](#documenting-custom-endpoints)
|
|
40
|
-
- [Annotating fields](#annotating-fields)
|
|
41
|
-
- [For plugin authors](#for-plugin-authors)
|
|
42
|
-
- [Extensions](#extensions)
|
|
43
|
-
- [Writing to a File](#writing-to-a-file)
|
|
44
|
-
- [Generate only, no runtime endpoint](#generate-only-no-runtime-endpoint)
|
|
45
|
-
- [Serving a pre-generated file in the docs UI](#serving-a-pre-generated-file-in-the-docs-ui)
|
|
46
|
-
- [Programmatic Use](#programmatic-use)
|
|
47
|
-
- [Internationalization](#internationalization)
|
|
48
|
-
- [Exports](#exports)
|
|
49
|
-
- [License](#license)
|
|
50
|
-
- [Related Plugins](#related-plugins)
|
|
51
|
-
|
|
52
|
-
## Requirements
|
|
53
|
-
|
|
54
|
-
- Payload `^3.53.0`
|
|
55
|
-
- Node.js `>=20`
|
|
56
|
-
|
|
57
|
-
## Installation
|
|
23
|
+
- **Full spec from your config** — collections, globals, auth, versions, and jobs are documented with zero annotation.
|
|
24
|
+
- **Interactive docs included** — mount Scalar or Swagger UI, or both on different paths.
|
|
25
|
+
- **Native metadata** — document custom endpoints and refine field schemas through Payload's own `custom.openapi` key. No wrapper, no separate registry.
|
|
26
|
+
- **Precise filtering** — choose exactly which entities and operations end up in the spec.
|
|
27
|
+
- **Security marking** — every operation is marked public or secured by probing your access functions, with per-entity and document-wide overrides.
|
|
28
|
+
- **Localized** — descriptions resolve through your Payload i18n, and the UI ships translations for 44 locales.
|
|
29
|
+
- **File generation** — write the spec to disk with `payload openapi:generate` for CI, schema diffs, or client codegen.
|
|
30
|
+
- **One option for the spec version** — serve OpenAPI 3.0, 3.1, or 3.2 from the same config.
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
Requires **Payload CMS 3.53.0 or later** and **Node.js 20 or later**.
|
|
35
|
+
|
|
36
|
+
### Install
|
|
58
37
|
|
|
59
38
|
```bash
|
|
60
|
-
pnpm add @seshuk/payload-plugin-openapi
|
|
61
|
-
# or
|
|
62
39
|
npm install @seshuk/payload-plugin-openapi
|
|
63
|
-
# or
|
|
64
40
|
yarn add @seshuk/payload-plugin-openapi
|
|
41
|
+
pnpm add @seshuk/payload-plugin-openapi
|
|
65
42
|
```
|
|
66
43
|
|
|
67
|
-
|
|
44
|
+
### Configure
|
|
68
45
|
|
|
69
|
-
|
|
70
|
-
|
|
46
|
+
Add the plugin to your Payload config, plus a docs UI renderer:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
71
49
|
import { buildConfig } from 'payload'
|
|
50
|
+
import { openapi, scalar } from '@seshuk/payload-plugin-openapi'
|
|
72
51
|
|
|
73
52
|
export default buildConfig({
|
|
74
|
-
// ...
|
|
75
53
|
plugins: [
|
|
76
54
|
openapi({
|
|
77
55
|
metadata: {
|
|
@@ -84,555 +62,50 @@ export default buildConfig({
|
|
|
84
62
|
})
|
|
85
63
|
```
|
|
86
64
|
|
|
87
|
-
|
|
65
|
+
Two endpoints are now live, both relative to your API route (`/api` by default):
|
|
88
66
|
|
|
89
67
|
- `GET /api/openapi.json` — the generated OpenAPI document
|
|
90
|
-
- `GET /api/docs` — interactive API reference
|
|
91
|
-
|
|
92
|
-
Both paths are mounted under your Payload API route (`routes.api`, `/api` by default), since the plugin registers them as Payload endpoints. The `path` and `specEndpoint` options you pass are relative to that route.
|
|
93
|
-
|
|
94
|
-
Prefer Swagger UI? Swap `scalar()` for `swaggerUi()` — or mount both on different paths.
|
|
95
|
-
|
|
96
|
-
> [!NOTE]
|
|
97
|
-
> `metadata.title` and `metadata.version` are required. The plugin throws on boot if either is missing.
|
|
98
|
-
|
|
99
|
-
## Configuration
|
|
100
|
-
|
|
101
|
-
### Plugin Options
|
|
102
|
-
|
|
103
|
-
| Option | Type | Default | Description |
|
|
104
|
-
| ----------------- | ------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
105
|
-
| `metadata` | `OpenApiMetadata` | — | API title, version, and description. **Required.** |
|
|
106
|
-
| `openapiVersion` | `'3.0' \| '3.1' \| '3.2'` | `'3.2'` | Spec version to serve |
|
|
107
|
-
| `specEndpoint` | `string` | `'/openapi.json'` | Path the spec is served from (relative to the API route) |
|
|
108
|
-
| `enabled` | `boolean` | `true` | Set `false` to disable the plugin entirely |
|
|
109
|
-
| `serve` | `boolean` | `true` | Set `false` to register only the CLI generator and serve nothing over HTTP ([details](#generate-only-no-runtime-endpoint)) |
|
|
110
|
-
| `filters` | `FilterOptions` | see below | Which entities and operations to document ([details](#filters)) |
|
|
111
|
-
| `interactiveAuth` | `boolean \| { endpoint }` | `false` | Username/password login for the docs UI |
|
|
112
|
-
| `nestedTags` | `boolean` | `false` | Emit an OpenAPI 3.2 nested tag hierarchy (see below) |
|
|
113
|
-
| `securityWhen` | `(ctx) => boolean \| undefined` | — | Override the auto-detected security marking per operation ([details](#security-marking)) |
|
|
114
|
-
| `cache` | `boolean` | `true` | Cache the built document for the life of the process |
|
|
115
|
-
| `extensions` | `OpenApiExtension[]` | `[]` | Inject paths, components, tags, or transform the document |
|
|
116
|
-
|
|
117
|
-
### Metadata
|
|
118
|
-
|
|
119
|
-
```ts
|
|
120
|
-
openapi({
|
|
121
|
-
metadata: {
|
|
122
|
-
title: 'My API',
|
|
123
|
-
version: '1.0.0',
|
|
124
|
-
description: 'Public API for the example app.',
|
|
125
|
-
},
|
|
126
|
-
})
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### OpenAPI Version
|
|
130
|
-
|
|
131
|
-
The document is always built as 3.2. The `openapiVersion` option controls what gets served:
|
|
132
|
-
|
|
133
|
-
| Version | Behavior |
|
|
134
|
-
| ------- | ----------------------------------------------------------------------------- |
|
|
135
|
-
| `'3.2'` | Served as built (default) |
|
|
136
|
-
| `'3.1'` | A downconversion pass strips 3.2-only fields |
|
|
137
|
-
| `'3.0'` | A downconversion pass to 3.0 (e.g. nullable handling, `examples` → `example`) |
|
|
138
|
-
|
|
139
|
-
> [!NOTE]
|
|
140
|
-
> Scalar and Swagger UI don't render OpenAPI 3.2 nested tags yet. The `nestedTags` option is **off by default**, so `doc.tags` is a flat list of per-entity tags with descriptions. Turn it on only if your consumer understands the 3.2 `kind`/`parent` tag tree.
|
|
141
|
-
|
|
142
|
-
### Filters
|
|
143
|
-
|
|
144
|
-
`filters` decides what ends up in the spec: which entities are documented, which Payload-internal collections show up, which endpoint groups are generated, and which operations are dropped. It's one flat object.
|
|
145
|
-
|
|
146
|
-
| Option | Type | Default | Description |
|
|
147
|
-
| ------------------- | ------------------ | ------- | ------------------------------------------------------------------------------- |
|
|
148
|
-
| `include` | `EntityMatcher[]` | `[]` | Allowlist. When non-empty, only matching entities are documented |
|
|
149
|
-
| `exclude` | `EntityMatcher[]` | `[]` | Entities to leave out entirely (schemas and paths) |
|
|
150
|
-
| `includeHidden` | `boolean` | `false` | Include collections flagged `hidden` / `admin.hidden` |
|
|
151
|
-
| `includeSystem` | `boolean` | `false` | Include Payload-internal collections (`payload-jobs`, `payload-preferences`, …) |
|
|
152
|
-
| `includeCustom` | `boolean` | `true` | Document endpoints carrying `custom.openapi` metadata |
|
|
153
|
-
| `includeAuth` | `boolean` | `true` | Document auth operations (login / logout / me / …) |
|
|
154
|
-
| `includeAdminAuth` | `boolean` | `false` | Document admin/bootstrap auth endpoints (`/init`, `/access`, first-register) |
|
|
155
|
-
| `includeVersions` | `boolean` | `true` | Document version operations (`/versions`, `/versions/{id}`) |
|
|
156
|
-
| `includeJobs` | `boolean` | `true` | Document jobs endpoints (`/payload-jobs/run`, `/handle-schedules`) |
|
|
157
|
-
| `excludeOperations` | `OperationRule[]` | `[]` | Per-operation removal rules |
|
|
158
|
-
| `excludeWhen` | `(ctx) => boolean` | — | Escape hatch: return `true` to drop an operation |
|
|
159
|
-
|
|
160
|
-
#### Choosing entities with `include` and `exclude`
|
|
161
|
-
|
|
162
|
-
These two lists decide which collections and globals are documented. Each entry is one of three things:
|
|
163
|
-
|
|
164
|
-
| Matcher | Matches | Example |
|
|
165
|
-
| ---------------- | -------------------------------------------------------------- | --------------------------------- |
|
|
166
|
-
| a string | one entity by its exact slug | `'posts'` |
|
|
167
|
-
| a `RegExp` | every entity whose slug matches the pattern | `/^marketing-/` |
|
|
168
|
-
| `{ kind, slug }` | one entity, when a collection and a global share the same slug | `{ kind: 'global', slug: 'nav' }` |
|
|
169
|
-
|
|
170
|
-
The rules are:
|
|
171
|
-
|
|
172
|
-
- `include` is an allowlist. Leave it empty and everything is documented; add anything and only matching entities survive.
|
|
173
|
-
- `exclude` always wins. If an entity matches both lists, it's dropped.
|
|
174
|
-
|
|
175
|
-
**Document only a few collections.** With `include` set, nothing else makes it into the spec:
|
|
176
|
-
|
|
177
|
-
```ts
|
|
178
|
-
filters: {
|
|
179
|
-
include: ['posts', 'media', 'categories'],
|
|
180
|
-
}
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
**Document everything except a couple of entities:**
|
|
184
|
-
|
|
185
|
-
```ts
|
|
186
|
-
filters: {
|
|
187
|
-
exclude: ['audit-log', 'internal-settings'],
|
|
188
|
-
}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
**Match a group of slugs with a regular expression.** The pattern is tested against the slug, so this keeps every collection whose slug starts with `public-`:
|
|
192
|
-
|
|
193
|
-
```ts
|
|
194
|
-
filters: {
|
|
195
|
-
// public-posts, public-media, public-authors … all kept; everything else dropped.
|
|
196
|
-
include: [/^public-/],
|
|
197
|
-
}
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
A few more patterns, for reference:
|
|
201
|
-
|
|
202
|
-
- `/^public-/` — slug starts with `public-`
|
|
203
|
-
- `/-draft$/` — slug ends with `-draft`
|
|
204
|
-
- `/^(posts|pages)$/` — slug is exactly `posts` or `pages`
|
|
205
|
-
- `/internal/i` — slug contains `internal`, case-insensitive
|
|
206
|
-
|
|
207
|
-
**Disambiguate a collection from a global.** If you have a collection and a global that share a slug, a plain string would match both. Use `{ kind, slug }` to target one:
|
|
208
|
-
|
|
209
|
-
```ts
|
|
210
|
-
filters: {
|
|
211
|
-
// Keep the `settings` collection, drop the `settings` global.
|
|
212
|
-
exclude: [{ kind: 'global', slug: 'settings' }],
|
|
213
|
-
}
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
**Mix them.** `include` narrows the set first, then `exclude` removes from what's left:
|
|
217
|
-
|
|
218
|
-
```ts
|
|
219
|
-
filters: {
|
|
220
|
-
// Document every `public-*` collection, but never the drafts one.
|
|
221
|
-
include: [/^public-/],
|
|
222
|
-
exclude: ['public-drafts'],
|
|
223
|
-
}
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
`include` and `exclude` only apply to your own collections and globals. Hidden and Payload-internal collections are governed by `includeHidden` and `includeSystem`, which run first — adding `payload-jobs` to `include` won't surface it unless `includeSystem` is on.
|
|
227
|
-
|
|
228
|
-
#### Dropping individual operations
|
|
229
|
-
|
|
230
|
-
`include` / `exclude` work at the entity level. To remove specific operations (a single method on one collection, every `DELETE`, anything under a path prefix), use `excludeOperations` or `excludeWhen`.
|
|
231
|
-
|
|
232
|
-
Each rule in `excludeOperations` is a set of conditions. Within one rule every field you set must match (AND). Across rules, an operation is dropped if any rule matches it. Leave a field out and it matches anything.
|
|
233
|
-
|
|
234
|
-
```ts
|
|
235
|
-
filters: {
|
|
236
|
-
excludeOperations: [
|
|
237
|
-
// Drop every DELETE, on any entity.
|
|
238
|
-
{ method: 'delete' },
|
|
239
|
-
// Drop writes to `posts`, but leave its reads alone.
|
|
240
|
-
{ slug: 'posts', method: ['post', 'patch', 'put'] },
|
|
241
|
-
// Drop anything matching a path, regardless of method or entity.
|
|
242
|
-
{ path: /\/preview$/ },
|
|
243
|
-
],
|
|
244
|
-
}
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
`excludeWhen` is the escape hatch for logic that doesn't fit a rule. It runs after `excludeOperations` and gets the method, path, slug, and kind for each operation:
|
|
248
|
-
|
|
249
|
-
```ts
|
|
250
|
-
filters: {
|
|
251
|
-
excludeWhen: ({ path, method }) => path.includes('/internal/') || method === 'put',
|
|
252
|
-
}
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
### Interactive Auth
|
|
256
|
-
|
|
257
|
-
By default the docs UI expects you to paste a bearer token. Turn on `interactiveAuth` to add a token endpoint and a matching security scheme, so Scalar/Swagger can show an **Authorize** dialog where users log in with their Payload credentials:
|
|
258
|
-
|
|
259
|
-
```ts
|
|
260
|
-
openapi({
|
|
261
|
-
metadata: { title: 'My API', version: '1.0.0' },
|
|
262
|
-
interactiveAuth: true, // token endpoint at /api/openapi-auth
|
|
263
|
-
// interactiveAuth: { endpoint: '/login' }, // or a custom path → /api/login
|
|
264
|
-
})
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
The endpoint logs in against your first auth-enabled collection (falling back to `users`) and returns the JWT. It accepts the username as either `email` or `username`, matching how your collection is configured.
|
|
268
|
-
|
|
269
|
-
> [!WARNING]
|
|
270
|
-
> The interactive auth endpoint exchanges credentials for a live JWT. Only enable it on docs you intend real users to authenticate against, and serve them over HTTPS.
|
|
271
|
-
|
|
272
|
-
### Security Marking
|
|
273
|
-
|
|
274
|
-
Each operation in the spec is marked either public or secured (referencing the `PayloadToken` scheme). The marking is a **static hint**, not a live access check — it tells a reader which endpoints need a token.
|
|
275
|
-
|
|
276
|
-
By default the plugin figures this out per operation by probing your Payload access functions as an **anonymous request** (`user: null`): an operation is marked public only if its access function settles on `true`. The probe is deliberately conservative:
|
|
277
|
-
|
|
278
|
-
- An `async` access function is awaited — `async () => true` is correctly public.
|
|
279
|
-
- A function that returns a `Where` query (partial access), throws, or times out is marked secured.
|
|
280
|
-
- A function that reaches into the database (`req.payload.find(...)`) is marked secured — the probe never runs live queries, so DB-driven access always errs on the side of a padlock.
|
|
281
|
-
|
|
282
|
-
The marking is per operation: `read` covers list/find-by-id/count, `create` covers create/duplicate, `update` covers update and bulk update, `delete` covers delete and bulk delete.
|
|
283
|
-
|
|
284
|
-
When the guess is wrong, override it — two ways, override wins over the probe:
|
|
285
|
-
|
|
286
|
-
**Per entity, with `custom.openapi.security`** on a collection or global:
|
|
287
|
-
|
|
288
|
-
```ts
|
|
289
|
-
export const Posts: CollectionConfig = {
|
|
290
|
-
slug: 'posts',
|
|
291
|
-
custom: {
|
|
292
|
-
openapi: {
|
|
293
|
-
// true → all operations public; false → all secured; or per operation:
|
|
294
|
-
security: { read: true, create: false, update: false, delete: false },
|
|
295
|
-
},
|
|
296
|
-
},
|
|
297
|
-
// ...
|
|
298
|
-
}
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
**Across the whole document, with `securityWhen`** — an escape hatch mirroring `filters.excludeWhen`. Return `true` to mark an operation public, `false` to mark it secured, or `undefined` to keep the detected marking. It runs last, after `custom.openapi.security` and the probe, over collection, global, auth, and version operations alike:
|
|
302
|
-
|
|
303
|
-
```ts
|
|
304
|
-
openapi({
|
|
305
|
-
metadata: { title: 'My API', version: '1.0.0' },
|
|
306
|
-
securityWhen: ({ slug, method }) => (slug === 'public-feed' && method === 'get' ? true : undefined),
|
|
307
|
-
})
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
> [!NOTE]
|
|
311
|
-
> This is public-access marking only — the plugin never runs per-user access checks. The generated spec matches what the HTTP endpoint enforces at runtime; the marking just documents it.
|
|
312
|
-
|
|
313
|
-
### Caching
|
|
314
|
-
|
|
315
|
-
The Payload config is static after boot, so the document is identical on every request apart from the server URL (which is always filled in fresh). Caching is on by default. Disable it in development so edits to your config show up without a restart:
|
|
316
|
-
|
|
317
|
-
```ts
|
|
318
|
-
openapi({
|
|
319
|
-
metadata: { title: 'My API', version: '1.0.0' },
|
|
320
|
-
cache: process.env.NODE_ENV === 'production',
|
|
321
|
-
})
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
## Docs UI
|
|
325
|
-
|
|
326
|
-
The plugin ships two UI renderers. Each is a separate Payload plugin you add alongside `openapi()` — mount one, or both on different paths:
|
|
327
|
-
|
|
328
|
-
```ts
|
|
329
|
-
import { openapi, scalar, swaggerUi } from '@seshuk/payload-plugin-openapi'
|
|
68
|
+
- `GET /api/docs` — the interactive API reference
|
|
330
69
|
|
|
331
|
-
|
|
332
|
-
openapi({ metadata: { title: 'My API', version: '1.0.0' } }),
|
|
333
|
-
scalar(), // Scalar at /api/docs
|
|
334
|
-
swaggerUi({ path: '/swagger' }), // Swagger UI at /api/swagger
|
|
335
|
-
]
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
Both renderers accept the same options:
|
|
339
|
-
|
|
340
|
-
| Option | Type | Default | Description |
|
|
341
|
-
| --------------- | ------------------------- | --------------------------- | --------------------------------------------------------------------------------- |
|
|
342
|
-
| `path` | `string` | `'/docs'` | Where the docs UI is served, relative to the API route (so `/docs` → `/api/docs`) |
|
|
343
|
-
| `specEndpoint` | `string` | `'<apiRoute>/openapi.json'` | URL the UI fetches the document from |
|
|
344
|
-
| `cdnBase` | `string` | official jsDelivr package | CDN base for the UI's assets |
|
|
345
|
-
| `configuration` | `Record<string, unknown>` | `{}` | Extra config merged into the library's init options |
|
|
346
|
-
| `enabled` | `boolean` | `true` | Set `false` to skip mounting the UI |
|
|
70
|
+
`metadata.title` and `metadata.version` are required; the plugin throws at boot without them. Prefer Swagger UI? Swap `scalar()` for `swaggerUi()`, or mount both on different paths.
|
|
347
71
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
```ts
|
|
351
|
-
scalar({
|
|
352
|
-
configuration: {
|
|
353
|
-
theme: 'purple',
|
|
354
|
-
hideDownloadButton: true,
|
|
355
|
-
},
|
|
356
|
-
})
|
|
357
|
-
```
|
|
72
|
+
The document is built lazily from the fully sanitized config, so collections, fields, and endpoints added by other plugins are picked up regardless of plugin order.
|
|
358
73
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
Any custom Payload endpoint with `custom.openapi` metadata is picked up automatically (as long as `filters.includeCustom` stays on). The metadata is a standard OpenAPI [Operation Object](https://spec.openapis.org/oas/v3.1.0#operation-object):
|
|
362
|
-
|
|
363
|
-
```ts
|
|
364
|
-
import type { Endpoint } from 'payload'
|
|
365
|
-
|
|
366
|
-
const healthEndpoint: Endpoint = {
|
|
367
|
-
path: '/health',
|
|
368
|
-
method: 'get',
|
|
369
|
-
handler: () => Response.json({ ok: true }),
|
|
370
|
-
custom: {
|
|
371
|
-
openapi: {
|
|
372
|
-
summary: 'Health check',
|
|
373
|
-
tags: ['System'],
|
|
374
|
-
responses: {
|
|
375
|
-
'200': { description: 'Service is healthy' },
|
|
376
|
-
},
|
|
377
|
-
},
|
|
378
|
-
},
|
|
379
|
-
}
|
|
380
|
-
```
|
|
74
|
+
Add `filters` to control what is exposed, `interactiveAuth` for a login dialog in the docs UI, `extensions` for anything the generator doesn't produce on its own — see the [configuration reference](https://payload-plugin-openapi.seshuk.im/configuration/overview).
|
|
381
75
|
|
|
382
|
-
|
|
76
|
+
## Documentation
|
|
383
77
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
### Annotating fields
|
|
387
|
-
|
|
388
|
-
The same `custom.openapi` convention works on individual fields. The plugin already infers each field's schema from its Payload type; anything you put under `custom.openapi` is merged on top of that schema, so you can add a `description`, an `example`, `format`, constraints, or override what was inferred:
|
|
389
|
-
|
|
390
|
-
```ts
|
|
391
|
-
import type { Field } from 'payload'
|
|
392
|
-
|
|
393
|
-
const slug: Field = {
|
|
394
|
-
name: 'slug',
|
|
395
|
-
type: 'text',
|
|
396
|
-
custom: {
|
|
397
|
-
openapi: {
|
|
398
|
-
description: 'URL-safe identifier, lowercase with dashes.',
|
|
399
|
-
pattern: '^[a-z0-9-]+$',
|
|
400
|
-
example: 'hello-world',
|
|
401
|
-
},
|
|
402
|
-
},
|
|
403
|
-
}
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
The merge is deep and your keys win, so `custom.openapi` only changes what you name and leaves the rest of the inferred schema intact. It applies wherever the field appears — read, create, and update schemas alike.
|
|
407
|
-
|
|
408
|
-
`description`, `title`, and `summary` are localizable. Give them a function or a locale-keyed object and they're resolved against the request language, the same way Payload labels are:
|
|
409
|
-
|
|
410
|
-
```ts
|
|
411
|
-
const slug: Field = {
|
|
412
|
-
name: 'slug',
|
|
413
|
-
type: 'text',
|
|
414
|
-
custom: {
|
|
415
|
-
openapi: {
|
|
416
|
-
// a function…
|
|
417
|
-
description: ({ t }) => t('fields:slugHelp'),
|
|
418
|
-
// …or a locale map:
|
|
419
|
-
// description: { en: 'URL-safe identifier', ru: 'URL-совместимый идентификатор' },
|
|
420
|
-
},
|
|
421
|
-
},
|
|
422
|
-
}
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
### For plugin authors
|
|
426
|
-
|
|
427
|
-
If you maintain a Payload plugin that adds its own endpoints, you can make them show up in the spec **without depending on this package** — and without your users having to wire anything up. Attach a `custom.openapi` Operation Object to each endpoint you add, exactly as above:
|
|
428
|
-
|
|
429
|
-
```ts
|
|
430
|
-
import type { Config, Plugin } from 'payload'
|
|
431
|
-
|
|
432
|
-
export const myPlugin =
|
|
433
|
-
(): Plugin =>
|
|
434
|
-
(config: Config): Config => ({
|
|
435
|
-
...config,
|
|
436
|
-
endpoints: [
|
|
437
|
-
...(config.endpoints ?? []),
|
|
438
|
-
{
|
|
439
|
-
path: '/my-feature/sync',
|
|
440
|
-
method: 'post',
|
|
441
|
-
handler: syncHandler,
|
|
442
|
-
custom: {
|
|
443
|
-
// Picked up automatically if the OpenAPI plugin is installed; ignored otherwise.
|
|
444
|
-
openapi: {
|
|
445
|
-
summary: 'Trigger a sync',
|
|
446
|
-
tags: ['My Feature'],
|
|
447
|
-
responses: { '202': { description: 'Sync queued' } },
|
|
448
|
-
},
|
|
449
|
-
},
|
|
450
|
-
},
|
|
451
|
-
],
|
|
452
|
-
})
|
|
453
|
-
```
|
|
78
|
+
Full docs are at **<https://payload-plugin-openapi.seshuk.im/>**:
|
|
454
79
|
|
|
455
|
-
|
|
80
|
+
- [Quick start](https://payload-plugin-openapi.seshuk.im/quick-start)
|
|
81
|
+
- [Configuration reference](https://payload-plugin-openapi.seshuk.im/configuration/overview)
|
|
82
|
+
- [Filters](https://payload-plugin-openapi.seshuk.im/configuration/filters)
|
|
83
|
+
- [Security marking](https://payload-plugin-openapi.seshuk.im/configuration/security-marking)
|
|
84
|
+
- [Docs UI — Scalar & Swagger](https://payload-plugin-openapi.seshuk.im/configuration/docs-ui)
|
|
85
|
+
- [Documenting custom endpoints](https://payload-plugin-openapi.seshuk.im/guides/custom-endpoints)
|
|
86
|
+
- [Field metadata](https://payload-plugin-openapi.seshuk.im/guides/field-metadata)
|
|
87
|
+
- [CLI — `openapi:generate`](https://payload-plugin-openapi.seshuk.im/cli/generate)
|
|
88
|
+
- [Examples](https://payload-plugin-openapi.seshuk.im/guides/examples)
|
|
456
89
|
|
|
457
|
-
|
|
90
|
+
## For plugin authors
|
|
458
91
|
|
|
459
|
-
|
|
92
|
+
If you maintain a Payload plugin, attach a `custom.openapi` Operation Object to the endpoints you add and they show up in the generated spec — no dependency on this package, and nothing for your users to wire up. The same key works on fields. If this plugin isn't installed, the metadata is inert. See [For plugin authors](https://payload-plugin-openapi.seshuk.im/guides/plugin-authors).
|
|
460
93
|
|
|
461
|
-
##
|
|
94
|
+
## Related plugins
|
|
462
95
|
|
|
463
|
-
|
|
96
|
+
- **[@seshuk/payload-storage-bunny](https://github.com/maximseshuk/payload-storage-bunny)** — store files and stream video from Payload CMS on Bunny's global CDN.
|
|
97
|
+
- **[@seshuk/payload-plugin-media-preview](https://github.com/maximseshuk/payload-plugin-media-preview)** — preview images, video, audio, and documents directly in the Payload admin panel.
|
|
464
98
|
|
|
465
|
-
|
|
466
|
-
import type { OpenApiExtension } from '@seshuk/payload-plugin-openapi'
|
|
467
|
-
|
|
468
|
-
const myExtension: OpenApiExtension = {
|
|
469
|
-
paths: {
|
|
470
|
-
'/webhooks/stripe': {
|
|
471
|
-
post: { summary: 'Stripe webhook', responses: { '200': { description: 'OK' } } },
|
|
472
|
-
},
|
|
473
|
-
},
|
|
474
|
-
components: {
|
|
475
|
-
securitySchemes: {
|
|
476
|
-
apiKey: { type: 'apiKey', in: 'header', name: 'X-API-Key' },
|
|
477
|
-
},
|
|
478
|
-
},
|
|
479
|
-
tags: [{ name: 'Webhooks', description: 'Inbound webhooks' }],
|
|
480
|
-
transform: (doc, ctx) => {
|
|
481
|
-
doc.info.termsOfService = 'https://example.com/terms'
|
|
482
|
-
return doc
|
|
483
|
-
},
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
openapi({
|
|
487
|
-
metadata: { title: 'My API', version: '1.0.0' },
|
|
488
|
-
extensions: [myExtension],
|
|
489
|
-
})
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
The `transform` hook receives the full document and a `BuildContext` (locales, API route, active i18n), and must return the document to serve.
|
|
493
|
-
|
|
494
|
-
## Writing to a File
|
|
495
|
-
|
|
496
|
-
The plugin registers a Payload `bin` script under the `openapi:generate` key, so you can write the document to a file without making an HTTP request — handy for committing the spec, running schema diffs in CI, or feeding a client-code generator.
|
|
497
|
-
|
|
498
|
-
```bash
|
|
499
|
-
# Writes openapi.json using the i18n fallback language.
|
|
500
|
-
payload openapi:generate
|
|
501
|
-
|
|
502
|
-
# Pick a language and an output path.
|
|
503
|
-
payload openapi:generate --lang ru --out ./spec/openapi.ru.json
|
|
504
|
-
|
|
505
|
-
# Set the base URL written to the spec's `servers`.
|
|
506
|
-
payload openapi:generate --server https://api.example.com
|
|
507
|
-
|
|
508
|
-
# Write one file per supported language: openapi.<lang>.json
|
|
509
|
-
payload openapi:generate --lang all
|
|
510
|
-
```
|
|
511
|
-
|
|
512
|
-
| Flag | Description |
|
|
513
|
-
| ---------- | ------------------------------------------------------------------------------------- |
|
|
514
|
-
| `--lang` | Locale for translated descriptions, or `all` to write one file per supported language |
|
|
515
|
-
| `--out` | Output path for a single-language run (default `openapi.json`) |
|
|
516
|
-
| `--server` | Base URL written to the spec's `servers`. Omit it to leave `servers` empty |
|
|
517
|
-
|
|
518
|
-
> [!NOTE]
|
|
519
|
-
> The generated spec reflects public-access marking only — it does not run per-user access checks. This matches the HTTP endpoint exactly.
|
|
520
|
-
|
|
521
|
-
**Setting the server URL.** Over HTTP the `servers` field is filled in per request from the incoming `Host` header, so there's nothing to configure. A file on disk has no request to read the host from, so you set it yourself with `--server` (or, when building the document in your own code, an explicit `servers` — see [Programmatic Use](#programmatic-use)). `metadata` carries the API's title, version, and description only, not its URL — keeping the base URL out of the spec is what lets the same document work behind any host, proxy, or environment.
|
|
522
|
-
|
|
523
|
-
### Generate only, no runtime endpoint
|
|
524
|
-
|
|
525
|
-
By default the plugin both registers the `openapi:generate` CLI **and** serves the spec at `/api/openapi.json`. If you only want the CLI — generate the file at build time and host it yourself, without exposing a live endpoint — set `serve: false`:
|
|
526
|
-
|
|
527
|
-
```ts
|
|
528
|
-
openapi({
|
|
529
|
-
metadata: { title: 'My API', version: '1.0.0' },
|
|
530
|
-
serve: false, // register the CLI generator only; nothing is served over HTTP
|
|
531
|
-
})
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
The plugin still needs to be in your `plugins` array (that's what registers the `bin` script and the resolved options it reads), but it won't mount the spec or interactive-auth endpoints. Then generate the file wherever you keep static assets:
|
|
535
|
-
|
|
536
|
-
```bash
|
|
537
|
-
payload openapi:generate --server https://api.example.com --out ./public/openapi.json
|
|
538
|
-
```
|
|
539
|
-
|
|
540
|
-
Now serve `public/openapi.json` like any other static file — through Next.js, a CDN, nginx, or by committing it to the repo. There's no per-request work and no way to hit a stale or unauthenticated spec at runtime.
|
|
541
|
-
|
|
542
|
-
### Serving a pre-generated file in the docs UI
|
|
543
|
-
|
|
544
|
-
The docs UI plugins (`scalar`/`swaggerUi`) load the spec from a URL, and that URL doesn't have to be the plugin's own endpoint. Point `specEndpoint` at your static file and the UI renders it directly — no runtime generation involved:
|
|
545
|
-
|
|
546
|
-
```ts
|
|
547
|
-
plugins: [
|
|
548
|
-
openapi({ metadata: { title: 'My API', version: '1.0.0' }, serve: false }),
|
|
549
|
-
scalar({ specEndpoint: '/openapi.json' }), // loads public/openapi.json
|
|
550
|
-
]
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
For multiple languages, generate one file per language (`--lang all`) and wire up a switcher exactly as in [Internationalization](#internationalization) — just point the `sources` at your static files (`/openapi.en.json`, …) instead of the runtime endpoint.
|
|
554
|
-
|
|
555
|
-
> [!TIP]
|
|
556
|
-
> Even with the default runtime endpoint, you're not re-generating on every request. With `cache: true` (the default) the document is built once on first hit and reused for the life of the process — only the server URL is refreshed per request. `serve: false` is for when you want **no** runtime endpoint at all, not merely to avoid rebuild cost.
|
|
557
|
-
|
|
558
|
-
## Programmatic Use
|
|
559
|
-
|
|
560
|
-
Need the document in your own code? `buildOpenApiDocument` builds it from a live Payload instance, and the downconverters are exported too:
|
|
561
|
-
|
|
562
|
-
```ts
|
|
563
|
-
import { buildOpenApiDocument, toOpenApi30 } from '@seshuk/payload-plugin-openapi'
|
|
564
|
-
|
|
565
|
-
const doc = await buildOpenApiDocument({
|
|
566
|
-
payload, // a BasePayload instance
|
|
567
|
-
options, // resolved plugin options
|
|
568
|
-
language: 'en', // optional
|
|
569
|
-
servers: [{ url: 'https://api.example.com' }],
|
|
570
|
-
})
|
|
571
|
-
|
|
572
|
-
const v30 = toOpenApi30(doc)
|
|
573
|
-
```
|
|
574
|
-
|
|
575
|
-
## Internationalization
|
|
576
|
-
|
|
577
|
-
The plugin includes UI translations for 44 locales, automatically merged into your Payload i18n configuration under the `@seshuk/payload-plugin-openapi` namespace. Only the languages present in your project's `i18n.supportedLanguages` are merged in.
|
|
578
|
-
|
|
579
|
-
Entity titles and descriptions in the generated spec are resolved through the active request locale. The spec endpoint also honors an explicit `?lang=` query param (when supported), so `GET /api/openapi.json?lang=ru` returns Russian descriptions.
|
|
580
|
-
|
|
581
|
-
To put a language switcher in the docs UI, point Scalar's `sources` at the same runtime endpoint with different `?lang=` values — nothing is pre-generated, each language resolves on request:
|
|
582
|
-
|
|
583
|
-
```ts
|
|
584
|
-
scalar({
|
|
585
|
-
configuration: {
|
|
586
|
-
sources: [
|
|
587
|
-
{ title: 'English', url: '/api/openapi.json?lang=en', default: true },
|
|
588
|
-
{ title: 'Русский', url: '/api/openapi.json?lang=ru' },
|
|
589
|
-
],
|
|
590
|
-
},
|
|
591
|
-
})
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
When `sources` is set, the renderer's own `specEndpoint` is ignored, and the first entry is the default unless another sets `default: true`. Swagger UI has no built-in switcher — for it, mount one instance per language on its own `path`:
|
|
595
|
-
|
|
596
|
-
```ts
|
|
597
|
-
swaggerUi({ path: '/docs/en', specEndpoint: '/api/openapi.json?lang=en' }),
|
|
598
|
-
swaggerUi({ path: '/docs/ru', specEndpoint: '/api/openapi.json?lang=ru' }),
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
The same switcher works against static files instead of the runtime endpoint — see [Serving a pre-generated file in the docs UI](#serving-a-pre-generated-file-in-the-docs-ui).
|
|
602
|
-
|
|
603
|
-
Supported locales: `ar`, `az`, `bg`, `bn` (BD/IN), `ca`, `cs`, `da`, `de`, `en`, `es`, `et`, `fa`, `fr`, `he`, `hr`, `hu`, `hy`, `id`, `is`, `it`, `ja`, `ko`, `lt`, `lv`, `my`, `nb`, `nl`, `pl`, `pt`, `ro`, `rs` (Cyrillic/Latin), `ru`, `sk`, `sl`, `sv`, `ta`, `th`, `tr`, `uk`, `vi`, `zh`, `zhTw`.
|
|
604
|
-
|
|
605
|
-
## Exports
|
|
606
|
-
|
|
607
|
-
Everything is exported from the single main entry point:
|
|
608
|
-
|
|
609
|
-
| Export | Kind | Description |
|
|
610
|
-
| ---------------------- | -------- | ----------------------------------------------- |
|
|
611
|
-
| `openapi` | plugin | The main plugin — generates and serves the spec |
|
|
612
|
-
| `scalar` | plugin | Mounts a Scalar API reference UI |
|
|
613
|
-
| `swaggerUi` | plugin | Mounts a Swagger UI |
|
|
614
|
-
| `buildOpenApiDocument` | function | Build the document from a live Payload instance |
|
|
615
|
-
| `toOpenApi30` | function | Downconvert a 3.2 document to 3.0 |
|
|
616
|
-
| `toOpenApi31` | function | Downconvert a 3.2 document to 3.1 |
|
|
99
|
+
## Support
|
|
617
100
|
|
|
618
|
-
|
|
101
|
+
Bug reports, feature requests, and questions go to [GitHub Issues](https://github.com/maximseshuk/payload-plugin-openapi/issues). For Payload itself, see the [Payload CMS docs](https://payloadcms.com/docs) and [Discord](https://discord.gg/payloadcms).
|
|
619
102
|
|
|
620
103
|
## License
|
|
621
104
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
## Related Plugins
|
|
625
|
-
|
|
626
|
-
- **[@seshuk/payload-storage-bunny](https://github.com/maximseshuk/payload-storage-bunny)** — Bunny.net storage adapter for Payload CMS
|
|
627
|
-
- **[@seshuk/payload-plugin-media-preview](https://github.com/maximseshuk/payload-plugin-media-preview)** — Preview images, video, audio, and documents directly in the Payload admin panel
|
|
628
|
-
|
|
629
|
-
## Support
|
|
630
|
-
|
|
631
|
-
- **Bug Reports**: [GitHub Issues](https://github.com/maximseshuk/payload-plugin-openapi/issues)
|
|
632
|
-
- **Questions**: Open a [GitHub Issue](https://github.com/maximseshuk/payload-plugin-openapi/issues) or ask in the [Payload CMS Discord](https://discord.gg/payloadcms)
|
|
105
|
+
MIT — see [LICENSE](LICENSE).
|
|
633
106
|
|
|
634
107
|
## Credits
|
|
635
108
|
|
|
636
|
-
Built
|
|
109
|
+
Built by [Maxim Seshuk](https://github.com/maximseshuk) for the Payload CMS community.
|
|
637
110
|
|
|
638
|
-
If
|
|
111
|
+
If this plugin saves you time, you can [buy me a coffee](https://ko-fi.com/seshuk) ☕
|