@tetsujs/openapi 0.5.2 → 0.6.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 +63 -18
- package/dist/src/document.d.ts +1 -0
- package/dist/src/document.d.ts.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +159 -26
- package/dist/src/index.js.map +7 -6
- package/dist/src/operation.d.ts.map +1 -1
- package/dist/src/refs.d.ts +36 -0
- package/dist/src/refs.d.ts.map +1 -0
- package/dist/src/testing.d.ts +3 -0
- package/dist/src/testing.d.ts.map +1 -1
- package/dist/src/testing.js +8 -4
- package/dist/src/testing.js.map +3 -3
- package/package.json +3 -3
- package/src/document.ts +1 -0
- package/src/index.ts +51 -11
- package/src/operation.ts +161 -12
- package/src/page.ts +13 -4
- package/src/refs.ts +136 -0
- package/src/testing.ts +20 -5
package/README.md
CHANGED
|
@@ -91,12 +91,12 @@ wrote, by a rule:
|
|
|
91
91
|
| parameters | `schema.params`, `query`, `headers`, `cookies` | required as the schema says |
|
|
92
92
|
| the request body | `schema.body` and `bodyType` | media types from `bodyType`; required unless `text` or `stream` |
|
|
93
93
|
| `security` | `secured()` hooks, of every level the route runs under | every hook required together; `anyOf` inside one hook is the alternatives |
|
|
94
|
-
| the statuses | the route's `schema.response`, the hooks' `documented()` and `secured()`, the framework | one schema is `200`, a map its statuses, `null` a status without a body; with no `
|
|
94
|
+
| the statuses | the route's `schema.response`, the hooks' `documented()` and `secured()`, the framework | one schema is `200`, a map its statuses, `null` or an entry without `body` a status without a body; with no `schema.response` at all, `200`; the framework's `422`, `400`, `413`, `500` where they can happen — see [Responses the framework adds](#responses-the-framework-adds) |
|
|
95
95
|
| a status's body | everything answering with that status | one flat `anyOf`, the route's own first, the same body once |
|
|
96
96
|
| an error envelope | any of them | one definition per status and code in `components`, named after the code; the route's own kept |
|
|
97
97
|
| `discriminator` | the envelopes of a status | on `error`, or the format's field, when every alternative is an envelope |
|
|
98
98
|
| a status's description | the schemas' `description`, the hooks', the framework's | one is the description, several a list led by code, none the reason phrase |
|
|
99
|
-
| a status's headers | the hooks' `documented()` headers | possible, not required |
|
|
99
|
+
| a status's headers | the route's entry — `{ body, headers, cookies }` — and the hooks' `documented()` headers | the route's required as its schema says, a property's `description` the header's; its cookies one `set-cookie` header listing them; a hook's possible, not required; the route's wins a name both give |
|
|
100
100
|
| the document's `tags` | the `tags` option | in its order, then the tags routes use and it leaves out |
|
|
101
101
|
|
|
102
102
|
`docs: { hidden: true }` leaves a route out of the document; it is served
|
|
@@ -124,9 +124,24 @@ the routes use and `tags` leaves out is listed after them, in the order
|
|
|
124
124
|
the routes first use it, and reported — it is usually one tag spelled two
|
|
125
125
|
ways, `sign_in` next to `sign-in`. So is a tag described and used by no
|
|
126
126
|
operation. Without `tags` the document lists none, and nothing is
|
|
127
|
-
reported.
|
|
128
|
-
|
|
129
|
-
`docs()`
|
|
127
|
+
reported.
|
|
128
|
+
|
|
129
|
+
A `docs()` documents the application it is mounted in, all of it. Several
|
|
130
|
+
surfaces with a document each — a public API and an admin one — are
|
|
131
|
+
several applications, each with its own `docs()` and only the tags of its
|
|
132
|
+
own routes:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
const api = Bun.serve({
|
|
136
|
+
...createApp({ routes: [orders, docs({ info, tags: { orders: "Orders" } })] }),
|
|
137
|
+
port: 3000,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const admin = Bun.serve({
|
|
141
|
+
...createApp({ routes: [users, docs({ info: adminInfo, tags: { users: "Users" } })] }),
|
|
142
|
+
port: 3001,
|
|
143
|
+
});
|
|
144
|
+
```
|
|
130
145
|
|
|
131
146
|
## Operation ids
|
|
132
147
|
|
|
@@ -210,18 +225,37 @@ A status nothing describes keeps its reason phrase — `Not Found`,
|
|
|
210
225
|
|
|
211
226
|
Every failure reaches the application's `onError` hooks — a thrown
|
|
212
227
|
`HttpError`, a validation or body failure, a `404`, a `405`, a rate limit's
|
|
213
|
-
refusal — so one hook answers all of them in
|
|
228
|
+
refusal, and an error nothing expected — so one hook answers all of them in
|
|
229
|
+
your format:
|
|
214
230
|
|
|
215
231
|
```ts
|
|
216
|
-
|
|
217
|
-
|
|
232
|
+
import type { ErrorBody } from "@tetsujs/core";
|
|
233
|
+
import { hook, HttpError, reportFailure } from "@tetsujs/core";
|
|
218
234
|
|
|
219
|
-
|
|
235
|
+
const inOurFormat = hook.onError((ctx) => {
|
|
236
|
+
const { error } = ctx;
|
|
220
237
|
|
|
221
|
-
|
|
238
|
+
if (error instanceof HttpError) {
|
|
239
|
+
const { status, error: code, ...rest } = error.body as ErrorBody;
|
|
240
|
+
|
|
241
|
+
return Response.json({ code, ...rest }, { status });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
reportFailure(ctx, "unhandled", error);
|
|
245
|
+
|
|
246
|
+
return Response.json(
|
|
247
|
+
{ code: "INTERNAL_SERVER_ERROR", message: "Internal Server Error" },
|
|
248
|
+
{ status: 500 },
|
|
249
|
+
);
|
|
222
250
|
});
|
|
223
251
|
```
|
|
224
252
|
|
|
253
|
+
An error the hook answers is one nothing else reports: `reportError` hears
|
|
254
|
+
of a failure only when no `onError` hook answered it. So the hook that
|
|
255
|
+
answers the unexpected ones — a database gone, a `TypeError` — reports them
|
|
256
|
+
itself; left out, they answer in your format and never reach the error
|
|
257
|
+
tracker.
|
|
258
|
+
|
|
225
259
|
The document is generated from the routes, not from that hook, so it is
|
|
226
260
|
told the same format with `errors`:
|
|
227
261
|
|
|
@@ -269,16 +303,19 @@ code: (schema) => {
|
|
|
269
303
|
|
|
270
304
|
The hook and `errors` describe one format in two places — the core knows
|
|
271
305
|
nothing about documents, and a function cannot be read for the shape it
|
|
272
|
-
returns — so keep a test that holds them together: provoke
|
|
273
|
-
check
|
|
274
|
-
|
|
306
|
+
returns — so keep a test that holds them together: provoke the failures,
|
|
307
|
+
the unexpected one included, and check each against the document with
|
|
308
|
+
[`assertDescribed`](#testing-against-the-document):
|
|
275
309
|
|
|
276
310
|
```ts
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
311
|
+
const boom = route({ method: "GET", path: "/boom", handler: () => { throw new Error("boom"); } });
|
|
312
|
+
const app = createApp({ hooks: { onError: [inOurFormat] }, routes: [api, boom] });
|
|
313
|
+
const request = serve(app);
|
|
314
|
+
const { document } = openapi(app, { info, errors });
|
|
315
|
+
|
|
316
|
+
test("failures are what the document says", async () => {
|
|
317
|
+
await assertDescribed(document, "POST /items", await request("/items", { method: "POST", body: "{}" }));
|
|
318
|
+
await assertDescribed(document, "GET /boom", await request("/boom"));
|
|
282
319
|
});
|
|
283
320
|
```
|
|
284
321
|
|
|
@@ -418,3 +455,11 @@ gap is reported instead of failing the startup:
|
|
|
418
455
|
This happens with a validator that does not emit JSON Schema, a schema
|
|
419
456
|
whose conversion throws, and two routes that map to the same OpenAPI path
|
|
420
457
|
(`/files/*` and `/files/:wildcard` are both `/files/{wildcard}`).
|
|
458
|
+
|
|
459
|
+
It also happens with a recursive or named schema. A schema is embedded as
|
|
460
|
+
the validator emits it, and the references inside it — `#` for its own
|
|
461
|
+
root, `#/$defs/…` for a definition next to it — resolve against the
|
|
462
|
+
document's root once embedded, where they lead nowhere. Zod's recursive
|
|
463
|
+
getters and `.meta({ id })`, ArkType's scopes and Valibot's `lazy` emit
|
|
464
|
+
them; TypeBox's `Type.Cyclic` names itself with `$id` and is described as
|
|
465
|
+
it is.
|
package/dist/src/document.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../../src/document.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../../src/document.ts"],"names":[],"mappings":"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"}
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,KAAK,EAAE,eAAe,EAA6B,MAAM,eAAe,CAAC;AAEhF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,KAAK,EAAE,eAAe,EAA6B,MAAM,eAAe,CAAC;AAEhF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAM/C,YAAY,EACV,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,QAAQ,EACR,oBAAoB,EACpB,mBAAmB,EACnB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACvD,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvD,YAAY,EACV,UAAU,EACV,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,eAAe,EACf,eAAe,EACf,eAAe,EACf,cAAc,EACd,cAAc,EACd,SAAS,GACV,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAClE,YAAY,EACV,UAAU,EACV,kBAAkB,EAClB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC;IAEvC,uCAAuC;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;IAE9C;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAE9B;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAClD;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,sEAAsE;AACtE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAChD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,cAAc,GAAG,eAAe,CA2G1E"}
|
package/dist/src/index.js
CHANGED
|
@@ -373,8 +373,8 @@ function operationOf(entry, options, components, envelopes, ids, warn) {
|
|
|
373
373
|
};
|
|
374
374
|
}
|
|
375
375
|
function declareEnvelopes(entry, envelopes) {
|
|
376
|
-
for (const [status,
|
|
377
|
-
const described =
|
|
376
|
+
for (const [status, { body }] of declaredResponses(entry)) {
|
|
377
|
+
const described = body === null ? undefined : emitted(body, "", () => {});
|
|
378
378
|
const code = envelopes.format.code;
|
|
379
379
|
for (const branch of described ? branchesOf(described, code) : []) {
|
|
380
380
|
const found = code(branch);
|
|
@@ -390,9 +390,62 @@ function declaredResponses(entry) {
|
|
|
390
390
|
return [];
|
|
391
391
|
}
|
|
392
392
|
if ("~standard" in response) {
|
|
393
|
-
return [["200", response]];
|
|
393
|
+
return [["200", { body: response }]];
|
|
394
394
|
}
|
|
395
|
-
return Object.entries(response)
|
|
395
|
+
return Object.entries(response).map(([status, value]) => [
|
|
396
|
+
status,
|
|
397
|
+
asDeclared(value)
|
|
398
|
+
]);
|
|
399
|
+
}
|
|
400
|
+
function asDeclared(value) {
|
|
401
|
+
if (value === null || "~standard" in value) {
|
|
402
|
+
return { body: value };
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
body: value.body ?? null,
|
|
406
|
+
...value.headers === undefined ? {} : { headers: value.headers },
|
|
407
|
+
...value.cookies === undefined ? {} : { cookies: value.cookies }
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function declaredHeaders(declared, warn) {
|
|
411
|
+
const headers = {};
|
|
412
|
+
const [properties, required] = shapeOf2(declared.headers, "a response's headers", warn);
|
|
413
|
+
for (const [name, property] of Object.entries(properties)) {
|
|
414
|
+
const { description, ...schema } = property;
|
|
415
|
+
headers[name] = {
|
|
416
|
+
...typeof description === "string" ? { description } : {},
|
|
417
|
+
...required.has(name) ? { required: true } : {},
|
|
418
|
+
schema
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
const cookies = setCookie(declared.cookies, warn);
|
|
422
|
+
if (cookies) {
|
|
423
|
+
headers["set-cookie"] = cookies;
|
|
424
|
+
}
|
|
425
|
+
return Object.keys(headers).length > 0 ? headers : undefined;
|
|
426
|
+
}
|
|
427
|
+
function setCookie(schema, warn) {
|
|
428
|
+
const [properties, required] = shapeOf2(schema, "a response's cookies", warn);
|
|
429
|
+
const names = Object.keys(properties);
|
|
430
|
+
if (names.length === 0) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const lines = names.map((name) => {
|
|
434
|
+
const description = properties[name]?.["description"];
|
|
435
|
+
return typeof description === "string" ? `- \`${name}\`: ${description}` : `- \`${name}\``;
|
|
436
|
+
});
|
|
437
|
+
return {
|
|
438
|
+
description: ["Sets these cookies:", "", ...lines].join(`
|
|
439
|
+
`),
|
|
440
|
+
...required.size > 0 ? { required: true } : {},
|
|
441
|
+
schema: { type: "string" }
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function shapeOf2(schema, subject, warn) {
|
|
445
|
+
const described = schema ? emitted(schema, subject, warn, "input") : undefined;
|
|
446
|
+
const properties = described?.properties ?? {};
|
|
447
|
+
const required = new Set(Array.isArray(described?.required) ? described.required : []);
|
|
448
|
+
return [properties, required];
|
|
396
449
|
}
|
|
397
450
|
function branchesFor(schema, status, envelopes, warn) {
|
|
398
451
|
return branchesOf(schema, envelopes.format.code).map((branch) => {
|
|
@@ -463,7 +516,7 @@ function derivedId(entry) {
|
|
|
463
516
|
}
|
|
464
517
|
function requestBody(entry, warn) {
|
|
465
518
|
const declared = entry.def.schema?.body;
|
|
466
|
-
const bodyType = entry.def.bodyType ?? (declared ? "json" : undefined);
|
|
519
|
+
const bodyType = entry.def.bodyType ?? (declared || entry.def.rawBody ? "json" : undefined);
|
|
467
520
|
if (!bodyType) {
|
|
468
521
|
return;
|
|
469
522
|
}
|
|
@@ -508,15 +561,18 @@ function responses(entry, options, contributed, components, envelopes, warn) {
|
|
|
508
561
|
const add = (status, answer) => {
|
|
509
562
|
answers.set(status, [...answers.get(status) ?? [], answer]);
|
|
510
563
|
};
|
|
511
|
-
|
|
512
|
-
|
|
564
|
+
const declared = declaredResponses(entry);
|
|
565
|
+
for (const [status, { body, ...rest }] of declared) {
|
|
566
|
+
const described = body === null ? undefined : emitted(body, "a response", warn);
|
|
567
|
+
const headers = declaredHeaders({ body, ...rest }, warn);
|
|
513
568
|
add(status, {
|
|
514
569
|
placeholder: describeStatus(status),
|
|
515
570
|
said: described ? wordsOf(described, envelopes) : [],
|
|
516
|
-
schemas: described ? branchesFor(unionOnly(described), Number(status), envelopes, warn) : []
|
|
571
|
+
schemas: described ? branchesFor(unionOnly(described), Number(status), envelopes, warn) : [],
|
|
572
|
+
...headers === undefined ? {} : { headers }
|
|
517
573
|
});
|
|
518
574
|
}
|
|
519
|
-
if (
|
|
575
|
+
if (declared.length === 0) {
|
|
520
576
|
add("200", { placeholder: "Successful response", said: [], schemas: [] });
|
|
521
577
|
}
|
|
522
578
|
for (const [status, answer] of failures(entry, options, contributed, components, envelopes, warn)) {
|
|
@@ -541,7 +597,7 @@ function failures(entry, options, contributed, components, envelopes, warn) {
|
|
|
541
597
|
const ref = error === undefined ? components.ref(failureName(status), described) : envelopes.ref(status, error, described, warn);
|
|
542
598
|
return ref;
|
|
543
599
|
};
|
|
544
|
-
if (schema?.params || schema?.query || schema?.headers || schema?.body) {
|
|
600
|
+
if (schema?.params || schema?.query || schema?.headers || schema?.cookies || schema?.body) {
|
|
545
601
|
found.push([
|
|
546
602
|
options.validationStatus,
|
|
547
603
|
{
|
|
@@ -587,7 +643,7 @@ function failures(entry, options, contributed, components, envelopes, warn) {
|
|
|
587
643
|
}
|
|
588
644
|
]);
|
|
589
645
|
}
|
|
590
|
-
if (schema?.body || entry.def.bodyType) {
|
|
646
|
+
if (schema?.body || entry.def.bodyType || entry.def.rawBody) {
|
|
591
647
|
const unparsable = parseFailure(entry.def.bodyType);
|
|
592
648
|
if (unparsable) {
|
|
593
649
|
found.push([
|
|
@@ -745,6 +801,63 @@ function capitalize(value) {
|
|
|
745
801
|
function lowerFirst(value) {
|
|
746
802
|
return value.charAt(0).toLowerCase() + value.slice(1);
|
|
747
803
|
}
|
|
804
|
+
|
|
805
|
+
// packages/openapi/src/refs.ts
|
|
806
|
+
var data = new Set(["const", "default", "enum", "example", "examples"]);
|
|
807
|
+
function unresolvedRefs(node, document) {
|
|
808
|
+
const unresolved = new Set;
|
|
809
|
+
const visited = new Map;
|
|
810
|
+
const pending = [[node, document]];
|
|
811
|
+
for (let next = pending.pop();next; next = pending.pop()) {
|
|
812
|
+
const [value, base] = next;
|
|
813
|
+
if (typeof value !== "object" || value === null) {
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
const bases = visited.get(value) ?? new Set;
|
|
817
|
+
if (bases.has(base)) {
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
visited.set(value, bases.add(base));
|
|
821
|
+
const schema = value;
|
|
822
|
+
const resource = !Array.isArray(value) && typeof schema["$id"] === "string" ? schema : base;
|
|
823
|
+
const ref = Array.isArray(value) ? undefined : schema["$ref"];
|
|
824
|
+
if (typeof ref === "string" && ref.startsWith("#") && !resolves(ref.slice(1), resource, document)) {
|
|
825
|
+
unresolved.add(ref);
|
|
826
|
+
}
|
|
827
|
+
const children = Object.entries(schema).filter(([keyword]) => Array.isArray(value) || !data.has(keyword));
|
|
828
|
+
for (let index = children.length - 1;index >= 0; index -= 1) {
|
|
829
|
+
pending.push([children[index]?.[1], resource]);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return [...unresolved];
|
|
833
|
+
}
|
|
834
|
+
function resolves(fragment, base, document) {
|
|
835
|
+
if (fragment === "") {
|
|
836
|
+
return base !== document;
|
|
837
|
+
}
|
|
838
|
+
if (!fragment.startsWith("/")) {
|
|
839
|
+
return true;
|
|
840
|
+
}
|
|
841
|
+
let target = base;
|
|
842
|
+
for (const token of fragment.slice(1).split("/")) {
|
|
843
|
+
if (typeof target !== "object" || target === null) {
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
const key = decoded(token);
|
|
847
|
+
if (key === undefined || !Object.hasOwn(target, key)) {
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
target = target[key];
|
|
851
|
+
}
|
|
852
|
+
return typeof target === "object" && target !== null || typeof target === "boolean";
|
|
853
|
+
}
|
|
854
|
+
function decoded(token) {
|
|
855
|
+
try {
|
|
856
|
+
return decodeURIComponent(token).replaceAll("~1", "/").replaceAll("~0", "~");
|
|
857
|
+
} catch {
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
748
861
|
// packages/openapi/src/controller.ts
|
|
749
862
|
import { onMount, route } from "@tetsujs/core";
|
|
750
863
|
|
|
@@ -787,7 +900,7 @@ function docsPage(options) {
|
|
|
787
900
|
"<body>",
|
|
788
901
|
body(options.ui, url),
|
|
789
902
|
`<script src="${escapeHtml(assets.script)}"${verified(assets.integrity?.script)}></script>`,
|
|
790
|
-
bootstrap(options.ui
|
|
903
|
+
bootstrap(options.ui),
|
|
791
904
|
"</body>",
|
|
792
905
|
"</html>"
|
|
793
906
|
].filter(Boolean).join(`
|
|
@@ -800,15 +913,16 @@ function body(ui, url) {
|
|
|
800
913
|
if (ui === "redoc") {
|
|
801
914
|
return `<redoc spec-url="${url}"></redoc>`;
|
|
802
915
|
}
|
|
803
|
-
return
|
|
916
|
+
return `<div id="swagger-ui" data-url="${url}"></div>`;
|
|
804
917
|
}
|
|
805
|
-
function bootstrap(ui
|
|
918
|
+
function bootstrap(ui) {
|
|
806
919
|
if (ui !== "swagger-ui") {
|
|
807
920
|
return "";
|
|
808
921
|
}
|
|
809
922
|
return [
|
|
810
923
|
"<script>",
|
|
811
|
-
|
|
924
|
+
'const root = document.getElementById("swagger-ui");',
|
|
925
|
+
'SwaggerUIBundle({ url: root.dataset.url, dom_id: "#swagger-ui" });',
|
|
812
926
|
"</script>"
|
|
813
927
|
].join(`
|
|
814
928
|
`);
|
|
@@ -938,17 +1052,36 @@ function openapi(app, options) {
|
|
|
938
1052
|
};
|
|
939
1053
|
}
|
|
940
1054
|
const tags = options.tags ? tagsOf(options.tags, used, warnings) : undefined;
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
...componentsOf(schemes, components.schemas)
|
|
949
|
-
},
|
|
950
|
-
warnings
|
|
1055
|
+
const document = {
|
|
1056
|
+
openapi: "3.1.0",
|
|
1057
|
+
info: options.info,
|
|
1058
|
+
...options.servers ? { servers: options.servers } : {},
|
|
1059
|
+
...tags ? { tags } : {},
|
|
1060
|
+
paths,
|
|
1061
|
+
...componentsOf(schemes, components.schemas)
|
|
951
1062
|
};
|
|
1063
|
+
refsOf(document, occupied, warnings);
|
|
1064
|
+
return { document, warnings };
|
|
1065
|
+
}
|
|
1066
|
+
function refsOf(document, routes, warnings) {
|
|
1067
|
+
const unresolved = (ref) => `refers to "${ref}", which is no schema in the document: references are embedded as they are, and resolve against the document's root`;
|
|
1068
|
+
for (const [template, item] of Object.entries(document.paths)) {
|
|
1069
|
+
for (const [method, operation] of Object.entries(item)) {
|
|
1070
|
+
const route = routes.get(`${method} ${template}`) ?? "";
|
|
1071
|
+
for (const ref of unresolvedRefs(operation, document)) {
|
|
1072
|
+
warnings.push({ route, message: `a schema ${unresolved(ref)}` });
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
const schemas = document.components?.schemas ?? {};
|
|
1077
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
1078
|
+
for (const ref of unresolvedRefs(schema, document)) {
|
|
1079
|
+
warnings.push({
|
|
1080
|
+
route: "",
|
|
1081
|
+
message: `components/schemas/${name} ${unresolved(ref)}`
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
952
1085
|
}
|
|
953
1086
|
function tagsOf(described, used, warnings) {
|
|
954
1087
|
const tags = Object.entries(described).map(([name, description]) => ({ name, description }));
|
|
@@ -988,5 +1121,5 @@ export {
|
|
|
988
1121
|
secured
|
|
989
1122
|
};
|
|
990
1123
|
|
|
991
|
-
//# debugId=
|
|
1124
|
+
//# debugId=A9BC9A29B6C6C1DA64756E2164756E21
|
|
992
1125
|
//# sourceMappingURL=index.js.map
|