@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.
Files changed (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +595 -0
  3. package/dist/bin/generateSpec.d.ts +25 -0
  4. package/dist/bin/generateSpec.js +61 -0
  5. package/dist/constants.d.ts +1 -0
  6. package/dist/constants.js +2 -0
  7. package/dist/endpoints/interactiveAuth.d.ts +4 -0
  8. package/dist/endpoints/interactiveAuth.js +27 -0
  9. package/dist/endpoints/spec.d.ts +5 -0
  10. package/dist/endpoints/spec.js +39 -0
  11. package/dist/index.d.ts +9 -0
  12. package/dist/index.js +67 -0
  13. package/dist/options.d.ts +2 -0
  14. package/dist/options.js +44 -0
  15. package/dist/spec/build.d.ts +14 -0
  16. package/dist/spec/build.js +36 -0
  17. package/dist/spec/buildDocument.d.ts +15 -0
  18. package/dist/spec/buildDocument.js +234 -0
  19. package/dist/spec/components.d.ts +46 -0
  20. package/dist/spec/components.js +280 -0
  21. package/dist/spec/downconvert.d.ts +3 -0
  22. package/dist/spec/downconvert.js +55 -0
  23. package/dist/spec/entitySchemas.d.ts +15 -0
  24. package/dist/spec/entitySchemas.js +246 -0
  25. package/dist/spec/fields.d.ts +5 -0
  26. package/dist/spec/fields.js +44 -0
  27. package/dist/spec/filters.d.ts +11 -0
  28. package/dist/spec/filters.js +74 -0
  29. package/dist/spec/names.d.ts +14 -0
  30. package/dist/spec/names.js +37 -0
  31. package/dist/spec/params.d.ts +47 -0
  32. package/dist/spec/params.js +344 -0
  33. package/dist/spec/paths/auth.d.ts +9 -0
  34. package/dist/spec/paths/auth.js +285 -0
  35. package/dist/spec/paths/collections.d.ts +7 -0
  36. package/dist/spec/paths/collections.js +261 -0
  37. package/dist/spec/paths/custom.d.ts +9 -0
  38. package/dist/spec/paths/custom.js +41 -0
  39. package/dist/spec/paths/globals.d.ts +7 -0
  40. package/dist/spec/paths/globals.js +68 -0
  41. package/dist/spec/paths/jobs.d.ts +7 -0
  42. package/dist/spec/paths/jobs.js +130 -0
  43. package/dist/spec/paths/versions.d.ts +18 -0
  44. package/dist/spec/paths/versions.js +243 -0
  45. package/dist/spec/tags.d.ts +27 -0
  46. package/dist/spec/tags.js +79 -0
  47. package/dist/translations/index.d.ts +6 -0
  48. package/dist/translations/index.js +96 -0
  49. package/dist/translations/locales/ar.d.ts +2 -0
  50. package/dist/translations/locales/ar.js +76 -0
  51. package/dist/translations/locales/az.d.ts +2 -0
  52. package/dist/translations/locales/az.js +76 -0
  53. package/dist/translations/locales/bg.d.ts +2 -0
  54. package/dist/translations/locales/bg.js +76 -0
  55. package/dist/translations/locales/bnBd.d.ts +2 -0
  56. package/dist/translations/locales/bnBd.js +76 -0
  57. package/dist/translations/locales/bnIn.d.ts +2 -0
  58. package/dist/translations/locales/bnIn.js +76 -0
  59. package/dist/translations/locales/ca.d.ts +2 -0
  60. package/dist/translations/locales/ca.js +76 -0
  61. package/dist/translations/locales/cs.d.ts +2 -0
  62. package/dist/translations/locales/cs.js +76 -0
  63. package/dist/translations/locales/da.d.ts +2 -0
  64. package/dist/translations/locales/da.js +76 -0
  65. package/dist/translations/locales/de.d.ts +2 -0
  66. package/dist/translations/locales/de.js +76 -0
  67. package/dist/translations/locales/en.d.ts +2 -0
  68. package/dist/translations/locales/en.js +76 -0
  69. package/dist/translations/locales/es.d.ts +2 -0
  70. package/dist/translations/locales/es.js +76 -0
  71. package/dist/translations/locales/et.d.ts +2 -0
  72. package/dist/translations/locales/et.js +76 -0
  73. package/dist/translations/locales/fa.d.ts +2 -0
  74. package/dist/translations/locales/fa.js +76 -0
  75. package/dist/translations/locales/fr.d.ts +2 -0
  76. package/dist/translations/locales/fr.js +76 -0
  77. package/dist/translations/locales/he.d.ts +2 -0
  78. package/dist/translations/locales/he.js +76 -0
  79. package/dist/translations/locales/hr.d.ts +2 -0
  80. package/dist/translations/locales/hr.js +76 -0
  81. package/dist/translations/locales/hu.d.ts +2 -0
  82. package/dist/translations/locales/hu.js +76 -0
  83. package/dist/translations/locales/hy.d.ts +2 -0
  84. package/dist/translations/locales/hy.js +76 -0
  85. package/dist/translations/locales/id.d.ts +2 -0
  86. package/dist/translations/locales/id.js +76 -0
  87. package/dist/translations/locales/is.d.ts +2 -0
  88. package/dist/translations/locales/is.js +76 -0
  89. package/dist/translations/locales/it.d.ts +2 -0
  90. package/dist/translations/locales/it.js +76 -0
  91. package/dist/translations/locales/ja.d.ts +2 -0
  92. package/dist/translations/locales/ja.js +76 -0
  93. package/dist/translations/locales/ko.d.ts +2 -0
  94. package/dist/translations/locales/ko.js +76 -0
  95. package/dist/translations/locales/lt.d.ts +2 -0
  96. package/dist/translations/locales/lt.js +76 -0
  97. package/dist/translations/locales/lv.d.ts +2 -0
  98. package/dist/translations/locales/lv.js +76 -0
  99. package/dist/translations/locales/my.d.ts +2 -0
  100. package/dist/translations/locales/my.js +76 -0
  101. package/dist/translations/locales/nb.d.ts +2 -0
  102. package/dist/translations/locales/nb.js +76 -0
  103. package/dist/translations/locales/nl.d.ts +2 -0
  104. package/dist/translations/locales/nl.js +76 -0
  105. package/dist/translations/locales/pl.d.ts +2 -0
  106. package/dist/translations/locales/pl.js +76 -0
  107. package/dist/translations/locales/pt.d.ts +2 -0
  108. package/dist/translations/locales/pt.js +76 -0
  109. package/dist/translations/locales/ro.d.ts +2 -0
  110. package/dist/translations/locales/ro.js +76 -0
  111. package/dist/translations/locales/rs.d.ts +2 -0
  112. package/dist/translations/locales/rs.js +76 -0
  113. package/dist/translations/locales/rsLatin.d.ts +2 -0
  114. package/dist/translations/locales/rsLatin.js +76 -0
  115. package/dist/translations/locales/ru.d.ts +2 -0
  116. package/dist/translations/locales/ru.js +76 -0
  117. package/dist/translations/locales/sk.d.ts +2 -0
  118. package/dist/translations/locales/sk.js +76 -0
  119. package/dist/translations/locales/sl.d.ts +2 -0
  120. package/dist/translations/locales/sl.js +76 -0
  121. package/dist/translations/locales/sv.d.ts +2 -0
  122. package/dist/translations/locales/sv.js +76 -0
  123. package/dist/translations/locales/ta.d.ts +2 -0
  124. package/dist/translations/locales/ta.js +76 -0
  125. package/dist/translations/locales/th.d.ts +2 -0
  126. package/dist/translations/locales/th.js +76 -0
  127. package/dist/translations/locales/tr.d.ts +2 -0
  128. package/dist/translations/locales/tr.js +76 -0
  129. package/dist/translations/locales/uk.d.ts +2 -0
  130. package/dist/translations/locales/uk.js +76 -0
  131. package/dist/translations/locales/vi.d.ts +2 -0
  132. package/dist/translations/locales/vi.js +76 -0
  133. package/dist/translations/locales/zh.d.ts +2 -0
  134. package/dist/translations/locales/zh.js +76 -0
  135. package/dist/translations/locales/zhTw.d.ts +2 -0
  136. package/dist/translations/locales/zhTw.js +76 -0
  137. package/dist/translations/types.d.ts +82 -0
  138. package/dist/translations/types.js +1 -0
  139. package/dist/types.d.ts +183 -0
  140. package/dist/types.js +1 -0
  141. package/dist/ui/createUiPlugin.d.ts +14 -0
  142. package/dist/ui/createUiPlugin.js +28 -0
  143. package/dist/ui/html.d.ts +8 -0
  144. package/dist/ui/html.js +8 -0
  145. package/dist/ui/scalar.d.ts +1 -0
  146. package/dist/ui/scalar.js +12 -0
  147. package/dist/ui/swagger.d.ts +1 -0
  148. package/dist/ui/swagger.js +12 -0
  149. package/dist/utils.d.ts +3 -0
  150. package/dist/utils.js +13 -0
  151. 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,2 @@
1
+ const PLUGIN_NAME = '@seshuk/payload-plugin-openapi';
2
+ export { PLUGIN_NAME };
@@ -0,0 +1,4 @@
1
+ import type { Endpoint } from 'payload';
2
+ type InteractiveAuthHandler = (authCollectionSlug: string) => Endpoint['handler'];
3
+ export declare const interactiveAuthHandler: InteractiveAuthHandler;
4
+ export {};
@@ -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,5 @@
1
+ import type { PayloadHandler } from 'payload';
2
+ import type { ResolvedOptions } from '../types.js';
3
+ type SpecHandler = (options: ResolvedOptions) => PayloadHandler;
4
+ export declare const specHandler: SpecHandler;
5
+ export {};
@@ -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 };
@@ -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 };
@@ -0,0 +1,2 @@
1
+ import type { OpenApiPluginOptions, ResolvedOptions } from './types.js';
2
+ export declare const resolveOptions: (options: OpenApiPluginOptions) => ResolvedOptions;
@@ -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 {};