@kizunajs/openapi 2.0.0-beta.2 → 2.0.0-beta.21
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/dist/index.cjs +210 -85
- package/dist/index.d.cts +181 -170
- package/dist/index.d.mts +180 -169
- package/dist/index.mjs +205 -86
- package/package.json +6 -6
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let zod = require("zod");
|
|
3
3
|
let kizunajs_plugin = require("kizunajs/plugin");
|
|
4
|
+
let kizunajs_generator = require("kizunajs/generator");
|
|
4
5
|
let kizunajs_adapter = require("kizunajs/adapter");
|
|
5
6
|
let yaml = require("yaml");
|
|
6
|
-
let kizunajs_generator = require("kizunajs/generator");
|
|
7
7
|
let kizunajs = require("kizunajs");
|
|
8
8
|
//#region src/generator.ts
|
|
9
9
|
const streamJsonSchema = (response, toJsonSchema) => {
|
|
@@ -80,12 +80,25 @@ const buildDiscriminatorBlock = (propertyName, options) => {
|
|
|
80
80
|
} : { propertyName };
|
|
81
81
|
};
|
|
82
82
|
/**
|
|
83
|
-
*
|
|
84
|
-
* `
|
|
85
|
-
* `examples` array; the message stays in outputs with a place for it, like
|
|
86
|
-
* Swift's `@available`.
|
|
83
|
+
* What the wire carries for a type JSON Schema has no keyword for, which
|
|
84
|
+
* `unrepresentable: 'any'` would otherwise leave as an empty schema.
|
|
87
85
|
*/
|
|
88
|
-
const
|
|
86
|
+
const JSON_ENCODING = {
|
|
87
|
+
date: {
|
|
88
|
+
type: "string",
|
|
89
|
+
format: "date-time"
|
|
90
|
+
},
|
|
91
|
+
bigint: { type: "string" }
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* kizuna's metadata widens JSON Schema: `deprecated` may carry a message,
|
|
95
|
+
* `example` holds one value or several, and `brand` has no keyword. Emit
|
|
96
|
+
* `deprecated: true`, an `examples` array and `x-kizuna-brand`; the message
|
|
97
|
+
* stays in outputs with a place for it, like Swift's `@available`.
|
|
98
|
+
*/
|
|
99
|
+
const normalizeMeta = ({ zodSchema, jsonSchema }) => {
|
|
100
|
+
const encoding = JSON_ENCODING[(0, kizunajs_generator.readDef)(zodSchema).type ?? ""];
|
|
101
|
+
if (encoding && Object.keys(jsonSchema).length === 0) Object.assign(jsonSchema, encoding);
|
|
89
102
|
if (typeof jsonSchema.deprecated === "string") jsonSchema.deprecated = true;
|
|
90
103
|
if ("example" in jsonSchema) {
|
|
91
104
|
const given = Array.isArray(jsonSchema.example) ? jsonSchema.example : [jsonSchema.example];
|
|
@@ -93,6 +106,10 @@ const normalizeMeta = ({ jsonSchema }) => {
|
|
|
93
106
|
jsonSchema.examples = [...given, ...declared];
|
|
94
107
|
delete jsonSchema.example;
|
|
95
108
|
}
|
|
109
|
+
if ("brand" in jsonSchema) {
|
|
110
|
+
jsonSchema["x-kizuna-brand"] = jsonSchema.brand;
|
|
111
|
+
delete jsonSchema.brand;
|
|
112
|
+
}
|
|
96
113
|
};
|
|
97
114
|
const toJsonSchema = (schema, io = "output") => {
|
|
98
115
|
const id = (0, kizunajs_generator.readMetaId)(schema);
|
|
@@ -149,10 +166,11 @@ const deriveHeadOperation = (getOperation) => {
|
|
|
149
166
|
for (const response of Object.values(operation.responses)) delete response.content;
|
|
150
167
|
return operation;
|
|
151
168
|
};
|
|
152
|
-
const
|
|
169
|
+
const openApiWalk = (options, api) => {
|
|
153
170
|
const paths = {};
|
|
154
171
|
return {
|
|
155
|
-
processRoute({ routeKey, route,
|
|
172
|
+
processRoute({ routeKey, route, routeGroups, deprecated, hidden }) {
|
|
173
|
+
if (hidden && options.includeHidden !== true) return;
|
|
156
174
|
const openApiPath = convertPath(route.path);
|
|
157
175
|
const method = route.method.toLowerCase();
|
|
158
176
|
let operation = {
|
|
@@ -162,9 +180,8 @@ const openApiGenerator = (0, kizunajs_generator.createGenerator)((options, api)
|
|
|
162
180
|
};
|
|
163
181
|
if (options.setOperationId) operation.operationId = options.setOperationId === "concatenated-path" ? routeKey : routeKey.slice(routeKey.lastIndexOf(".") + 1);
|
|
164
182
|
if (deprecated) operation.deprecated = true;
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
if (mergedTags.length > 0) operation.tags = mergedTags;
|
|
183
|
+
const tags = [...new Set(routeGroups.flatMap((group) => options.groupTags?.get(group) ?? [group]))];
|
|
184
|
+
if (tags.length > 0) operation.tags = tags;
|
|
168
185
|
if (route.security && route.security.length > 0) {
|
|
169
186
|
const emittedSecurity = toOpenApiSecurity(route.security, api);
|
|
170
187
|
if (emittedSecurity.length > 0) operation.security = emittedSecurity;
|
|
@@ -207,6 +224,15 @@ const openApiGenerator = (0, kizunajs_generator.createGenerator)((options, api)
|
|
|
207
224
|
schema: valueSchema
|
|
208
225
|
});
|
|
209
226
|
}
|
|
227
|
+
const streamsEvents = Object.values(route.responses).some((response) => (0, kizunajs_generator.isStreamResponse)(response) && (0, kizunajs_generator.streamContentType)(response) === "text/event-stream");
|
|
228
|
+
const declaresLastEventId = parameters.some((parameter) => parameter.in === "header" && parameter.name.toLowerCase() === "last-event-id");
|
|
229
|
+
if (streamsEvents && !declaresLastEventId) parameters.push({
|
|
230
|
+
name: "Last-Event-ID",
|
|
231
|
+
in: "header",
|
|
232
|
+
required: false,
|
|
233
|
+
description: "The id of the last event received, to resume a dropped stream after it",
|
|
234
|
+
schema: { type: "string" }
|
|
235
|
+
});
|
|
210
236
|
if (parameters.length > 0) operation.parameters = parameters;
|
|
211
237
|
if (route.body && !(0, kizunajs_generator.isVoidSchema)(route.body)) {
|
|
212
238
|
const contentType = route.contentType ?? "application/json";
|
|
@@ -366,7 +392,7 @@ const openApiGenerator = (0, kizunajs_generator.createGenerator)((options, api)
|
|
|
366
392
|
return renderer;
|
|
367
393
|
}
|
|
368
394
|
};
|
|
369
|
-
}
|
|
395
|
+
};
|
|
370
396
|
/**
|
|
371
397
|
* The permissions a route requires, as `x-kizuna-requires`: what is acted on,
|
|
372
398
|
* mapped to the verbs the caller has to hold.
|
|
@@ -425,19 +451,29 @@ const buildSecuritySchemes = (api) => {
|
|
|
425
451
|
}
|
|
426
452
|
return Object.keys(result).length > 0 ? result : void 0;
|
|
427
453
|
};
|
|
428
|
-
const buildTagLookup = (api) => new Map(Object.entries(api.tags?.tags ?? {}));
|
|
429
454
|
/**
|
|
430
|
-
*
|
|
431
|
-
*
|
|
455
|
+
* The tag for each group path: its top-level group's title, since OpenAPI 3.1.0
|
|
456
|
+
* has no nested tags.
|
|
457
|
+
*/
|
|
458
|
+
const buildGroupTags = (api) => {
|
|
459
|
+
const groups = api.groups?.groups;
|
|
460
|
+
const tags = /* @__PURE__ */ new Map();
|
|
461
|
+
if (!groups) return tags;
|
|
462
|
+
for (const group of groups.values()) tags.set(group.path, [groups.get(group.lineage[0]).title]);
|
|
463
|
+
return tags;
|
|
464
|
+
};
|
|
465
|
+
/**
|
|
466
|
+
* One tag per top-level group.
|
|
432
467
|
*/
|
|
433
468
|
const tagsFromApi = (api) => {
|
|
434
|
-
const
|
|
435
|
-
if (!
|
|
469
|
+
const groups = api.groups?.groups;
|
|
470
|
+
if (!groups) return [];
|
|
436
471
|
const tags = [];
|
|
437
|
-
for (const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if (
|
|
472
|
+
for (const group of groups.values()) {
|
|
473
|
+
if (group.lineage.length > 1) continue;
|
|
474
|
+
const entry = { name: group.title };
|
|
475
|
+
if (group.description) entry.description = group.description;
|
|
476
|
+
if (group.externalDocs) entry.externalDocs = group.externalDocs;
|
|
441
477
|
tags.push(entry);
|
|
442
478
|
}
|
|
443
479
|
return tags;
|
|
@@ -446,17 +482,17 @@ const tagsFromApi = (api) => {
|
|
|
446
482
|
* So a build step does not restate the options and drift from what is served.
|
|
447
483
|
*/
|
|
448
484
|
const optionsFromInstalledPlugin = (api) => {
|
|
449
|
-
for (const
|
|
485
|
+
for (const plugin of Object.values(api.plugins ?? {})) if (plugin.definedSlug === "openApi") return plugin.options;
|
|
450
486
|
throw new Error("generateOpenApi reads its options from the config. Name `openApiPlugin` under `plugins` with the API's `info`.");
|
|
451
487
|
};
|
|
452
488
|
/**
|
|
453
489
|
* Render from options held directly, for the plugin's own routes.
|
|
454
490
|
*/
|
|
455
491
|
function renderOpenApi(api, options) {
|
|
456
|
-
const renderer =
|
|
492
|
+
const renderer = (0, kizunajs_generator.walkApi)(api, openApiWalk({
|
|
457
493
|
...options,
|
|
458
|
-
|
|
459
|
-
});
|
|
494
|
+
groupTags: buildGroupTags(api)
|
|
495
|
+
}, api));
|
|
460
496
|
const tags = tagsFromApi(api);
|
|
461
497
|
if (tags.length > 0) renderer("json").tags = tags;
|
|
462
498
|
return renderer;
|
|
@@ -548,22 +584,6 @@ const renderDocsHtml = (options) => {
|
|
|
548
584
|
});
|
|
549
585
|
};
|
|
550
586
|
//#endregion
|
|
551
|
-
//#region src/document-target.ts
|
|
552
|
-
/**
|
|
553
|
-
* An OpenAPI document target for `kizuna.config.ts`.
|
|
554
|
-
*
|
|
555
|
-
* @example
|
|
556
|
-
* openApiDocument({ output: './openapi.yaml' });
|
|
557
|
-
*/
|
|
558
|
-
const openApiDocument = ({ output, ...overrides }) => ({
|
|
559
|
-
kind: "openapi",
|
|
560
|
-
output,
|
|
561
|
-
generate: (contract) => {
|
|
562
|
-
const render = generateOpenApi(contract, overrides);
|
|
563
|
-
return output.endsWith(".json") ? `${JSON.stringify(render("json"), null, 4)}\n` : render("yaml");
|
|
564
|
-
}
|
|
565
|
-
});
|
|
566
|
-
//#endregion
|
|
567
587
|
//#region src/server.ts
|
|
568
588
|
const HTML = "text/html; charset=utf-8";
|
|
569
589
|
const JSON_TYPE = "application/json";
|
|
@@ -573,24 +593,129 @@ const sent = (body, contentType) => new Response(body, {
|
|
|
573
593
|
headers: { "content-type": contentType }
|
|
574
594
|
});
|
|
575
595
|
/**
|
|
576
|
-
*
|
|
577
|
-
*
|
|
596
|
+
* The routes `openApiPlugin` serves: the reference UI, and the document itself
|
|
597
|
+
* where the options publish it. The document is rendered on the first request,
|
|
598
|
+
* once the api it describes has assembled.
|
|
578
599
|
*/
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
600
|
+
const openApiRoutes = (props, api) => {
|
|
601
|
+
let renderer;
|
|
602
|
+
const spec = ((format) => {
|
|
603
|
+
renderer ??= renderOpenApi((0, kizunajs_adapter.contractOf)(api), props);
|
|
604
|
+
return renderer(format);
|
|
605
|
+
});
|
|
606
|
+
const routes = {};
|
|
607
|
+
if (props.docsPath !== void 0) routes.page = (0, kizunajs_plugin.route)({
|
|
608
|
+
method: "GET",
|
|
609
|
+
path: props.docsPath,
|
|
610
|
+
summary: "API reference",
|
|
611
|
+
responses: { 200: zod.z.string() }
|
|
612
|
+
}).handler(() => (0, kizunajs_adapter.rawResponse)(sent(renderDocsHtml({
|
|
613
|
+
specUrl: props.jsonPath,
|
|
614
|
+
specContent: props.jsonPath === void 0 ? spec("json") : void 0,
|
|
615
|
+
provider: props.provider,
|
|
616
|
+
pageTitle: props.pageTitle ?? spec("json").info.title,
|
|
617
|
+
cdnUrl: props.cdnUrl,
|
|
618
|
+
configuration: props.configuration
|
|
619
|
+
}), HTML)));
|
|
620
|
+
if (props.jsonPath !== void 0) routes.json = (0, kizunajs_plugin.route)({
|
|
621
|
+
method: "GET",
|
|
622
|
+
path: props.jsonPath,
|
|
623
|
+
summary: "OpenAPI document",
|
|
624
|
+
responses: { 200: zod.z.unknown() }
|
|
625
|
+
}).handler(() => (0, kizunajs_adapter.rawResponse)(sent(JSON.stringify(spec("json")), JSON_TYPE)));
|
|
626
|
+
if (props.yamlPath !== void 0) routes.yaml = (0, kizunajs_plugin.route)({
|
|
627
|
+
method: "GET",
|
|
628
|
+
path: props.yamlPath,
|
|
629
|
+
summary: "OpenAPI document, as YAML",
|
|
630
|
+
responses: { 200: zod.z.string() }
|
|
631
|
+
}).handler(() => (0, kizunajs_adapter.rawResponse)(sent(spec("yaml"), YAML)));
|
|
632
|
+
return routes;
|
|
593
633
|
};
|
|
634
|
+
//#endregion
|
|
635
|
+
//#region src/types.ts
|
|
636
|
+
const GenerateOpenApiOptionsSchema = zod.z.object({
|
|
637
|
+
/**
|
|
638
|
+
* The OpenAPI Specification version to declare in the document's `openapi` field.
|
|
639
|
+
*
|
|
640
|
+
* Defaults to `'3.1.0'`.
|
|
641
|
+
*/
|
|
642
|
+
openApiVersion: zod.z.literal("3.1.0").optional(),
|
|
643
|
+
info: zod.z.object({
|
|
644
|
+
title: zod.z.string(),
|
|
645
|
+
version: zod.z.string(),
|
|
646
|
+
description: zod.z.string().optional()
|
|
647
|
+
}),
|
|
648
|
+
servers: zod.z.array(zod.z.object({
|
|
649
|
+
url: zod.z.string(),
|
|
650
|
+
description: zod.z.string().optional()
|
|
651
|
+
})).optional(),
|
|
652
|
+
externalDocs: zod.z.object({
|
|
653
|
+
url: zod.z.string(),
|
|
654
|
+
description: zod.z.string().optional()
|
|
655
|
+
}).optional(),
|
|
656
|
+
setOperationId: zod.z.union([zod.z.boolean(), zod.z.literal("concatenated-path")]).optional(),
|
|
657
|
+
/**
|
|
658
|
+
* Document `hidden` routes too, for a document your own team reads.
|
|
659
|
+
*
|
|
660
|
+
* @default false
|
|
661
|
+
*/
|
|
662
|
+
includeHidden: zod.z.boolean().optional(),
|
|
663
|
+
operationMapper: zod.z.custom((value) => typeof value === "function", { error: "must be a function" }).optional(),
|
|
664
|
+
/**
|
|
665
|
+
* Document the derived `head` operation on every `GET` path without a declared one.
|
|
666
|
+
*
|
|
667
|
+
* @default false
|
|
668
|
+
*/
|
|
669
|
+
derivedHead: zod.z.boolean().optional()
|
|
670
|
+
});
|
|
671
|
+
//#endregion
|
|
672
|
+
//#region src/plugin.ts
|
|
673
|
+
const OPENAPI_PLUGIN_SLUG = "openApi";
|
|
674
|
+
const OpenApiPluginOptionsSchema = GenerateOpenApiOptionsSchema.extend({
|
|
675
|
+
/**
|
|
676
|
+
* Where `kizuna generate` writes the document. `.json` renders JSON,
|
|
677
|
+
* anything else YAML.
|
|
678
|
+
*
|
|
679
|
+
* @example
|
|
680
|
+
* output: './openapi.yaml',
|
|
681
|
+
*/
|
|
682
|
+
output: zod.z.string().optional(),
|
|
683
|
+
/**
|
|
684
|
+
* Where the reference UI is served.
|
|
685
|
+
*/
|
|
686
|
+
docsPath: kizunajs_plugin.RoutePathSchema.optional(),
|
|
687
|
+
/**
|
|
688
|
+
* Where the document is served. The UI embeds it, so this is only for
|
|
689
|
+
* publishing the file.
|
|
690
|
+
*/
|
|
691
|
+
jsonPath: zod.z.custom((value) => typeof value === "string" && value.startsWith("/") && value.endsWith(".json"), { error: "must start with / and end in .json" }).optional(),
|
|
692
|
+
/**
|
|
693
|
+
* Where the document is served as YAML.
|
|
694
|
+
*/
|
|
695
|
+
yamlPath: zod.z.custom((value) => typeof value === "string" && value.startsWith("/") && value.endsWith(".yaml"), { error: "must start with / and end in .yaml" }).optional(),
|
|
696
|
+
/**
|
|
697
|
+
* Which API reference UI to render.
|
|
698
|
+
*
|
|
699
|
+
* @default 'scalar'
|
|
700
|
+
*/
|
|
701
|
+
provider: zod.z.enum(["scalar", "swagger"]).optional(),
|
|
702
|
+
/**
|
|
703
|
+
* Where to load the UI's assets from, for a self-hosted copy. The script
|
|
704
|
+
* URL for `'scalar'`; the directory holding `swagger-ui.css` and
|
|
705
|
+
* `swagger-ui-bundle.js` for `'swagger'`.
|
|
706
|
+
*/
|
|
707
|
+
cdnUrl: zod.z.string().optional(),
|
|
708
|
+
/**
|
|
709
|
+
* The page's `<title>`.
|
|
710
|
+
*
|
|
711
|
+
* @default the document's `info.title`
|
|
712
|
+
*/
|
|
713
|
+
pageTitle: zod.z.string().optional(),
|
|
714
|
+
/**
|
|
715
|
+
* Merged into `Scalar.createApiReference` or `SwaggerUIBundle`.
|
|
716
|
+
*/
|
|
717
|
+
configuration: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
|
|
718
|
+
});
|
|
594
719
|
/**
|
|
595
720
|
* Serve an API reference UI for the OpenAPI document, and the document itself
|
|
596
721
|
* if you publish it.
|
|
@@ -608,42 +733,42 @@ const openApiServe = (props, api) => {
|
|
|
608
733
|
* title: 'My API',
|
|
609
734
|
* version: '1.0.0',
|
|
610
735
|
* },
|
|
736
|
+
* docsPath: '/docs',
|
|
611
737
|
* }),
|
|
612
738
|
* ],
|
|
613
739
|
* });
|
|
614
740
|
* ```
|
|
615
741
|
*/
|
|
616
|
-
const
|
|
617
|
-
slug,
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
742
|
+
const openApiPlugin = (0, kizunajs_plugin.definePlugin)({
|
|
743
|
+
slug: OPENAPI_PLUGIN_SLUG,
|
|
744
|
+
options: OpenApiPluginOptionsSchema,
|
|
745
|
+
setup: ({ options, api }) => {
|
|
746
|
+
const { output, ...document } = options;
|
|
747
|
+
return {
|
|
748
|
+
routes: openApiRoutes(options, api),
|
|
749
|
+
generators: output === void 0 ? [] : [openApiDocumentGenerator({
|
|
750
|
+
output,
|
|
751
|
+
json: output.endsWith(".json"),
|
|
752
|
+
document
|
|
753
|
+
})]
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
/**
|
|
758
|
+
* Writes one document, in the format its `output` names.
|
|
759
|
+
*/
|
|
760
|
+
const openApiDocumentGenerator = (0, kizunajs_generator.defineGenerator)({
|
|
761
|
+
options: zod.z.object({
|
|
762
|
+
json: zod.z.boolean(),
|
|
763
|
+
document: zod.z.custom()
|
|
764
|
+
}),
|
|
765
|
+
generate: ({ options, api }) => ({ finalize: () => {
|
|
766
|
+
const render = renderOpenApi(api, options.document);
|
|
767
|
+
return options.json ? `${JSON.stringify(render("json"), null, 4)}\n` : render("yaml");
|
|
768
|
+
} })
|
|
640
769
|
});
|
|
641
|
-
function openApiPlugin(props) {
|
|
642
|
-
return declare(props.slug ?? "openApi", props);
|
|
643
|
-
}
|
|
644
770
|
//#endregion
|
|
645
771
|
exports.generateOpenApi = generateOpenApi;
|
|
646
|
-
exports.openApiDocument = openApiDocument;
|
|
647
772
|
exports.openApiPlugin = openApiPlugin;
|
|
648
773
|
exports.renderDocsHtml = renderDocsHtml;
|
|
649
774
|
exports.renderOpenApi = renderOpenApi;
|