@seshuk/payload-plugin-openapi 0.1.0
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/LICENSE +21 -0
- package/README.md +595 -0
- package/dist/bin/generateSpec.d.ts +25 -0
- package/dist/bin/generateSpec.js +61 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +2 -0
- package/dist/endpoints/interactiveAuth.d.ts +4 -0
- package/dist/endpoints/interactiveAuth.js +27 -0
- package/dist/endpoints/spec.d.ts +5 -0
- package/dist/endpoints/spec.js +39 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +67 -0
- package/dist/options.d.ts +2 -0
- package/dist/options.js +44 -0
- package/dist/spec/build.d.ts +14 -0
- package/dist/spec/build.js +36 -0
- package/dist/spec/buildDocument.d.ts +15 -0
- package/dist/spec/buildDocument.js +234 -0
- package/dist/spec/components.d.ts +46 -0
- package/dist/spec/components.js +280 -0
- package/dist/spec/downconvert.d.ts +3 -0
- package/dist/spec/downconvert.js +55 -0
- package/dist/spec/entitySchemas.d.ts +15 -0
- package/dist/spec/entitySchemas.js +246 -0
- package/dist/spec/fields.d.ts +5 -0
- package/dist/spec/fields.js +44 -0
- package/dist/spec/filters.d.ts +11 -0
- package/dist/spec/filters.js +74 -0
- package/dist/spec/names.d.ts +14 -0
- package/dist/spec/names.js +37 -0
- package/dist/spec/params.d.ts +47 -0
- package/dist/spec/params.js +344 -0
- package/dist/spec/paths/auth.d.ts +9 -0
- package/dist/spec/paths/auth.js +285 -0
- package/dist/spec/paths/collections.d.ts +7 -0
- package/dist/spec/paths/collections.js +261 -0
- package/dist/spec/paths/custom.d.ts +9 -0
- package/dist/spec/paths/custom.js +41 -0
- package/dist/spec/paths/globals.d.ts +7 -0
- package/dist/spec/paths/globals.js +68 -0
- package/dist/spec/paths/jobs.d.ts +7 -0
- package/dist/spec/paths/jobs.js +130 -0
- package/dist/spec/paths/versions.d.ts +18 -0
- package/dist/spec/paths/versions.js +243 -0
- package/dist/spec/tags.d.ts +27 -0
- package/dist/spec/tags.js +79 -0
- package/dist/translations/index.d.ts +6 -0
- package/dist/translations/index.js +96 -0
- package/dist/translations/locales/ar.d.ts +2 -0
- package/dist/translations/locales/ar.js +76 -0
- package/dist/translations/locales/az.d.ts +2 -0
- package/dist/translations/locales/az.js +76 -0
- package/dist/translations/locales/bg.d.ts +2 -0
- package/dist/translations/locales/bg.js +76 -0
- package/dist/translations/locales/bnBd.d.ts +2 -0
- package/dist/translations/locales/bnBd.js +76 -0
- package/dist/translations/locales/bnIn.d.ts +2 -0
- package/dist/translations/locales/bnIn.js +76 -0
- package/dist/translations/locales/ca.d.ts +2 -0
- package/dist/translations/locales/ca.js +76 -0
- package/dist/translations/locales/cs.d.ts +2 -0
- package/dist/translations/locales/cs.js +76 -0
- package/dist/translations/locales/da.d.ts +2 -0
- package/dist/translations/locales/da.js +76 -0
- package/dist/translations/locales/de.d.ts +2 -0
- package/dist/translations/locales/de.js +76 -0
- package/dist/translations/locales/en.d.ts +2 -0
- package/dist/translations/locales/en.js +76 -0
- package/dist/translations/locales/es.d.ts +2 -0
- package/dist/translations/locales/es.js +76 -0
- package/dist/translations/locales/et.d.ts +2 -0
- package/dist/translations/locales/et.js +76 -0
- package/dist/translations/locales/fa.d.ts +2 -0
- package/dist/translations/locales/fa.js +76 -0
- package/dist/translations/locales/fr.d.ts +2 -0
- package/dist/translations/locales/fr.js +76 -0
- package/dist/translations/locales/he.d.ts +2 -0
- package/dist/translations/locales/he.js +76 -0
- package/dist/translations/locales/hr.d.ts +2 -0
- package/dist/translations/locales/hr.js +76 -0
- package/dist/translations/locales/hu.d.ts +2 -0
- package/dist/translations/locales/hu.js +76 -0
- package/dist/translations/locales/hy.d.ts +2 -0
- package/dist/translations/locales/hy.js +76 -0
- package/dist/translations/locales/id.d.ts +2 -0
- package/dist/translations/locales/id.js +76 -0
- package/dist/translations/locales/is.d.ts +2 -0
- package/dist/translations/locales/is.js +76 -0
- package/dist/translations/locales/it.d.ts +2 -0
- package/dist/translations/locales/it.js +76 -0
- package/dist/translations/locales/ja.d.ts +2 -0
- package/dist/translations/locales/ja.js +76 -0
- package/dist/translations/locales/ko.d.ts +2 -0
- package/dist/translations/locales/ko.js +76 -0
- package/dist/translations/locales/lt.d.ts +2 -0
- package/dist/translations/locales/lt.js +76 -0
- package/dist/translations/locales/lv.d.ts +2 -0
- package/dist/translations/locales/lv.js +76 -0
- package/dist/translations/locales/my.d.ts +2 -0
- package/dist/translations/locales/my.js +76 -0
- package/dist/translations/locales/nb.d.ts +2 -0
- package/dist/translations/locales/nb.js +76 -0
- package/dist/translations/locales/nl.d.ts +2 -0
- package/dist/translations/locales/nl.js +76 -0
- package/dist/translations/locales/pl.d.ts +2 -0
- package/dist/translations/locales/pl.js +76 -0
- package/dist/translations/locales/pt.d.ts +2 -0
- package/dist/translations/locales/pt.js +76 -0
- package/dist/translations/locales/ro.d.ts +2 -0
- package/dist/translations/locales/ro.js +76 -0
- package/dist/translations/locales/rs.d.ts +2 -0
- package/dist/translations/locales/rs.js +76 -0
- package/dist/translations/locales/rsLatin.d.ts +2 -0
- package/dist/translations/locales/rsLatin.js +76 -0
- package/dist/translations/locales/ru.d.ts +2 -0
- package/dist/translations/locales/ru.js +76 -0
- package/dist/translations/locales/sk.d.ts +2 -0
- package/dist/translations/locales/sk.js +76 -0
- package/dist/translations/locales/sl.d.ts +2 -0
- package/dist/translations/locales/sl.js +76 -0
- package/dist/translations/locales/sv.d.ts +2 -0
- package/dist/translations/locales/sv.js +76 -0
- package/dist/translations/locales/ta.d.ts +2 -0
- package/dist/translations/locales/ta.js +76 -0
- package/dist/translations/locales/th.d.ts +2 -0
- package/dist/translations/locales/th.js +76 -0
- package/dist/translations/locales/tr.d.ts +2 -0
- package/dist/translations/locales/tr.js +76 -0
- package/dist/translations/locales/uk.d.ts +2 -0
- package/dist/translations/locales/uk.js +76 -0
- package/dist/translations/locales/vi.d.ts +2 -0
- package/dist/translations/locales/vi.js +76 -0
- package/dist/translations/locales/zh.d.ts +2 -0
- package/dist/translations/locales/zh.js +76 -0
- package/dist/translations/locales/zhTw.d.ts +2 -0
- package/dist/translations/locales/zhTw.js +76 -0
- package/dist/translations/types.d.ts +82 -0
- package/dist/translations/types.js +1 -0
- package/dist/types.d.ts +183 -0
- package/dist/types.js +1 -0
- package/dist/ui/createUiPlugin.d.ts +14 -0
- package/dist/ui/createUiPlugin.js +28 -0
- package/dist/ui/html.d.ts +8 -0
- package/dist/ui/html.js +8 -0
- package/dist/ui/scalar.d.ts +1 -0
- package/dist/ui/scalar.js +12 -0
- package/dist/ui/swagger.d.ts +1 -0
- package/dist/ui/swagger.js +12 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +13 -0
- package/package.json +84 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import node_path from "node:path";
|
|
3
|
+
import { getPayload } from "payload";
|
|
4
|
+
import { PLUGIN_NAME } from "../constants.js";
|
|
5
|
+
import { buildOpenApiDocument } from "../spec/build.js";
|
|
6
|
+
const parseArgs = (argv)=>{
|
|
7
|
+
const args = {};
|
|
8
|
+
for(let i = 0; i < argv.length; i++){
|
|
9
|
+
const arg = argv[i];
|
|
10
|
+
if (void 0 === arg) continue;
|
|
11
|
+
const read = (key, flag)=>{
|
|
12
|
+
if (arg === flag) {
|
|
13
|
+
args[key] = argv[++i];
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
if (arg.startsWith(`${flag}=`)) {
|
|
17
|
+
args[key] = arg.slice(flag.length + 1);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
};
|
|
22
|
+
if (!read('lang', '--lang')) {
|
|
23
|
+
if (!read('out', '--out')) read('server', '--server');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return args;
|
|
27
|
+
};
|
|
28
|
+
const script = async (config)=>{
|
|
29
|
+
const resolved = config.custom?.[PLUGIN_NAME];
|
|
30
|
+
if (!resolved) throw new Error(`${PLUGIN_NAME}: plugin is not registered in payload.config — no resolved options found on config.custom['${PLUGIN_NAME}'].`);
|
|
31
|
+
const payload = await getPayload({
|
|
32
|
+
config
|
|
33
|
+
});
|
|
34
|
+
const { lang, out, server } = parseArgs(process.argv);
|
|
35
|
+
const servers = server ? [
|
|
36
|
+
{
|
|
37
|
+
url: server
|
|
38
|
+
}
|
|
39
|
+
] : void 0;
|
|
40
|
+
const fallback = config.i18n?.fallbackLanguage ?? 'en';
|
|
41
|
+
const supported = config.i18n?.supportedLanguages ? Object.keys(config.i18n.supportedLanguages) : [
|
|
42
|
+
fallback
|
|
43
|
+
];
|
|
44
|
+
const langs = 'all' === lang ? supported : [
|
|
45
|
+
lang ?? fallback
|
|
46
|
+
];
|
|
47
|
+
for (const language of langs){
|
|
48
|
+
const doc = await buildOpenApiDocument({
|
|
49
|
+
payload,
|
|
50
|
+
options: resolved,
|
|
51
|
+
language,
|
|
52
|
+
servers
|
|
53
|
+
});
|
|
54
|
+
const file = langs.length > 1 ? `openapi.${language}.json` : out ?? 'openapi.json';
|
|
55
|
+
await writeFile(node_path.resolve(file), `${JSON.stringify(doc, null, 2)}\n`);
|
|
56
|
+
payload.logger.info(`Wrote ${file}`);
|
|
57
|
+
}
|
|
58
|
+
await payload.destroy();
|
|
59
|
+
process.exit(0);
|
|
60
|
+
};
|
|
61
|
+
export { parseArgs, script };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const PLUGIN_NAME = "@seshuk/payload-plugin-openapi";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const interactiveAuthHandler = (authCollectionSlug)=>async (req)=>{
|
|
2
|
+
const form = await req.formData?.();
|
|
3
|
+
const username = form?.get('username')?.toString() ?? '';
|
|
4
|
+
const password = form?.get('password')?.toString() ?? '';
|
|
5
|
+
try {
|
|
6
|
+
const result = await req.payload.login({
|
|
7
|
+
collection: authCollectionSlug,
|
|
8
|
+
data: {
|
|
9
|
+
email: username,
|
|
10
|
+
username,
|
|
11
|
+
password
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
return Response.json({
|
|
15
|
+
access_token: result.token,
|
|
16
|
+
token_type: 'JWT',
|
|
17
|
+
expires_in: result.exp
|
|
18
|
+
});
|
|
19
|
+
} catch {
|
|
20
|
+
return Response.json({
|
|
21
|
+
error: 'invalid_grant'
|
|
22
|
+
}, {
|
|
23
|
+
status: 400
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
export { interactiveAuthHandler };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { buildOpenApiDocument } from "../spec/build.js";
|
|
2
|
+
const deriveServerUrl = (req)=>{
|
|
3
|
+
const host = req.headers.get('host') ?? 'localhost:3000';
|
|
4
|
+
const protocol = host.startsWith('localhost') || host.startsWith('127.') ? 'http' : 'https';
|
|
5
|
+
return `${protocol}://${host}`;
|
|
6
|
+
};
|
|
7
|
+
const requestedLang = (req)=>{
|
|
8
|
+
const raw = new URL(req.url ?? '', 'http://localhost').searchParams.get('lang');
|
|
9
|
+
if (!raw) return;
|
|
10
|
+
const supported = req.payload.config.i18n?.supportedLanguages;
|
|
11
|
+
return !supported || raw in supported ? raw : void 0;
|
|
12
|
+
};
|
|
13
|
+
const specHandler = (options)=>{
|
|
14
|
+
const cache = new Map();
|
|
15
|
+
return async (req)=>{
|
|
16
|
+
const requested = requestedLang(req);
|
|
17
|
+
const lang = requested ?? req.i18n?.language ?? req.payload.config.i18n?.fallbackLanguage ?? 'en';
|
|
18
|
+
let doc = cache.get(lang);
|
|
19
|
+
if (!options.cache || !doc) {
|
|
20
|
+
doc = await buildOpenApiDocument({
|
|
21
|
+
payload: req.payload,
|
|
22
|
+
options,
|
|
23
|
+
language: lang,
|
|
24
|
+
i18n: requested ? void 0 : req.i18n
|
|
25
|
+
});
|
|
26
|
+
cache.set(lang, doc);
|
|
27
|
+
}
|
|
28
|
+
const document = {
|
|
29
|
+
...doc,
|
|
30
|
+
servers: [
|
|
31
|
+
{
|
|
32
|
+
url: deriveServerUrl(req)
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
};
|
|
36
|
+
return Response.json(document);
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
export { specHandler };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Plugin } from 'payload';
|
|
2
|
+
import type { OpenApiPluginOptions } from './types.js';
|
|
3
|
+
export declare const openapi: (options: OpenApiPluginOptions) => Plugin;
|
|
4
|
+
export { buildOpenApiDocument } from './spec/build.js';
|
|
5
|
+
export type { BuildOpenApiInput } from './spec/build.js';
|
|
6
|
+
export { toOpenApi30, toOpenApi31 } from './spec/downconvert.js';
|
|
7
|
+
export { scalar } from './ui/scalar.js';
|
|
8
|
+
export { swaggerUi } from './ui/swagger.js';
|
|
9
|
+
export type { BuildContext, FilterOptions, OpenApiExtension, OpenApiMetadata, OpenApiPluginOptions, OpenApiVersion, UiPluginOptions, } from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { PLUGIN_NAME } from "./constants.js";
|
|
3
|
+
import { resolveOptions } from "./options.js";
|
|
4
|
+
import { interactiveAuthHandler } from "./endpoints/interactiveAuth.js";
|
|
5
|
+
import { specHandler } from "./endpoints/spec.js";
|
|
6
|
+
import { translations } from "./translations/index.js";
|
|
7
|
+
const BIN_KEY = 'openapi:generate';
|
|
8
|
+
const resolveBinScriptPath = ()=>{
|
|
9
|
+
const selfUrl = new URL(import.meta.url);
|
|
10
|
+
const ext = selfUrl.pathname.endsWith('.ts') ? '.ts' : '.js';
|
|
11
|
+
return fileURLToPath(new URL(`./bin/generateSpec${ext}`, selfUrl));
|
|
12
|
+
};
|
|
13
|
+
const openapi = (options)=>(config)=>{
|
|
14
|
+
if (false === options.enabled) return config;
|
|
15
|
+
const resolved = resolveOptions(options);
|
|
16
|
+
const authCollection = (config.collections ?? []).find((c)=>Boolean(c.auth))?.slug ?? 'users';
|
|
17
|
+
config.i18n = config.i18n ?? {};
|
|
18
|
+
config.i18n.translations = config.i18n.translations ?? {};
|
|
19
|
+
const supportedKeys = config.i18n.supportedLanguages ? Object.keys(config.i18n.supportedLanguages) : [
|
|
20
|
+
'en'
|
|
21
|
+
];
|
|
22
|
+
for (const lang of supportedKeys){
|
|
23
|
+
const pluginEntry = translations[lang];
|
|
24
|
+
if (!pluginEntry) continue;
|
|
25
|
+
const typedLang = lang;
|
|
26
|
+
config.i18n.translations[typedLang] = {
|
|
27
|
+
...config.i18n.translations[typedLang],
|
|
28
|
+
[PLUGIN_NAME]: pluginEntry[PLUGIN_NAME]
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
...config,
|
|
33
|
+
custom: {
|
|
34
|
+
...config.custom,
|
|
35
|
+
[PLUGIN_NAME]: resolved
|
|
36
|
+
},
|
|
37
|
+
bin: [
|
|
38
|
+
...config.bin ?? [],
|
|
39
|
+
{
|
|
40
|
+
key: BIN_KEY,
|
|
41
|
+
scriptPath: resolveBinScriptPath()
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
endpoints: [
|
|
45
|
+
...config.endpoints ?? [],
|
|
46
|
+
...resolved.serve ? [
|
|
47
|
+
{
|
|
48
|
+
path: resolved.specEndpoint,
|
|
49
|
+
method: 'get',
|
|
50
|
+
handler: specHandler(resolved)
|
|
51
|
+
},
|
|
52
|
+
...resolved.interactiveAuth.enabled ? [
|
|
53
|
+
{
|
|
54
|
+
path: resolved.interactiveAuth.endpoint,
|
|
55
|
+
method: 'post',
|
|
56
|
+
handler: interactiveAuthHandler(authCollection)
|
|
57
|
+
}
|
|
58
|
+
] : []
|
|
59
|
+
] : []
|
|
60
|
+
]
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
export { buildOpenApiDocument } from "./spec/build.js";
|
|
64
|
+
export { toOpenApi30, toOpenApi31 } from "./spec/downconvert.js";
|
|
65
|
+
export { scalar } from "./ui/scalar.js";
|
|
66
|
+
export { swaggerUi } from "./ui/swagger.js";
|
|
67
|
+
export { openapi };
|
package/dist/options.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { PLUGIN_NAME } from "./constants.js";
|
|
2
|
+
const resolveFilters = (filters = {})=>({
|
|
3
|
+
include: filters.include ?? [],
|
|
4
|
+
exclude: filters.exclude ?? [],
|
|
5
|
+
includeHidden: filters.includeHidden ?? false,
|
|
6
|
+
includeSystem: filters.includeSystem ?? false,
|
|
7
|
+
includeCustom: filters.includeCustom ?? true,
|
|
8
|
+
includeAuth: filters.includeAuth ?? true,
|
|
9
|
+
includeAdminAuth: filters.includeAdminAuth ?? false,
|
|
10
|
+
includeVersions: filters.includeVersions ?? true,
|
|
11
|
+
includeJobs: filters.includeJobs ?? true,
|
|
12
|
+
excludeOperations: filters.excludeOperations ?? [],
|
|
13
|
+
excludeWhen: filters.excludeWhen
|
|
14
|
+
});
|
|
15
|
+
const DEFAULT_AUTH_ENDPOINT = '/openapi-auth';
|
|
16
|
+
const resolveInteractiveAuth = (option)=>{
|
|
17
|
+
if (!option) return {
|
|
18
|
+
enabled: false,
|
|
19
|
+
endpoint: DEFAULT_AUTH_ENDPOINT
|
|
20
|
+
};
|
|
21
|
+
if (true === option) return {
|
|
22
|
+
enabled: true,
|
|
23
|
+
endpoint: DEFAULT_AUTH_ENDPOINT
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
enabled: true,
|
|
27
|
+
endpoint: option.endpoint ?? DEFAULT_AUTH_ENDPOINT
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
const resolveOptions = (options)=>{
|
|
31
|
+
if (!options.metadata?.title || !options.metadata?.version) throw new Error(`${PLUGIN_NAME}: metadata.title and metadata.version are required.`);
|
|
32
|
+
return {
|
|
33
|
+
metadata: options.metadata,
|
|
34
|
+
openapiVersion: options.openapiVersion ?? '3.2',
|
|
35
|
+
specEndpoint: options.specEndpoint ?? '/openapi.json',
|
|
36
|
+
serve: options.serve ?? true,
|
|
37
|
+
filters: resolveFilters(options.filters),
|
|
38
|
+
interactiveAuth: resolveInteractiveAuth(options.interactiveAuth),
|
|
39
|
+
nestedTags: options.nestedTags ?? false,
|
|
40
|
+
cache: options.cache ?? true,
|
|
41
|
+
extensions: options.extensions ?? []
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
export { resolveOptions };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BasePayload } from 'payload';
|
|
2
|
+
import type { I18n } from '@payloadcms/translations';
|
|
3
|
+
import type { Document } from '@scalar/openapi-types/3.2';
|
|
4
|
+
import type { ResolvedOptions } from '../types.js';
|
|
5
|
+
export interface BuildOpenApiInput {
|
|
6
|
+
payload: BasePayload;
|
|
7
|
+
options: ResolvedOptions;
|
|
8
|
+
language?: string;
|
|
9
|
+
i18n?: I18n;
|
|
10
|
+
servers?: {
|
|
11
|
+
url: string;
|
|
12
|
+
}[];
|
|
13
|
+
}
|
|
14
|
+
export declare const buildOpenApiDocument: (input: BuildOpenApiInput) => Promise<Document>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getLocalI18n } from "payload";
|
|
2
|
+
import { buildDocument } from "./buildDocument.js";
|
|
3
|
+
import { toOpenApi30, toOpenApi31 } from "./downconvert.js";
|
|
4
|
+
const buildOpenApiDocument = async (input)=>{
|
|
5
|
+
const { payload, options, language, servers } = input;
|
|
6
|
+
const { config } = payload;
|
|
7
|
+
const locales = false === config.localization ? [] : config.localization.locales.map((locale)=>locale.code);
|
|
8
|
+
const i18n = input.i18n ?? await getLocalI18n({
|
|
9
|
+
config,
|
|
10
|
+
language: language ?? config.i18n?.fallbackLanguage
|
|
11
|
+
});
|
|
12
|
+
const supported = Object.keys(config.i18n?.supportedLanguages ?? {});
|
|
13
|
+
const docLanguages = supported.length > 0 ? supported : [
|
|
14
|
+
'en'
|
|
15
|
+
];
|
|
16
|
+
const ctx = {
|
|
17
|
+
defaultIDType: payload.db.defaultIDType,
|
|
18
|
+
locales,
|
|
19
|
+
apiRoute: config.routes?.api ?? '/api',
|
|
20
|
+
docLanguages,
|
|
21
|
+
i18n
|
|
22
|
+
};
|
|
23
|
+
const doc = await buildDocument({
|
|
24
|
+
options,
|
|
25
|
+
servers: servers ?? [],
|
|
26
|
+
collections: Object.values(payload.collections).map((collection)=>collection.config),
|
|
27
|
+
globals: config.globals,
|
|
28
|
+
config,
|
|
29
|
+
ctx,
|
|
30
|
+
logger: payload.logger
|
|
31
|
+
});
|
|
32
|
+
if ('3.0' === options.openapiVersion) return toOpenApi30(doc);
|
|
33
|
+
if ('3.1' === options.openapiVersion) return toOpenApi31(doc);
|
|
34
|
+
return doc;
|
|
35
|
+
};
|
|
36
|
+
export { buildOpenApiDocument };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PayloadLogger, SanitizedCollectionConfig, SanitizedConfig, SanitizedGlobalConfig } from 'payload';
|
|
2
|
+
import type { Document } from '@scalar/openapi-types/3.2';
|
|
3
|
+
import type { BuildContext, ResolvedOptions } from '../types.js';
|
|
4
|
+
export interface BuildInput {
|
|
5
|
+
options: ResolvedOptions;
|
|
6
|
+
servers: {
|
|
7
|
+
url: string;
|
|
8
|
+
}[];
|
|
9
|
+
collections: SanitizedCollectionConfig[];
|
|
10
|
+
globals: SanitizedGlobalConfig[];
|
|
11
|
+
config: SanitizedConfig;
|
|
12
|
+
ctx: BuildContext;
|
|
13
|
+
logger: PayloadLogger;
|
|
14
|
+
}
|
|
15
|
+
export declare const buildDocument: (input: BuildInput) => Promise<Document>;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { getTranslation } from "@payloadcms/translations";
|
|
2
|
+
import { PLUGIN_NAME } from "../constants.js";
|
|
3
|
+
import { deepMerge } from "../utils.js";
|
|
4
|
+
import { makeT } from "../translations/index.js";
|
|
5
|
+
import { ERROR_SCHEMA_NAME, INTERACTIVE_SCHEME_NAME, SECURITY_SCHEME_NAME, buildErrorResponseSchema, buildListEnvelopeSchema, interactiveSecurityScheme, securityScheme } from "./components.js";
|
|
6
|
+
import { buildBlockSchema, buildEntitySchemas } from "./entitySchemas.js";
|
|
7
|
+
import { filterOperations, shouldIncludeCollection, shouldIncludeGlobal } from "./filters.js";
|
|
8
|
+
import { blockSchemaName, createSchemaName, globalSchemaName, joinsSchemaName, listSchemaName, populateSchemaName, querySchemaName, schemaName, selectSchemaName, updateSchemaName } from "./names.js";
|
|
9
|
+
import { buildParamSchemas, buildQueryOperationsSchema, collectionHasFilters } from "./params.js";
|
|
10
|
+
import { buildTagHierarchy } from "./tags.js";
|
|
11
|
+
import { buildAuthPaths } from "./paths/auth.js";
|
|
12
|
+
import { buildCollectionPaths } from "./paths/collections.js";
|
|
13
|
+
import { buildCustomEndpointPaths } from "./paths/custom.js";
|
|
14
|
+
import { buildGlobalPaths } from "./paths/globals.js";
|
|
15
|
+
import { buildJobsPaths } from "./paths/jobs.js";
|
|
16
|
+
import { buildVersionPaths, versionComponentSchemas } from "./paths/versions.js";
|
|
17
|
+
const resolveDescription = (description, ctx)=>{
|
|
18
|
+
if (null == description) return;
|
|
19
|
+
const resolved = getTranslation(description, ctx.i18n);
|
|
20
|
+
return 'string' == typeof resolved && resolved.length > 0 ? resolved : void 0;
|
|
21
|
+
};
|
|
22
|
+
const registerParamSchemas = (schemas, base, fields, ctx)=>{
|
|
23
|
+
const { select, populate, joins } = buildParamSchemas({
|
|
24
|
+
fields,
|
|
25
|
+
ctx
|
|
26
|
+
});
|
|
27
|
+
schemas[selectSchemaName(base)] = select;
|
|
28
|
+
if (populate) schemas[populateSchemaName(base)] = populate;
|
|
29
|
+
if (joins) schemas[joinsSchemaName(base)] = joins;
|
|
30
|
+
};
|
|
31
|
+
const buildDocument = async (input)=>{
|
|
32
|
+
const { options, ctx, config, logger } = input;
|
|
33
|
+
const t = makeT(ctx.i18n);
|
|
34
|
+
const paths = {};
|
|
35
|
+
const schemas = {};
|
|
36
|
+
const { filters } = options;
|
|
37
|
+
const collections = input.collections.filter((c)=>shouldIncludeCollection(c, filters));
|
|
38
|
+
const globals = input.globals.filter((g)=>shouldIncludeGlobal(g, filters));
|
|
39
|
+
const collectionTagInfos = [];
|
|
40
|
+
const globalTagInfos = [];
|
|
41
|
+
for (const block of config.blocks ?? [])try {
|
|
42
|
+
schemas[blockSchemaName(block.slug)] = buildBlockSchema(block, config);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
logger.warn(`${PLUGIN_NAME}: skipped block "${block.slug}": ${error.message}`);
|
|
45
|
+
}
|
|
46
|
+
for (const collection of collections)try {
|
|
47
|
+
const { read, create, update } = await buildEntitySchemas({
|
|
48
|
+
entity: collection,
|
|
49
|
+
config,
|
|
50
|
+
ctx
|
|
51
|
+
});
|
|
52
|
+
const base = schemaName(collection.slug);
|
|
53
|
+
collectionTagInfos.push({
|
|
54
|
+
base,
|
|
55
|
+
hasAuth: filters.includeAuth && Boolean(collection.auth),
|
|
56
|
+
hasVersions: Boolean(filters.includeVersions && collection.versions),
|
|
57
|
+
description: resolveDescription(collection.admin?.description, ctx)
|
|
58
|
+
});
|
|
59
|
+
schemas[base] = read;
|
|
60
|
+
schemas[createSchemaName(base)] = create;
|
|
61
|
+
schemas[updateSchemaName(base)] = update;
|
|
62
|
+
schemas[listSchemaName(base)] = buildListEnvelopeSchema(base);
|
|
63
|
+
if (collectionHasFilters(collection.fields)) schemas[querySchemaName(base)] = buildQueryOperationsSchema({
|
|
64
|
+
base,
|
|
65
|
+
fields: collection.fields,
|
|
66
|
+
ctx
|
|
67
|
+
});
|
|
68
|
+
registerParamSchemas(schemas, base, collection.fields, ctx);
|
|
69
|
+
const collectionPaths = {
|
|
70
|
+
...buildCollectionPaths({
|
|
71
|
+
collection,
|
|
72
|
+
ctx
|
|
73
|
+
})
|
|
74
|
+
};
|
|
75
|
+
if (filters.includeAuth) Object.assign(collectionPaths, buildAuthPaths({
|
|
76
|
+
collection,
|
|
77
|
+
ctx,
|
|
78
|
+
includeAdmin: filters.includeAdminAuth,
|
|
79
|
+
nestedTags: options.nestedTags
|
|
80
|
+
}));
|
|
81
|
+
if (filters.includeVersions && collection.versions) {
|
|
82
|
+
const { version, versionList } = versionComponentSchemas({
|
|
83
|
+
docBase: base,
|
|
84
|
+
base,
|
|
85
|
+
idType: ctx.defaultIDType
|
|
86
|
+
});
|
|
87
|
+
schemas[`${base}Version`] = version;
|
|
88
|
+
schemas[`${base}VersionList`] = versionList;
|
|
89
|
+
Object.assign(collectionPaths, buildVersionPaths({
|
|
90
|
+
entity: collection,
|
|
91
|
+
pathBase: `${ctx.apiRoute}/${collection.slug}`,
|
|
92
|
+
ctx,
|
|
93
|
+
schemaBase: base,
|
|
94
|
+
nestedTags: options.nestedTags
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
Object.assign(paths, filterOperations({
|
|
98
|
+
paths: collectionPaths,
|
|
99
|
+
slug: collection.slug,
|
|
100
|
+
kind: 'collection',
|
|
101
|
+
filters
|
|
102
|
+
}));
|
|
103
|
+
} catch (error) {
|
|
104
|
+
logger.warn(`${PLUGIN_NAME}: skipped collection "${collection.slug}": ${error.message}`);
|
|
105
|
+
}
|
|
106
|
+
for (const global of globals)try {
|
|
107
|
+
const { read, update } = await buildEntitySchemas({
|
|
108
|
+
entity: global,
|
|
109
|
+
config,
|
|
110
|
+
ctx
|
|
111
|
+
});
|
|
112
|
+
const gbase = globalSchemaName(global.slug);
|
|
113
|
+
globalTagInfos.push({
|
|
114
|
+
base: gbase,
|
|
115
|
+
hasVersions: Boolean(filters.includeVersions && global.versions),
|
|
116
|
+
description: resolveDescription(global.admin?.description, ctx)
|
|
117
|
+
});
|
|
118
|
+
schemas[gbase] = read;
|
|
119
|
+
schemas[updateSchemaName(gbase)] = update;
|
|
120
|
+
registerParamSchemas(schemas, gbase, global.fields, ctx);
|
|
121
|
+
const globalPaths = {
|
|
122
|
+
...buildGlobalPaths({
|
|
123
|
+
global,
|
|
124
|
+
ctx
|
|
125
|
+
})
|
|
126
|
+
};
|
|
127
|
+
if (filters.includeVersions && global.versions) {
|
|
128
|
+
const { version, versionList } = versionComponentSchemas({
|
|
129
|
+
docBase: gbase,
|
|
130
|
+
base: gbase,
|
|
131
|
+
idType: ctx.defaultIDType
|
|
132
|
+
});
|
|
133
|
+
schemas[`${gbase}Version`] = version;
|
|
134
|
+
schemas[`${gbase}VersionList`] = versionList;
|
|
135
|
+
Object.assign(globalPaths, buildVersionPaths({
|
|
136
|
+
entity: global,
|
|
137
|
+
pathBase: `${ctx.apiRoute}/globals/${global.slug}`,
|
|
138
|
+
ctx,
|
|
139
|
+
schemaBase: gbase,
|
|
140
|
+
nestedTags: options.nestedTags
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
Object.assign(paths, filterOperations({
|
|
144
|
+
paths: globalPaths,
|
|
145
|
+
slug: global.slug,
|
|
146
|
+
kind: 'global',
|
|
147
|
+
filters
|
|
148
|
+
}));
|
|
149
|
+
} catch (error) {
|
|
150
|
+
logger.warn(`${PLUGIN_NAME}: skipped global "${global.slug}": ${error.message}`);
|
|
151
|
+
}
|
|
152
|
+
if (filters.includeCustom) try {
|
|
153
|
+
Object.assign(paths, buildCustomEndpointPaths({
|
|
154
|
+
config,
|
|
155
|
+
collections,
|
|
156
|
+
globals,
|
|
157
|
+
ctx
|
|
158
|
+
}));
|
|
159
|
+
} catch (error) {
|
|
160
|
+
logger.warn(`${PLUGIN_NAME}: failed to collect custom endpoints: ${error.message}`);
|
|
161
|
+
}
|
|
162
|
+
const jobsConfig = config.jobs;
|
|
163
|
+
const hasJobs = filters.includeJobs && Boolean(jobsConfig?.tasks?.length || jobsConfig?.workflows?.length);
|
|
164
|
+
if (filters.includeJobs) try {
|
|
165
|
+
Object.assign(paths, buildJobsPaths({
|
|
166
|
+
config,
|
|
167
|
+
ctx
|
|
168
|
+
}));
|
|
169
|
+
} catch (error) {
|
|
170
|
+
logger.warn(`${PLUGIN_NAME}: failed to add jobs endpoint: ${error.message}`);
|
|
171
|
+
}
|
|
172
|
+
const supportedTimezones = config.admin?.timezones?.supportedTimezones;
|
|
173
|
+
if (Array.isArray(supportedTimezones) && supportedTimezones.length > 0) schemas[schemaName('supportedTimezones')] = {
|
|
174
|
+
type: 'string',
|
|
175
|
+
description: t('schemaSupportedTimezones'),
|
|
176
|
+
enum: supportedTimezones.map((tz)=>'string' == typeof tz ? tz : tz.value)
|
|
177
|
+
};
|
|
178
|
+
schemas[ERROR_SCHEMA_NAME] = buildErrorResponseSchema();
|
|
179
|
+
const cookiePrefix = config.cookiePrefix || 'payload';
|
|
180
|
+
const securitySchemes = {
|
|
181
|
+
[SECURITY_SCHEME_NAME]: securityScheme({
|
|
182
|
+
cookiePrefix,
|
|
183
|
+
t
|
|
184
|
+
})
|
|
185
|
+
};
|
|
186
|
+
if (options.interactiveAuth.enabled) securitySchemes[INTERACTIVE_SCHEME_NAME] = interactiveSecurityScheme({
|
|
187
|
+
tokenUrl: `${ctx.apiRoute}${options.interactiveAuth.endpoint}`,
|
|
188
|
+
t
|
|
189
|
+
});
|
|
190
|
+
const localeNote = ctx.locales.length > 0 ? `\n\n## ${t('localizationHeading')}\n\n` + t('localizationNote', {
|
|
191
|
+
locales: ctx.locales.map((l)=>`\`${l}\``).join(', ')
|
|
192
|
+
}) : '';
|
|
193
|
+
const docLanguagesNote = ctx.docLanguages.length > 1 ? `\n\n${t('docLanguagesNote', {
|
|
194
|
+
languages: ctx.docLanguages.map((l)=>`\`${l}\``).join(', ')
|
|
195
|
+
})}` : '';
|
|
196
|
+
const info = {
|
|
197
|
+
...options.metadata,
|
|
198
|
+
description: `${options.metadata.description ?? ''}${localeNote}${docLanguagesNote}`.trim() || void 0
|
|
199
|
+
};
|
|
200
|
+
let doc = {
|
|
201
|
+
openapi: '3.2.0',
|
|
202
|
+
info,
|
|
203
|
+
servers: input.servers,
|
|
204
|
+
paths,
|
|
205
|
+
components: {
|
|
206
|
+
schemas,
|
|
207
|
+
securitySchemes
|
|
208
|
+
},
|
|
209
|
+
tags: buildTagHierarchy({
|
|
210
|
+
collections: collectionTagInfos,
|
|
211
|
+
globals: globalTagInfos,
|
|
212
|
+
hasJobs,
|
|
213
|
+
t,
|
|
214
|
+
nested: options.nestedTags
|
|
215
|
+
}),
|
|
216
|
+
'x-doc-languages': ctx.docLanguages
|
|
217
|
+
};
|
|
218
|
+
for (const ext of options.extensions)try {
|
|
219
|
+
if (ext.paths) doc.paths = deepMerge(doc.paths ?? {}, ext.paths);
|
|
220
|
+
if (ext.components) doc.components = deepMerge(doc.components ?? {}, ext.components);
|
|
221
|
+
if (ext.tags) {
|
|
222
|
+
const seen = new Set((doc.tags ?? []).map((tag)=>tag.name));
|
|
223
|
+
doc.tags = [
|
|
224
|
+
...doc.tags ?? [],
|
|
225
|
+
...ext.tags.filter((tag)=>!seen.has(tag.name))
|
|
226
|
+
];
|
|
227
|
+
}
|
|
228
|
+
if (ext.transform) doc = ext.transform(doc, ctx);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
logger.warn(`${PLUGIN_NAME}: extension failed and was skipped: ${error.message}`);
|
|
231
|
+
}
|
|
232
|
+
return doc;
|
|
233
|
+
};
|
|
234
|
+
export { buildDocument };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Access } from 'payload';
|
|
2
|
+
import type { ReferenceObject, RequestBodyObject, ResponseObject, ResponsesObject, SchemaObject, SecuritySchemeObject } from '@scalar/openapi-types/3.2';
|
|
3
|
+
import type { Translate } from '../translations/types.js';
|
|
4
|
+
type SchemaOrRef = SchemaObject | ReferenceObject;
|
|
5
|
+
export declare const nullableType: (type: "integer" | "string" | "number" | "boolean") => SchemaObject;
|
|
6
|
+
export declare const jsonResponse: (description: string, schema: SchemaOrRef) => ResponseObject;
|
|
7
|
+
export declare const jsonOk: (description: string, schema: SchemaOrRef) => ResponsesObject;
|
|
8
|
+
export declare const jsonBody: (schema: SchemaOrRef) => RequestBodyObject;
|
|
9
|
+
export declare const messageResponse: (description: string) => ResponsesObject;
|
|
10
|
+
export type ErrorCode = '400' | '401' | '403' | '404' | '500';
|
|
11
|
+
export declare const ERROR_SCHEMA_NAME = "ErrorResponse";
|
|
12
|
+
export declare const ERRORS: {
|
|
13
|
+
readonly list: ErrorCode[];
|
|
14
|
+
readonly read: ErrorCode[];
|
|
15
|
+
readonly create: ErrorCode[];
|
|
16
|
+
readonly update: ErrorCode[];
|
|
17
|
+
readonly delete: ErrorCode[];
|
|
18
|
+
readonly globalRead: ErrorCode[];
|
|
19
|
+
readonly globalUpdate: ErrorCode[];
|
|
20
|
+
readonly login: ErrorCode[];
|
|
21
|
+
readonly session: ErrorCode[];
|
|
22
|
+
readonly authAction: ErrorCode[];
|
|
23
|
+
readonly versionList: ErrorCode[];
|
|
24
|
+
readonly versionRead: ErrorCode[];
|
|
25
|
+
readonly versionRestore: ErrorCode[];
|
|
26
|
+
};
|
|
27
|
+
export declare const buildErrorResponseSchema: () => SchemaObject;
|
|
28
|
+
export declare const errorResponses: (codes: ErrorCode[], t: Translate) => ResponsesObject;
|
|
29
|
+
export declare const buildListEnvelopeSchema: (base: string) => SchemaObject;
|
|
30
|
+
export declare const uploadRequestBody: ({ bodyRef, fileRequired, t, }: {
|
|
31
|
+
bodyRef: ReferenceObject;
|
|
32
|
+
fileRequired: boolean;
|
|
33
|
+
t: Translate;
|
|
34
|
+
}) => RequestBodyObject;
|
|
35
|
+
export declare const SECURITY_SCHEME_NAME = "PayloadToken";
|
|
36
|
+
export declare const INTERACTIVE_SCHEME_NAME = "PayloadLogin";
|
|
37
|
+
export declare const interactiveSecurityScheme: ({ tokenUrl, t, }: {
|
|
38
|
+
tokenUrl: string;
|
|
39
|
+
t: Translate;
|
|
40
|
+
}) => SecuritySchemeObject;
|
|
41
|
+
export declare const securityScheme: ({ cookiePrefix, t }: {
|
|
42
|
+
cookiePrefix: string;
|
|
43
|
+
t: Translate;
|
|
44
|
+
}) => SecuritySchemeObject;
|
|
45
|
+
export declare const isOpenToPublic: (access: Access | undefined) => boolean;
|
|
46
|
+
export {};
|