silgi 0.53.0 → 0.53.2
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/builder.mjs +32 -6
- package/dist/caller.mjs +65 -55
- package/dist/compile.d.mts +15 -8
- package/dist/compile.mjs +157 -142
- package/dist/core/handler.d.mts +3 -3
- package/dist/core/handler.mjs +69 -73
- package/dist/core/input.mjs +95 -33
- package/dist/core/schema-converter.d.mts +68 -63
- package/dist/core/schema-converter.mjs +85 -56
- package/dist/core/serve.d.mts +18 -17
- package/dist/core/serve.mjs +154 -64
- package/dist/core/sse.d.mts +5 -6
- package/dist/core/sse.mjs +86 -46
- package/dist/core/task.d.mts +15 -4
- package/dist/core/task.mjs +160 -76
- package/dist/plugins/cache.d.mts +62 -126
- package/dist/plugins/cache.mjs +146 -128
- package/dist/scalar.d.mts +24 -13
- package/dist/scalar.mjs +292 -201
- package/dist/silgi.mjs +160 -117
- package/dist/ws.d.mts +26 -27
- package/dist/ws.mjs +126 -87
- package/package.json +1 -1
package/dist/scalar.mjs
CHANGED
|
@@ -2,27 +2,43 @@ import { collectProcedures } from "./core/router-utils.mjs";
|
|
|
2
2
|
import { schemaToJsonSchema } from "./core/schema-converter.mjs";
|
|
3
3
|
//#region src/scalar.ts
|
|
4
4
|
/**
|
|
5
|
-
* Scalar API Reference
|
|
5
|
+
* Scalar API Reference + OpenAPI 3.1.0 generation
|
|
6
|
+
* --------------------------------------------------
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
8
|
+
* Walks a silgi `RouterDef` and emits an OpenAPI 3.1.0 document, then
|
|
9
|
+
* wraps a Fetch handler so that two additional routes are served:
|
|
10
|
+
*
|
|
11
|
+
* `{basePath}/openapi.json` — the raw spec.
|
|
12
|
+
* `{basePath}/reference` — the Scalar API Reference UI (HTML).
|
|
13
|
+
*
|
|
14
|
+
* Every procedure becomes one OpenAPI operation per HTTP method. Input
|
|
15
|
+
* schemas become `parameters[in:query]` for GET and `requestBody` for
|
|
16
|
+
* the rest. Typed errors declared via `.$errors()` become typed response
|
|
17
|
+
* shapes grouped by status code. Subscriptions are documented as SSE
|
|
18
|
+
* endpoints (clients are still expected to use the native WebSocket
|
|
19
|
+
* channel; the REST representation is for discoverability).
|
|
20
|
+
*
|
|
21
|
+
* The document respects per-procedure escape hatches in that order:
|
|
22
|
+
*
|
|
23
|
+
* `route.spec` — either a function `(op) => op` or an object merge.
|
|
24
|
+
* `route.security` — per-op security override (or `false` for public).
|
|
25
|
+
* `route.successStatus` / `route.successDescription` — success shape.
|
|
12
26
|
*/
|
|
13
27
|
/**
|
|
14
|
-
* Convert `:param
|
|
15
|
-
*
|
|
28
|
+
* Convert silgi's `:param`, `:param?`, `:param(regex)`, and `**`
|
|
29
|
+
* route syntax to OpenAPI's `{param}` form, and collect the extracted
|
|
30
|
+
* parameter names so callers can list them in the operation's
|
|
31
|
+
* `parameters`.
|
|
16
32
|
*/
|
|
17
33
|
function toOpenAPIPath(raw) {
|
|
18
34
|
const pathParams = [];
|
|
19
|
-
const httpPath = raw.replace(/:(\w+)\([^)]*\)/g, (
|
|
35
|
+
const httpPath = raw.replace(/:(\w+)\([^)]*\)/g, (_match, name) => {
|
|
20
36
|
pathParams.push(name);
|
|
21
37
|
return `{${name}}`;
|
|
22
|
-
}).replace(/:(\w+)\?/g, (
|
|
38
|
+
}).replace(/:(\w+)\?/g, (_match, name) => {
|
|
23
39
|
pathParams.push(name);
|
|
24
40
|
return `{${name}}`;
|
|
25
|
-
}).replace(/:(\w+)/g, (
|
|
41
|
+
}).replace(/:(\w+)/g, (_match, name) => {
|
|
26
42
|
pathParams.push(name);
|
|
27
43
|
return `{${name}}`;
|
|
28
44
|
}).replace(/\/\*\*$/g, "/{path}").replace(/\/\*\*/g, "/{path}");
|
|
@@ -32,208 +48,286 @@ function toOpenAPIPath(raw) {
|
|
|
32
48
|
pathParams
|
|
33
49
|
};
|
|
34
50
|
}
|
|
51
|
+
const HTTP_METHODS = [
|
|
52
|
+
"get",
|
|
53
|
+
"put",
|
|
54
|
+
"post",
|
|
55
|
+
"delete",
|
|
56
|
+
"options",
|
|
57
|
+
"head",
|
|
58
|
+
"patch",
|
|
59
|
+
"trace"
|
|
60
|
+
];
|
|
61
|
+
/** Escape a string for inclusion in HTML attribute or text content. */
|
|
62
|
+
function escapeHtml(s) {
|
|
63
|
+
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Turn a flat object schema into OpenAPI query-parameter entries. Used
|
|
67
|
+
* for `GET` routes: the body is moved to the URL.
|
|
68
|
+
*/
|
|
69
|
+
function objectSchemaToParams(schema) {
|
|
70
|
+
if (schema.type !== "object" || !schema.properties) return [];
|
|
71
|
+
const required = new Set(schema.required ?? []);
|
|
72
|
+
return Object.entries(schema.properties).map(([name, propSchema]) => ({
|
|
73
|
+
name,
|
|
74
|
+
in: "query",
|
|
75
|
+
required: required.has(name),
|
|
76
|
+
schema: propSchema,
|
|
77
|
+
...propSchema.description ? { description: propSchema.description } : {}
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Collect every typed error declared by a procedure *and* by any guard
|
|
82
|
+
* it uses, keyed by HTTP status. Guard errors merge into the procedure
|
|
83
|
+
* errors on a collision (procedure wins).
|
|
84
|
+
*/
|
|
85
|
+
function collectTypedErrors(proc) {
|
|
86
|
+
const guards = (proc.use ?? []).filter((m) => typeof m === "object" && m !== null && m.kind === "guard" && !!m.errors);
|
|
87
|
+
let merged = proc.errors ? { ...proc.errors } : null;
|
|
88
|
+
for (const guard of guards) merged = merged ? {
|
|
89
|
+
...merged,
|
|
90
|
+
...guard.errors
|
|
91
|
+
} : { ...guard.errors };
|
|
92
|
+
return merged;
|
|
93
|
+
}
|
|
94
|
+
/** Group typed errors by status code; each status may carry N codes. */
|
|
95
|
+
function groupErrorsByStatus(errors, schemaToJson) {
|
|
96
|
+
const byStatus = /* @__PURE__ */ new Map();
|
|
97
|
+
for (const [code, rawDef] of Object.entries(errors)) {
|
|
98
|
+
const status = typeof rawDef === "number" ? rawDef : rawDef.status;
|
|
99
|
+
if (!byStatus.has(status)) byStatus.set(status, []);
|
|
100
|
+
const entry = { code };
|
|
101
|
+
if (typeof rawDef === "object" && rawDef !== null) {
|
|
102
|
+
const def = rawDef;
|
|
103
|
+
if (def.message) entry.message = def.message;
|
|
104
|
+
if (def.data) entry.schema = schemaToJson(def.data);
|
|
105
|
+
}
|
|
106
|
+
byStatus.get(status).push(entry);
|
|
107
|
+
}
|
|
108
|
+
return byStatus;
|
|
109
|
+
}
|
|
110
|
+
/** Build one response-object schema for a typed error. */
|
|
111
|
+
function errorEntryToJsonSchema(entry, status) {
|
|
112
|
+
const schema = {
|
|
113
|
+
type: "object",
|
|
114
|
+
properties: {
|
|
115
|
+
code: {
|
|
116
|
+
const: entry.code,
|
|
117
|
+
type: "string"
|
|
118
|
+
},
|
|
119
|
+
status: {
|
|
120
|
+
const: status,
|
|
121
|
+
type: "integer"
|
|
122
|
+
},
|
|
123
|
+
message: {
|
|
124
|
+
type: "string",
|
|
125
|
+
...entry.message ? { default: entry.message } : {}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
required: [
|
|
129
|
+
"code",
|
|
130
|
+
"status",
|
|
131
|
+
"message"
|
|
132
|
+
]
|
|
133
|
+
};
|
|
134
|
+
if (entry.schema) {
|
|
135
|
+
schema.properties.data = entry.schema;
|
|
136
|
+
schema.required.push("data");
|
|
137
|
+
}
|
|
138
|
+
return schema;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Build one OpenAPI operation object (the value for `paths[p][method]`).
|
|
142
|
+
*
|
|
143
|
+
* Parameters are path-params first, then query-params for GETs. Body
|
|
144
|
+
* goes on the request side for non-GETs. Responses are the declared
|
|
145
|
+
* success status plus an auto-documented 400 (when input validation
|
|
146
|
+
* exists) plus one slot per typed-error status.
|
|
147
|
+
*/
|
|
148
|
+
function buildOperation(args) {
|
|
149
|
+
const { proc, path, method, inputSchema, schemaToJson } = args;
|
|
150
|
+
const op = {
|
|
151
|
+
operationId: args.operationId,
|
|
152
|
+
responses: {}
|
|
153
|
+
};
|
|
154
|
+
if (args.tags?.length) op.tags = args.tags;
|
|
155
|
+
if (args.summary) op.summary = args.summary;
|
|
156
|
+
if (args.description) op.description = args.description;
|
|
157
|
+
if (args.deprecated) op.deprecated = true;
|
|
158
|
+
const route = proc.route;
|
|
159
|
+
if (route?.security === false) op.security = [];
|
|
160
|
+
else if (route?.security) op.security = route.security.map((s) => ({ [s]: [] }));
|
|
161
|
+
else if (args.globalSecurity) op.security = [{ auth: [] }];
|
|
162
|
+
const parameters = args.pathParams.map((name) => ({
|
|
163
|
+
name,
|
|
164
|
+
in: "path",
|
|
165
|
+
required: true,
|
|
166
|
+
schema: { type: "string" }
|
|
167
|
+
}));
|
|
168
|
+
if (inputSchema) if (method === "get") parameters.push(...objectSchemaToParams(inputSchema));
|
|
169
|
+
else op.requestBody = {
|
|
170
|
+
required: true,
|
|
171
|
+
content: { "application/json": { schema: inputSchema } }
|
|
172
|
+
};
|
|
173
|
+
if (parameters.length > 0) op.parameters = parameters;
|
|
174
|
+
const responses = op.responses;
|
|
175
|
+
const successStatus = route?.successStatus ?? 200;
|
|
176
|
+
const successDesc = route?.successDescription ?? "Successful response";
|
|
177
|
+
if (proc.type === "subscription") {
|
|
178
|
+
const outputSchema = proc.output ? schemaToJson(proc.output, "output") : { type: "string" };
|
|
179
|
+
responses[String(successStatus)] = {
|
|
180
|
+
description: "SSE event stream",
|
|
181
|
+
content: { "text/event-stream": { schema: {
|
|
182
|
+
type: "string",
|
|
183
|
+
description: `Each line: data: ${JSON.stringify(outputSchema)}`
|
|
184
|
+
} } }
|
|
185
|
+
};
|
|
186
|
+
} else if (proc.output) responses[String(successStatus)] = {
|
|
187
|
+
description: successDesc,
|
|
188
|
+
content: { "application/json": { schema: schemaToJson(proc.output, "output") } }
|
|
189
|
+
};
|
|
190
|
+
else responses[String(successStatus)] = { description: successDesc };
|
|
191
|
+
if (proc.input) responses["400"] = {
|
|
192
|
+
description: "BAD_REQUEST — input validation failed",
|
|
193
|
+
content: { "application/json": { schema: {
|
|
194
|
+
type: "object",
|
|
195
|
+
properties: {
|
|
196
|
+
code: {
|
|
197
|
+
const: "BAD_REQUEST",
|
|
198
|
+
type: "string"
|
|
199
|
+
},
|
|
200
|
+
status: {
|
|
201
|
+
const: 400,
|
|
202
|
+
type: "integer"
|
|
203
|
+
},
|
|
204
|
+
message: { type: "string" },
|
|
205
|
+
data: {
|
|
206
|
+
type: "object",
|
|
207
|
+
properties: { issues: { type: "array" } }
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
required: [
|
|
211
|
+
"code",
|
|
212
|
+
"status",
|
|
213
|
+
"message"
|
|
214
|
+
]
|
|
215
|
+
} } }
|
|
216
|
+
};
|
|
217
|
+
const typedErrors = collectTypedErrors(proc);
|
|
218
|
+
if (typedErrors) {
|
|
219
|
+
const byStatus = groupErrorsByStatus(typedErrors, schemaToJson);
|
|
220
|
+
for (const [status, entries] of byStatus) {
|
|
221
|
+
const schemas = entries.map((entry) => errorEntryToJsonSchema(entry, status));
|
|
222
|
+
responses[String(status)] = {
|
|
223
|
+
description: entries.map((e) => e.code).join(" | "),
|
|
224
|
+
content: { "application/json": { schema: schemas.length === 1 ? schemas[0] : { oneOf: schemas } } }
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
let finalOp = op;
|
|
229
|
+
if (route?.spec) finalOp = typeof route.spec === "function" ? route.spec(op) : {
|
|
230
|
+
...op,
|
|
231
|
+
...route.spec
|
|
232
|
+
};
|
|
233
|
+
return finalOp;
|
|
234
|
+
}
|
|
235
|
+
/** Build the `info` field of the doc, plus optional `servers` / `externalDocs`. */
|
|
236
|
+
function buildDocHeader(options) {
|
|
237
|
+
const info = {
|
|
238
|
+
title: options.title ?? "Silgi API",
|
|
239
|
+
version: options.version ?? "1.0.0"
|
|
240
|
+
};
|
|
241
|
+
if (options.description) info.description = options.description;
|
|
242
|
+
if (options.contact) info.contact = options.contact;
|
|
243
|
+
if (options.license) info.license = options.license;
|
|
244
|
+
const extras = {};
|
|
245
|
+
if (options.servers?.length) extras.servers = options.servers;
|
|
246
|
+
if (options.externalDocs) extras.externalDocs = options.externalDocs;
|
|
247
|
+
return {
|
|
248
|
+
info,
|
|
249
|
+
extras
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Translate our compact `security` option into the OpenAPI
|
|
254
|
+
* `components.securitySchemes` shape. The key under which the scheme
|
|
255
|
+
* lives is hard-coded as `auth` — per-operation `security` values
|
|
256
|
+
* reference it by that name.
|
|
257
|
+
*/
|
|
258
|
+
function buildSecurityScheme(security) {
|
|
259
|
+
const scheme = { type: security.type };
|
|
260
|
+
if (security.type === "http") {
|
|
261
|
+
scheme.scheme = security.scheme ?? "bearer";
|
|
262
|
+
if (security.bearerFormat) scheme.bearerFormat = security.bearerFormat;
|
|
263
|
+
} else if (security.type === "apiKey") {
|
|
264
|
+
scheme.in = security.in ?? "header";
|
|
265
|
+
scheme.name = security.name ?? "x-api-key";
|
|
266
|
+
}
|
|
267
|
+
if (security.description) scheme.description = security.description;
|
|
268
|
+
return { securitySchemes: { auth: scheme } };
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Generate an OpenAPI 3.1.0 document for a `RouterDef`.
|
|
272
|
+
*
|
|
273
|
+
* The document is a plain object and can be re-serialized, cached,
|
|
274
|
+
* piped into any OpenAPI consumer (codegen, docs site, validators). We
|
|
275
|
+
* do not return a typed `OpenAPIV3_1.Document` because the typings in
|
|
276
|
+
* the ecosystem are noisy; downstream consumers usually only care
|
|
277
|
+
* about a handful of keys anyway.
|
|
278
|
+
*/
|
|
35
279
|
function generateOpenAPI(router, options = {}, basePath = "", registry) {
|
|
36
|
-
const
|
|
280
|
+
const schemaToJson = (schema, strategy = "input") => schemaToJsonSchema(schema, strategy, registry);
|
|
37
281
|
const paths = {};
|
|
38
282
|
const tags = /* @__PURE__ */ new Map();
|
|
39
283
|
collectProcedures(router, (path, proc) => {
|
|
40
284
|
const route = proc.route;
|
|
41
285
|
const { httpPath: routePath, pathParams } = toOpenAPIPath(route?.path ?? "/" + path.join("/"));
|
|
42
286
|
const httpPath = basePath ? basePath.replace(/\/$/, "") + routePath : routePath;
|
|
43
|
-
const
|
|
44
|
-
const methods =
|
|
45
|
-
"get",
|
|
46
|
-
"put",
|
|
47
|
-
"post",
|
|
48
|
-
"delete",
|
|
49
|
-
"options",
|
|
50
|
-
"head",
|
|
51
|
-
"patch",
|
|
52
|
-
"trace"
|
|
53
|
-
] : [rawMethod];
|
|
287
|
+
const declaredMethod = route?.method?.toLowerCase() ?? "post";
|
|
288
|
+
const methods = declaredMethod === "*" ? [...HTTP_METHODS] : [declaredMethod];
|
|
54
289
|
const baseOperationId = route?.operationId ?? path.join("_");
|
|
55
290
|
const opTags = route?.tags ?? (path.length > 1 ? [path[0]] : void 0);
|
|
56
291
|
if (opTags) {
|
|
57
|
-
for (const
|
|
292
|
+
for (const tag of opTags) if (!tags.has(tag)) tags.set(tag, {});
|
|
58
293
|
}
|
|
59
294
|
let description = route?.description;
|
|
60
295
|
if (proc.type === "subscription") {
|
|
61
296
|
const wsNote = "Streams over WebSocket (`ws://…/_ws`). Send `{ id, path: \"" + path.join("/") + "\", input }` as JSON.";
|
|
62
297
|
description = description ? `${description}\n\n${wsNote}` : wsNote;
|
|
63
298
|
}
|
|
64
|
-
const
|
|
65
|
-
operationId: baseOperationId,
|
|
66
|
-
tags: opTags,
|
|
67
|
-
summary: route?.summary,
|
|
68
|
-
description,
|
|
69
|
-
deprecated: route?.deprecated || void 0,
|
|
70
|
-
responses: {}
|
|
71
|
-
};
|
|
72
|
-
if (!operation.summary) delete operation.summary;
|
|
73
|
-
if (!operation.description) delete operation.description;
|
|
74
|
-
if (!operation.deprecated) delete operation.deprecated;
|
|
75
|
-
if (!operation.tags) delete operation.tags;
|
|
76
|
-
if (route?.security === false) operation.security = [];
|
|
77
|
-
else if (route?.security) operation.security = route.security.map((s) => ({ [s]: [] }));
|
|
78
|
-
else if (options.security) operation.security = [{ auth: [] }];
|
|
79
|
-
const inputSchema = proc.input ? schemaToJsonSchema$1(proc.input, "input") : null;
|
|
80
|
-
const successStatus = route?.successStatus ?? 200;
|
|
81
|
-
const successDesc = route?.successDescription ?? "Successful response";
|
|
82
|
-
const guards = (proc.use ?? []).filter((m) => m.kind === "guard" && m.errors);
|
|
83
|
-
let allErrors = proc.errors ? { ...proc.errors } : null;
|
|
84
|
-
for (const guard of guards) {
|
|
85
|
-
const ge = guard.errors;
|
|
86
|
-
if (ge) allErrors = allErrors ? {
|
|
87
|
-
...allErrors,
|
|
88
|
-
...ge
|
|
89
|
-
} : { ...ge };
|
|
90
|
-
}
|
|
299
|
+
const inputSchema = proc.input ? schemaToJson(proc.input, "input") : null;
|
|
91
300
|
paths[httpPath] ??= {};
|
|
92
301
|
for (const method of methods) {
|
|
93
|
-
const op = {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
302
|
+
const op = buildOperation({
|
|
303
|
+
proc,
|
|
304
|
+
path,
|
|
305
|
+
method,
|
|
306
|
+
operationId: methods.length > 1 ? `${baseOperationId}_${method}` : baseOperationId,
|
|
307
|
+
tags: opTags,
|
|
308
|
+
summary: route?.summary,
|
|
309
|
+
description,
|
|
310
|
+
deprecated: route?.deprecated || void 0,
|
|
311
|
+
pathParams,
|
|
312
|
+
inputSchema,
|
|
313
|
+
schemaToJson,
|
|
314
|
+
globalSecurity: options.security
|
|
104
315
|
});
|
|
105
|
-
|
|
106
|
-
else op.requestBody = {
|
|
107
|
-
required: true,
|
|
108
|
-
content: { "application/json": { schema: inputSchema } }
|
|
109
|
-
};
|
|
110
|
-
if (params.length > 0) op.parameters = params;
|
|
111
|
-
if (proc.type === "subscription") {
|
|
112
|
-
const outputSchema = proc.output ? schemaToJsonSchema$1(proc.output, "output") : { type: "string" };
|
|
113
|
-
op.responses[String(successStatus)] = {
|
|
114
|
-
description: "SSE event stream",
|
|
115
|
-
content: { "text/event-stream": { schema: {
|
|
116
|
-
type: "string",
|
|
117
|
-
description: `Each line: data: ${JSON.stringify(outputSchema)}`
|
|
118
|
-
} } }
|
|
119
|
-
};
|
|
120
|
-
} else if (proc.output) op.responses[String(successStatus)] = {
|
|
121
|
-
description: successDesc,
|
|
122
|
-
content: { "application/json": { schema: schemaToJsonSchema$1(proc.output, "output") } }
|
|
123
|
-
};
|
|
124
|
-
else op.responses[String(successStatus)] = { description: successDesc };
|
|
125
|
-
if (proc.input) op.responses["400"] = {
|
|
126
|
-
description: "BAD_REQUEST — input validation failed",
|
|
127
|
-
content: { "application/json": { schema: {
|
|
128
|
-
type: "object",
|
|
129
|
-
properties: {
|
|
130
|
-
code: {
|
|
131
|
-
const: "BAD_REQUEST",
|
|
132
|
-
type: "string"
|
|
133
|
-
},
|
|
134
|
-
status: {
|
|
135
|
-
const: 400,
|
|
136
|
-
type: "integer"
|
|
137
|
-
},
|
|
138
|
-
message: { type: "string" },
|
|
139
|
-
data: {
|
|
140
|
-
type: "object",
|
|
141
|
-
properties: { issues: { type: "array" } }
|
|
142
|
-
}
|
|
143
|
-
},
|
|
144
|
-
required: [
|
|
145
|
-
"code",
|
|
146
|
-
"status",
|
|
147
|
-
"message"
|
|
148
|
-
]
|
|
149
|
-
} } }
|
|
150
|
-
};
|
|
151
|
-
if (allErrors) {
|
|
152
|
-
const byStatus = /* @__PURE__ */ new Map();
|
|
153
|
-
for (const [code, def] of Object.entries(allErrors)) {
|
|
154
|
-
const status = typeof def === "number" ? def : def.status;
|
|
155
|
-
if (!byStatus.has(status)) byStatus.set(status, []);
|
|
156
|
-
const entry = { code };
|
|
157
|
-
if (typeof def === "object") {
|
|
158
|
-
if (def.message) entry.message = def.message;
|
|
159
|
-
if (def.data) entry.schema = schemaToJsonSchema$1(def.data);
|
|
160
|
-
}
|
|
161
|
-
byStatus.get(status).push(entry);
|
|
162
|
-
}
|
|
163
|
-
for (const [status, errors] of byStatus) {
|
|
164
|
-
const errorSchemas = errors.map((e) => {
|
|
165
|
-
const s = {
|
|
166
|
-
type: "object",
|
|
167
|
-
properties: {
|
|
168
|
-
code: {
|
|
169
|
-
const: e.code,
|
|
170
|
-
type: "string"
|
|
171
|
-
},
|
|
172
|
-
status: {
|
|
173
|
-
const: status,
|
|
174
|
-
type: "integer"
|
|
175
|
-
},
|
|
176
|
-
message: {
|
|
177
|
-
type: "string",
|
|
178
|
-
...e.message ? { default: e.message } : {}
|
|
179
|
-
}
|
|
180
|
-
},
|
|
181
|
-
required: [
|
|
182
|
-
"code",
|
|
183
|
-
"status",
|
|
184
|
-
"message"
|
|
185
|
-
]
|
|
186
|
-
};
|
|
187
|
-
if (e.schema) {
|
|
188
|
-
s.properties.data = e.schema;
|
|
189
|
-
s.required.push("data");
|
|
190
|
-
}
|
|
191
|
-
return s;
|
|
192
|
-
});
|
|
193
|
-
op.responses[String(status)] = {
|
|
194
|
-
description: errors.map((e) => e.code).join(" | "),
|
|
195
|
-
content: { "application/json": { schema: errorSchemas.length === 1 ? errorSchemas[0] : { oneOf: errorSchemas } } }
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
let finalOp = op;
|
|
200
|
-
if (route?.spec) if (typeof route.spec === "function") finalOp = route.spec(finalOp);
|
|
201
|
-
else finalOp = {
|
|
202
|
-
...finalOp,
|
|
203
|
-
...route.spec
|
|
204
|
-
};
|
|
205
|
-
paths[httpPath][method] = finalOp;
|
|
316
|
+
paths[httpPath][method] = op;
|
|
206
317
|
}
|
|
207
318
|
});
|
|
319
|
+
const { info, extras } = buildDocHeader(options);
|
|
208
320
|
const doc = {
|
|
209
321
|
openapi: "3.1.0",
|
|
210
|
-
info
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
...options.description ? { description: options.description } : {},
|
|
214
|
-
...options.contact ? { contact: options.contact } : {},
|
|
215
|
-
...options.license ? { license: options.license } : {}
|
|
216
|
-
},
|
|
217
|
-
paths
|
|
322
|
+
info,
|
|
323
|
+
paths,
|
|
324
|
+
...extras
|
|
218
325
|
};
|
|
219
|
-
if (options.servers?.length) doc.servers = options.servers;
|
|
220
|
-
if (options.externalDocs) doc.externalDocs = options.externalDocs;
|
|
221
326
|
if (tags.size > 0) doc.tags = [...tags.entries()].map(([name, meta]) => ({
|
|
222
327
|
name,
|
|
223
328
|
...meta.description ? { description: meta.description } : {}
|
|
224
329
|
}));
|
|
225
|
-
if (options.security)
|
|
226
|
-
const scheme = { type: options.security.type };
|
|
227
|
-
if (options.security.type === "http") {
|
|
228
|
-
scheme.scheme = options.security.scheme ?? "bearer";
|
|
229
|
-
if (options.security.bearerFormat) scheme.bearerFormat = options.security.bearerFormat;
|
|
230
|
-
} else if (options.security.type === "apiKey") {
|
|
231
|
-
scheme.in = options.security.in ?? "header";
|
|
232
|
-
scheme.name = options.security.name ?? "x-api-key";
|
|
233
|
-
}
|
|
234
|
-
if (options.security.description) scheme.description = options.security.description;
|
|
235
|
-
doc.components = { securitySchemes: { auth: scheme } };
|
|
236
|
-
}
|
|
330
|
+
if (options.security) doc.components = buildSecurityScheme(options.security);
|
|
237
331
|
return doc;
|
|
238
332
|
}
|
|
239
333
|
const SCALAR_CDN_SOURCES = {
|
|
@@ -241,6 +335,10 @@ const SCALAR_CDN_SOURCES = {
|
|
|
241
335
|
unpkg: "https://unpkg.com/@scalar/api-reference",
|
|
242
336
|
local: "/__silgi/scalar.js"
|
|
243
337
|
};
|
|
338
|
+
/**
|
|
339
|
+
* Render the minimal HTML shell the Scalar UI needs. The UI itself is
|
|
340
|
+
* a single script that reads the `data-url` attribute to pull the spec.
|
|
341
|
+
*/
|
|
244
342
|
function scalarHTML(specUrl, options = {}) {
|
|
245
343
|
const title = escapeHtml(options.title ?? "Silgi API");
|
|
246
344
|
const safeUrl = escapeHtml(specUrl);
|
|
@@ -258,23 +356,16 @@ function scalarHTML(specUrl, options = {}) {
|
|
|
258
356
|
</body>
|
|
259
357
|
</html>`;
|
|
260
358
|
}
|
|
261
|
-
function escapeHtml(s) {
|
|
262
|
-
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
263
|
-
}
|
|
264
|
-
function objectSchemaToParams(schema) {
|
|
265
|
-
if (schema.type !== "object" || !schema.properties) return [];
|
|
266
|
-
const required = new Set(schema.required ?? []);
|
|
267
|
-
return Object.entries(schema.properties).map(([name, propSchema]) => ({
|
|
268
|
-
name,
|
|
269
|
-
in: "query",
|
|
270
|
-
required: required.has(name),
|
|
271
|
-
schema: propSchema,
|
|
272
|
-
...propSchema.description ? { description: propSchema.description } : {}
|
|
273
|
-
}));
|
|
274
|
-
}
|
|
275
359
|
/**
|
|
276
|
-
* Wrap a
|
|
277
|
-
*
|
|
360
|
+
* Wrap a Fetch handler so that two extra paths are served:
|
|
361
|
+
*
|
|
362
|
+
* `{basePath}/reference` — the Scalar UI.
|
|
363
|
+
* `{basePath}/openapi.json` — the spec.
|
|
364
|
+
*
|
|
365
|
+
* The generated spec is JSON-stringified once at wrap time so every
|
|
366
|
+
* hit to `/openapi.json` is a constant-cost `new Response(cachedJson)`.
|
|
367
|
+
* Requests that do not match either path fall through to the inner
|
|
368
|
+
* handler with zero overhead.
|
|
278
369
|
*/
|
|
279
370
|
function wrapWithScalar(handler, routerDef, options = {}, prefix = "/api", registry) {
|
|
280
371
|
const normPrefix = (prefix.startsWith("/") ? prefix : "/" + prefix).replace(/\/+$/, "");
|