@nifrajs/schema 1.3.1 → 1.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/dist/openapi.d.ts +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +65 -42
- package/dist/openapi.js.map +1 -1
- package/package.json +2 -2
- package/src/openapi.ts +81 -58
package/dist/openapi.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ContractShape, Server } from "@nifrajs/core";
|
|
2
|
+
import { type JsonSchema } from "@nifrajs/core/reflection";
|
|
2
3
|
/**
|
|
3
4
|
* OpenAPI 3.1 generation. We model a practical slice of the spec — enough to feed Swagger UI / codegen
|
|
4
5
|
* and to validate structurally: paths, parameters, request bodies, responses (incl. non-200 and
|
|
@@ -44,7 +45,6 @@ export interface ToOpenAPIOptions {
|
|
|
44
45
|
*/
|
|
45
46
|
readonly operations?: Readonly<Record<string, Record<string, unknown>>>;
|
|
46
47
|
}
|
|
47
|
-
type JsonSchema = Record<string, unknown>;
|
|
48
48
|
interface OpenAPIParameter {
|
|
49
49
|
readonly name: string;
|
|
50
50
|
readonly in: "path" | "query";
|
package/dist/openapi.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"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"}
|
package/dist/openapi.js
CHANGED
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
return ("jsonSchema" in schema &&
|
|
3
|
-
schema.jsonSchema !== undefined &&
|
|
4
|
-
schema.jsonSchema !== null);
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Extract clean JSON Schema from a schema, or `undefined` for a BYO Standard Schema with no JSON
|
|
8
|
-
* Schema. The `JSON` round-trip strips TypeBox's Symbol-keyed metadata; a string `$id` survives it and
|
|
9
|
-
* drives `$ref` reuse.
|
|
10
|
-
*/
|
|
11
|
-
function toJsonSchema(schema) {
|
|
12
|
-
if (!hasJsonSchema(schema))
|
|
13
|
-
return undefined;
|
|
14
|
-
return JSON.parse(JSON.stringify(schema.jsonSchema));
|
|
15
|
-
}
|
|
1
|
+
import { reflectRoutes, reflectSchema, } from "@nifrajs/core/reflection";
|
|
16
2
|
/**
|
|
17
3
|
* Collects schemas that carry a `$id` into `components.schemas`, returning a `$ref` in their place —
|
|
18
4
|
* so a schema used by N operations is emitted once. Schemas without a `$id` stay inline (the existing
|
|
@@ -20,19 +6,47 @@ function toJsonSchema(schema) {
|
|
|
20
6
|
*/
|
|
21
7
|
class SchemaStore {
|
|
22
8
|
schemas = {};
|
|
9
|
+
componentNames = new Map();
|
|
10
|
+
componentIds = new Map();
|
|
11
|
+
componentName(id) {
|
|
12
|
+
const known = this.componentNames.get(id);
|
|
13
|
+
if (known !== undefined)
|
|
14
|
+
return known;
|
|
15
|
+
const encoded = [...new TextEncoder().encode(id)]
|
|
16
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
17
|
+
.join("");
|
|
18
|
+
const base = /^[A-Za-z0-9._-]+$/.test(id) ? id : `Schema_${encoded}`;
|
|
19
|
+
let name = base;
|
|
20
|
+
let suffix = 2;
|
|
21
|
+
while (this.componentIds.has(name) && this.componentIds.get(name) !== id) {
|
|
22
|
+
name = `${base}_${suffix}`;
|
|
23
|
+
suffix += 1;
|
|
24
|
+
}
|
|
25
|
+
this.componentNames.set(id, name);
|
|
26
|
+
this.componentIds.set(name, id);
|
|
27
|
+
return name;
|
|
28
|
+
}
|
|
23
29
|
/** Hoist a schema with a `$id` into components + return a `$ref`; otherwise return it inline. */
|
|
24
30
|
collect(schema) {
|
|
25
31
|
if (schema === undefined)
|
|
26
32
|
return undefined;
|
|
33
|
+
if (typeof schema === "boolean")
|
|
34
|
+
return schema;
|
|
27
35
|
const id = typeof schema.$id === "string" ? schema.$id : undefined;
|
|
28
36
|
if (id === undefined)
|
|
29
37
|
return schema;
|
|
30
|
-
|
|
38
|
+
const componentName = this.componentName(id);
|
|
39
|
+
if (!Object.hasOwn(this.schemas, componentName)) {
|
|
31
40
|
const hoisted = { ...schema };
|
|
32
41
|
delete hoisted.$id; // the component key is the name; drop the redundant base-URI hint
|
|
33
|
-
this.schemas
|
|
42
|
+
Object.defineProperty(this.schemas, componentName, {
|
|
43
|
+
value: hoisted,
|
|
44
|
+
enumerable: true,
|
|
45
|
+
configurable: true,
|
|
46
|
+
writable: true,
|
|
47
|
+
});
|
|
34
48
|
}
|
|
35
|
-
return { $ref: `#/components/schemas/${
|
|
49
|
+
return { $ref: `#/components/schemas/${componentName}` };
|
|
36
50
|
}
|
|
37
51
|
}
|
|
38
52
|
/** Name of a path segment's parameter, or `undefined` for a static segment. */
|
|
@@ -66,17 +80,15 @@ function pathParameters(path) {
|
|
|
66
80
|
function queryParameters(schema) {
|
|
67
81
|
if (schema === undefined)
|
|
68
82
|
return [];
|
|
69
|
-
const
|
|
70
|
-
// Only an object schema decomposes into individual query parameters.
|
|
71
|
-
if (
|
|
83
|
+
const fields = schema.fields;
|
|
84
|
+
// Only an introspectable object schema decomposes into individual query parameters.
|
|
85
|
+
if (fields === undefined)
|
|
72
86
|
return [];
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return Object.entries(properties).map(([name, propSchema]) => ({
|
|
76
|
-
name,
|
|
87
|
+
return fields.map((field) => ({
|
|
88
|
+
name: field.name,
|
|
77
89
|
in: "query",
|
|
78
|
-
required: required
|
|
79
|
-
schema:
|
|
90
|
+
required: field.required,
|
|
91
|
+
schema: field.schema,
|
|
80
92
|
}));
|
|
81
93
|
}
|
|
82
94
|
const STATUS_TEXT = {
|
|
@@ -94,21 +106,21 @@ const STATUS_TEXT = {
|
|
|
94
106
|
"502": "Bad Gateway",
|
|
95
107
|
"503": "Service Unavailable",
|
|
96
108
|
};
|
|
97
|
-
|
|
98
|
-
* {@link buildResponses} emits as non-2xx OpenAPI responses. */
|
|
99
|
-
function errorsToResponses(errors) {
|
|
109
|
+
function reflectedErrorsToResponses(errors) {
|
|
100
110
|
if (errors === undefined)
|
|
101
111
|
return undefined;
|
|
102
112
|
const out = {};
|
|
103
|
-
for (const [status,
|
|
104
|
-
|
|
113
|
+
for (const [status, reflection] of Object.entries(errors)) {
|
|
114
|
+
if (reflection.jsonSchema !== undefined || reflection.standard !== undefined) {
|
|
115
|
+
out[status] = { description: STATUS_TEXT[status] ?? "Error", schema: reflection };
|
|
116
|
+
}
|
|
105
117
|
}
|
|
106
|
-
return out;
|
|
118
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
107
119
|
}
|
|
108
120
|
function buildResponses(input, store) {
|
|
109
121
|
const responses = {};
|
|
110
122
|
if (input.response !== undefined) {
|
|
111
|
-
const schema = store.collect(
|
|
123
|
+
const schema = store.collect(input.response.jsonSchema);
|
|
112
124
|
responses["200"] =
|
|
113
125
|
schema !== undefined
|
|
114
126
|
? {
|
|
@@ -124,7 +136,7 @@ function buildResponses(input, store) {
|
|
|
124
136
|
if (input.responses !== undefined) {
|
|
125
137
|
for (const [status, def] of Object.entries(input.responses)) {
|
|
126
138
|
const response = { description: def.description ?? "" };
|
|
127
|
-
const schema = def.schema !== undefined ? store.collect(
|
|
139
|
+
const schema = def.schema !== undefined ? store.collect(def.schema.jsonSchema) : undefined;
|
|
128
140
|
if (schema !== undefined)
|
|
129
141
|
response.content = { [def.contentType ?? "application/json"]: { schema } };
|
|
130
142
|
responses[status] = response;
|
|
@@ -150,7 +162,7 @@ function buildOperation(input, store) {
|
|
|
150
162
|
if (parameters.length > 0)
|
|
151
163
|
operation.parameters = parameters;
|
|
152
164
|
if (input.body !== undefined) {
|
|
153
|
-
const schema = store.collect(
|
|
165
|
+
const schema = store.collect(input.body.jsonSchema);
|
|
154
166
|
if (schema !== undefined) {
|
|
155
167
|
operation.requestBody = {
|
|
156
168
|
required: true,
|
|
@@ -181,7 +193,7 @@ export function toOpenAPI(input, options = {}) {
|
|
|
181
193
|
const paths = {};
|
|
182
194
|
const store = new SchemaStore();
|
|
183
195
|
if (isApp(input)) {
|
|
184
|
-
for (const route of input
|
|
196
|
+
for (const route of reflectRoutes(input)) {
|
|
185
197
|
addOperation(paths, route.method, {
|
|
186
198
|
path: route.path,
|
|
187
199
|
body: route.schema?.body,
|
|
@@ -189,7 +201,7 @@ export function toOpenAPI(input, options = {}) {
|
|
|
189
201
|
// A route may now declare a `response` contract — emit it as the 200 body schema.
|
|
190
202
|
response: route.schema?.response,
|
|
191
203
|
// …and an `errors` contract — emit each as a non-2xx response.
|
|
192
|
-
responses:
|
|
204
|
+
responses: reflectedErrorsToResponses(route.schema?.errors),
|
|
193
205
|
operationId: undefined,
|
|
194
206
|
}, store, options.operations);
|
|
195
207
|
}
|
|
@@ -198,9 +210,9 @@ export function toOpenAPI(input, options = {}) {
|
|
|
198
210
|
for (const [name, op] of Object.entries(input)) {
|
|
199
211
|
addOperation(paths, op.method, {
|
|
200
212
|
path: op.path,
|
|
201
|
-
body: op.body,
|
|
202
|
-
query: op.query,
|
|
203
|
-
response: op.response,
|
|
213
|
+
body: op.body === undefined ? undefined : reflectSchema(op.body),
|
|
214
|
+
query: op.query === undefined ? undefined : reflectSchema(op.query),
|
|
215
|
+
response: op.response === undefined ? undefined : reflectSchema(op.response),
|
|
204
216
|
operationId: name,
|
|
205
217
|
summary: op.summary,
|
|
206
218
|
description: op.description,
|
|
@@ -209,7 +221,18 @@ export function toOpenAPI(input, options = {}) {
|
|
|
209
221
|
security: op.security,
|
|
210
222
|
requestContentType: op.requestContentType,
|
|
211
223
|
responseContentType: op.responseContentType,
|
|
212
|
-
responses: op.responses
|
|
224
|
+
responses: op.responses === undefined
|
|
225
|
+
? undefined
|
|
226
|
+
: Object.fromEntries(Object.entries(op.responses).map(([status, response]) => {
|
|
227
|
+
const { schema, ...metadata } = response;
|
|
228
|
+
return [
|
|
229
|
+
status,
|
|
230
|
+
{
|
|
231
|
+
...metadata,
|
|
232
|
+
...(schema === undefined ? {} : { schema: reflectSchema(schema) }),
|
|
233
|
+
},
|
|
234
|
+
];
|
|
235
|
+
})),
|
|
213
236
|
}, store, options.operations);
|
|
214
237
|
}
|
|
215
238
|
}
|
package/dist/openapi.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openapi.js","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"openapi.js","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,aAAa,EACb,aAAa,GAEd,MAAM,0BAA0B,CAAA;AAoGjC;;;;GAIG;AACH,MAAM,WAAW;IACN,OAAO,GAA+B,EAAE,CAAA;IAChC,cAAc,GAAG,IAAI,GAAG,EAAkB,CAAA;IAC1C,YAAY,GAAG,IAAI,GAAG,EAAkB,CAAA;IAEjD,aAAa,CAAC,EAAU;QAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACzC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,KAAK,CAAA;QACrC,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;aAC9C,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;aACjD,IAAI,CAAC,EAAE,CAAC,CAAA;QACX,MAAM,IAAI,GAAG,mBAAmB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,OAAO,EAAE,CAAA;QACpE,IAAI,IAAI,GAAG,IAAI,CAAA;QACf,IAAI,MAAM,GAAG,CAAC,CAAA;QACd,OAAO,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC;YACzE,IAAI,GAAG,GAAG,IAAI,IAAI,MAAM,EAAE,CAAA;YAC1B,MAAM,IAAI,CAAC,CAAA;QACb,CAAC;QACD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,CAAA;QACjC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;QAC/B,OAAO,IAAI,CAAA;IACb,CAAC;IAED,iGAAiG;IACjG,OAAO,CAAC,MAA8B;QACpC,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,SAAS,CAAA;QAC1C,IAAI,OAAO,MAAM,KAAK,SAAS;YAAE,OAAO,MAAM,CAAA;QAC9C,MAAM,EAAE,GAAG,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAA;QAClE,IAAI,EAAE,KAAK,SAAS;YAAE,OAAO,MAAM,CAAA;QACnC,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC,CAAA;QAC5C,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,EAAE,CAAC;YAChD,MAAM,OAAO,GAAG,EAAE,GAAG,MAAM,EAAE,CAAA;YAC7B,OAAO,OAAO,CAAC,GAAG,CAAA,CAAC,kEAAkE;YACrF,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,EAAE;gBACjD,KAAK,EAAE,OAAO;gBACd,UAAU,EAAE,IAAI;gBAChB,YAAY,EAAE,IAAI;gBAClB,QAAQ,EAAE,IAAI;aACf,CAAC,CAAA;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,wBAAwB,aAAa,EAAE,EAAE,CAAA;IAC1D,CAAC;CACF;AAED,+EAA+E;AAC/E,SAAS,YAAY,CAAC,OAAe;IACnC,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IACpD,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAA;IACtF,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,2EAA2E;AAC3E,SAAS,eAAe,CAAC,IAAY;IACnC,OAAO,IAAI;SACR,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;QACf,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,CAAA;QACnC,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG,CAAA;IACrD,CAAC,CAAC;SACD,IAAI,CAAC,GAAG,CAAC,CAAA;AACd,CAAC;AAED,SAAS,cAAc,CAAC,IAAY;IAClC,MAAM,MAAM,GAAuB,EAAE,CAAA;IACrC,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,CAAC,CAAA;QAClC,yDAAyD;QACzD,IAAI,IAAI,KAAK,SAAS;YACpB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;IACjF,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED,SAAS,eAAe,CAAC,MAAoC;IAC3D,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,EAAE,CAAA;IACnC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAA;IAC5B,oFAAoF;IACpF,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,EAAE,CAAA;IACnC,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAC5B,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,EAAE,EAAE,OAAO;QACX,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,MAAM,EAAE,KAAK,CAAC,MAAM;KACrB,CAAC,CAAC,CAAA;AACL,CAAC;AAwBD,MAAM,WAAW,GAAqC;IACpD,KAAK,EAAE,aAAa;IACpB,KAAK,EAAE,cAAc;IACrB,KAAK,EAAE,WAAW;IAClB,KAAK,EAAE,WAAW;IAClB,KAAK,EAAE,oBAAoB;IAC3B,KAAK,EAAE,UAAU;IACjB,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,wBAAwB;IAC/B,KAAK,EAAE,sBAAsB;IAC7B,KAAK,EAAE,mBAAmB;IAC1B,KAAK,EAAE,uBAAuB;IAC9B,KAAK,EAAE,aAAa;IACpB,KAAK,EAAE,qBAAqB;CAC7B,CAAA;AAED,SAAS,0BAA0B,CACjC,MAAsC;IAEtC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IAC1C,MAAM,GAAG,GAAwE,EAAE,CAAA;IACnF,KAAK,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1D,IAAI,UAAU,CAAC,UAAU,KAAK,SAAS,IAAI,UAAU,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC7E,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,IAAI,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAA;QACnF,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAA;AACtD,CAAC;AAED,SAAS,cAAc,CACrB,KAAqB,EACrB,KAAkB;IAElB,MAAM,SAAS,GAAoC,EAAE,CAAA;IACrD,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QACjC,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAA;QACvD,SAAS,CAAC,KAAK,CAAC;YACd,MAAM,KAAK,SAAS;gBAClB,CAAC,CAAC;oBACE,WAAW,EAAE,IAAI;oBACjB,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,mBAAmB,IAAI,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE;iBAC3E;gBACH,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAA;IAC7B,CAAC;SAAM,CAAC;QACN,SAAS,CAAC,KAAK,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,CAAA;IAC1C,CAAC;IACD,oEAAoE;IACpE,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QAClC,KAAK,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;YAC5D,MAAM,QAAQ,GAAoB,EAAE,WAAW,EAAE,GAAG,CAAC,WAAW,IAAI,EAAE,EAAE,CAAA;YACxE,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;YAC1F,IAAI,MAAM,KAAK,SAAS;gBACtB,QAAQ,CAAC,OAAO,GAAG,EAAE,CAAC,GAAG,CAAC,WAAW,IAAI,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAA;YAC5E,SAAS,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAA;QAC9B,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,SAAS,cAAc,CAAC,KAAqB,EAAE,KAAkB;IAC/D,MAAM,SAAS,GAAqB,EAAE,SAAS,EAAE,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAA;IAC/E,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS;QAAE,SAAS,CAAC,WAAW,GAAG,KAAK,CAAC,WAAW,CAAA;IAC9E,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;QAAE,SAAS,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAA;IAClE,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS;QAAE,SAAS,CAAC,WAAW,GAAG,KAAK,CAAC,WAAW,CAAA;IAC9E,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;QAAE,SAAS,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAA;IAClF,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI;QAAE,SAAS,CAAC,UAAU,GAAG,IAAI,CAAA;IAC1D,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS;QAAE,SAAS,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAA,CAAC,2BAA2B;IAEjG,MAAM,UAAU,GAAG,CAAC,GAAG,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAA;IACnF,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;QAAE,SAAS,CAAC,UAAU,GAAG,UAAU,CAAA;IAE5D,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;QACnD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,SAAS,CAAC,WAAW,GAAG;gBACtB,QAAQ,EAAE,IAAI;gBACd,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,kBAAkB,IAAI,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE;aAC1E,CAAA;QACH,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,SAAS,YAAY,CACnB,KAAuD,EACvD,MAAc,EACd,KAAqB,EACrB,KAAkB,EAClB,UAA0C;IAE1C,MAAM,SAAS,GAAG,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAC7C,MAAM,QAAQ,GAAG,KAAK,CAAC,SAAS,CAAC,IAAI,EAAE,CAAA;IACvC,KAAK,CAAC,SAAS,CAAC,GAAG,QAAQ,CAAA;IAC3B,IAAI,SAAS,GAAG,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;IAC5C,oEAAoE;IACpE,MAAM,QAAQ,GACZ,UAAU,EAAE,CAAC,KAAK,CAAC,WAAW,IAAI,EAAE,CAAC,IAAI,UAAU,EAAE,CAAC,GAAG,MAAM,CAAC,WAAW,EAAE,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,CAAA;IAChG,IAAI,QAAQ,KAAK,SAAS;QAAE,SAAS,GAAG,EAAE,GAAG,SAAS,EAAE,GAAG,QAAQ,EAAsB,CAAA;IACzF,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,GAAG,SAAS,CAAA;AAC5C,CAAC;AAED,SAAS,KAAK,CAAC,KAA6B;IAC1C,uFAAuF;IACvF,8EAA8E;IAC9E,OAAO,OAAQ,KAA8B,CAAC,MAAM,KAAK,UAAU,CAAA;AACrE,CAAC;AAED,kHAAkH;AAClH,MAAM,UAAU,SAAS,CACvB,KAA6B,EAC7B,UAA4B,EAAE;IAE9B,MAAM,KAAK,GAAqD,EAAE,CAAA;IAClE,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAA;IAE/B,IAAI,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QACjB,KAAK,MAAM,KAAK,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;YACzC,YAAY,CACV,KAAK,EACL,KAAK,CAAC,MAAM,EACZ;gBACE,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,IAAI;gBACxB,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK;gBAC1B,kFAAkF;gBAClF,QAAQ,EAAE,KAAK,CAAC,MAAM,EAAE,QAAQ;gBAChC,+DAA+D;gBAC/D,SAAS,EAAE,0BAA0B,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC;gBAC3D,WAAW,EAAE,SAAS;aACvB,EACD,KAAK,EACL,OAAO,CAAC,UAAU,CACnB,CAAA;QACH,CAAC;IACH,CAAC;SAAM,CAAC;QACN,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAC/C,YAAY,CACV,KAAK,EACL,EAAE,CAAC,MAAM,EACT;gBACE,IAAI,EAAE,EAAE,CAAC,IAAI;gBACb,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,CAAC,IAAI,CAAC;gBAChE,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,CAAC,KAAK,CAAC;gBACnE,QAAQ,EAAE,EAAE,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,CAAC,QAAQ,CAAC;gBAC5E,WAAW,EAAE,IAAI;gBACjB,OAAO,EAAE,EAAE,CAAC,OAAO;gBACnB,WAAW,EAAE,EAAE,CAAC,WAAW;gBAC3B,IAAI,EAAE,EAAE,CAAC,IAAI;gBACb,UAAU,EAAE,EAAE,CAAC,UAAU;gBACzB,QAAQ,EAAE,EAAE,CAAC,QAAQ;gBACrB,kBAAkB,EAAE,EAAE,CAAC,kBAAkB;gBACzC,mBAAmB,EAAE,EAAE,CAAC,mBAAmB;gBAC3C,SAAS,EACP,EAAE,CAAC,SAAS,KAAK,SAAS;oBACxB,CAAC,CAAC,SAAS;oBACX,CAAC,CAAC,MAAM,CAAC,WAAW,CAChB,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,EAAE;wBACtD,MAAM,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,GAAG,QAAQ,CAAA;wBACxC,OAAO;4BACL,MAAM;4BACN;gCACE,GAAG,QAAQ;gCACX,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC;6BACnE;yBACF,CAAA;oBACH,CAAC,CAAC,CACH;aACR,EACD,KAAK,EACL,OAAO,CAAC,UAAU,CACnB,CAAA;QACH,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAsB,EAAE,CAAA;IACxC,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC;QAAE,UAAU,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAA;IAC7E,IAAI,OAAO,CAAC,eAAe,KAAK,SAAS;QAAE,UAAU,CAAC,eAAe,GAAG,OAAO,CAAC,eAAe,CAAA;IAE/F,OAAO;QACL,OAAO,EAAE,OAAO;QAChB,IAAI,EAAE;YACJ,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,KAAK;YAC7B,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,OAAO;YACnC,GAAG,CAAC,OAAO,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACnF;QACD,KAAK;QACL,GAAG,CAAC,OAAO,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtE,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7D,GAAG,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9D,CAAA;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/schema",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Built-in schema builder `t` — TypeBox-backed (free JSON Schema + compiled validators), implements Standard Schema so nifra routes validate it natively.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@sinclair/typebox": "^0.34.49"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"@nifrajs/core": "^1.
|
|
51
|
+
"@nifrajs/core": "^1.5.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@nifrajs/core": "workspace:*",
|
package/src/openapi.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import type { ContractShape, Server
|
|
1
|
+
import type { ContractShape, Server } from "@nifrajs/core"
|
|
2
|
+
import {
|
|
3
|
+
type JsonSchema,
|
|
4
|
+
type ReflectedRouteSchema,
|
|
5
|
+
reflectRoutes,
|
|
6
|
+
reflectSchema,
|
|
7
|
+
type SchemaReflection,
|
|
8
|
+
} from "@nifrajs/core/reflection"
|
|
2
9
|
|
|
3
10
|
/**
|
|
4
11
|
* OpenAPI 3.1 generation. We model a practical slice of the spec — enough to feed Swagger UI / codegen
|
|
@@ -50,8 +57,6 @@ export interface ToOpenAPIOptions {
|
|
|
50
57
|
readonly operations?: Readonly<Record<string, Record<string, unknown>>>
|
|
51
58
|
}
|
|
52
59
|
|
|
53
|
-
type JsonSchema = Record<string, unknown>
|
|
54
|
-
|
|
55
60
|
interface OpenAPIParameter {
|
|
56
61
|
readonly name: string
|
|
57
62
|
readonly in: "path" | "query"
|
|
@@ -100,27 +105,6 @@ export interface OpenAPIDocument {
|
|
|
100
105
|
readonly components?: OpenAPIComponents
|
|
101
106
|
}
|
|
102
107
|
|
|
103
|
-
/** A Standard Schema that also carries a raw JSON Schema (i.e. a `t`/TypeBox schema). */
|
|
104
|
-
type WithJsonSchema = StandardSchemaV1 & { readonly jsonSchema: unknown }
|
|
105
|
-
|
|
106
|
-
function hasJsonSchema(schema: StandardSchemaV1): schema is WithJsonSchema {
|
|
107
|
-
return (
|
|
108
|
-
"jsonSchema" in schema &&
|
|
109
|
-
(schema as { jsonSchema?: unknown }).jsonSchema !== undefined &&
|
|
110
|
-
(schema as { jsonSchema?: unknown }).jsonSchema !== null
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Extract clean JSON Schema from a schema, or `undefined` for a BYO Standard Schema with no JSON
|
|
116
|
-
* Schema. The `JSON` round-trip strips TypeBox's Symbol-keyed metadata; a string `$id` survives it and
|
|
117
|
-
* drives `$ref` reuse.
|
|
118
|
-
*/
|
|
119
|
-
function toJsonSchema(schema: StandardSchemaV1): JsonSchema | undefined {
|
|
120
|
-
if (!hasJsonSchema(schema)) return undefined
|
|
121
|
-
return JSON.parse(JSON.stringify(schema.jsonSchema)) as JsonSchema
|
|
122
|
-
}
|
|
123
|
-
|
|
124
108
|
/**
|
|
125
109
|
* Collects schemas that carry a `$id` into `components.schemas`, returning a `$ref` in their place —
|
|
126
110
|
* so a schema used by N operations is emitted once. Schemas without a `$id` stay inline (the existing
|
|
@@ -128,18 +112,45 @@ function toJsonSchema(schema: StandardSchemaV1): JsonSchema | undefined {
|
|
|
128
112
|
*/
|
|
129
113
|
class SchemaStore {
|
|
130
114
|
readonly schemas: Record<string, JsonSchema> = {}
|
|
115
|
+
private readonly componentNames = new Map<string, string>()
|
|
116
|
+
private readonly componentIds = new Map<string, string>()
|
|
117
|
+
|
|
118
|
+
private componentName(id: string): string {
|
|
119
|
+
const known = this.componentNames.get(id)
|
|
120
|
+
if (known !== undefined) return known
|
|
121
|
+
const encoded = [...new TextEncoder().encode(id)]
|
|
122
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
123
|
+
.join("")
|
|
124
|
+
const base = /^[A-Za-z0-9._-]+$/.test(id) ? id : `Schema_${encoded}`
|
|
125
|
+
let name = base
|
|
126
|
+
let suffix = 2
|
|
127
|
+
while (this.componentIds.has(name) && this.componentIds.get(name) !== id) {
|
|
128
|
+
name = `${base}_${suffix}`
|
|
129
|
+
suffix += 1
|
|
130
|
+
}
|
|
131
|
+
this.componentNames.set(id, name)
|
|
132
|
+
this.componentIds.set(name, id)
|
|
133
|
+
return name
|
|
134
|
+
}
|
|
131
135
|
|
|
132
136
|
/** Hoist a schema with a `$id` into components + return a `$ref`; otherwise return it inline. */
|
|
133
137
|
collect(schema: JsonSchema | undefined): JsonSchema | undefined {
|
|
134
138
|
if (schema === undefined) return undefined
|
|
139
|
+
if (typeof schema === "boolean") return schema
|
|
135
140
|
const id = typeof schema.$id === "string" ? schema.$id : undefined
|
|
136
141
|
if (id === undefined) return schema
|
|
137
|
-
|
|
142
|
+
const componentName = this.componentName(id)
|
|
143
|
+
if (!Object.hasOwn(this.schemas, componentName)) {
|
|
138
144
|
const hoisted = { ...schema }
|
|
139
145
|
delete hoisted.$id // the component key is the name; drop the redundant base-URI hint
|
|
140
|
-
this.schemas
|
|
146
|
+
Object.defineProperty(this.schemas, componentName, {
|
|
147
|
+
value: hoisted,
|
|
148
|
+
enumerable: true,
|
|
149
|
+
configurable: true,
|
|
150
|
+
writable: true,
|
|
151
|
+
})
|
|
141
152
|
}
|
|
142
|
-
return { $ref: `#/components/schemas/${
|
|
153
|
+
return { $ref: `#/components/schemas/${componentName}` }
|
|
143
154
|
}
|
|
144
155
|
}
|
|
145
156
|
|
|
@@ -172,26 +183,24 @@ function pathParameters(path: string): OpenAPIParameter[] {
|
|
|
172
183
|
return params
|
|
173
184
|
}
|
|
174
185
|
|
|
175
|
-
function queryParameters(schema:
|
|
186
|
+
function queryParameters(schema: SchemaReflection | undefined): OpenAPIParameter[] {
|
|
176
187
|
if (schema === undefined) return []
|
|
177
|
-
const
|
|
178
|
-
// Only an object schema decomposes into individual query parameters.
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return Object.entries(properties).map(([name, propSchema]) => ({
|
|
183
|
-
name,
|
|
188
|
+
const fields = schema.fields
|
|
189
|
+
// Only an introspectable object schema decomposes into individual query parameters.
|
|
190
|
+
if (fields === undefined) return []
|
|
191
|
+
return fields.map((field) => ({
|
|
192
|
+
name: field.name,
|
|
184
193
|
in: "query",
|
|
185
|
-
required: required
|
|
186
|
-
schema:
|
|
194
|
+
required: field.required,
|
|
195
|
+
schema: field.schema,
|
|
187
196
|
}))
|
|
188
197
|
}
|
|
189
198
|
|
|
190
199
|
interface OperationInput {
|
|
191
200
|
readonly path: string
|
|
192
|
-
readonly body:
|
|
193
|
-
readonly query:
|
|
194
|
-
readonly response:
|
|
201
|
+
readonly body: SchemaReflection | undefined
|
|
202
|
+
readonly query: SchemaReflection | undefined
|
|
203
|
+
readonly response: SchemaReflection | undefined
|
|
195
204
|
readonly operationId: string | undefined
|
|
196
205
|
// `| undefined` (not just `?`) so a contract op's optional fields — `string | undefined` etc. — are
|
|
197
206
|
// assignable under `exactOptionalPropertyTypes` when spread into this literal.
|
|
@@ -204,7 +213,7 @@ interface OperationInput {
|
|
|
204
213
|
readonly responseContentType?: string | undefined
|
|
205
214
|
readonly responses?:
|
|
206
215
|
| Readonly<
|
|
207
|
-
Record<string, { description?: string; schema?:
|
|
216
|
+
Record<string, { description?: string; schema?: SchemaReflection; contentType?: string }>
|
|
208
217
|
>
|
|
209
218
|
| undefined
|
|
210
219
|
}
|
|
@@ -225,17 +234,17 @@ const STATUS_TEXT: Readonly<Record<string, string>> = {
|
|
|
225
234
|
"503": "Service Unavailable",
|
|
226
235
|
}
|
|
227
236
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
function errorsToResponses(
|
|
231
|
-
errors: Readonly<Record<number, StandardSchemaV1>> | undefined,
|
|
237
|
+
function reflectedErrorsToResponses(
|
|
238
|
+
errors: ReflectedRouteSchema["errors"],
|
|
232
239
|
): OperationInput["responses"] {
|
|
233
240
|
if (errors === undefined) return undefined
|
|
234
|
-
const out: Record<string, { description?: string; schema?:
|
|
235
|
-
for (const [status,
|
|
236
|
-
|
|
241
|
+
const out: Record<string, { description?: string; schema?: SchemaReflection }> = {}
|
|
242
|
+
for (const [status, reflection] of Object.entries(errors)) {
|
|
243
|
+
if (reflection.jsonSchema !== undefined || reflection.standard !== undefined) {
|
|
244
|
+
out[status] = { description: STATUS_TEXT[status] ?? "Error", schema: reflection }
|
|
245
|
+
}
|
|
237
246
|
}
|
|
238
|
-
return out
|
|
247
|
+
return Object.keys(out).length > 0 ? out : undefined
|
|
239
248
|
}
|
|
240
249
|
|
|
241
250
|
function buildResponses(
|
|
@@ -244,7 +253,7 @@ function buildResponses(
|
|
|
244
253
|
): Record<string, OpenAPIResponse> {
|
|
245
254
|
const responses: Record<string, OpenAPIResponse> = {}
|
|
246
255
|
if (input.response !== undefined) {
|
|
247
|
-
const schema = store.collect(
|
|
256
|
+
const schema = store.collect(input.response.jsonSchema)
|
|
248
257
|
responses["200"] =
|
|
249
258
|
schema !== undefined
|
|
250
259
|
? {
|
|
@@ -259,7 +268,7 @@ function buildResponses(
|
|
|
259
268
|
if (input.responses !== undefined) {
|
|
260
269
|
for (const [status, def] of Object.entries(input.responses)) {
|
|
261
270
|
const response: OpenAPIResponse = { description: def.description ?? "" }
|
|
262
|
-
const schema = def.schema !== undefined ? store.collect(
|
|
271
|
+
const schema = def.schema !== undefined ? store.collect(def.schema.jsonSchema) : undefined
|
|
263
272
|
if (schema !== undefined)
|
|
264
273
|
response.content = { [def.contentType ?? "application/json"]: { schema } }
|
|
265
274
|
responses[status] = response
|
|
@@ -281,7 +290,7 @@ function buildOperation(input: OperationInput, store: SchemaStore): OpenAPIOpera
|
|
|
281
290
|
if (parameters.length > 0) operation.parameters = parameters
|
|
282
291
|
|
|
283
292
|
if (input.body !== undefined) {
|
|
284
|
-
const schema = store.collect(
|
|
293
|
+
const schema = store.collect(input.body.jsonSchema)
|
|
285
294
|
if (schema !== undefined) {
|
|
286
295
|
operation.requestBody = {
|
|
287
296
|
required: true,
|
|
@@ -325,7 +334,7 @@ export function toOpenAPI(
|
|
|
325
334
|
const store = new SchemaStore()
|
|
326
335
|
|
|
327
336
|
if (isApp(input)) {
|
|
328
|
-
for (const route of input
|
|
337
|
+
for (const route of reflectRoutes(input)) {
|
|
329
338
|
addOperation(
|
|
330
339
|
paths,
|
|
331
340
|
route.method,
|
|
@@ -336,7 +345,7 @@ export function toOpenAPI(
|
|
|
336
345
|
// A route may now declare a `response` contract — emit it as the 200 body schema.
|
|
337
346
|
response: route.schema?.response,
|
|
338
347
|
// …and an `errors` contract — emit each as a non-2xx response.
|
|
339
|
-
responses:
|
|
348
|
+
responses: reflectedErrorsToResponses(route.schema?.errors),
|
|
340
349
|
operationId: undefined,
|
|
341
350
|
},
|
|
342
351
|
store,
|
|
@@ -350,9 +359,9 @@ export function toOpenAPI(
|
|
|
350
359
|
op.method,
|
|
351
360
|
{
|
|
352
361
|
path: op.path,
|
|
353
|
-
body: op.body,
|
|
354
|
-
query: op.query,
|
|
355
|
-
response: op.response,
|
|
362
|
+
body: op.body === undefined ? undefined : reflectSchema(op.body),
|
|
363
|
+
query: op.query === undefined ? undefined : reflectSchema(op.query),
|
|
364
|
+
response: op.response === undefined ? undefined : reflectSchema(op.response),
|
|
356
365
|
operationId: name,
|
|
357
366
|
summary: op.summary,
|
|
358
367
|
description: op.description,
|
|
@@ -361,7 +370,21 @@ export function toOpenAPI(
|
|
|
361
370
|
security: op.security,
|
|
362
371
|
requestContentType: op.requestContentType,
|
|
363
372
|
responseContentType: op.responseContentType,
|
|
364
|
-
responses:
|
|
373
|
+
responses:
|
|
374
|
+
op.responses === undefined
|
|
375
|
+
? undefined
|
|
376
|
+
: Object.fromEntries(
|
|
377
|
+
Object.entries(op.responses).map(([status, response]) => {
|
|
378
|
+
const { schema, ...metadata } = response
|
|
379
|
+
return [
|
|
380
|
+
status,
|
|
381
|
+
{
|
|
382
|
+
...metadata,
|
|
383
|
+
...(schema === undefined ? {} : { schema: reflectSchema(schema) }),
|
|
384
|
+
},
|
|
385
|
+
]
|
|
386
|
+
}),
|
|
387
|
+
),
|
|
365
388
|
},
|
|
366
389
|
store,
|
|
367
390
|
options.operations,
|