@tetsujs/openapi 0.1.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/LICENSE +21 -0
- package/README.md +146 -0
- package/dist/src/annotations.d.ts +141 -0
- package/dist/src/annotations.d.ts.map +1 -0
- package/dist/src/components.d.ts +45 -0
- package/dist/src/components.d.ts.map +1 -0
- package/dist/src/controller.d.ts +91 -0
- package/dist/src/controller.d.ts.map +1 -0
- package/dist/src/document.d.ts +80 -0
- package/dist/src/document.d.ts.map +1 -0
- package/dist/src/emit.d.ts +40 -0
- package/dist/src/emit.d.ts.map +1 -0
- package/dist/src/index.d.ts +75 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +616 -0
- package/dist/src/index.js.map +17 -0
- package/dist/src/operation.d.ts +39 -0
- package/dist/src/operation.d.ts.map +1 -0
- package/dist/src/page.d.ts +54 -0
- package/dist/src/page.d.ts.map +1 -0
- package/dist/src/parameters.d.ts +47 -0
- package/dist/src/parameters.d.ts.map +1 -0
- package/package.json +52 -0
- package/src/annotations.ts +225 -0
- package/src/components.ts +90 -0
- package/src/controller.ts +175 -0
- package/src/document.ts +90 -0
- package/src/emit.ts +98 -0
- package/src/index.ts +203 -0
- package/src/operation.ts +583 -0
- package/src/page.ts +139 -0
- package/src/parameters.ts +145 -0
|
@@ -0,0 +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,EAAkB,MAAM,eAAe,CAAC;AAIrE,YAAY,EACV,kBAAkB,EAClB,iBAAiB,EACjB,QAAQ,EACR,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,eAAe,EACf,eAAe,EACf,eAAe,EACf,cAAc,EACd,cAAc,GACf,MAAM,eAAe,CAAC;AACvB,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;CAC/C;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B,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,CAsF1E"}
|
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/openapi/src/annotations.ts
|
|
3
|
+
var docsKey = "~tetsu/hook-docs";
|
|
4
|
+
function documented(hook, docs) {
|
|
5
|
+
const annotated = { ...hook };
|
|
6
|
+
Object.defineProperty(annotated, docsKey, {
|
|
7
|
+
enumerable: false,
|
|
8
|
+
value: { ...docsOf(hook), ...docs }
|
|
9
|
+
});
|
|
10
|
+
return annotated;
|
|
11
|
+
}
|
|
12
|
+
function secured(hook, requirement) {
|
|
13
|
+
return documented(hook, { security: requirement });
|
|
14
|
+
}
|
|
15
|
+
function docsOf(hook) {
|
|
16
|
+
return hook[docsKey];
|
|
17
|
+
}
|
|
18
|
+
function contributionsOf(chains, warn) {
|
|
19
|
+
const security = new Map;
|
|
20
|
+
const responses = new Map;
|
|
21
|
+
for (const slot of Object.values(chains)) {
|
|
22
|
+
for (const hook of slot) {
|
|
23
|
+
const docs = docsOf(hook);
|
|
24
|
+
if (!docs) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (docs.security) {
|
|
28
|
+
const claimed = security.get(docs.security.name);
|
|
29
|
+
if (!claimed) {
|
|
30
|
+
security.set(docs.security.name, docs.security);
|
|
31
|
+
} else if (JSON.stringify(claimed.scheme) !== JSON.stringify(docs.security.scheme)) {
|
|
32
|
+
warn?.(`two hooks claim the security scheme "${docs.security.name}" with different definitions; one of them is not described`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const response of docs.responses ?? []) {
|
|
36
|
+
const key = `${response.status} ${response.description}`;
|
|
37
|
+
if (!responses.has(key)) {
|
|
38
|
+
responses.set(key, response);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
security: [...security.values()],
|
|
45
|
+
responses: [...responses.values()]
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// packages/openapi/src/components.ts
|
|
50
|
+
function schemaComponents() {
|
|
51
|
+
const schemas = {};
|
|
52
|
+
const registered = new Map;
|
|
53
|
+
return {
|
|
54
|
+
schemas,
|
|
55
|
+
ref(preferred, schema) {
|
|
56
|
+
const fingerprint = JSON.stringify(schema);
|
|
57
|
+
const already = registered.get(fingerprint);
|
|
58
|
+
if (already) {
|
|
59
|
+
return { $ref: `#/components/schemas/${already}` };
|
|
60
|
+
}
|
|
61
|
+
let name = preferred;
|
|
62
|
+
for (let attempt = 2;name in schemas; attempt += 1) {
|
|
63
|
+
name = `${preferred}${attempt}`;
|
|
64
|
+
}
|
|
65
|
+
schemas[name] = schema;
|
|
66
|
+
registered.set(fingerprint, name);
|
|
67
|
+
return { $ref: `#/components/schemas/${name}` };
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function failureName(status, error) {
|
|
72
|
+
const name = (error ?? "").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
73
|
+
return name || `Failure${status}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// packages/openapi/src/emit.ts
|
|
77
|
+
import { toJsonSchema } from "@tetsujs/core";
|
|
78
|
+
var target = { target: "draft-2020-12" };
|
|
79
|
+
function emitted(schema, subject, warn, direction = "output") {
|
|
80
|
+
let described;
|
|
81
|
+
try {
|
|
82
|
+
described = toJsonSchema(schema, target, direction);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
warn(`${subject} schema failed to emit JSON Schema: ${reason(error)}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!described) {
|
|
88
|
+
warn(`${subject} schema does not emit JSON Schema`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
return withoutDialect(described);
|
|
92
|
+
}
|
|
93
|
+
function withoutDialect(schema) {
|
|
94
|
+
if (!("$schema" in schema)) {
|
|
95
|
+
return schema;
|
|
96
|
+
}
|
|
97
|
+
const { $schema: _, ...rest } = schema;
|
|
98
|
+
return rest;
|
|
99
|
+
}
|
|
100
|
+
function reason(error) {
|
|
101
|
+
return error instanceof Error ? error.message : String(error);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// packages/openapi/src/parameters.ts
|
|
105
|
+
var wildcardName = "wildcard";
|
|
106
|
+
function toTemplate(path) {
|
|
107
|
+
return path.split("/").map((segment) => {
|
|
108
|
+
if (segment.startsWith(":")) {
|
|
109
|
+
return `{${segment.slice(1)}}`;
|
|
110
|
+
}
|
|
111
|
+
return segment === "*" ? `{${wildcardName}}` : segment;
|
|
112
|
+
}).join("/");
|
|
113
|
+
}
|
|
114
|
+
function templateNames(path) {
|
|
115
|
+
const names = [];
|
|
116
|
+
for (const segment of path.split("/")) {
|
|
117
|
+
if (segment.startsWith(":")) {
|
|
118
|
+
names.push(segment.slice(1));
|
|
119
|
+
} else if (segment === "*") {
|
|
120
|
+
names.push(wildcardName);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return names;
|
|
124
|
+
}
|
|
125
|
+
function pathParameters(path, schema, warn) {
|
|
126
|
+
const properties = objectProperties(schema, "params", warn);
|
|
127
|
+
return templateNames(path).map((name) => ({
|
|
128
|
+
name,
|
|
129
|
+
in: "path",
|
|
130
|
+
required: true,
|
|
131
|
+
...name === wildcardName ? {
|
|
132
|
+
description: "Matches the rest of the path, including further segments."
|
|
133
|
+
} : {},
|
|
134
|
+
schema: properties[name] ?? { type: "string" }
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
function partParameters(schema, where, warn) {
|
|
138
|
+
if (!schema) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
const described = emitted(schema, `the ${where}`, warn, "input");
|
|
142
|
+
if (!described) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
const properties = described.properties ?? {};
|
|
146
|
+
const required = new Set(Array.isArray(described.required) ? described.required : []);
|
|
147
|
+
return Object.entries(properties).map(([name, property]) => ({
|
|
148
|
+
name,
|
|
149
|
+
in: where,
|
|
150
|
+
required: required.has(name),
|
|
151
|
+
schema: property
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
function objectProperties(schema, where, warn) {
|
|
155
|
+
if (!schema) {
|
|
156
|
+
return {};
|
|
157
|
+
}
|
|
158
|
+
const described = emitted(schema, `the ${where}`, warn, "input");
|
|
159
|
+
if (!described) {
|
|
160
|
+
return {};
|
|
161
|
+
}
|
|
162
|
+
return described.properties ?? {};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// packages/openapi/src/operation.ts
|
|
166
|
+
function operationOf(entry, options, components, ids, warn) {
|
|
167
|
+
const schema = entry.def.schema;
|
|
168
|
+
const parameters = [
|
|
169
|
+
...pathParameters(entry.path, schema?.params, warn),
|
|
170
|
+
...partParameters(schema?.query, "query", warn),
|
|
171
|
+
...partParameters(schema?.headers, "header", warn),
|
|
172
|
+
...partParameters(schema?.cookies, "cookie", warn)
|
|
173
|
+
];
|
|
174
|
+
const body = requestBody(entry, warn);
|
|
175
|
+
const contributed = contributionsOf(entry.hooks, warn);
|
|
176
|
+
return {
|
|
177
|
+
operationId: ids.take(entry),
|
|
178
|
+
...entry.def.docs?.summary === undefined ? {} : { summary: entry.def.docs.summary },
|
|
179
|
+
...entry.def.docs?.description === undefined ? {} : { description: entry.def.docs.description },
|
|
180
|
+
...entry.def.docs?.tags === undefined ? {} : { tags: entry.def.docs.tags },
|
|
181
|
+
...entry.def.docs?.deprecated === undefined ? {} : { deprecated: entry.def.docs.deprecated },
|
|
182
|
+
...parameters.length > 0 ? { parameters } : {},
|
|
183
|
+
...body ? { requestBody: body } : {},
|
|
184
|
+
...contributed.security.length > 0 ? {
|
|
185
|
+
security: contributed.security.map((requirement) => ({
|
|
186
|
+
[requirement.name]: requirement.scopes ?? []
|
|
187
|
+
}))
|
|
188
|
+
} : {},
|
|
189
|
+
responses: responses(entry, options, contributed, components, warn)
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function operationIds() {
|
|
193
|
+
const taken = new Set;
|
|
194
|
+
return {
|
|
195
|
+
take(entry) {
|
|
196
|
+
const preferred = declaredId(entry) ?? derivedId(entry);
|
|
197
|
+
if (!taken.has(preferred)) {
|
|
198
|
+
taken.add(preferred);
|
|
199
|
+
return preferred;
|
|
200
|
+
}
|
|
201
|
+
const derived = derivedId(entry);
|
|
202
|
+
if (!taken.has(derived)) {
|
|
203
|
+
taken.add(derived);
|
|
204
|
+
return derived;
|
|
205
|
+
}
|
|
206
|
+
let name = derived;
|
|
207
|
+
for (let attempt = 2;taken.has(name); attempt += 1) {
|
|
208
|
+
name = `${derived}${attempt}`;
|
|
209
|
+
}
|
|
210
|
+
taken.add(name);
|
|
211
|
+
return name;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function declaredId(entry) {
|
|
216
|
+
if (entry.name === undefined) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const controller = entry.controller.replace(/Controller$/, "");
|
|
220
|
+
return `${lowerFirst(controller)}${capitalize(entry.name)}`;
|
|
221
|
+
}
|
|
222
|
+
function derivedId(entry) {
|
|
223
|
+
return `${entry.method.toLowerCase()}${entry.path.split(/[/:*]/).filter(Boolean).map(capitalize).join("")}`;
|
|
224
|
+
}
|
|
225
|
+
function requestBody(entry, warn) {
|
|
226
|
+
const declared = entry.def.schema?.body;
|
|
227
|
+
const bodyType = entry.def.bodyType ?? (declared ? "json" : undefined);
|
|
228
|
+
if (!bodyType) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const described = declared ? emitted(declared, "the body", warn, "input") : undefined;
|
|
232
|
+
const schema = described ? { schema: described } : {};
|
|
233
|
+
const required = bodyType !== "text" && bodyType !== "stream";
|
|
234
|
+
if (bodyType === "stream") {
|
|
235
|
+
return { required, content: { "application/octet-stream": {} } };
|
|
236
|
+
}
|
|
237
|
+
if (bodyType === "text") {
|
|
238
|
+
return { required, content: { "text/plain": schema } };
|
|
239
|
+
}
|
|
240
|
+
if (bodyType === "json") {
|
|
241
|
+
return { required, content: { "application/json": schema } };
|
|
242
|
+
}
|
|
243
|
+
const content = { "multipart/form-data": schema };
|
|
244
|
+
if (!described || !carriesBinary(described)) {
|
|
245
|
+
content["application/x-www-form-urlencoded"] = schema;
|
|
246
|
+
}
|
|
247
|
+
return { required, content };
|
|
248
|
+
}
|
|
249
|
+
function carriesBinary(root) {
|
|
250
|
+
const pending = [root];
|
|
251
|
+
const seen = new Set;
|
|
252
|
+
while (pending.length > 0) {
|
|
253
|
+
const node = pending.pop();
|
|
254
|
+
if (node === null || typeof node !== "object" || seen.has(node)) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
seen.add(node);
|
|
258
|
+
if (node.contentEncoding === "binary") {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
for (const value of Object.values(node)) {
|
|
262
|
+
pending.push(value);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
function responses(entry, options, contributed, components, warn) {
|
|
268
|
+
const declared = {};
|
|
269
|
+
const response = entry.def.schema?.response;
|
|
270
|
+
if (response && "~standard" in response) {
|
|
271
|
+
declared["200"] = jsonResponse(response, "Successful response", warn);
|
|
272
|
+
} else if (response) {
|
|
273
|
+
for (const [status, schema] of Object.entries(response)) {
|
|
274
|
+
declared[status] = schema === null ? { description: describeStatus(status) } : jsonResponse(schema, describeStatus(status), warn);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (!declared["200"] && !hasSuccess(declared)) {
|
|
278
|
+
declared["200"] = { description: "Successful response" };
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
...declared,
|
|
282
|
+
...frameworkFailures(entry, options, contributed, declared, components, warn)
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function frameworkFailures(entry, options, contributed, declared, components, warn) {
|
|
286
|
+
const schema = entry.def.schema;
|
|
287
|
+
const failures = new Map;
|
|
288
|
+
const add = (status, failure) => {
|
|
289
|
+
const key = String(status);
|
|
290
|
+
failures.set(key, [...failures.get(key) ?? [], failure]);
|
|
291
|
+
};
|
|
292
|
+
const envelopeRef = (status, error, message) => components.ref(failureName(status, error), envelope(status, error, message));
|
|
293
|
+
if (schema?.params || schema?.query || schema?.headers || schema?.body) {
|
|
294
|
+
add(options.validationStatus, {
|
|
295
|
+
description: "Request failed schema validation",
|
|
296
|
+
schema: components.ref(failureName(options.validationStatus, "VALIDATION_FAILED"), validationFailed(options.validationStatus))
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
for (const requirement of contributed.security) {
|
|
300
|
+
const status = requirement.status ?? 401;
|
|
301
|
+
add(status, {
|
|
302
|
+
description: requirement.description ?? `Request did not satisfy ${requirement.name}`,
|
|
303
|
+
schema: envelopeRef(status, requirement.error, requirement.message)
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
for (const response of contributed.responses) {
|
|
307
|
+
const described = response.schema ? emitted(response.schema, "a hook's response", warn) : undefined;
|
|
308
|
+
add(response.status, {
|
|
309
|
+
description: response.description,
|
|
310
|
+
schema: described ?? envelopeRef(response.status, response.error, response.message)
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
if (schema?.body || entry.def.bodyType) {
|
|
314
|
+
const unparsable = parseFailure(entry.def.bodyType);
|
|
315
|
+
if (unparsable) {
|
|
316
|
+
add(400, {
|
|
317
|
+
description: "Body could not be parsed in the declared shape",
|
|
318
|
+
schema: envelopeRef(400, unparsable.error, unparsable.message)
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
add(413, {
|
|
322
|
+
description: "Body exceeded the configured size limit",
|
|
323
|
+
schema: envelopeRef(413, "BODY_TOO_LARGE", "Body exceeds the configured limit")
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
add(500, {
|
|
327
|
+
description: "The request failed and nothing mapped the failure",
|
|
328
|
+
schema: envelopeRef(500, "INTERNAL_SERVER_ERROR", "Internal Server Error")
|
|
329
|
+
});
|
|
330
|
+
const responses = {};
|
|
331
|
+
for (const [status, list] of failures) {
|
|
332
|
+
responses[status] = merge(declared[status], list);
|
|
333
|
+
}
|
|
334
|
+
return responses;
|
|
335
|
+
}
|
|
336
|
+
function merge(declared, list) {
|
|
337
|
+
const descriptions = [
|
|
338
|
+
...declared ? [declared.description] : [],
|
|
339
|
+
...list.map((failure) => failure.description)
|
|
340
|
+
];
|
|
341
|
+
const schemas = distinct([
|
|
342
|
+
...declaredSchemas(declared),
|
|
343
|
+
...list.map((failure) => failure.schema)
|
|
344
|
+
]);
|
|
345
|
+
const [only] = schemas;
|
|
346
|
+
return {
|
|
347
|
+
description: descriptions.join("; "),
|
|
348
|
+
...schemas.length === 1 && only ? { content: { "application/json": { schema: only } } } : { content: { "application/json": { schema: { anyOf: schemas } } } }
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function declaredSchemas(declared) {
|
|
352
|
+
const schema = declared?.content?.["application/json"]?.schema;
|
|
353
|
+
return schema ? [schema] : [];
|
|
354
|
+
}
|
|
355
|
+
function distinct(schemas) {
|
|
356
|
+
const seen = new Set;
|
|
357
|
+
return schemas.filter((schema) => {
|
|
358
|
+
const fingerprint = JSON.stringify(schema);
|
|
359
|
+
if (seen.has(fingerprint)) {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
seen.add(fingerprint);
|
|
363
|
+
return true;
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
function jsonResponse(schema, description, warn) {
|
|
367
|
+
const described = emitted(schema, "a response", warn);
|
|
368
|
+
if (!described) {
|
|
369
|
+
return { description };
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
description,
|
|
373
|
+
content: { "application/json": { schema: described } }
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function hasSuccess(declared) {
|
|
377
|
+
return Object.keys(declared).some((status) => status.startsWith("2"));
|
|
378
|
+
}
|
|
379
|
+
function describeStatus(status) {
|
|
380
|
+
if (status === "204") {
|
|
381
|
+
return "No content";
|
|
382
|
+
}
|
|
383
|
+
return status.startsWith("2") ? "Successful response" : `Response ${status}`;
|
|
384
|
+
}
|
|
385
|
+
function envelope(status, error, message) {
|
|
386
|
+
return {
|
|
387
|
+
type: "object",
|
|
388
|
+
required: ["status", "message", "error"],
|
|
389
|
+
properties: {
|
|
390
|
+
status: { type: "integer", const: status },
|
|
391
|
+
message: message ? { type: "string", examples: [message] } : { type: "string" },
|
|
392
|
+
error: error ? { type: "string", const: error } : { type: "string" }
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function validationFailed(status) {
|
|
397
|
+
const base = envelope(status, "VALIDATION_FAILED", "Validation failed");
|
|
398
|
+
return {
|
|
399
|
+
...base,
|
|
400
|
+
required: ["status", "message", "error", "issues"],
|
|
401
|
+
properties: {
|
|
402
|
+
...base.properties,
|
|
403
|
+
issues: {
|
|
404
|
+
type: "array",
|
|
405
|
+
items: {
|
|
406
|
+
type: "object",
|
|
407
|
+
required: ["message", "path"],
|
|
408
|
+
properties: {
|
|
409
|
+
message: { type: "string" },
|
|
410
|
+
path: { type: "array", items: { type: ["string", "number"] } }
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
function parseFailure(bodyType) {
|
|
418
|
+
if (bodyType === "text" || bodyType === "stream") {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
return bodyType === "form" ? { error: "MALFORMED_FORM", message: "Body is not a valid form" } : { error: "MALFORMED_JSON", message: "Body is not valid JSON" };
|
|
422
|
+
}
|
|
423
|
+
function capitalize(value) {
|
|
424
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
425
|
+
}
|
|
426
|
+
function lowerFirst(value) {
|
|
427
|
+
return value.charAt(0).toLowerCase() + value.slice(1);
|
|
428
|
+
}
|
|
429
|
+
// packages/openapi/src/controller.ts
|
|
430
|
+
import { onMount, route } from "@tetsujs/core";
|
|
431
|
+
|
|
432
|
+
// packages/openapi/src/page.ts
|
|
433
|
+
var defaults = {
|
|
434
|
+
scalar: {
|
|
435
|
+
script: "https://cdn.jsdelivr.net/npm/@scalar/api-reference"
|
|
436
|
+
},
|
|
437
|
+
"swagger-ui": {
|
|
438
|
+
script: "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js",
|
|
439
|
+
style: "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css"
|
|
440
|
+
},
|
|
441
|
+
redoc: {
|
|
442
|
+
script: "https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js"
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
function docsPage(options) {
|
|
446
|
+
const assets = options.assets ?? defaults[options.ui];
|
|
447
|
+
const title = escapeHtml(options.title ?? "API documentation");
|
|
448
|
+
const url = escapeHtml(options.documentUrl);
|
|
449
|
+
return [
|
|
450
|
+
"<!doctype html>",
|
|
451
|
+
'<html lang="en">',
|
|
452
|
+
"<head>",
|
|
453
|
+
'<meta charset="utf-8" />',
|
|
454
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
455
|
+
`<title>${title}</title>`,
|
|
456
|
+
assets.style ? `<link rel="stylesheet" href="${escapeHtml(assets.style)}" />` : "",
|
|
457
|
+
"</head>",
|
|
458
|
+
"<body>",
|
|
459
|
+
body(options.ui, url),
|
|
460
|
+
`<script src="${escapeHtml(assets.script)}"></script>`,
|
|
461
|
+
bootstrap(options.ui, url),
|
|
462
|
+
"</body>",
|
|
463
|
+
"</html>"
|
|
464
|
+
].filter(Boolean).join(`
|
|
465
|
+
`);
|
|
466
|
+
}
|
|
467
|
+
function body(ui, url) {
|
|
468
|
+
if (ui === "scalar") {
|
|
469
|
+
return `<script id="api-reference" data-url="${url}"></script>`;
|
|
470
|
+
}
|
|
471
|
+
if (ui === "redoc") {
|
|
472
|
+
return `<redoc spec-url="${url}"></redoc>`;
|
|
473
|
+
}
|
|
474
|
+
return '<div id="swagger-ui"></div>';
|
|
475
|
+
}
|
|
476
|
+
function bootstrap(ui, url) {
|
|
477
|
+
if (ui !== "swagger-ui") {
|
|
478
|
+
return "";
|
|
479
|
+
}
|
|
480
|
+
return [
|
|
481
|
+
"<script>",
|
|
482
|
+
`SwaggerUIBundle({ url: "${url}", dom_id: "#swagger-ui" });`,
|
|
483
|
+
"</script>"
|
|
484
|
+
].join(`
|
|
485
|
+
`);
|
|
486
|
+
}
|
|
487
|
+
function escapeHtml(value) {
|
|
488
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// packages/openapi/src/controller.ts
|
|
492
|
+
class DocsController {
|
|
493
|
+
options;
|
|
494
|
+
json;
|
|
495
|
+
ui;
|
|
496
|
+
document;
|
|
497
|
+
constructor(options, documentPath, uiPath, page) {
|
|
498
|
+
this.options = options;
|
|
499
|
+
const hidden = !options.documentSelf;
|
|
500
|
+
const json = route({
|
|
501
|
+
method: "GET",
|
|
502
|
+
path: "/openapi.json",
|
|
503
|
+
docs: { summary: "This document", tags: ["docs"], hidden },
|
|
504
|
+
handler: () => this.document
|
|
505
|
+
});
|
|
506
|
+
const ui = route({
|
|
507
|
+
method: "GET",
|
|
508
|
+
path: "/docs",
|
|
509
|
+
docs: { summary: "Rendered documentation", tags: ["docs"], hidden },
|
|
510
|
+
handler: () => new Response(page, {
|
|
511
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
512
|
+
})
|
|
513
|
+
});
|
|
514
|
+
this.json = { ...json, path: documentPath };
|
|
515
|
+
this.ui = { ...ui, path: uiPath };
|
|
516
|
+
}
|
|
517
|
+
[onMount](app) {
|
|
518
|
+
const generated = openapi(app, {
|
|
519
|
+
info: this.options.info,
|
|
520
|
+
...this.options.servers ? { servers: this.options.servers } : {}
|
|
521
|
+
});
|
|
522
|
+
this.document = generated.document;
|
|
523
|
+
const report = this.options.onWarning ?? warn;
|
|
524
|
+
for (const warning of generated.warnings) {
|
|
525
|
+
report(warning);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function docs(options) {
|
|
530
|
+
const documentPath = options.path ?? "/openapi.json";
|
|
531
|
+
const uiPath = options.uiPath ?? "/docs";
|
|
532
|
+
const page = docsPage({
|
|
533
|
+
ui: options.ui ?? "scalar",
|
|
534
|
+
documentUrl: documentPath,
|
|
535
|
+
title: options.title ?? options.info.title,
|
|
536
|
+
...options.assets ? { assets: options.assets } : {}
|
|
537
|
+
});
|
|
538
|
+
return new DocsController(options, documentPath, uiPath, page);
|
|
539
|
+
}
|
|
540
|
+
function warn(warning) {
|
|
541
|
+
console.warn(`[openapi] ${warning.route}: ${warning.message}`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// packages/openapi/src/index.ts
|
|
545
|
+
function openapi(app, options) {
|
|
546
|
+
const paths = {};
|
|
547
|
+
const warnings = [];
|
|
548
|
+
const schemes = {};
|
|
549
|
+
const components = schemaComponents();
|
|
550
|
+
const ids = operationIds();
|
|
551
|
+
const occupied = new Map;
|
|
552
|
+
for (const entry of app.entries) {
|
|
553
|
+
if (entry.ws || entry.def.docs?.hidden) {
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
const template = toTemplate(entry.path);
|
|
557
|
+
const route = `${entry.method} ${entry.path}`;
|
|
558
|
+
const operation = operationOf(entry, app.options, components, ids, (message) => {
|
|
559
|
+
warnings.push({ route, message });
|
|
560
|
+
});
|
|
561
|
+
for (const requirement of contributionsOf(entry.hooks).security) {
|
|
562
|
+
const defined = schemes[requirement.name];
|
|
563
|
+
if (defined === undefined) {
|
|
564
|
+
schemes[requirement.name] = requirement.scheme;
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
if (JSON.stringify(defined) !== JSON.stringify(requirement.scheme)) {
|
|
568
|
+
warnings.push({
|
|
569
|
+
route,
|
|
570
|
+
message: `the security scheme "${requirement.name}" is already defined differently; this definition is ignored`
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const slot = `${entry.method.toLowerCase()} ${template}`;
|
|
575
|
+
const taken = occupied.get(slot);
|
|
576
|
+
if (taken) {
|
|
577
|
+
warnings.push({
|
|
578
|
+
route,
|
|
579
|
+
message: `${template} is already described by ${taken}, whose operation this replaces`
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
occupied.set(slot, route);
|
|
583
|
+
paths[template] = {
|
|
584
|
+
...paths[template],
|
|
585
|
+
[entry.method.toLowerCase()]: operation
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
document: {
|
|
590
|
+
openapi: "3.1.0",
|
|
591
|
+
info: options.info,
|
|
592
|
+
...options.servers ? { servers: options.servers } : {},
|
|
593
|
+
paths,
|
|
594
|
+
...componentsOf(schemes, components.schemas)
|
|
595
|
+
},
|
|
596
|
+
warnings
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
function componentsOf(securitySchemes, schemas) {
|
|
600
|
+
const components = {
|
|
601
|
+
...Object.keys(securitySchemes).length > 0 ? { securitySchemes } : {},
|
|
602
|
+
...Object.keys(schemas).length > 0 ? { schemas } : {}
|
|
603
|
+
};
|
|
604
|
+
return Object.keys(components).length > 0 ? { components } : {};
|
|
605
|
+
}
|
|
606
|
+
export {
|
|
607
|
+
DocsController,
|
|
608
|
+
docs,
|
|
609
|
+
docsPage,
|
|
610
|
+
documented,
|
|
611
|
+
openapi,
|
|
612
|
+
secured
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
//# debugId=698CDE5A6513C23964756E2164756E21
|
|
616
|
+
//# sourceMappingURL=index.js.map
|