@tetsujs/openapi 0.4.1 → 0.5.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/README.md +140 -3
- package/dist/src/annotations.d.ts +40 -0
- package/dist/src/annotations.d.ts.map +1 -1
- package/dist/src/controller.d.ts +32 -9
- package/dist/src/controller.d.ts.map +1 -1
- package/dist/src/document.d.ts +7 -0
- package/dist/src/document.d.ts.map +1 -1
- package/dist/src/emit.d.ts +3 -1
- package/dist/src/emit.d.ts.map +1 -1
- package/dist/src/envelopes.d.ts +72 -0
- package/dist/src/envelopes.d.ts.map +1 -0
- package/dist/src/errors.d.ts +74 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/index.d.ts +31 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +374 -135
- package/dist/src/index.js.map +11 -9
- package/dist/src/json-schema.d.ts +91 -0
- package/dist/src/json-schema.d.ts.map +1 -0
- package/dist/src/operation.d.ts +10 -1
- package/dist/src/operation.d.ts.map +1 -1
- package/dist/src/page.d.ts +20 -0
- package/dist/src/page.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/annotations.ts +43 -0
- package/src/controller.ts +69 -22
- package/src/document.ts +9 -0
- package/src/emit.ts +3 -1
- package/src/envelopes.ts +268 -0
- package/src/errors.ts +177 -0
- package/src/index.ts +52 -6
- package/src/json-schema.ts +108 -0
- package/src/operation.ts +328 -187
- package/src/page.ts +65 -6
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Schema 2020-12, as written by hand.
|
|
3
|
+
*
|
|
4
|
+
* Every other schema in a document comes from a validator, which describes
|
|
5
|
+
* itself. What a hook adds to its answer — a field next to the envelope, a
|
|
6
|
+
* header — has no validator behind it, and is written as JSON Schema
|
|
7
|
+
* directly. Typed closed, keyword by keyword, so that an editor offers the
|
|
8
|
+
* keywords and a misspelled one is an error: an open record would take
|
|
9
|
+
* `minimun` and `{ type: "int" }` and put them in the document as they are.
|
|
10
|
+
*
|
|
11
|
+
* Closed on purpose, not by omission. The generator asks every schema for
|
|
12
|
+
* the 2020-12 dialect, and a dialect's keywords are a fixed list. The one
|
|
13
|
+
* open door is what OpenAPI itself allows in a schema — extensions, named
|
|
14
|
+
* `x-` — and no misspelling of a keyword starts with it.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
/** A primitive type name of JSON Schema. */
|
|
19
|
+
export type JsonSchemaType = "string" | "number" | "integer" | "boolean" | "object" | "array" | "null";
|
|
20
|
+
/**
|
|
21
|
+
* A schema: an object of keywords, or `true` and `false` — the schemas
|
|
22
|
+
* everything and nothing satisfy.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const retryAfter: JsonSchema = { type: "integer", minimum: 0 };
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export type JsonSchema = JsonSchemaKeywords | boolean;
|
|
30
|
+
/** The keywords of JSON Schema 2020-12. */
|
|
31
|
+
export interface JsonSchemaKeywords {
|
|
32
|
+
readonly $schema?: string;
|
|
33
|
+
readonly $id?: string;
|
|
34
|
+
readonly $ref?: string;
|
|
35
|
+
readonly $anchor?: string;
|
|
36
|
+
readonly $dynamicRef?: string;
|
|
37
|
+
readonly $dynamicAnchor?: string;
|
|
38
|
+
readonly $vocabulary?: Readonly<Record<string, boolean>>;
|
|
39
|
+
readonly $comment?: string;
|
|
40
|
+
readonly $defs?: Readonly<Record<string, JsonSchema>>;
|
|
41
|
+
readonly type?: JsonSchemaType | readonly JsonSchemaType[];
|
|
42
|
+
readonly enum?: readonly unknown[];
|
|
43
|
+
readonly const?: unknown;
|
|
44
|
+
readonly multipleOf?: number;
|
|
45
|
+
readonly maximum?: number;
|
|
46
|
+
readonly exclusiveMaximum?: number;
|
|
47
|
+
readonly minimum?: number;
|
|
48
|
+
readonly exclusiveMinimum?: number;
|
|
49
|
+
readonly maxLength?: number;
|
|
50
|
+
readonly minLength?: number;
|
|
51
|
+
readonly pattern?: string;
|
|
52
|
+
readonly format?: string;
|
|
53
|
+
readonly contentEncoding?: string;
|
|
54
|
+
readonly contentMediaType?: string;
|
|
55
|
+
readonly contentSchema?: JsonSchema;
|
|
56
|
+
readonly prefixItems?: readonly JsonSchema[];
|
|
57
|
+
readonly items?: JsonSchema;
|
|
58
|
+
readonly contains?: JsonSchema;
|
|
59
|
+
readonly maxItems?: number;
|
|
60
|
+
readonly minItems?: number;
|
|
61
|
+
readonly uniqueItems?: boolean;
|
|
62
|
+
readonly maxContains?: number;
|
|
63
|
+
readonly minContains?: number;
|
|
64
|
+
readonly unevaluatedItems?: JsonSchema;
|
|
65
|
+
readonly properties?: Readonly<Record<string, JsonSchema>>;
|
|
66
|
+
readonly patternProperties?: Readonly<Record<string, JsonSchema>>;
|
|
67
|
+
readonly additionalProperties?: JsonSchema;
|
|
68
|
+
readonly unevaluatedProperties?: JsonSchema;
|
|
69
|
+
readonly propertyNames?: JsonSchema;
|
|
70
|
+
readonly required?: readonly string[];
|
|
71
|
+
readonly dependentRequired?: Readonly<Record<string, readonly string[]>>;
|
|
72
|
+
readonly dependentSchemas?: Readonly<Record<string, JsonSchema>>;
|
|
73
|
+
readonly maxProperties?: number;
|
|
74
|
+
readonly minProperties?: number;
|
|
75
|
+
readonly allOf?: readonly JsonSchema[];
|
|
76
|
+
readonly anyOf?: readonly JsonSchema[];
|
|
77
|
+
readonly oneOf?: readonly JsonSchema[];
|
|
78
|
+
readonly not?: JsonSchema;
|
|
79
|
+
readonly if?: JsonSchema;
|
|
80
|
+
readonly then?: JsonSchema;
|
|
81
|
+
readonly else?: JsonSchema;
|
|
82
|
+
readonly title?: string;
|
|
83
|
+
readonly description?: string;
|
|
84
|
+
readonly default?: unknown;
|
|
85
|
+
readonly deprecated?: boolean;
|
|
86
|
+
readonly readOnly?: boolean;
|
|
87
|
+
readonly writeOnly?: boolean;
|
|
88
|
+
readonly examples?: readonly unknown[];
|
|
89
|
+
readonly [extension: `x-${string}`]: unknown;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=json-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-schema.d.ts","sourceRoot":"","sources":["../../src/json-schema.ts"],"names":[],"mappings":"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"}
|
package/dist/src/operation.d.ts
CHANGED
|
@@ -12,8 +12,17 @@
|
|
|
12
12
|
import type { AppOptions, RouteTableEntry } from "@tetsujs/core";
|
|
13
13
|
import type { SchemaComponents } from "./components.ts";
|
|
14
14
|
import type { OperationObject } from "./document.ts";
|
|
15
|
+
import type { Envelopes } from "./envelopes.ts";
|
|
15
16
|
/** Describes one route. */
|
|
16
|
-
export declare function operationOf(entry: RouteTableEntry, options: AppOptions, components: SchemaComponents, ids: OperationIds, warn: (message: string) => void): OperationObject;
|
|
17
|
+
export declare function operationOf(entry: RouteTableEntry, options: AppOptions, components: SchemaComponents, envelopes: Envelopes, ids: OperationIds, warn: (message: string) => void): OperationObject;
|
|
18
|
+
/**
|
|
19
|
+
* Records the envelopes a route declares, so that its definitions are the
|
|
20
|
+
* ones the document keeps — whichever operation refers to them first.
|
|
21
|
+
*
|
|
22
|
+
* Runs before any operation is built. A schema that cannot describe itself
|
|
23
|
+
* is skipped without a word: building the operation says so, once.
|
|
24
|
+
*/
|
|
25
|
+
export declare function declareEnvelopes(entry: RouteTableEntry, envelopes: Envelopes): void;
|
|
17
26
|
/**
|
|
18
27
|
* Hands out the `operationId` of every route in one document.
|
|
19
28
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operation.d.ts","sourceRoot":"","sources":["../../src/operation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAEV,UAAU,EAEV,eAAe,EAChB,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"operation.d.ts","sourceRoot":"","sources":["../../src/operation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAEV,UAAU,EAEV,eAAe,EAChB,MAAM,eAAe,CAAC;AAQvB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAExD,OAAO,KAAK,EAGV,eAAe,EAEhB,MAAM,eAAe,CAAC;AAGvB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAKhD,2BAA2B;AAC3B,wBAAgB,WAAW,CACzB,KAAK,EAAE,eAAe,EACtB,OAAO,EAAE,UAAU,EACnB,UAAU,EAAE,gBAAgB,EAC5B,SAAS,EAAE,SAAS,EACpB,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC9B,eAAe,CAyCjB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,eAAe,EACtB,SAAS,EAAE,SAAS,GACnB,IAAI,CAeN;AA+GD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,IAAI,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAAC;CACtC;AAED,wBAAgB,YAAY,IAAI,YAAY,CAmB3C"}
|
package/dist/src/page.d.ts
CHANGED
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
* a parameter, so a self-hosted copy is one option away. Nothing is
|
|
13
13
|
* fetched unless this function is called.
|
|
14
14
|
*
|
|
15
|
+
* What the CDN serves runs on the application's origin, with its cookies.
|
|
16
|
+
* The default renderers are therefore pinned to an exact version and
|
|
17
|
+
* carry the hash of that file: a CDN, or a package release, that serves
|
|
18
|
+
* something else is refused by the browser rather than run. A page on an
|
|
19
|
+
* origin with a session is still somebody else's application in the
|
|
20
|
+
* user's session — which is why `docs()` can serve the document alone.
|
|
21
|
+
*
|
|
15
22
|
* @module
|
|
16
23
|
*/
|
|
17
24
|
/** The renderers this package knows how to bootstrap. */
|
|
@@ -20,6 +27,19 @@ export type DocsUi = "scalar" | "swagger-ui" | "redoc";
|
|
|
20
27
|
export interface DocsAssets {
|
|
21
28
|
readonly script: string;
|
|
22
29
|
readonly style?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Subresource Integrity hashes of the two files, for a copy that is not
|
|
32
|
+
* on the application's own origin: the browser runs the file only if it
|
|
33
|
+
* hashes to this. Absent, the files are loaded as they are.
|
|
34
|
+
*
|
|
35
|
+
* One line computes one:
|
|
36
|
+
* `curl -s <url> | openssl dgst -sha384 -binary | openssl base64 -A`,
|
|
37
|
+
* prefixed with `sha384-`.
|
|
38
|
+
*/
|
|
39
|
+
readonly integrity?: {
|
|
40
|
+
readonly script?: string;
|
|
41
|
+
readonly style?: string;
|
|
42
|
+
};
|
|
23
43
|
}
|
|
24
44
|
/** What the page needs to know. */
|
|
25
45
|
export interface DocsPageOptions {
|
package/dist/src/page.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/page.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,yDAAyD;AACzD,MAAM,MAAM,MAAM,GAAG,QAAQ,GAAG,YAAY,GAAG,OAAO,CAAC;AAEvD,uCAAuC;AACvC,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;;;;OAQG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE;QACnB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAED,mCAAmC;AACnC,MAAM,WAAW,eAAe;IAC9B,mCAAmC;IACnC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAEpB,qEAAqE;IACrE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,wDAAwD;IACxD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAExB,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC;CAC9B;AAyCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAyBzD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tetsujs/openapi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "An OpenAPI 3.1 document and docs page generated from Tetsu routes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@tetsujs/core": "^0.
|
|
37
|
+
"@tetsujs/core": "^0.5.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@tetsujs/core": "0.
|
|
40
|
+
"@tetsujs/core": "0.5.0"
|
|
41
41
|
},
|
|
42
42
|
"publishConfig": {
|
|
43
43
|
"access": "public"
|
package/src/annotations.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import type { AnyHook, AnySchema } from "@tetsujs/core";
|
|
20
|
+
import type { JsonSchema } from "./json-schema.ts";
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* The definition of a scheme, as OpenAPI spells it.
|
|
@@ -83,6 +84,48 @@ export interface DocumentedResponse {
|
|
|
83
84
|
|
|
84
85
|
/** An example of the envelope's `message`, for the same reason. */
|
|
85
86
|
readonly message?: string;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Fields the hook adds to the envelope, as JSON Schema by name — the
|
|
90
|
+
* `retryAfter` of `{ ...errorBody(429, "RATE_LIMITED"), retryAfter }`.
|
|
91
|
+
*
|
|
92
|
+
* Each is documented as always present. The envelope's own `status`,
|
|
93
|
+
* `message` and `error` are not redefined by a field of the same name:
|
|
94
|
+
* `error` is what a client discriminates on, and it stays the code the
|
|
95
|
+
* hook declared. Ignored when the response has a `schema`, which says
|
|
96
|
+
* everything about the body itself.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* documented(limiter, {
|
|
101
|
+
* responses: [
|
|
102
|
+
* {
|
|
103
|
+
* status: 429,
|
|
104
|
+
* description: "Too many requests",
|
|
105
|
+
* error: "RATE_LIMITED",
|
|
106
|
+
* fields: { retryAfter: { type: "integer", minimum: 0 } },
|
|
107
|
+
* },
|
|
108
|
+
* ],
|
|
109
|
+
* });
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
readonly fields?: Readonly<Record<string, JsonSchema>>;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Headers the hook sets on this response, by name — the `retry-after`
|
|
116
|
+
* of a refusal.
|
|
117
|
+
*
|
|
118
|
+
* Documented as possible rather than required: a status several sources
|
|
119
|
+
* answer with is one response in the document, and a header one of them
|
|
120
|
+
* sets is not on the others'.
|
|
121
|
+
*/
|
|
122
|
+
readonly headers?: Readonly<Record<string, DocumentedHeader>>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A header a hook's response carries. */
|
|
126
|
+
export interface DocumentedHeader {
|
|
127
|
+
readonly description?: string;
|
|
128
|
+
readonly schema: JsonSchema;
|
|
86
129
|
}
|
|
87
130
|
|
|
88
131
|
/**
|
package/src/controller.ts
CHANGED
|
@@ -16,27 +16,46 @@
|
|
|
16
16
|
import type { App, RouteDef, SchemaConfig, ValidatePath } from "@tetsujs/core";
|
|
17
17
|
import { onMount, route } from "@tetsujs/core";
|
|
18
18
|
import type { OpenApiDocument } from "./document.ts";
|
|
19
|
+
import type { ErrorFormat } from "./errors.ts";
|
|
19
20
|
import type { GeneratorWarning } from "./index.ts";
|
|
20
21
|
import { openapi } from "./index.ts";
|
|
21
22
|
import type { DocsAssets, DocsUi } from "./page.ts";
|
|
22
23
|
import { docsPage } from "./page.ts";
|
|
23
24
|
|
|
24
25
|
/** What the documentation controller serves, and from where. */
|
|
25
|
-
export interface DocsOptions<
|
|
26
|
+
export interface DocsOptions<
|
|
27
|
+
Path extends string,
|
|
28
|
+
UiPath extends string,
|
|
29
|
+
Ui extends DocsUi | false = DocsUi | false,
|
|
30
|
+
> {
|
|
26
31
|
/** Title, version and the rest of the document's `info` block. */
|
|
27
32
|
readonly info: OpenApiDocument["info"];
|
|
28
33
|
|
|
29
34
|
/** Servers the API is reachable at. */
|
|
30
35
|
readonly servers?: OpenApiDocument["servers"];
|
|
31
36
|
|
|
37
|
+
/**
|
|
38
|
+
* How the application's errors look, when not like the framework's
|
|
39
|
+
* envelope — see `errors` on {@link openapi}.
|
|
40
|
+
*/
|
|
41
|
+
readonly errors?: ErrorFormat;
|
|
42
|
+
|
|
32
43
|
/** Where the document is served. Defaults to `/openapi.json`. */
|
|
33
44
|
readonly path?: Path & ValidatePath<Path>;
|
|
34
45
|
|
|
35
46
|
/** Where the page is served. Defaults to `/docs`. */
|
|
36
47
|
readonly uiPath?: UiPath & ValidatePath<UiPath>;
|
|
37
48
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Which renderer the page bootstraps. Defaults to `"scalar"`.
|
|
51
|
+
*
|
|
52
|
+
* `false` serves the document without a page — what production wants
|
|
53
|
+
* on an origin that carries a session. The page runs its renderer on the
|
|
54
|
+
* application's origin, so a renderer that is not what it should be acts
|
|
55
|
+
* in the signed-in user's name there; the document alone runs nothing.
|
|
56
|
+
* `uiPath`, `title` and `assets` are then unused.
|
|
57
|
+
*/
|
|
58
|
+
readonly ui?: Ui;
|
|
40
59
|
|
|
41
60
|
/** Browser title of the page. Defaults to the document's own title. */
|
|
42
61
|
readonly title?: string;
|
|
@@ -69,20 +88,27 @@ export interface DocsOptions<Path extends string, UiPath extends string> {
|
|
|
69
88
|
* Instantiated through the factory, never directly — the routes carry the
|
|
70
89
|
* configured paths in their types, which only the factory can infer.
|
|
71
90
|
*/
|
|
72
|
-
export class DocsController<
|
|
91
|
+
export class DocsController<
|
|
92
|
+
Path extends string,
|
|
93
|
+
UiPath extends string,
|
|
94
|
+
Page extends boolean = true,
|
|
95
|
+
> {
|
|
73
96
|
/** The document itself, as `application/json`. */
|
|
74
97
|
readonly json: RouteDef<Path, SchemaConfig, never, undefined, "GET">;
|
|
75
98
|
|
|
76
|
-
/** The page that renders it
|
|
77
|
-
readonly ui:
|
|
99
|
+
/** The page that renders it — none with `ui: false`. */
|
|
100
|
+
readonly ui: Page extends true
|
|
101
|
+
? RouteDef<UiPath, SchemaConfig, never, undefined, "GET">
|
|
102
|
+
: undefined;
|
|
78
103
|
|
|
79
104
|
private document: OpenApiDocument | undefined;
|
|
80
105
|
|
|
106
|
+
private page: string | undefined;
|
|
107
|
+
|
|
81
108
|
constructor(
|
|
82
109
|
private readonly options: DocsOptions<Path, UiPath>,
|
|
83
110
|
documentPath: string,
|
|
84
111
|
uiPath: string,
|
|
85
|
-
page: string,
|
|
86
112
|
) {
|
|
87
113
|
const hidden = !options.documentSelf;
|
|
88
114
|
|
|
@@ -98,7 +124,7 @@ export class DocsController<Path extends string, UiPath extends string> {
|
|
|
98
124
|
path: "/docs",
|
|
99
125
|
docs: { summary: "Rendered documentation", tags: ["docs"], hidden },
|
|
100
126
|
handler: () =>
|
|
101
|
-
new Response(page, {
|
|
127
|
+
new Response(this.page, {
|
|
102
128
|
headers: { "content-type": "text/html; charset=utf-8" },
|
|
103
129
|
}),
|
|
104
130
|
});
|
|
@@ -113,18 +139,42 @@ export class DocsController<Path extends string, UiPath extends string> {
|
|
|
113
139
|
* configured path lands at runtime.
|
|
114
140
|
*/
|
|
115
141
|
this.json = { ...json, path: documentPath } as unknown as this["json"];
|
|
116
|
-
this.ui =
|
|
142
|
+
this.ui = (
|
|
143
|
+
options.ui === false ? undefined : { ...ui, path: uiPath }
|
|
144
|
+
) as this["ui"];
|
|
117
145
|
}
|
|
118
146
|
|
|
119
|
-
/**
|
|
147
|
+
/**
|
|
148
|
+
* Generates the document of the application this was mounted in, and
|
|
149
|
+
* the page that renders it.
|
|
150
|
+
*
|
|
151
|
+
* The page is built here rather than with the routes because only the
|
|
152
|
+
* application knows where the document ended up: a group this controller
|
|
153
|
+
* is mounted under prefixes its path, and a page pointing at the path as
|
|
154
|
+
* configured would fetch a `404`.
|
|
155
|
+
*/
|
|
120
156
|
[onMount](app: App): void {
|
|
121
157
|
const generated = openapi(app, {
|
|
122
158
|
info: this.options.info,
|
|
123
159
|
...(this.options.servers ? { servers: this.options.servers } : {}),
|
|
160
|
+
...(this.options.errors ? { errors: this.options.errors } : {}),
|
|
124
161
|
});
|
|
125
162
|
|
|
126
163
|
this.document = generated.document;
|
|
127
164
|
|
|
165
|
+
const ui = this.options.ui ?? "scalar";
|
|
166
|
+
const json: unknown = this.json;
|
|
167
|
+
const served = app.entries.find((entry) => entry.def === json);
|
|
168
|
+
|
|
169
|
+
if (ui !== false && served) {
|
|
170
|
+
this.page = docsPage({
|
|
171
|
+
ui,
|
|
172
|
+
documentUrl: served.path,
|
|
173
|
+
title: this.options.title ?? this.options.info.title,
|
|
174
|
+
...(this.options.assets ? { assets: this.options.assets } : {}),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
128
178
|
const report = this.options.onWarning ?? warn;
|
|
129
179
|
|
|
130
180
|
for (const warning of generated.warnings) {
|
|
@@ -156,18 +206,15 @@ export class DocsController<Path extends string, UiPath extends string> {
|
|
|
156
206
|
export function docs<
|
|
157
207
|
const Path extends string = "/openapi.json",
|
|
158
208
|
const UiPath extends string = "/docs",
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
return new DocsController(options, documentPath, uiPath, page);
|
|
209
|
+
const Ui extends DocsUi | false = "scalar",
|
|
210
|
+
>(
|
|
211
|
+
options: DocsOptions<Path, UiPath, Ui>,
|
|
212
|
+
): DocsController<Path, UiPath, Ui extends false ? false : true> {
|
|
213
|
+
return new DocsController(
|
|
214
|
+
options,
|
|
215
|
+
options.path ?? "/openapi.json",
|
|
216
|
+
options.uiPath ?? "/docs",
|
|
217
|
+
) as DocsController<Path, UiPath, Ui extends false ? false : true>;
|
|
171
218
|
}
|
|
172
219
|
|
|
173
220
|
function warn(warning: GeneratorWarning): void {
|
package/src/document.ts
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* @module
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import type { JsonSchema } from "./json-schema.ts";
|
|
19
|
+
|
|
18
20
|
/** Metadata every OpenAPI document must carry. */
|
|
19
21
|
export interface DocumentInfo {
|
|
20
22
|
readonly title: string;
|
|
@@ -44,9 +46,16 @@ export type ContentMap = Record<
|
|
|
44
46
|
{ readonly schema?: Record<string, unknown> }
|
|
45
47
|
>;
|
|
46
48
|
|
|
49
|
+
/** A header a response carries. */
|
|
50
|
+
export interface HeaderObject {
|
|
51
|
+
readonly description?: string;
|
|
52
|
+
readonly schema: JsonSchema;
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
/** One response of an operation. */
|
|
48
56
|
export interface ResponseObject {
|
|
49
57
|
readonly description: string;
|
|
58
|
+
readonly headers?: Record<string, HeaderObject>;
|
|
50
59
|
readonly content?: ContentMap;
|
|
51
60
|
}
|
|
52
61
|
|
package/src/emit.ts
CHANGED
|
@@ -20,13 +20,15 @@ import { toJsonSchema } from "@tetsujs/core";
|
|
|
20
20
|
const target = { target: "draft-2020-12" } as const;
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* A JSON Schema as the generator reads it: any keyword, with the
|
|
23
|
+
* A JSON Schema as the generator reads it: any keyword, with the ones it
|
|
24
24
|
* looks into named, so reading them does not depend on the reader's
|
|
25
25
|
* `noPropertyAccessFromIndexSignature`.
|
|
26
26
|
*/
|
|
27
27
|
export interface JsonSchemaObject {
|
|
28
28
|
properties?: unknown;
|
|
29
29
|
required?: unknown;
|
|
30
|
+
anyOf?: unknown;
|
|
31
|
+
oneOf?: unknown;
|
|
30
32
|
[keyword: string]: unknown;
|
|
31
33
|
}
|
|
32
34
|
|