@tetsujs/openapi 0.4.2 → 0.5.1
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 +94 -0
- package/dist/src/controller.d.ts +6 -0
- package/dist/src/controller.d.ts.map +1 -1
- package/dist/src/emit.d.ts +1 -0
- package/dist/src/emit.d.ts.map +1 -1
- package/dist/src/envelopes.d.ts +9 -11
- package/dist/src/envelopes.d.ts.map +1 -1
- package/dist/src/errors.d.ts +74 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/index.d.ts +28 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +186 -86
- package/dist/src/index.js.map +9 -8
- package/dist/src/operation.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/controller.ts +8 -0
- package/src/emit.ts +1 -0
- package/src/envelopes.ts +84 -58
- package/src/errors.ts +177 -0
- package/src/index.ts +34 -1
- package/src/operation.ts +174 -93
package/dist/src/index.js
CHANGED
|
@@ -103,7 +103,7 @@ function failureName(status, error) {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
// packages/openapi/src/envelopes.ts
|
|
106
|
-
function envelopes(components) {
|
|
106
|
+
function envelopes(components, format) {
|
|
107
107
|
const declared = new Map;
|
|
108
108
|
const defined = new Map;
|
|
109
109
|
const refs = new Map;
|
|
@@ -111,6 +111,7 @@ function envelopes(components) {
|
|
|
111
111
|
const names = new Map;
|
|
112
112
|
const reported = new Set;
|
|
113
113
|
return {
|
|
114
|
+
format,
|
|
114
115
|
declare(status, code, schema) {
|
|
115
116
|
const key = keyOf(status, code);
|
|
116
117
|
if (!declared.has(key)) {
|
|
@@ -149,31 +150,18 @@ function envelopes(components) {
|
|
|
149
150
|
return `${name}${status}`;
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
|
-
function
|
|
153
|
-
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
154
|
-
if (!required.includes("error")) {
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
const error = schema.properties?.error;
|
|
158
|
-
if (typeof error?.const === "string") {
|
|
159
|
-
return error.const;
|
|
160
|
-
}
|
|
161
|
-
if (Array.isArray(error?.enum) && error.enum.length === 1 && typeof error.enum[0] === "string") {
|
|
162
|
-
return error.enum[0];
|
|
163
|
-
}
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
function branchesOf(schema) {
|
|
153
|
+
function branchesOf(schema, code) {
|
|
167
154
|
const keys = Object.keys(schema);
|
|
168
155
|
if (keys.length !== 1) {
|
|
169
156
|
return [schema];
|
|
170
157
|
}
|
|
158
|
+
const within = (branch) => branchesOf(branch, code);
|
|
171
159
|
if (Array.isArray(schema.anyOf)) {
|
|
172
|
-
return schema.anyOf.flatMap(
|
|
160
|
+
return schema.anyOf.flatMap(within);
|
|
173
161
|
}
|
|
174
162
|
if (Array.isArray(schema.oneOf)) {
|
|
175
|
-
const branches = schema.oneOf.flatMap(
|
|
176
|
-
const codes = branches.map(
|
|
163
|
+
const branches = schema.oneOf.flatMap(within);
|
|
164
|
+
const codes = branches.map(code);
|
|
177
165
|
if (codes.every((code) => code !== undefined) && new Set(codes).size === codes.length) {
|
|
178
166
|
return branches;
|
|
179
167
|
}
|
|
@@ -184,17 +172,89 @@ function keyOf(status, code) {
|
|
|
184
172
|
return `${status} ${code}`;
|
|
185
173
|
}
|
|
186
174
|
function differenceOf(kept, other) {
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
175
|
+
const keptShape = shapeOf(kept);
|
|
176
|
+
const otherShape = shapeOf(other);
|
|
177
|
+
const changed = [
|
|
178
|
+
...otherShape.fields.filter((path) => !keptShape.fields.includes(path)),
|
|
179
|
+
...keptShape.fields.filter((path) => !otherShape.fields.includes(path))
|
|
180
|
+
];
|
|
193
181
|
if (changed.length > 0) {
|
|
194
182
|
return `fields: ${changed.join(", ")}`;
|
|
195
183
|
}
|
|
196
|
-
|
|
197
|
-
|
|
184
|
+
return JSON.stringify(keptShape.required) === JSON.stringify(otherShape.required) ? undefined : "which fields are required";
|
|
185
|
+
}
|
|
186
|
+
var shapeDepth = 8;
|
|
187
|
+
function shapeOf(schema) {
|
|
188
|
+
const fields = [];
|
|
189
|
+
const required = [];
|
|
190
|
+
const walk = (node, prefix, depth) => {
|
|
191
|
+
const properties = node.properties;
|
|
192
|
+
if (depth > shapeDepth || properties === null || typeof properties !== "object") {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const requiredHere = Array.isArray(node.required) ? node.required : [];
|
|
196
|
+
for (const [name, child] of Object.entries(properties)) {
|
|
197
|
+
const path = `${prefix}${name}`;
|
|
198
|
+
fields.push(path);
|
|
199
|
+
if (requiredHere.includes(name)) {
|
|
200
|
+
required.push(path);
|
|
201
|
+
}
|
|
202
|
+
if (child !== null && typeof child === "object") {
|
|
203
|
+
walk(child, `${path}.`, depth + 1);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
walk(schema, "", 0);
|
|
208
|
+
return { fields: fields.sort(), required: required.sort() };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// packages/openapi/src/errors.ts
|
|
212
|
+
function resolveFormat(format) {
|
|
213
|
+
if (format === undefined) {
|
|
214
|
+
return {
|
|
215
|
+
describe: envelope,
|
|
216
|
+
code: codeIn("error"),
|
|
217
|
+
discriminator: "error"
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const discriminator = format.discriminator;
|
|
221
|
+
const read = format.code;
|
|
222
|
+
return {
|
|
223
|
+
describe: (failure) => format.schema(failure),
|
|
224
|
+
code: read ? (schema) => read(schema) : discriminator === undefined ? () => {
|
|
225
|
+
return;
|
|
226
|
+
} : codeIn(discriminator),
|
|
227
|
+
discriminator
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function envelope(failure) {
|
|
231
|
+
const own = {
|
|
232
|
+
status: { type: "integer", const: failure.status },
|
|
233
|
+
message: failure.message ? { type: "string", examples: [failure.message] } : { type: "string" },
|
|
234
|
+
error: failure.error ? { type: "string", const: failure.error } : { type: "string" }
|
|
235
|
+
};
|
|
236
|
+
const added = Object.keys(failure.fields).filter((name) => !Object.hasOwn(own, name));
|
|
237
|
+
return {
|
|
238
|
+
type: "object",
|
|
239
|
+
required: ["status", "message", "error", ...added],
|
|
240
|
+
properties: { ...failure.fields, ...own }
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function codeIn(field) {
|
|
244
|
+
return (schema) => {
|
|
245
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
246
|
+
if (!required.includes(field)) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const value = schema.properties?.[field];
|
|
250
|
+
if (typeof value?.const === "string") {
|
|
251
|
+
return value.const;
|
|
252
|
+
}
|
|
253
|
+
if (Array.isArray(value?.enum) && value.enum.length === 1 && typeof value.enum[0] === "string") {
|
|
254
|
+
return value.enum[0];
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
};
|
|
198
258
|
}
|
|
199
259
|
|
|
200
260
|
// packages/openapi/src/operation.ts
|
|
@@ -315,10 +375,11 @@ function operationOf(entry, options, components, envelopes, ids, warn) {
|
|
|
315
375
|
function declareEnvelopes(entry, envelopes) {
|
|
316
376
|
for (const [status, schema] of declaredResponses(entry)) {
|
|
317
377
|
const described = schema === null ? undefined : emitted(schema, "", () => {});
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
378
|
+
const code = envelopes.format.code;
|
|
379
|
+
for (const branch of described ? branchesOf(described, code) : []) {
|
|
380
|
+
const found = code(branch);
|
|
381
|
+
if (found !== undefined) {
|
|
382
|
+
envelopes.declare(Number(status), found, branch);
|
|
322
383
|
}
|
|
323
384
|
}
|
|
324
385
|
}
|
|
@@ -334,8 +395,8 @@ function declaredResponses(entry) {
|
|
|
334
395
|
return Object.entries(response);
|
|
335
396
|
}
|
|
336
397
|
function branchesFor(schema, status, envelopes, warn) {
|
|
337
|
-
return branchesOf(schema).map((branch) => {
|
|
338
|
-
const code =
|
|
398
|
+
return branchesOf(schema, envelopes.format.code).map((branch) => {
|
|
399
|
+
const code = envelopes.format.code(branch);
|
|
339
400
|
return code === undefined ? branch : envelopes.ref(status, code, branch, warn);
|
|
340
401
|
});
|
|
341
402
|
}
|
|
@@ -450,17 +511,13 @@ function responses(entry, options, contributed, components, envelopes, warn) {
|
|
|
450
511
|
for (const [status, schema] of declaredResponses(entry)) {
|
|
451
512
|
const described = schema === null ? undefined : emitted(schema, "a response", warn);
|
|
452
513
|
add(status, {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
schemas: described ? branchesFor(described, Number(status), envelopes, warn) : []
|
|
514
|
+
placeholder: describeStatus(status),
|
|
515
|
+
said: described ? wordsOf(described, envelopes) : [],
|
|
516
|
+
schemas: described ? branchesFor(unionOnly(described), Number(status), envelopes, warn) : []
|
|
456
517
|
});
|
|
457
518
|
}
|
|
458
519
|
if (![...answers.keys()].some((status) => status.startsWith("2"))) {
|
|
459
|
-
add("200", {
|
|
460
|
-
description: "Successful response",
|
|
461
|
-
placeholder: true,
|
|
462
|
-
schemas: []
|
|
463
|
-
});
|
|
520
|
+
add("200", { placeholder: "Successful response", said: [], schemas: [] });
|
|
464
521
|
}
|
|
465
522
|
for (const [status, answer] of failures(entry, options, contributed, components, envelopes, warn)) {
|
|
466
523
|
add(String(status), answer);
|
|
@@ -475,7 +532,12 @@ function failures(entry, options, contributed, components, envelopes, warn) {
|
|
|
475
532
|
const schema = entry.def.schema;
|
|
476
533
|
const found = [];
|
|
477
534
|
const envelopeRef = (status, error, message, fields) => {
|
|
478
|
-
const described =
|
|
535
|
+
const described = envelopes.format.describe({
|
|
536
|
+
status,
|
|
537
|
+
...error === undefined ? {} : { error },
|
|
538
|
+
...message === undefined ? {} : { message },
|
|
539
|
+
fields: fields ?? {}
|
|
540
|
+
});
|
|
479
541
|
const ref = error === undefined ? components.ref(failureName(status), described) : envelopes.ref(status, error, described, warn);
|
|
480
542
|
return ref;
|
|
481
543
|
};
|
|
@@ -483,9 +545,19 @@ function failures(entry, options, contributed, components, envelopes, warn) {
|
|
|
483
545
|
found.push([
|
|
484
546
|
options.validationStatus,
|
|
485
547
|
{
|
|
486
|
-
|
|
548
|
+
said: [
|
|
549
|
+
{
|
|
550
|
+
code: "VALIDATION_FAILED",
|
|
551
|
+
text: "Request failed schema validation"
|
|
552
|
+
}
|
|
553
|
+
],
|
|
487
554
|
schemas: [
|
|
488
|
-
envelopes.ref(options.validationStatus, "VALIDATION_FAILED",
|
|
555
|
+
envelopes.ref(options.validationStatus, "VALIDATION_FAILED", envelopes.format.describe({
|
|
556
|
+
status: options.validationStatus,
|
|
557
|
+
error: "VALIDATION_FAILED",
|
|
558
|
+
message: "Validation failed",
|
|
559
|
+
fields: { issues }
|
|
560
|
+
}), warn)
|
|
489
561
|
]
|
|
490
562
|
}
|
|
491
563
|
]);
|
|
@@ -495,7 +567,9 @@ function failures(entry, options, contributed, components, envelopes, warn) {
|
|
|
495
567
|
found.push([
|
|
496
568
|
status,
|
|
497
569
|
{
|
|
498
|
-
|
|
570
|
+
said: [
|
|
571
|
+
said(requirement.error, requirement.description ?? `Request did not satisfy ${requirement.name}`)
|
|
572
|
+
],
|
|
499
573
|
schemas: [envelopeRef(status, requirement.error, requirement.message)]
|
|
500
574
|
}
|
|
501
575
|
]);
|
|
@@ -505,7 +579,7 @@ function failures(entry, options, contributed, components, envelopes, warn) {
|
|
|
505
579
|
found.push([
|
|
506
580
|
response.status,
|
|
507
581
|
{
|
|
508
|
-
|
|
582
|
+
said: [said(response.error, response.description)],
|
|
509
583
|
schemas: described ? branchesFor(described, response.status, envelopes, warn) : [
|
|
510
584
|
envelopeRef(response.status, response.error, response.message, response.fields)
|
|
511
585
|
],
|
|
@@ -519,7 +593,9 @@ function failures(entry, options, contributed, components, envelopes, warn) {
|
|
|
519
593
|
found.push([
|
|
520
594
|
400,
|
|
521
595
|
{
|
|
522
|
-
|
|
596
|
+
said: [
|
|
597
|
+
said(unparsable.error, "Body could not be parsed in the declared shape")
|
|
598
|
+
],
|
|
523
599
|
schemas: [envelopeRef(400, unparsable.error, unparsable.message)]
|
|
524
600
|
}
|
|
525
601
|
]);
|
|
@@ -527,7 +603,9 @@ function failures(entry, options, contributed, components, envelopes, warn) {
|
|
|
527
603
|
found.push([
|
|
528
604
|
413,
|
|
529
605
|
{
|
|
530
|
-
|
|
606
|
+
said: [
|
|
607
|
+
said("BODY_TOO_LARGE", "Body exceeded the configured size limit")
|
|
608
|
+
],
|
|
531
609
|
schemas: [
|
|
532
610
|
envelopeRef(413, "BODY_TOO_LARGE", "Body exceeds the configured limit")
|
|
533
611
|
]
|
|
@@ -537,7 +615,9 @@ function failures(entry, options, contributed, components, envelopes, warn) {
|
|
|
537
615
|
found.push([
|
|
538
616
|
500,
|
|
539
617
|
{
|
|
540
|
-
|
|
618
|
+
said: [
|
|
619
|
+
said("INTERNAL_SERVER_ERROR", "The request failed and nothing mapped the failure")
|
|
620
|
+
],
|
|
541
621
|
schemas: [
|
|
542
622
|
envelopeRef(500, "INTERNAL_SERVER_ERROR", "Internal Server Error")
|
|
543
623
|
]
|
|
@@ -545,9 +625,24 @@ function failures(entry, options, contributed, components, envelopes, warn) {
|
|
|
545
625
|
]);
|
|
546
626
|
return found;
|
|
547
627
|
}
|
|
628
|
+
function said(code, text) {
|
|
629
|
+
return code === undefined ? { text } : { code, text };
|
|
630
|
+
}
|
|
631
|
+
function wordsOf(schema, envelopes) {
|
|
632
|
+
if (typeof schema.description === "string") {
|
|
633
|
+
return [said(envelopes.format.code(schema), schema.description)];
|
|
634
|
+
}
|
|
635
|
+
return branchesOf(unionOnly(schema), envelopes.format.code).flatMap((branch) => typeof branch.description === "string" ? [said(envelopes.format.code(branch), branch.description)] : []);
|
|
636
|
+
}
|
|
637
|
+
function unionOnly(schema) {
|
|
638
|
+
if (typeof schema.description !== "string" || !Array.isArray(schema.anyOf) && !Array.isArray(schema.oneOf)) {
|
|
639
|
+
return schema;
|
|
640
|
+
}
|
|
641
|
+
const { description: _, ...union } = schema;
|
|
642
|
+
return union;
|
|
643
|
+
}
|
|
548
644
|
function merge(list, envelopes) {
|
|
549
645
|
const schemas = distinct(list.flatMap((answer) => answer.schemas));
|
|
550
|
-
const worded = list.filter((answer) => !answer.placeholder);
|
|
551
646
|
const headers = {};
|
|
552
647
|
for (const answer of list) {
|
|
553
648
|
for (const [name, header] of Object.entries(answer.headers ?? {})) {
|
|
@@ -555,7 +650,7 @@ function merge(list, envelopes) {
|
|
|
555
650
|
}
|
|
556
651
|
}
|
|
557
652
|
return {
|
|
558
|
-
description: (
|
|
653
|
+
description: wording(list),
|
|
559
654
|
...Object.keys(headers).length > 0 ? { headers } : {},
|
|
560
655
|
...schemas.length > 0 ? {
|
|
561
656
|
content: {
|
|
@@ -564,11 +659,38 @@ function merge(list, envelopes) {
|
|
|
564
659
|
} : {}
|
|
565
660
|
};
|
|
566
661
|
}
|
|
662
|
+
function wording(list) {
|
|
663
|
+
const seen = new Set;
|
|
664
|
+
const items = [];
|
|
665
|
+
for (const answer of list) {
|
|
666
|
+
for (const item of answer.said) {
|
|
667
|
+
const key = `${item.code ?? ""}
|
|
668
|
+
${item.text}`;
|
|
669
|
+
if (!seen.has(key)) {
|
|
670
|
+
seen.add(key);
|
|
671
|
+
items.push(item);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
const [only] = items;
|
|
676
|
+
if (items.length === 1 && only) {
|
|
677
|
+
return only.text;
|
|
678
|
+
}
|
|
679
|
+
if (items.length > 1) {
|
|
680
|
+
return items.map((item) => item.code === undefined ? `- ${item.text}` : `- \`${item.code}\`: ${item.text}`).join(`
|
|
681
|
+
`);
|
|
682
|
+
}
|
|
683
|
+
return list.find((answer) => answer.placeholder)?.placeholder ?? "";
|
|
684
|
+
}
|
|
567
685
|
function unionOf(schemas, envelopes) {
|
|
568
686
|
const [only] = schemas;
|
|
569
687
|
if (schemas.length === 1 && only) {
|
|
570
688
|
return only;
|
|
571
689
|
}
|
|
690
|
+
const propertyName = envelopes.format.discriminator;
|
|
691
|
+
if (propertyName === undefined) {
|
|
692
|
+
return { anyOf: schemas };
|
|
693
|
+
}
|
|
572
694
|
const mapping = {};
|
|
573
695
|
for (const schema of schemas) {
|
|
574
696
|
const ref = schema.$ref;
|
|
@@ -580,7 +702,7 @@ function unionOf(schemas, envelopes) {
|
|
|
580
702
|
}
|
|
581
703
|
return {
|
|
582
704
|
anyOf: schemas,
|
|
583
|
-
discriminator: { propertyName
|
|
705
|
+
discriminator: { propertyName, mapping }
|
|
584
706
|
};
|
|
585
707
|
}
|
|
586
708
|
function distinct(schemas) {
|
|
@@ -600,40 +722,17 @@ function describeStatus(status) {
|
|
|
600
722
|
}
|
|
601
723
|
return status.startsWith("2") ? "Successful response" : errorBody(Number(status)).message;
|
|
602
724
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
message: message ? { type: "string", examples: [message] } : { type: "string" },
|
|
607
|
-
error: error ? { type: "string", const: error } : { type: "string" }
|
|
608
|
-
};
|
|
609
|
-
const added = Object.keys(fields).filter((name) => !Object.hasOwn(own, name));
|
|
610
|
-
return {
|
|
725
|
+
var issues = {
|
|
726
|
+
type: "array",
|
|
727
|
+
items: {
|
|
611
728
|
type: "object",
|
|
612
|
-
required: ["
|
|
613
|
-
properties: { ...fields, ...own }
|
|
614
|
-
};
|
|
615
|
-
}
|
|
616
|
-
function validationFailed(status) {
|
|
617
|
-
const base = envelope(status, "VALIDATION_FAILED", "Validation failed");
|
|
618
|
-
return {
|
|
619
|
-
...base,
|
|
620
|
-
required: ["status", "message", "error", "issues"],
|
|
729
|
+
required: ["message", "path"],
|
|
621
730
|
properties: {
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
type: "array",
|
|
625
|
-
items: {
|
|
626
|
-
type: "object",
|
|
627
|
-
required: ["message", "path"],
|
|
628
|
-
properties: {
|
|
629
|
-
message: { type: "string" },
|
|
630
|
-
path: { type: "array", items: { type: ["string", "number"] } }
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}
|
|
731
|
+
message: { type: "string" },
|
|
732
|
+
path: { type: "array", items: { type: ["string", "number"] } }
|
|
634
733
|
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
734
|
+
}
|
|
735
|
+
};
|
|
637
736
|
function parseFailure(bodyType) {
|
|
638
737
|
if (bodyType === "text" || bodyType === "stream") {
|
|
639
738
|
return;
|
|
@@ -751,7 +850,8 @@ class DocsController {
|
|
|
751
850
|
[onMount](app) {
|
|
752
851
|
const generated = openapi(app, {
|
|
753
852
|
info: this.options.info,
|
|
754
|
-
...this.options.servers ? { servers: this.options.servers } : {}
|
|
853
|
+
...this.options.servers ? { servers: this.options.servers } : {},
|
|
854
|
+
...this.options.errors ? { errors: this.options.errors } : {}
|
|
755
855
|
});
|
|
756
856
|
this.document = generated.document;
|
|
757
857
|
const ui = this.options.ui ?? "scalar";
|
|
@@ -784,7 +884,7 @@ function openapi(app, options) {
|
|
|
784
884
|
const warnings = [];
|
|
785
885
|
const schemes = {};
|
|
786
886
|
const components = schemaComponents();
|
|
787
|
-
const envelopes2 = envelopes(components);
|
|
887
|
+
const envelopes2 = envelopes(components, resolveFormat(options.errors));
|
|
788
888
|
const ids = operationIds();
|
|
789
889
|
const described = app.entries.filter((entry) => !entry.ws && !entry.def.docs?.hidden);
|
|
790
890
|
for (const entry of described) {
|
|
@@ -851,5 +951,5 @@ export {
|
|
|
851
951
|
secured
|
|
852
952
|
};
|
|
853
953
|
|
|
854
|
-
//# debugId=
|
|
954
|
+
//# debugId=4A68DE756C1883B864756E2164756E21
|
|
855
955
|
//# sourceMappingURL=index.js.map
|