@kizunajs/openapi 2.0.0-beta.1 → 2.0.0-beta.11
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 +171 -68
- package/dist/index.d.cts +178 -170
- package/dist/index.d.mts +177 -169
- package/dist/index.mjs +166 -69
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { RoutePathSchema, definePlugin, route } from "kizunajs/plugin";
|
|
3
|
+
import { cacheHeaders, defineGenerator, deprecationHeaders, globalRegistrySchemas, isBinarySchema, isFileSchema, isNamedStream, isStreamResponse, isVoidSchema, parsePath, readDef, readDiscriminatedUnion, readDiscriminatorLiteral, readMeta, readMetaId, readObjectShape, resolveResponseBody, resolveResponseCache, resolveResponseContentType, resolveResponseEtag, resolveResponseHeaders, streamContentType, streamMode, walkApi } from "kizunajs/generator";
|
|
3
4
|
import { contractOf, rawResponse } from "kizunajs/adapter";
|
|
4
5
|
import { stringify } from "yaml";
|
|
5
|
-
import { cacheHeaders, createGenerator, deprecationHeaders, globalRegistrySchemas, isBinarySchema, isFileSchema, isNamedStream, isStreamResponse, isVoidSchema, parsePath, readDiscriminatedUnion, readDiscriminatorLiteral, readMeta, readMetaId, readObjectShape, resolveResponseBody, resolveResponseCache, resolveResponseContentType, resolveResponseEtag, resolveResponseHeaders, streamContentType, streamMode } from "kizunajs/generator";
|
|
6
6
|
import { getStatusText } from "kizunajs";
|
|
7
7
|
//#region src/generator.ts
|
|
8
8
|
const streamJsonSchema = (response, toJsonSchema) => {
|
|
@@ -79,12 +79,25 @@ const buildDiscriminatorBlock = (propertyName, options) => {
|
|
|
79
79
|
} : { propertyName };
|
|
80
80
|
};
|
|
81
81
|
/**
|
|
82
|
+
* What the wire carries for a type JSON Schema has no keyword for, which
|
|
83
|
+
* `unrepresentable: 'any'` would otherwise leave as an empty schema.
|
|
84
|
+
*/
|
|
85
|
+
const JSON_ENCODING = {
|
|
86
|
+
date: {
|
|
87
|
+
type: "string",
|
|
88
|
+
format: "date-time"
|
|
89
|
+
},
|
|
90
|
+
bigint: { type: "string" }
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
82
93
|
* kizuna's metadata widens JSON Schema: `deprecated` may carry a message and
|
|
83
94
|
* `example` holds one value or several. Emit `deprecated: true` and an
|
|
84
95
|
* `examples` array; the message stays in outputs with a place for it, like
|
|
85
96
|
* Swift's `@available`.
|
|
86
97
|
*/
|
|
87
|
-
const normalizeMeta = ({ jsonSchema }) => {
|
|
98
|
+
const normalizeMeta = ({ zodSchema, jsonSchema }) => {
|
|
99
|
+
const encoding = JSON_ENCODING[readDef(zodSchema).type ?? ""];
|
|
100
|
+
if (encoding && Object.keys(jsonSchema).length === 0) Object.assign(jsonSchema, encoding);
|
|
88
101
|
if (typeof jsonSchema.deprecated === "string") jsonSchema.deprecated = true;
|
|
89
102
|
if ("example" in jsonSchema) {
|
|
90
103
|
const given = Array.isArray(jsonSchema.example) ? jsonSchema.example : [jsonSchema.example];
|
|
@@ -148,10 +161,11 @@ const deriveHeadOperation = (getOperation) => {
|
|
|
148
161
|
for (const response of Object.values(operation.responses)) delete response.content;
|
|
149
162
|
return operation;
|
|
150
163
|
};
|
|
151
|
-
const
|
|
164
|
+
const openApiWalk = (options, api) => {
|
|
152
165
|
const paths = {};
|
|
153
166
|
return {
|
|
154
|
-
processRoute({ routeKey, route, routeTags, deprecated }) {
|
|
167
|
+
processRoute({ routeKey, route, routeTags, deprecated, hidden }) {
|
|
168
|
+
if (hidden && options.includeHidden !== true) return;
|
|
155
169
|
const openApiPath = convertPath(route.path);
|
|
156
170
|
const method = route.method.toLowerCase();
|
|
157
171
|
let operation = {
|
|
@@ -365,7 +379,7 @@ const openApiGenerator = createGenerator((options, api) => {
|
|
|
365
379
|
return renderer;
|
|
366
380
|
}
|
|
367
381
|
};
|
|
368
|
-
}
|
|
382
|
+
};
|
|
369
383
|
/**
|
|
370
384
|
* The permissions a route requires, as `x-kizuna-requires`: what is acted on,
|
|
371
385
|
* mapped to the verbs the caller has to hold.
|
|
@@ -445,17 +459,17 @@ const tagsFromApi = (api) => {
|
|
|
445
459
|
* So a build step does not restate the options and drift from what is served.
|
|
446
460
|
*/
|
|
447
461
|
const optionsFromInstalledPlugin = (api) => {
|
|
448
|
-
for (const
|
|
462
|
+
for (const plugin of Object.values(api.plugins ?? {})) if (plugin.definedSlug === "openApi") return plugin.options;
|
|
449
463
|
throw new Error("generateOpenApi reads its options from the config. Name `openApiPlugin` under `plugins` with the API's `info`.");
|
|
450
464
|
};
|
|
451
465
|
/**
|
|
452
466
|
* Render from options held directly, for the plugin's own routes.
|
|
453
467
|
*/
|
|
454
468
|
function renderOpenApi(api, options) {
|
|
455
|
-
const renderer =
|
|
469
|
+
const renderer = walkApi(api, openApiWalk({
|
|
456
470
|
...options,
|
|
457
471
|
tagLookup: buildTagLookup(api)
|
|
458
|
-
});
|
|
472
|
+
}, api));
|
|
459
473
|
const tags = tagsFromApi(api);
|
|
460
474
|
if (tags.length > 0) renderer("json").tags = tags;
|
|
461
475
|
return renderer;
|
|
@@ -547,22 +561,6 @@ const renderDocsHtml = (options) => {
|
|
|
547
561
|
});
|
|
548
562
|
};
|
|
549
563
|
//#endregion
|
|
550
|
-
//#region src/document-target.ts
|
|
551
|
-
/**
|
|
552
|
-
* An OpenAPI document target for `kizuna.config.ts`.
|
|
553
|
-
*
|
|
554
|
-
* @example
|
|
555
|
-
* openApiDocument({ output: './openapi.yaml' });
|
|
556
|
-
*/
|
|
557
|
-
const openApiDocument = ({ output, ...overrides }) => ({
|
|
558
|
-
kind: "openapi",
|
|
559
|
-
output,
|
|
560
|
-
generate: (contract) => {
|
|
561
|
-
const render = generateOpenApi(contract, overrides);
|
|
562
|
-
return output.endsWith(".json") ? `${JSON.stringify(render("json"), null, 4)}\n` : render("yaml");
|
|
563
|
-
}
|
|
564
|
-
});
|
|
565
|
-
//#endregion
|
|
566
564
|
//#region src/server.ts
|
|
567
565
|
const HTML = "text/html; charset=utf-8";
|
|
568
566
|
const JSON_TYPE = "application/json";
|
|
@@ -572,23 +570,43 @@ const sent = (body, contentType) => new Response(body, {
|
|
|
572
570
|
headers: { "content-type": contentType }
|
|
573
571
|
});
|
|
574
572
|
/**
|
|
575
|
-
*
|
|
576
|
-
*
|
|
573
|
+
* The routes `openApiPlugin` serves: the reference UI, and the document itself
|
|
574
|
+
* where the options publish it. The document is rendered on the first request,
|
|
575
|
+
* once the api it describes has assembled.
|
|
577
576
|
*/
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
577
|
+
const openApiRoutes = (props, api) => {
|
|
578
|
+
let renderer;
|
|
579
|
+
const spec = ((format) => {
|
|
580
|
+
renderer ??= renderOpenApi(contractOf(api), props);
|
|
581
|
+
return renderer(format);
|
|
582
|
+
});
|
|
583
|
+
const routes = {};
|
|
584
|
+
if (props.docsPath !== void 0) routes.page = route({
|
|
585
|
+
method: "GET",
|
|
586
|
+
path: props.docsPath,
|
|
587
|
+
summary: "API reference",
|
|
588
|
+
responses: { 200: z.string() }
|
|
589
|
+
}).handler(() => rawResponse(sent(renderDocsHtml({
|
|
590
|
+
specUrl: props.jsonPath,
|
|
591
|
+
specContent: props.jsonPath === void 0 ? spec("json") : void 0,
|
|
592
|
+
provider: props.provider,
|
|
593
|
+
pageTitle: props.pageTitle ?? spec("json").info.title,
|
|
594
|
+
cdnUrl: props.cdnUrl,
|
|
595
|
+
configuration: props.configuration
|
|
596
|
+
}), HTML)));
|
|
597
|
+
if (props.jsonPath !== void 0) routes.json = route({
|
|
598
|
+
method: "GET",
|
|
599
|
+
path: props.jsonPath,
|
|
600
|
+
summary: "OpenAPI document",
|
|
601
|
+
responses: { 200: z.unknown() }
|
|
602
|
+
}).handler(() => rawResponse(sent(JSON.stringify(spec("json")), JSON_TYPE)));
|
|
603
|
+
if (props.yamlPath !== void 0) routes.yaml = route({
|
|
604
|
+
method: "GET",
|
|
605
|
+
path: props.yamlPath,
|
|
606
|
+
summary: "OpenAPI document, as YAML",
|
|
607
|
+
responses: { 200: z.string() }
|
|
608
|
+
}).handler(() => rawResponse(sent(spec("yaml"), YAML)));
|
|
609
|
+
return routes;
|
|
592
610
|
};
|
|
593
611
|
/**
|
|
594
612
|
* Serve an API reference UI for the OpenAPI document, and the document itself
|
|
@@ -607,38 +625,117 @@ const openApiServe = (props, api) => {
|
|
|
607
625
|
* title: 'My API',
|
|
608
626
|
* version: '1.0.0',
|
|
609
627
|
* },
|
|
628
|
+
* docsPath: '/docs',
|
|
610
629
|
* }),
|
|
611
630
|
* ],
|
|
612
631
|
* });
|
|
613
632
|
* ```
|
|
614
633
|
*/
|
|
615
|
-
const
|
|
616
|
-
slug,
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
634
|
+
const openApiPlugin = definePlugin({
|
|
635
|
+
slug: "openApi",
|
|
636
|
+
options: 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: z.literal("3.1.0").optional(),
|
|
643
|
+
info: z.object({
|
|
644
|
+
title: z.string(),
|
|
645
|
+
version: z.string(),
|
|
646
|
+
description: z.string().optional()
|
|
647
|
+
}),
|
|
648
|
+
servers: z.array(z.object({
|
|
649
|
+
url: z.string(),
|
|
650
|
+
description: z.string().optional()
|
|
651
|
+
})).optional(),
|
|
652
|
+
externalDocs: z.object({
|
|
653
|
+
url: z.string(),
|
|
654
|
+
description: z.string().optional()
|
|
655
|
+
}).optional(),
|
|
656
|
+
setOperationId: z.union([z.boolean(), 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: z.boolean().optional(),
|
|
663
|
+
operationMapper: 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: z.boolean().optional()
|
|
670
|
+
}).extend({
|
|
671
|
+
/**
|
|
672
|
+
* Where `kizuna generate` writes the document. `.json` renders JSON,
|
|
673
|
+
* anything else YAML.
|
|
674
|
+
*
|
|
675
|
+
* @example
|
|
676
|
+
* output: './openapi.yaml',
|
|
677
|
+
*/
|
|
678
|
+
output: z.string().optional(),
|
|
679
|
+
/**
|
|
680
|
+
* Where the reference UI is served.
|
|
681
|
+
*/
|
|
682
|
+
docsPath: RoutePathSchema.optional(),
|
|
683
|
+
/**
|
|
684
|
+
* Where the document is served. The UI embeds it, so this is only for
|
|
685
|
+
* publishing the file.
|
|
686
|
+
*/
|
|
687
|
+
jsonPath: z.custom((value) => typeof value === "string" && value.startsWith("/") && value.endsWith(".json"), { error: "must start with / and end in .json" }).optional(),
|
|
688
|
+
/**
|
|
689
|
+
* Where the document is served as YAML.
|
|
690
|
+
*/
|
|
691
|
+
yamlPath: z.custom((value) => typeof value === "string" && value.startsWith("/") && value.endsWith(".yaml"), { error: "must start with / and end in .yaml" }).optional(),
|
|
692
|
+
/**
|
|
693
|
+
* Which API reference UI to render.
|
|
694
|
+
*
|
|
695
|
+
* @default 'scalar'
|
|
696
|
+
*/
|
|
697
|
+
provider: z.enum(["scalar", "swagger"]).optional(),
|
|
698
|
+
/**
|
|
699
|
+
* Where to load the UI's assets from, for a self-hosted copy. The script
|
|
700
|
+
* URL for `'scalar'`; the directory holding `swagger-ui.css` and
|
|
701
|
+
* `swagger-ui-bundle.js` for `'swagger'`.
|
|
702
|
+
*/
|
|
703
|
+
cdnUrl: z.string().optional(),
|
|
704
|
+
/**
|
|
705
|
+
* The page's `<title>`.
|
|
706
|
+
*
|
|
707
|
+
* @default the document's `info.title`
|
|
708
|
+
*/
|
|
709
|
+
pageTitle: z.string().optional(),
|
|
710
|
+
/**
|
|
711
|
+
* Merged into `Scalar.createApiReference` or `SwaggerUIBundle`.
|
|
712
|
+
*/
|
|
713
|
+
configuration: z.record(z.string(), z.unknown()).optional()
|
|
714
|
+
}),
|
|
715
|
+
setup: ({ options, api }) => {
|
|
716
|
+
const { output, ...document } = options;
|
|
717
|
+
return {
|
|
718
|
+
routes: openApiRoutes(options, api),
|
|
719
|
+
generators: output === void 0 ? [] : [openApiDocumentGenerator({
|
|
720
|
+
output,
|
|
721
|
+
json: output.endsWith(".json"),
|
|
722
|
+
document
|
|
723
|
+
})]
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
/**
|
|
728
|
+
* Writes one document, in the format its `output` names.
|
|
729
|
+
*/
|
|
730
|
+
const openApiDocumentGenerator = defineGenerator({
|
|
731
|
+
options: z.object({
|
|
732
|
+
json: z.boolean(),
|
|
733
|
+
document: z.custom()
|
|
734
|
+
}),
|
|
735
|
+
generate: ({ options, api }) => ({ finalize: () => {
|
|
736
|
+
const render = renderOpenApi(api, options.document);
|
|
737
|
+
return options.json ? `${JSON.stringify(render("json"), null, 4)}\n` : render("yaml");
|
|
738
|
+
} })
|
|
639
739
|
});
|
|
640
|
-
function openApiPlugin(props) {
|
|
641
|
-
return declare(props.slug ?? "openApi", props);
|
|
642
|
-
}
|
|
643
740
|
//#endregion
|
|
644
|
-
export { generateOpenApi,
|
|
741
|
+
export { generateOpenApi, openApiPlugin, renderDocsHtml, renderOpenApi };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kizunajs/openapi",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.11",
|
|
4
4
|
"description": "OpenAPI 3.1 generation for Kizuna.js APIs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kizunajs",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
62
|
"zod": "^4.0.0",
|
|
63
|
-
"kizunajs": "2.0.0-beta.
|
|
63
|
+
"kizunajs": "2.0.0-beta.11"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/express": "^5.0.0",
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
"tsdown": "^0.21.0",
|
|
69
69
|
"typescript": "^6.0.3",
|
|
70
70
|
"zod": "^4.0.0",
|
|
71
|
-
"kizunajs": "2.0.0-beta.
|
|
72
|
-
"
|
|
71
|
+
"@kizunajs/express": "2.0.0-beta.11",
|
|
72
|
+
"kizunajs": "2.0.0-beta.11"
|
|
73
73
|
},
|
|
74
74
|
"scripts": {
|
|
75
75
|
"build": "tsdown src/index.ts --format esm,cjs --dts --clean --external kizunajs --external zod",
|