@forinda/kickjs-swagger 1.2.11 → 1.2.12
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/index.js +7 -514
- package/package.json +2 -2
- package/dist/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,420 +1,9 @@
|
|
|
1
|
-
var
|
|
2
|
-
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
-
|
|
4
|
-
// src/schema-parser.ts
|
|
5
|
-
var zodSchemaParser = {
|
|
6
|
-
name: "zod",
|
|
7
|
-
supports(schema) {
|
|
8
|
-
return schema != null && typeof schema === "object" && typeof schema.safeParse === "function" && typeof schema.toJSONSchema === "function";
|
|
9
|
-
},
|
|
10
|
-
toJsonSchema(schema) {
|
|
11
|
-
const { $schema: _, ...rest } = schema.toJSONSchema();
|
|
12
|
-
return rest;
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
// src/decorators.ts
|
|
17
|
-
import "reflect-metadata";
|
|
18
|
-
var SWAGGER_KEYS = {
|
|
19
|
-
OPERATION: /* @__PURE__ */ Symbol("kick:swagger:operation"),
|
|
20
|
-
RESPONSES: /* @__PURE__ */ Symbol("kick:swagger:responses"),
|
|
21
|
-
TAGS: /* @__PURE__ */ Symbol("kick:swagger:tags"),
|
|
22
|
-
BEARER_AUTH: /* @__PURE__ */ Symbol("kick:swagger:bearer"),
|
|
23
|
-
EXCLUDE: /* @__PURE__ */ Symbol("kick:swagger:exclude")
|
|
24
|
-
};
|
|
25
|
-
function ApiOperation(options) {
|
|
26
|
-
return (target, propertyKey) => {
|
|
27
|
-
Reflect.defineMetadata(SWAGGER_KEYS.OPERATION, options, target.constructor, propertyKey);
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
__name(ApiOperation, "ApiOperation");
|
|
31
|
-
function ApiResponse(options) {
|
|
32
|
-
return (target, propertyKey) => {
|
|
33
|
-
const existing = Reflect.getMetadata(SWAGGER_KEYS.RESPONSES, target.constructor, propertyKey) || [];
|
|
34
|
-
Reflect.defineMetadata(SWAGGER_KEYS.RESPONSES, [
|
|
35
|
-
...existing,
|
|
36
|
-
options
|
|
37
|
-
], target.constructor, propertyKey);
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
__name(ApiResponse, "ApiResponse");
|
|
41
|
-
function ApiTags(...tags) {
|
|
42
|
-
return (target, propertyKey) => {
|
|
43
|
-
if (propertyKey) {
|
|
44
|
-
Reflect.defineMetadata(SWAGGER_KEYS.TAGS, tags, target.constructor, propertyKey);
|
|
45
|
-
} else {
|
|
46
|
-
Reflect.defineMetadata(SWAGGER_KEYS.TAGS, tags, target);
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
__name(ApiTags, "ApiTags");
|
|
51
|
-
function ApiBearerAuth(name = "BearerAuth") {
|
|
52
|
-
return (target, propertyKey) => {
|
|
53
|
-
if (propertyKey) {
|
|
54
|
-
Reflect.defineMetadata(SWAGGER_KEYS.BEARER_AUTH, name, target.constructor, propertyKey);
|
|
55
|
-
} else {
|
|
56
|
-
Reflect.defineMetadata(SWAGGER_KEYS.BEARER_AUTH, name, target);
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
__name(ApiBearerAuth, "ApiBearerAuth");
|
|
61
|
-
function ApiExclude() {
|
|
62
|
-
return (target, propertyKey) => {
|
|
63
|
-
if (propertyKey) {
|
|
64
|
-
Reflect.defineMetadata(SWAGGER_KEYS.EXCLUDE, true, target.constructor, propertyKey);
|
|
65
|
-
} else {
|
|
66
|
-
Reflect.defineMetadata(SWAGGER_KEYS.EXCLUDE, true, target);
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
__name(ApiExclude, "ApiExclude");
|
|
71
|
-
|
|
72
|
-
// src/openapi-builder.ts
|
|
73
|
-
import "reflect-metadata";
|
|
74
|
-
import { METADATA, joinPaths } from "@forinda/kickjs-core";
|
|
75
|
-
var registeredRoutes = [];
|
|
76
|
-
function registerControllerForDocs(controllerClass, mountPath) {
|
|
77
|
-
registeredRoutes.push({
|
|
78
|
-
controllerClass,
|
|
79
|
-
mountPath
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
__name(registerControllerForDocs, "registerControllerForDocs");
|
|
83
|
-
function clearRegisteredRoutes() {
|
|
84
|
-
registeredRoutes.length = 0;
|
|
85
|
-
}
|
|
86
|
-
__name(clearRegisteredRoutes, "clearRegisteredRoutes");
|
|
87
|
-
function buildOpenAPISpec(options = {}) {
|
|
88
|
-
const parser = options.schemaParser ?? zodSchemaParser;
|
|
89
|
-
const toJsonSchema = /* @__PURE__ */ __name((schema) => {
|
|
90
|
-
try {
|
|
91
|
-
if (!parser.supports(schema)) return null;
|
|
92
|
-
return parser.toJsonSchema(schema);
|
|
93
|
-
} catch {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
}, "toJsonSchema");
|
|
97
|
-
const componentSchemas = {};
|
|
98
|
-
let schemaCounter = 0;
|
|
99
|
-
const registerSchema = /* @__PURE__ */ __name((jsonSchema, hint) => {
|
|
100
|
-
let name = jsonSchema.title || jsonSchema.label || hint || "";
|
|
101
|
-
if (!name) {
|
|
102
|
-
name = `Schema${++schemaCounter}`;
|
|
103
|
-
}
|
|
104
|
-
name = name.replace(/[^a-zA-Z0-9]/g, "");
|
|
105
|
-
if (!componentSchemas[name]) {
|
|
106
|
-
const clean = {
|
|
107
|
-
...jsonSchema
|
|
108
|
-
};
|
|
109
|
-
delete clean.title;
|
|
110
|
-
delete clean.label;
|
|
111
|
-
delete clean.$schema;
|
|
112
|
-
componentSchemas[name] = clean;
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
$ref: `#/components/schemas/${name}`
|
|
116
|
-
};
|
|
117
|
-
}, "registerSchema");
|
|
118
|
-
const spec = {
|
|
119
|
-
openapi: "3.0.3",
|
|
120
|
-
info: {
|
|
121
|
-
title: options.info?.title || "API",
|
|
122
|
-
version: options.info?.version || "1.0.0",
|
|
123
|
-
...options.info?.description ? {
|
|
124
|
-
description: options.info.description
|
|
125
|
-
} : {}
|
|
126
|
-
},
|
|
127
|
-
paths: {},
|
|
128
|
-
components: {
|
|
129
|
-
schemas: {},
|
|
130
|
-
securitySchemes: {}
|
|
131
|
-
},
|
|
132
|
-
tags: []
|
|
133
|
-
};
|
|
134
|
-
if (options.servers) {
|
|
135
|
-
spec.servers = options.servers;
|
|
136
|
-
}
|
|
137
|
-
const allTags = /* @__PURE__ */ new Set();
|
|
138
|
-
const securitySchemes = {};
|
|
139
|
-
for (const { controllerClass, mountPath } of registeredRoutes) {
|
|
140
|
-
if (Reflect.getMetadata(SWAGGER_KEYS.EXCLUDE, controllerClass)) continue;
|
|
141
|
-
const routes = Reflect.getMetadata(METADATA.ROUTES, controllerClass) || [];
|
|
142
|
-
const classTags = Reflect.getMetadata(SWAGGER_KEYS.TAGS, controllerClass) || [];
|
|
143
|
-
const classAuth = Reflect.getMetadata(SWAGGER_KEYS.BEARER_AUTH, controllerClass);
|
|
144
|
-
for (const route of routes) {
|
|
145
|
-
if (Reflect.getMetadata(SWAGGER_KEYS.EXCLUDE, controllerClass, route.handlerName)) continue;
|
|
146
|
-
const fullPath = joinPaths(mountPath, route.path);
|
|
147
|
-
const openApiPath = fullPath.replace(/:([a-zA-Z_]+)/g, "{$1}");
|
|
148
|
-
const method = route.method.toLowerCase();
|
|
149
|
-
const operation = Reflect.getMetadata(SWAGGER_KEYS.OPERATION, controllerClass, route.handlerName) || {};
|
|
150
|
-
const responses = Reflect.getMetadata(SWAGGER_KEYS.RESPONSES, controllerClass, route.handlerName) || [];
|
|
151
|
-
const methodTags = Reflect.getMetadata(SWAGGER_KEYS.TAGS, controllerClass, route.handlerName) || [];
|
|
152
|
-
const methodAuth = Reflect.getMetadata(SWAGGER_KEYS.BEARER_AUTH, controllerClass, route.handlerName);
|
|
153
|
-
const tags = methodTags.length > 0 ? methodTags : classTags;
|
|
154
|
-
tags.forEach((t) => allTags.add(t));
|
|
155
|
-
const op = {
|
|
156
|
-
...tags.length > 0 ? {
|
|
157
|
-
tags
|
|
158
|
-
} : {},
|
|
159
|
-
...operation.summary ? {
|
|
160
|
-
summary: operation.summary
|
|
161
|
-
} : {},
|
|
162
|
-
...operation.description ? {
|
|
163
|
-
description: operation.description
|
|
164
|
-
} : {},
|
|
165
|
-
...operation.operationId ? {
|
|
166
|
-
operationId: operation.operationId
|
|
167
|
-
} : {},
|
|
168
|
-
...operation.deprecated ? {
|
|
169
|
-
deprecated: true
|
|
170
|
-
} : {},
|
|
171
|
-
parameters: [],
|
|
172
|
-
responses: {}
|
|
173
|
-
};
|
|
174
|
-
const paramMatches = fullPath.match(/:([a-zA-Z_]+)/g) || [];
|
|
175
|
-
for (const match of paramMatches) {
|
|
176
|
-
const paramName = match.slice(1);
|
|
177
|
-
let schema = {
|
|
178
|
-
type: "string"
|
|
179
|
-
};
|
|
180
|
-
if (route.validation?.params) {
|
|
181
|
-
const jsonSchema = toJsonSchema(route.validation.params);
|
|
182
|
-
if (jsonSchema?.properties && typeof jsonSchema.properties === "object") {
|
|
183
|
-
const props = jsonSchema.properties;
|
|
184
|
-
if (props[paramName]) {
|
|
185
|
-
schema = props[paramName];
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
op.parameters.push({
|
|
190
|
-
name: paramName,
|
|
191
|
-
in: "path",
|
|
192
|
-
required: true,
|
|
193
|
-
schema
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
if (route.validation?.query) {
|
|
197
|
-
const jsonSchema = toJsonSchema(route.validation.query);
|
|
198
|
-
if (jsonSchema?.properties && typeof jsonSchema.properties === "object") {
|
|
199
|
-
const required = Array.isArray(jsonSchema.required) ? jsonSchema.required : [];
|
|
200
|
-
for (const [name, propSchema] of Object.entries(jsonSchema.properties)) {
|
|
201
|
-
op.parameters.push({
|
|
202
|
-
name,
|
|
203
|
-
in: "query",
|
|
204
|
-
required: required.includes(name),
|
|
205
|
-
schema: propSchema
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
const queryParamsConfig = Reflect.getMetadata(METADATA.QUERY_PARAMS, controllerClass, route.handlerName);
|
|
211
|
-
if (queryParamsConfig) {
|
|
212
|
-
if (queryParamsConfig.filterable?.length) {
|
|
213
|
-
op.parameters.push({
|
|
214
|
-
name: "filter",
|
|
215
|
-
in: "query",
|
|
216
|
-
required: false,
|
|
217
|
-
description: `Filter fields: ${queryParamsConfig.filterable.join(", ")}. Format: \`field:operator:value\`. Operators: eq, neq, gt, gte, lt, lte, contains, starts, ends, in, between`,
|
|
218
|
-
schema: {
|
|
219
|
-
type: "array",
|
|
220
|
-
items: {
|
|
221
|
-
type: "string"
|
|
222
|
-
}
|
|
223
|
-
},
|
|
224
|
-
style: "form",
|
|
225
|
-
explode: true
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
if (queryParamsConfig.sortable?.length) {
|
|
229
|
-
op.parameters.push({
|
|
230
|
-
name: "sort",
|
|
231
|
-
in: "query",
|
|
232
|
-
required: false,
|
|
233
|
-
description: `Sort fields: ${queryParamsConfig.sortable.join(", ")}. Format: \`field:asc\` or \`field:desc\``,
|
|
234
|
-
schema: {
|
|
235
|
-
type: "array",
|
|
236
|
-
items: {
|
|
237
|
-
type: "string"
|
|
238
|
-
}
|
|
239
|
-
},
|
|
240
|
-
style: "form",
|
|
241
|
-
explode: true
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
if (queryParamsConfig.searchable?.length) {
|
|
245
|
-
op.parameters.push({
|
|
246
|
-
name: "q",
|
|
247
|
-
in: "query",
|
|
248
|
-
required: false,
|
|
249
|
-
description: `Search across: ${queryParamsConfig.searchable.join(", ")}`,
|
|
250
|
-
schema: {
|
|
251
|
-
type: "string"
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
op.parameters.push({
|
|
256
|
-
name: "page",
|
|
257
|
-
in: "query",
|
|
258
|
-
required: false,
|
|
259
|
-
description: "Page number (default: 1)",
|
|
260
|
-
schema: {
|
|
261
|
-
type: "integer",
|
|
262
|
-
minimum: 1,
|
|
263
|
-
default: 1
|
|
264
|
-
}
|
|
265
|
-
}, {
|
|
266
|
-
name: "limit",
|
|
267
|
-
in: "query",
|
|
268
|
-
required: false,
|
|
269
|
-
description: "Items per page (default: 20, max: 100)",
|
|
270
|
-
schema: {
|
|
271
|
-
type: "integer",
|
|
272
|
-
minimum: 1,
|
|
273
|
-
maximum: 100,
|
|
274
|
-
default: 20
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
if (op.parameters.length === 0) delete op.parameters;
|
|
279
|
-
if (route.validation?.body && [
|
|
280
|
-
"post",
|
|
281
|
-
"put",
|
|
282
|
-
"patch"
|
|
283
|
-
].includes(method)) {
|
|
284
|
-
const bodySchema = toJsonSchema(route.validation.body);
|
|
285
|
-
if (bodySchema) {
|
|
286
|
-
const bodyName = route.validation.name || `${route.handlerName}Body`;
|
|
287
|
-
const ref = registerSchema(bodySchema, bodyName);
|
|
288
|
-
op.requestBody = {
|
|
289
|
-
required: true,
|
|
290
|
-
content: {
|
|
291
|
-
"application/json": {
|
|
292
|
-
schema: ref
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
const fileUpload = Reflect.getMetadata(METADATA.FILE_UPLOAD, controllerClass, route.handlerName);
|
|
299
|
-
if (fileUpload) {
|
|
300
|
-
const fieldName = fileUpload.fieldName ?? "file";
|
|
301
|
-
const properties = {};
|
|
302
|
-
if (fileUpload.mode === "array") {
|
|
303
|
-
properties[fieldName] = {
|
|
304
|
-
type: "array",
|
|
305
|
-
items: {
|
|
306
|
-
type: "string",
|
|
307
|
-
format: "binary"
|
|
308
|
-
}
|
|
309
|
-
};
|
|
310
|
-
} else if (fileUpload.mode !== "none") {
|
|
311
|
-
properties[fieldName] = {
|
|
312
|
-
type: "string",
|
|
313
|
-
format: "binary"
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
op.requestBody = {
|
|
317
|
-
required: true,
|
|
318
|
-
content: {
|
|
319
|
-
"multipart/form-data": {
|
|
320
|
-
schema: {
|
|
321
|
-
type: "object",
|
|
322
|
-
properties
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
if (responses.length > 0) {
|
|
329
|
-
for (const resp of responses) {
|
|
330
|
-
op.responses[String(resp.status)] = {
|
|
331
|
-
description: resp.description || "",
|
|
332
|
-
...resp.schema ? (() => {
|
|
333
|
-
const converted = typeof resp.schema === "function" || typeof resp.schema === "object" ? toJsonSchema(resp.schema) : null;
|
|
334
|
-
const schemaName = resp.name || `${route.handlerName}Response${resp.status}`;
|
|
335
|
-
const finalSchema = converted ? registerSchema(converted, schemaName) : typeof resp.schema === "object" ? resp.schema : void 0;
|
|
336
|
-
return finalSchema ? {
|
|
337
|
-
content: {
|
|
338
|
-
"application/json": {
|
|
339
|
-
schema: finalSchema
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
} : {};
|
|
343
|
-
})() : {}
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
} else {
|
|
347
|
-
const defaultStatus = method === "post" ? "201" : method === "delete" ? "204" : "200";
|
|
348
|
-
op.responses[defaultStatus] = {
|
|
349
|
-
description: "Successful operation"
|
|
350
|
-
};
|
|
351
|
-
if (route.validation?.body) {
|
|
352
|
-
op.responses["422"] = {
|
|
353
|
-
description: "Validation error"
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
const authName = methodAuth || classAuth;
|
|
358
|
-
if (authName) {
|
|
359
|
-
op.security = [
|
|
360
|
-
{
|
|
361
|
-
[authName]: []
|
|
362
|
-
}
|
|
363
|
-
];
|
|
364
|
-
securitySchemes[authName] = {
|
|
365
|
-
type: "http",
|
|
366
|
-
scheme: "bearer",
|
|
367
|
-
bearerFormat: "JWT"
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
if (!spec.paths[openApiPath]) spec.paths[openApiPath] = {};
|
|
371
|
-
spec.paths[openApiPath][method] = op;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
spec.tags = Array.from(allTags).map((name) => ({
|
|
375
|
-
name
|
|
376
|
-
}));
|
|
377
|
-
spec.components.securitySchemes = securitySchemes;
|
|
378
|
-
if (options.bearerAuth) {
|
|
379
|
-
if (!securitySchemes.BearerAuth) {
|
|
380
|
-
spec.components.securitySchemes.BearerAuth = {
|
|
381
|
-
type: "http",
|
|
382
|
-
scheme: "bearer",
|
|
383
|
-
bearerFormat: "JWT"
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
spec.security = [
|
|
387
|
-
{
|
|
388
|
-
BearerAuth: []
|
|
389
|
-
}
|
|
390
|
-
];
|
|
391
|
-
}
|
|
392
|
-
spec.components.schemas = componentSchemas;
|
|
393
|
-
if (Object.keys(spec.components.schemas).length === 0) delete spec.components.schemas;
|
|
394
|
-
if (Object.keys(spec.components.securitySchemes).length === 0) delete spec.components.securitySchemes;
|
|
395
|
-
if (Object.keys(spec.components).length === 0) delete spec.components;
|
|
396
|
-
return spec;
|
|
397
|
-
}
|
|
398
|
-
__name(buildOpenAPISpec, "buildOpenAPISpec");
|
|
399
|
-
|
|
400
|
-
// src/swagger.adapter.ts
|
|
401
|
-
import { Router } from "express";
|
|
402
|
-
import { Logger } from "@forinda/kickjs-core";
|
|
403
|
-
|
|
404
|
-
// src/ui.ts
|
|
405
|
-
function escapeHtml(str) {
|
|
406
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
407
|
-
}
|
|
408
|
-
__name(escapeHtml, "escapeHtml");
|
|
409
|
-
function swaggerUIHtml(specUrl, title = "API Docs") {
|
|
410
|
-
const safeTitle = escapeHtml(title);
|
|
411
|
-
const safeUrl = JSON.stringify(specUrl).replace(/</g, "\\u003c");
|
|
412
|
-
return `<!DOCTYPE html>
|
|
1
|
+
var z=Object.defineProperty;var c=(e,t)=>z(e,"name",{value:t,configurable:!0});var v={name:"zod",supports(e){return e!=null&&typeof e=="object"&&typeof e.safeParse=="function"&&typeof e.toJSONSchema=="function"},toJsonSchema(e){let{$schema:t,...o}=e.toJSONSchema();return o}};import"reflect-metadata";var p={OPERATION:Symbol("kick:swagger:operation"),RESPONSES:Symbol("kick:swagger:responses"),TAGS:Symbol("kick:swagger:tags"),BEARER_AUTH:Symbol("kick:swagger:bearer"),EXCLUDE:Symbol("kick:swagger:exclude")};function W(e){return(t,o)=>{Reflect.defineMetadata(p.OPERATION,e,t.constructor,o)}}c(W,"ApiOperation");function X(e){return(t,o)=>{let n=Reflect.getMetadata(p.RESPONSES,t.constructor,o)||[];Reflect.defineMetadata(p.RESPONSES,[...n,e],t.constructor,o)}}c(X,"ApiResponse");function Y(...e){return(t,o)=>{o?Reflect.defineMetadata(p.TAGS,e,t.constructor,o):Reflect.defineMetadata(p.TAGS,e,t)}}c(Y,"ApiTags");function Z(e="BearerAuth"){return(t,o)=>{o?Reflect.defineMetadata(p.BEARER_AUTH,e,t.constructor,o):Reflect.defineMetadata(p.BEARER_AUTH,e,t)}}c(Z,"ApiBearerAuth");function Q(){return(e,t)=>{t?Reflect.defineMetadata(p.EXCLUDE,!0,e.constructor,t):Reflect.defineMetadata(p.EXCLUDE,!0,e)}}c(Q,"ApiExclude");import"reflect-metadata";import{METADATA as x,joinPaths as V}from"@forinda/kickjs-core";var U=[];function j(e,t){U.push({controllerClass:e,mountPath:t})}c(j,"registerControllerForDocs");function M(){U.length=0}c(M,"clearRegisteredRoutes");function N(e={}){let t=e.schemaParser??v,o=c(a=>{try{return t.supports(a)?t.toJsonSchema(a):null}catch{return null}},"toJsonSchema"),n={},S=0,u=c((a,w)=>{let h=a.title||a.label||w||"";if(h||(h=`Schema${++S}`),h=h.replace(/[^a-zA-Z0-9]/g,""),!n[h]){let b={...a};delete b.title,delete b.label,delete b.$schema,n[h]=b}return{$ref:`#/components/schemas/${h}`}},"registerSchema"),r={openapi:"3.0.3",info:{title:e.info?.title||"API",version:e.info?.version||"1.0.0",...e.info?.description?{description:e.info.description}:{}},paths:{},components:{schemas:{},securitySchemes:{}},tags:[]};e.servers&&(r.servers=e.servers);let f=new Set,m={};for(let{controllerClass:a,mountPath:w}of U){if(Reflect.getMetadata(p.EXCLUDE,a))continue;let h=Reflect.getMetadata(x.ROUTES,a)||[],b=Reflect.getMetadata(p.TAGS,a)||[],L=Reflect.getMetadata(p.BEARER_AUTH,a);for(let i of h){if(Reflect.getMetadata(p.EXCLUDE,a,i.handlerName))continue;let D=V(w,i.path),q=D.replace(/:([a-zA-Z_]+)/g,"{$1}"),E=i.method.toLowerCase(),A=Reflect.getMetadata(p.OPERATION,a,i.handlerName)||{},C=Reflect.getMetadata(p.RESPONSES,a,i.handlerName)||[],H=Reflect.getMetadata(p.TAGS,a,i.handlerName)||[],G=Reflect.getMetadata(p.BEARER_AUTH,a,i.handlerName),T=H.length>0?H:b;T.forEach(s=>f.add(s));let l={...T.length>0?{tags:T}:{},...A.summary?{summary:A.summary}:{},...A.description?{description:A.description}:{},...A.operationId?{operationId:A.operationId}:{},...A.deprecated?{deprecated:!0}:{},parameters:[],responses:{}},J=D.match(/:([a-zA-Z_]+)/g)||[];for(let s of J){let d=s.slice(1),g={type:"string"};if(i.validation?.params){let y=o(i.validation.params);if(y?.properties&&typeof y.properties=="object"){let F=y.properties;F[d]&&(g=F[d])}}l.parameters.push({name:d,in:"path",required:!0,schema:g})}if(i.validation?.query){let s=o(i.validation.query);if(s?.properties&&typeof s.properties=="object"){let d=Array.isArray(s.required)?s.required:[];for(let[g,y]of Object.entries(s.properties))l.parameters.push({name:g,in:"query",required:d.includes(g),schema:y})}}let R=Reflect.getMetadata(x.QUERY_PARAMS,a,i.handlerName);if(R&&(R.filterable?.length&&l.parameters.push({name:"filter",in:"query",required:!1,description:`Filter fields: ${R.filterable.join(", ")}. Format: \`field:operator:value\`. Operators: eq, neq, gt, gte, lt, lte, contains, starts, ends, in, between`,schema:{type:"array",items:{type:"string"}},style:"form",explode:!0}),R.sortable?.length&&l.parameters.push({name:"sort",in:"query",required:!1,description:`Sort fields: ${R.sortable.join(", ")}. Format: \`field:asc\` or \`field:desc\``,schema:{type:"array",items:{type:"string"}},style:"form",explode:!0}),R.searchable?.length&&l.parameters.push({name:"q",in:"query",required:!1,description:`Search across: ${R.searchable.join(", ")}`,schema:{type:"string"}}),l.parameters.push({name:"page",in:"query",required:!1,description:"Page number (default: 1)",schema:{type:"integer",minimum:1,default:1}},{name:"limit",in:"query",required:!1,description:"Items per page (default: 20, max: 100)",schema:{type:"integer",minimum:1,maximum:100,default:20}})),l.parameters.length===0&&delete l.parameters,i.validation?.body&&["post","put","patch"].includes(E)){let s=o(i.validation.body);if(s){let d=i.validation.name||`${i.handlerName}Body`,g=u(s,d);l.requestBody={required:!0,content:{"application/json":{schema:g}}}}}let P=Reflect.getMetadata(x.FILE_UPLOAD,a,i.handlerName);if(P){let s=P.fieldName??"file",d={};P.mode==="array"?d[s]={type:"array",items:{type:"string",format:"binary"}}:P.mode!=="none"&&(d[s]={type:"string",format:"binary"}),l.requestBody={required:!0,content:{"multipart/form-data":{schema:{type:"object",properties:d}}}}}if(C.length>0)for(let s of C)l.responses[String(s.status)]={description:s.description||"",...s.schema?(()=>{let d=typeof s.schema=="function"||typeof s.schema=="object"?o(s.schema):null,g=s.name||`${i.handlerName}Response${s.status}`,y=d?u(d,g):typeof s.schema=="object"?s.schema:void 0;return y?{content:{"application/json":{schema:y}}}:{}})():{}};else{let s=E==="post"?"201":E==="delete"?"204":"200";l.responses[s]={description:"Successful operation"},i.validation?.body&&(l.responses[422]={description:"Validation error"})}let O=G||L;O&&(l.security=[{[O]:[]}],m[O]={type:"http",scheme:"bearer",bearerFormat:"JWT"}),r.paths[q]||(r.paths[q]={}),r.paths[q][E]=l}}return r.tags=Array.from(f).map(a=>({name:a})),r.components.securitySchemes=m,e.bearerAuth&&(m.BearerAuth||(r.components.securitySchemes.BearerAuth={type:"http",scheme:"bearer",bearerFormat:"JWT"}),r.security=[{BearerAuth:[]}]),r.components.schemas=n,Object.keys(r.components.schemas).length===0&&delete r.components.schemas,Object.keys(r.components.securitySchemes).length===0&&delete r.components.securitySchemes,Object.keys(r.components).length===0&&delete r.components,r}c(N,"buildOpenAPISpec");import{Router as K}from"express";import{Logger as ee}from"@forinda/kickjs-core";function $(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}c($,"escapeHtml");function k(e,t="API Docs"){let o=$(t),n=JSON.stringify(e).replace(/</g,"\\u003c");return`<!DOCTYPE html>
|
|
413
2
|
<html lang="en">
|
|
414
3
|
<head>
|
|
415
4
|
<meta charset="UTF-8">
|
|
416
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
417
|
-
<title>${
|
|
6
|
+
<title>${o}</title>
|
|
418
7
|
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
|
|
419
8
|
</head>
|
|
420
9
|
<body>
|
|
@@ -423,7 +12,7 @@ function swaggerUIHtml(specUrl, title = "API Docs") {
|
|
|
423
12
|
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
|
|
424
13
|
<script>
|
|
425
14
|
SwaggerUIBundle({
|
|
426
|
-
url: ${
|
|
15
|
+
url: ${n},
|
|
427
16
|
dom_id: '#swagger-ui',
|
|
428
17
|
deepLinking: true,
|
|
429
18
|
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
|
|
@@ -432,111 +21,15 @@ function swaggerUIHtml(specUrl, title = "API Docs") {
|
|
|
432
21
|
});
|
|
433
22
|
</script>
|
|
434
23
|
</body>
|
|
435
|
-
</html
|
|
436
|
-
}
|
|
437
|
-
__name(swaggerUIHtml, "swaggerUIHtml");
|
|
438
|
-
function redocHtml(specUrl, title = "API Docs") {
|
|
439
|
-
const safeTitle = escapeHtml(title);
|
|
440
|
-
const safeUrl = escapeHtml(specUrl);
|
|
441
|
-
return `<!DOCTYPE html>
|
|
24
|
+
</html>`}c(k,"swaggerUIHtml");function I(e,t="API Docs"){let o=$(t),n=$(e);return`<!DOCTYPE html>
|
|
442
25
|
<html lang="en">
|
|
443
26
|
<head>
|
|
444
27
|
<meta charset="UTF-8">
|
|
445
28
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
446
|
-
<title>${
|
|
29
|
+
<title>${o}</title>
|
|
447
30
|
</head>
|
|
448
31
|
<body>
|
|
449
|
-
<redoc spec-url="${
|
|
32
|
+
<redoc spec-url="${n}"></redoc>
|
|
450
33
|
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
|
|
451
34
|
</body>
|
|
452
|
-
</html
|
|
453
|
-
}
|
|
454
|
-
__name(redocHtml, "redocHtml");
|
|
455
|
-
|
|
456
|
-
// src/swagger.adapter.ts
|
|
457
|
-
var log = Logger.for("SwaggerAdapter");
|
|
458
|
-
var SwaggerAdapter = class {
|
|
459
|
-
static {
|
|
460
|
-
__name(this, "SwaggerAdapter");
|
|
461
|
-
}
|
|
462
|
-
options;
|
|
463
|
-
name = "SwaggerAdapter";
|
|
464
|
-
constructor(options = {}) {
|
|
465
|
-
this.options = options;
|
|
466
|
-
}
|
|
467
|
-
/** Auto-detect server URLs from the running HTTP server and peer adapters */
|
|
468
|
-
afterStart(server, _container) {
|
|
469
|
-
const addr = server?.address?.();
|
|
470
|
-
if (!addr || typeof addr !== "object") return;
|
|
471
|
-
const host = addr.address === "::" || addr.address === "0.0.0.0" ? "localhost" : addr.address;
|
|
472
|
-
if (!this.options.servers || this.options.servers.length === 0) {
|
|
473
|
-
this.options.servers = [
|
|
474
|
-
{
|
|
475
|
-
url: `http://${host}:${addr.port}`,
|
|
476
|
-
description: "HTTP server"
|
|
477
|
-
}
|
|
478
|
-
];
|
|
479
|
-
}
|
|
480
|
-
const wsAdapter = this.options.adapters?.find((a) => a.name === "WsAdapter" && typeof a.getStats === "function");
|
|
481
|
-
if (wsAdapter) {
|
|
482
|
-
const stats = wsAdapter.getStats();
|
|
483
|
-
for (const namespace of Object.keys(stats.namespaces || {})) {
|
|
484
|
-
this.options.servers.push({
|
|
485
|
-
url: `ws://${host}:${addr.port}${namespace}`,
|
|
486
|
-
description: `WebSocket: ${namespace}`
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
/** Collect controller metadata as routes are mounted */
|
|
492
|
-
onRouteMount(controllerClass, mountPath) {
|
|
493
|
-
registerControllerForDocs(controllerClass, mountPath);
|
|
494
|
-
}
|
|
495
|
-
beforeMount(app, _container) {
|
|
496
|
-
clearRegisteredRoutes();
|
|
497
|
-
const docsPath = this.options.docsPath ?? "/docs";
|
|
498
|
-
const redocPath = this.options.redocPath ?? "/redoc";
|
|
499
|
-
const specPath = this.options.specPath ?? "/openapi.json";
|
|
500
|
-
const docsRouter = Router();
|
|
501
|
-
docsRouter.use((_req, res, next) => {
|
|
502
|
-
res.setHeader("Content-Security-Policy", [
|
|
503
|
-
"default-src 'self'",
|
|
504
|
-
"script-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.redoc.ly https://cdn.jsdelivr.net",
|
|
505
|
-
"style-src 'self' 'unsafe-inline' https://unpkg.com https://fonts.googleapis.com",
|
|
506
|
-
"font-src 'self' https://fonts.gstatic.com",
|
|
507
|
-
"img-src 'self' data: https://unpkg.com",
|
|
508
|
-
"connect-src 'self'"
|
|
509
|
-
].join("; "));
|
|
510
|
-
next();
|
|
511
|
-
});
|
|
512
|
-
docsRouter.get(specPath, (_req, res) => {
|
|
513
|
-
const spec = buildOpenAPISpec(this.options);
|
|
514
|
-
res.json(spec);
|
|
515
|
-
});
|
|
516
|
-
docsRouter.get(docsPath, (_req, res) => {
|
|
517
|
-
res.type("html").send(swaggerUIHtml(specPath, this.options.info?.title));
|
|
518
|
-
});
|
|
519
|
-
docsRouter.get(redocPath, (_req, res) => {
|
|
520
|
-
res.type("html").send(redocHtml(specPath, this.options.info?.title));
|
|
521
|
-
});
|
|
522
|
-
app.use(docsRouter);
|
|
523
|
-
log.info(`Swagger UI: ${docsPath}`);
|
|
524
|
-
log.info(`ReDoc: ${redocPath}`);
|
|
525
|
-
log.info(`OpenAPI spec: ${specPath}`);
|
|
526
|
-
}
|
|
527
|
-
};
|
|
528
|
-
export {
|
|
529
|
-
ApiBearerAuth,
|
|
530
|
-
ApiExclude,
|
|
531
|
-
ApiOperation,
|
|
532
|
-
ApiResponse,
|
|
533
|
-
ApiTags,
|
|
534
|
-
SwaggerAdapter,
|
|
535
|
-
buildOpenAPISpec,
|
|
536
|
-
clearRegisteredRoutes,
|
|
537
|
-
redocHtml,
|
|
538
|
-
registerControllerForDocs,
|
|
539
|
-
swaggerUIHtml,
|
|
540
|
-
zodSchemaParser
|
|
541
|
-
};
|
|
542
|
-
//# sourceMappingURL=index.js.map
|
|
35
|
+
</html>`}c(I,"redocHtml");var _=ee.for("SwaggerAdapter"),B=class{static{c(this,"SwaggerAdapter")}options;name="SwaggerAdapter";constructor(t={}){this.options=t}afterStart(t,o){let n=t?.address?.();if(!n||typeof n!="object")return;let S=n.address==="::"||n.address==="0.0.0.0"?"localhost":n.address;(!this.options.servers||this.options.servers.length===0)&&(this.options.servers=[{url:`http://${S}:${n.port}`,description:"HTTP server"}]);let u=this.options.adapters?.find(r=>r.name==="WsAdapter"&&typeof r.getStats=="function");if(u){let r=u.getStats();for(let f of Object.keys(r.namespaces||{}))this.options.servers.push({url:`ws://${S}:${n.port}${f}`,description:`WebSocket: ${f}`})}}onRouteMount(t,o){j(t,o)}beforeMount(t,o){M();let n=this.options.docsPath??"/docs",S=this.options.redocPath??"/redoc",u=this.options.specPath??"/openapi.json",r=K();r.use((f,m,a)=>{m.setHeader("Content-Security-Policy",["default-src 'self'","script-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.redoc.ly https://cdn.jsdelivr.net","style-src 'self' 'unsafe-inline' https://unpkg.com https://fonts.googleapis.com","font-src 'self' https://fonts.gstatic.com","img-src 'self' data: https://unpkg.com","connect-src 'self'"].join("; ")),a()}),r.get(u,(f,m)=>{let a=N(this.options);m.json(a)}),r.get(n,(f,m)=>{m.type("html").send(k(u,this.options.info?.title))}),r.get(S,(f,m)=>{m.type("html").send(I(u,this.options.info?.title))}),t.use(r),_.info(`Swagger UI: ${n}`),_.info(`ReDoc: ${S}`),_.info(`OpenAPI spec: ${u}`)}};export{Z as ApiBearerAuth,Q as ApiExclude,W as ApiOperation,X as ApiResponse,Y as ApiTags,B as SwaggerAdapter,N as buildOpenAPISpec,M as clearRegisteredRoutes,I as redocHtml,j as registerControllerForDocs,k as swaggerUIHtml,v as zodSchemaParser};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forinda/kickjs-swagger",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.12",
|
|
4
4
|
"description": "OpenAPI spec generation from decorators, Swagger UI and ReDoc serving for KickJS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kickjs",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"reflect-metadata": "^0.2.2",
|
|
43
|
-
"@forinda/kickjs-core": "1.2.
|
|
43
|
+
"@forinda/kickjs-core": "1.2.12"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"express": "^5.1.0",
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/schema-parser.ts","../src/decorators.ts","../src/openapi-builder.ts","../src/swagger.adapter.ts","../src/ui.ts"],"sourcesContent":["/**\n * Interface for converting validation library schemas to JSON Schema.\n *\n * KickJS ships with a Zod parser by default. To use a different validation\n * library (Yup, Joi, Valibot, ArkType, etc.), implement this interface and\n * pass it to the SwaggerAdapter.\n *\n * @example\n * ```ts\n * import Joi from 'joi'\n * import joiToJson from 'joi-to-json'\n *\n * const joiParser: SchemaParser = {\n * name: 'joi',\n * supports: (schema) => Joi.isSchema(schema),\n * toJsonSchema: (schema) => joiToJson(schema),\n * }\n *\n * new SwaggerAdapter({ schemaParser: joiParser })\n * ```\n */\nexport interface SchemaParser {\n /** Human-readable name for logging/debugging */\n readonly name: string\n\n /**\n * Return true if this parser can handle the given schema object.\n * Called before `toJsonSchema` to allow graceful fallback.\n */\n supports(schema: unknown): boolean\n\n /**\n * Convert a validation schema to a JSON Schema object.\n * Should return a plain object conforming to JSON Schema draft-07 or later.\n * Must not include the top-level `$schema` key — the builder adds it.\n */\n toJsonSchema(schema: unknown): Record<string, unknown>\n}\n\n/**\n * Default schema parser for Zod v4+.\n * Uses Zod's built-in `.toJSONSchema()` instance method.\n */\nexport const zodSchemaParser: SchemaParser = {\n name: 'zod',\n\n supports(schema: unknown): boolean {\n return (\n schema != null &&\n typeof schema === 'object' &&\n typeof (schema as any).safeParse === 'function' &&\n typeof (schema as any).toJSONSchema === 'function'\n )\n },\n\n toJsonSchema(schema: unknown): Record<string, unknown> {\n const { $schema: _, ...rest } = (schema as any).toJSONSchema() as Record<string, unknown>\n return rest\n },\n}\n","import 'reflect-metadata'\n\nconst SWAGGER_KEYS = {\n OPERATION: Symbol('kick:swagger:operation'),\n RESPONSES: Symbol('kick:swagger:responses'),\n TAGS: Symbol('kick:swagger:tags'),\n BEARER_AUTH: Symbol('kick:swagger:bearer'),\n EXCLUDE: Symbol('kick:swagger:exclude'),\n}\n\nexport { SWAGGER_KEYS }\n\nexport interface ApiOperationOptions {\n summary?: string\n description?: string\n operationId?: string\n deprecated?: boolean\n}\n\nexport interface ApiResponseOptions {\n status: number\n description?: string\n schema?: any\n /** Schema name in components/schemas (e.g., 'UserResponse', 'ErrorBody'). Auto-generated from handler name if omitted. */\n name?: string\n}\n\n/** Attach operation metadata to a route handler */\nexport function ApiOperation(options: ApiOperationOptions): MethodDecorator {\n return (target, propertyKey) => {\n Reflect.defineMetadata(SWAGGER_KEYS.OPERATION, options, target.constructor, propertyKey)\n }\n}\n\n/** Document a response status. Can be stacked multiple times. */\nexport function ApiResponse(options: ApiResponseOptions): MethodDecorator {\n return (target, propertyKey) => {\n const existing: ApiResponseOptions[] =\n Reflect.getMetadata(SWAGGER_KEYS.RESPONSES, target.constructor, propertyKey) || []\n Reflect.defineMetadata(\n SWAGGER_KEYS.RESPONSES,\n [...existing, options],\n target.constructor,\n propertyKey,\n )\n }\n}\n\n/** Apply OpenAPI tags at class or method level */\nexport function ApiTags(...tags: string[]): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n Reflect.defineMetadata(SWAGGER_KEYS.TAGS, tags, target.constructor, propertyKey)\n } else {\n Reflect.defineMetadata(SWAGGER_KEYS.TAGS, tags, target)\n }\n }\n}\n\n/** Mark endpoint as requiring Bearer token auth */\nexport function ApiBearerAuth(name = 'BearerAuth'): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n Reflect.defineMetadata(SWAGGER_KEYS.BEARER_AUTH, name, target.constructor, propertyKey)\n } else {\n Reflect.defineMetadata(SWAGGER_KEYS.BEARER_AUTH, name, target)\n }\n }\n}\n\n/** Exclude a controller or method from the OpenAPI spec */\nexport function ApiExclude(): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n Reflect.defineMetadata(SWAGGER_KEYS.EXCLUDE, true, target.constructor, propertyKey)\n } else {\n Reflect.defineMetadata(SWAGGER_KEYS.EXCLUDE, true, target)\n }\n }\n}\n","import 'reflect-metadata'\nimport { METADATA, joinPaths, type RouteDefinition } from '@forinda/kickjs-core'\nimport { SWAGGER_KEYS, type ApiOperationOptions, type ApiResponseOptions } from './decorators'\nimport { zodSchemaParser, type SchemaParser } from './schema-parser'\n\nexport interface OpenAPIInfo {\n title: string\n version: string\n description?: string\n}\n\nexport interface SwaggerOptions {\n info?: Partial<OpenAPIInfo>\n servers?: { url: string; description?: string }[]\n bearerAuth?: boolean\n /**\n * Pluggable schema parser for converting validation schemas to JSON Schema.\n * Defaults to `zodSchemaParser` which handles Zod v4+ schemas.\n *\n * Override this to use Yup, Joi, Valibot, ArkType, or any other library.\n *\n * @example\n * ```ts\n * new SwaggerAdapter({\n * schemaParser: myYupParser,\n * })\n * ```\n */\n schemaParser?: SchemaParser\n}\n\ninterface RegisteredRoute {\n controllerClass: any\n mountPath: string\n}\n\nconst registeredRoutes: RegisteredRoute[] = []\n\n/** Register a controller for OpenAPI introspection (called by Application during route mounting) */\nexport function registerControllerForDocs(controllerClass: any, mountPath: string): void {\n registeredRoutes.push({ controllerClass, mountPath })\n}\n\n/** Clear all registered routes (for HMR) */\nexport function clearRegisteredRoutes(): void {\n registeredRoutes.length = 0\n}\n\n/** Build a full OpenAPI 3.0.3 spec from registered controllers and their decorators */\nexport function buildOpenAPISpec(options: SwaggerOptions = {}): any {\n const parser = options.schemaParser ?? zodSchemaParser\n\n /** Convert a validation schema to JSON Schema using the configured parser */\n const toJsonSchema = (schema: unknown): Record<string, unknown> | null => {\n try {\n if (!parser.supports(schema)) return null\n return parser.toJsonSchema(schema)\n } catch {\n return null\n }\n }\n\n const componentSchemas: Record<string, any> = {}\n let schemaCounter = 0\n\n /**\n * Register a schema in components.schemas and return a $ref pointer.\n * If the schema has a title/label, use that as the name. Otherwise generate one.\n */\n const registerSchema = (jsonSchema: Record<string, unknown>, hint?: string): any => {\n // Try to extract a name from the schema\n let name = (jsonSchema.title as string) || (jsonSchema.label as string) || hint || ''\n if (!name) {\n name = `Schema${++schemaCounter}`\n }\n // Sanitize name for OpenAPI (remove spaces, special chars)\n name = name.replace(/[^a-zA-Z0-9]/g, '')\n\n // Avoid duplicates — if already registered with same name, reuse\n if (!componentSchemas[name]) {\n const clean = { ...jsonSchema }\n delete clean.title\n delete clean.label\n delete clean.$schema\n componentSchemas[name] = clean\n }\n return { $ref: `#/components/schemas/${name}` }\n }\n\n const spec: any = {\n openapi: '3.0.3',\n info: {\n title: options.info?.title || 'API',\n version: options.info?.version || '1.0.0',\n ...(options.info?.description ? { description: options.info.description } : {}),\n },\n paths: {},\n components: { schemas: {}, securitySchemes: {} },\n tags: [],\n }\n\n if (options.servers) {\n spec.servers = options.servers\n }\n\n const allTags = new Set<string>()\n const securitySchemes: Record<string, any> = {}\n\n for (const { controllerClass, mountPath } of registeredRoutes) {\n // Skip excluded controllers\n if (Reflect.getMetadata(SWAGGER_KEYS.EXCLUDE, controllerClass)) continue\n\n const routes: RouteDefinition[] = Reflect.getMetadata(METADATA.ROUTES, controllerClass) || []\n const classTags: string[] = Reflect.getMetadata(SWAGGER_KEYS.TAGS, controllerClass) || []\n const classAuth: string | undefined = Reflect.getMetadata(\n SWAGGER_KEYS.BEARER_AUTH,\n controllerClass,\n )\n for (const route of routes) {\n // Skip excluded methods\n if (Reflect.getMetadata(SWAGGER_KEYS.EXCLUDE, controllerClass, route.handlerName)) continue\n\n // Build the full path — mountPath is the actual Express mount prefix (from onRouteMount),\n // and route.path is the method-level path. @Controller path is not included here\n // because buildRoutes does not bake it into the router.\n const fullPath = joinPaths(mountPath, route.path)\n\n // Convert Express :param to OpenAPI {param}\n const openApiPath = fullPath.replace(/:([a-zA-Z_]+)/g, '{$1}')\n const method = route.method.toLowerCase()\n\n // Gather metadata\n const operation: ApiOperationOptions =\n Reflect.getMetadata(SWAGGER_KEYS.OPERATION, controllerClass, route.handlerName) || {}\n const responses: ApiResponseOptions[] =\n Reflect.getMetadata(SWAGGER_KEYS.RESPONSES, controllerClass, route.handlerName) || []\n const methodTags: string[] =\n Reflect.getMetadata(SWAGGER_KEYS.TAGS, controllerClass, route.handlerName) || []\n const methodAuth: string | undefined = Reflect.getMetadata(\n SWAGGER_KEYS.BEARER_AUTH,\n controllerClass,\n route.handlerName,\n )\n\n // Tags — method level overrides class level\n const tags = methodTags.length > 0 ? methodTags : classTags\n tags.forEach((t) => allTags.add(t))\n\n // Build operation object\n const op: any = {\n ...(tags.length > 0 ? { tags } : {}),\n ...(operation.summary ? { summary: operation.summary } : {}),\n ...(operation.description ? { description: operation.description } : {}),\n ...(operation.operationId ? { operationId: operation.operationId } : {}),\n ...(operation.deprecated ? { deprecated: true } : {}),\n parameters: [],\n responses: {},\n }\n\n // Path parameters\n const paramMatches = fullPath.match(/:([a-zA-Z_]+)/g) || []\n for (const match of paramMatches) {\n const paramName = match.slice(1)\n let schema: any = { type: 'string' }\n\n // Try to get type from params validation schema\n if (route.validation?.params) {\n const jsonSchema = toJsonSchema(route.validation.params)\n if (jsonSchema?.properties && typeof jsonSchema.properties === 'object') {\n const props = jsonSchema.properties as Record<string, any>\n if (props[paramName]) {\n schema = props[paramName]\n }\n }\n }\n\n op.parameters.push({\n name: paramName,\n in: 'path',\n required: true,\n schema,\n })\n }\n\n // Query parameters\n if (route.validation?.query) {\n const jsonSchema = toJsonSchema(route.validation.query)\n if (jsonSchema?.properties && typeof jsonSchema.properties === 'object') {\n const required = Array.isArray(jsonSchema.required) ? jsonSchema.required : []\n for (const [name, propSchema] of Object.entries(\n jsonSchema.properties as Record<string, any>,\n )) {\n op.parameters.push({\n name,\n in: 'query',\n required: required.includes(name),\n schema: propSchema,\n })\n }\n }\n }\n\n // @ApiQueryParams decorator — document filterable/sortable/searchable fields\n const queryParamsConfig = Reflect.getMetadata(\n METADATA.QUERY_PARAMS,\n controllerClass,\n route.handlerName,\n )\n if (queryParamsConfig) {\n if (queryParamsConfig.filterable?.length) {\n op.parameters.push({\n name: 'filter',\n in: 'query',\n required: false,\n description: `Filter fields: ${queryParamsConfig.filterable.join(', ')}. Format: \\`field:operator:value\\`. Operators: eq, neq, gt, gte, lt, lte, contains, starts, ends, in, between`,\n schema: { type: 'array', items: { type: 'string' } },\n style: 'form',\n explode: true,\n })\n }\n if (queryParamsConfig.sortable?.length) {\n op.parameters.push({\n name: 'sort',\n in: 'query',\n required: false,\n description: `Sort fields: ${queryParamsConfig.sortable.join(', ')}. Format: \\`field:asc\\` or \\`field:desc\\``,\n schema: { type: 'array', items: { type: 'string' } },\n style: 'form',\n explode: true,\n })\n }\n if (queryParamsConfig.searchable?.length) {\n op.parameters.push({\n name: 'q',\n in: 'query',\n required: false,\n description: `Search across: ${queryParamsConfig.searchable.join(', ')}`,\n schema: { type: 'string' },\n })\n }\n op.parameters.push(\n {\n name: 'page',\n in: 'query',\n required: false,\n description: 'Page number (default: 1)',\n schema: { type: 'integer', minimum: 1, default: 1 },\n },\n {\n name: 'limit',\n in: 'query',\n required: false,\n description: 'Items per page (default: 20, max: 100)',\n schema: { type: 'integer', minimum: 1, maximum: 100, default: 20 },\n },\n )\n }\n\n // Remove empty parameters array\n if (op.parameters.length === 0) delete op.parameters\n\n // Request body\n if (route.validation?.body && ['post', 'put', 'patch'].includes(method)) {\n const bodySchema = toJsonSchema(route.validation.body)\n if (bodySchema) {\n const bodyName = route.validation.name || `${route.handlerName}Body`\n const ref = registerSchema(bodySchema, bodyName)\n op.requestBody = {\n required: true,\n content: { 'application/json': { schema: ref } },\n }\n }\n }\n\n // File upload detection\n const fileUpload = Reflect.getMetadata(\n METADATA.FILE_UPLOAD,\n controllerClass,\n route.handlerName,\n )\n if (fileUpload) {\n const fieldName = fileUpload.fieldName ?? 'file'\n const properties: any = {}\n\n if (fileUpload.mode === 'array') {\n properties[fieldName] = {\n type: 'array',\n items: { type: 'string', format: 'binary' },\n }\n } else if (fileUpload.mode !== 'none') {\n properties[fieldName] = {\n type: 'string',\n format: 'binary',\n }\n }\n\n op.requestBody = {\n required: true,\n content: {\n 'multipart/form-data': {\n schema: { type: 'object', properties },\n },\n },\n }\n }\n\n // Responses\n if (responses.length > 0) {\n for (const resp of responses) {\n op.responses[String(resp.status)] = {\n description: resp.description || '',\n ...(resp.schema\n ? (() => {\n const converted =\n typeof resp.schema === 'function' || typeof resp.schema === 'object'\n ? toJsonSchema(resp.schema)\n : null\n const schemaName = resp.name || `${route.handlerName}Response${resp.status}`\n const finalSchema = converted\n ? registerSchema(converted, schemaName)\n : typeof resp.schema === 'object'\n ? resp.schema\n : undefined\n return finalSchema\n ? { content: { 'application/json': { schema: finalSchema } } }\n : {}\n })()\n : {}),\n }\n }\n } else {\n // Auto-generate default responses\n const defaultStatus = method === 'post' ? '201' : method === 'delete' ? '204' : '200'\n op.responses[defaultStatus] = { description: 'Successful operation' }\n\n if (route.validation?.body) {\n op.responses['422'] = { description: 'Validation error' }\n }\n }\n\n // Security\n const authName = methodAuth || classAuth\n if (authName) {\n op.security = [{ [authName]: [] }]\n securitySchemes[authName] = {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT',\n }\n }\n\n // Mount\n if (!spec.paths[openApiPath]) spec.paths[openApiPath] = {}\n spec.paths[openApiPath][method] = op\n }\n }\n\n // Finalize\n spec.tags = Array.from(allTags).map((name) => ({ name }))\n spec.components.securitySchemes = securitySchemes\n\n if (options.bearerAuth) {\n if (!securitySchemes.BearerAuth) {\n spec.components.securitySchemes.BearerAuth = {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT',\n }\n }\n spec.security = [{ BearerAuth: [] }]\n }\n\n // Merge collected schemas into components\n spec.components.schemas = componentSchemas\n\n // Clean up empty components\n if (Object.keys(spec.components.schemas).length === 0) delete spec.components.schemas\n if (Object.keys(spec.components.securitySchemes).length === 0)\n delete spec.components.securitySchemes\n if (Object.keys(spec.components).length === 0) delete spec.components\n\n return spec\n}\n","import { Router } from 'express'\nimport type { Express } from 'express'\nimport { Logger, type AppAdapter, type Container } from '@forinda/kickjs-core'\nimport {\n buildOpenAPISpec,\n registerControllerForDocs,\n clearRegisteredRoutes,\n type SwaggerOptions,\n} from './openapi-builder'\nimport { swaggerUIHtml, redocHtml } from './ui'\n\nconst log = Logger.for('SwaggerAdapter')\n\nexport interface SwaggerAdapterOptions extends SwaggerOptions {\n /** Path to serve Swagger UI (default: '/docs') */\n docsPath?: string\n /** Path to serve ReDoc (default: '/redoc') */\n redocPath?: string\n /** Path to serve the raw JSON spec (default: '/openapi.json') */\n specPath?: string\n /** Other adapters to discover (e.g., WsAdapter for WebSocket server URLs) */\n adapters?: any[]\n}\n\n/**\n * Swagger adapter — auto-generates OpenAPI spec from decorators and serves docs.\n *\n * @example\n * ```ts\n * bootstrap({\n * modules,\n * adapters: [\n * new SwaggerAdapter({\n * info: { title: 'My API', version: '1.0.0' },\n * }),\n * ],\n * })\n * ```\n *\n * Endpoints:\n * GET /docs — Swagger UI\n * GET /redoc — ReDoc\n * GET /openapi.json — Raw OpenAPI 3.0.3 spec\n */\nexport class SwaggerAdapter implements AppAdapter {\n name = 'SwaggerAdapter'\n\n constructor(private options: SwaggerAdapterOptions = {}) {}\n\n /** Auto-detect server URLs from the running HTTP server and peer adapters */\n afterStart(server: any, _container: Container): void {\n const addr = server?.address?.()\n if (!addr || typeof addr !== 'object') return\n\n const host = addr.address === '::' || addr.address === '0.0.0.0' ? 'localhost' : addr.address\n\n // Auto-add HTTP server URL if none configured\n if (!this.options.servers || this.options.servers.length === 0) {\n this.options.servers = [{ url: `http://${host}:${addr.port}`, description: 'HTTP server' }]\n }\n\n // Auto-add WebSocket server URLs from WsAdapter\n const wsAdapter = this.options.adapters?.find(\n (a) => a.name === 'WsAdapter' && typeof a.getStats === 'function',\n )\n if (wsAdapter) {\n const stats = wsAdapter.getStats()\n for (const namespace of Object.keys(stats.namespaces || {})) {\n this.options.servers!.push({\n url: `ws://${host}:${addr.port}${namespace}`,\n description: `WebSocket: ${namespace}`,\n })\n }\n }\n }\n\n /** Collect controller metadata as routes are mounted */\n onRouteMount(controllerClass: any, mountPath: string): void {\n registerControllerForDocs(controllerClass, mountPath)\n }\n\n beforeMount(app: Express, _container: Container): void {\n // Clear previous registrations (supports HMR rebuild)\n clearRegisteredRoutes()\n const docsPath = this.options.docsPath ?? '/docs'\n const redocPath = this.options.redocPath ?? '/redoc'\n const specPath = this.options.specPath ?? '/openapi.json'\n\n // Use a sub-router with relaxed CSP so CDN scripts load\n const docsRouter = Router()\n\n docsRouter.use((_req, res, next) => {\n res.setHeader(\n 'Content-Security-Policy',\n [\n \"default-src 'self'\",\n \"script-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.redoc.ly https://cdn.jsdelivr.net\",\n \"style-src 'self' 'unsafe-inline' https://unpkg.com https://fonts.googleapis.com\",\n \"font-src 'self' https://fonts.gstatic.com\",\n \"img-src 'self' data: https://unpkg.com\",\n \"connect-src 'self'\",\n ].join('; '),\n )\n next()\n })\n\n // Spec endpoint (JSON)\n docsRouter.get(specPath, (_req, res) => {\n const spec = buildOpenAPISpec(this.options)\n res.json(spec)\n })\n\n // Swagger UI\n docsRouter.get(docsPath, (_req, res) => {\n res.type('html').send(swaggerUIHtml(specPath, this.options.info?.title))\n })\n\n // ReDoc\n docsRouter.get(redocPath, (_req, res) => {\n res.type('html').send(redocHtml(specPath, this.options.info?.title))\n })\n\n app.use(docsRouter)\n\n log.info(`Swagger UI: ${docsPath}`)\n log.info(`ReDoc: ${redocPath}`)\n log.info(`OpenAPI spec: ${specPath}`)\n }\n}\n\n// Re-export for use by Application when mounting module routes\nexport { registerControllerForDocs, clearRegisteredRoutes }\n","/** Escape a string for safe HTML attribute/content interpolation */\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n}\n\n/** Generate Swagger UI HTML that loads from CDN */\nexport function swaggerUIHtml(specUrl: string, title = 'API Docs'): string {\n const safeTitle = escapeHtml(title)\n const safeUrl = JSON.stringify(specUrl).replace(/</g, '\\\\u003c')\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${safeTitle}</title>\n <link rel=\"stylesheet\" href=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui.css\">\n</head>\n<body>\n <div id=\"swagger-ui\"></div>\n <script src=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js\"></script>\n <script src=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js\"></script>\n <script>\n SwaggerUIBundle({\n url: ${safeUrl},\n dom_id: '#swagger-ui',\n deepLinking: true,\n presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],\n plugins: [SwaggerUIBundle.plugins.DownloadUrl],\n layout: 'StandaloneLayout',\n });\n </script>\n</body>\n</html>`\n}\n\n/** Generate ReDoc HTML that loads from CDN */\nexport function redocHtml(specUrl: string, title = 'API Docs'): string {\n const safeTitle = escapeHtml(title)\n const safeUrl = escapeHtml(specUrl)\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${safeTitle}</title>\n</head>\n<body>\n <redoc spec-url=\"${safeUrl}\"></redoc>\n <script src=\"https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js\"></script>\n</body>\n</html>`\n}\n"],"mappings":"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","names":["zodSchemaParser","name","supports","schema","safeParse","toJSONSchema","toJsonSchema","$schema","_","rest","SWAGGER_KEYS","OPERATION","Symbol","RESPONSES","TAGS","BEARER_AUTH","EXCLUDE","ApiOperation","options","target","propertyKey","Reflect","defineMetadata","SWAGGER_KEYS","OPERATION","ApiResponse","existing","getMetadata","RESPONSES","ApiTags","tags","TAGS","ApiBearerAuth","name","BEARER_AUTH","ApiExclude","EXCLUDE","METADATA","joinPaths","registeredRoutes","registerControllerForDocs","controllerClass","mountPath","push","clearRegisteredRoutes","length","buildOpenAPISpec","options","parser","schemaParser","zodSchemaParser","toJsonSchema","schema","supports","componentSchemas","schemaCounter","registerSchema","jsonSchema","hint","name","title","label","replace","clean","$schema","$ref","spec","openapi","info","version","description","paths","components","schemas","securitySchemes","tags","servers","allTags","Set","Reflect","getMetadata","SWAGGER_KEYS","EXCLUDE","routes","METADATA","ROUTES","classTags","TAGS","classAuth","BEARER_AUTH","route","handlerName","fullPath","joinPaths","path","openApiPath","method","toLowerCase","operation","OPERATION","responses","RESPONSES","methodTags","methodAuth","forEach","t","add","op","summary","operationId","deprecated","parameters","paramMatches","match","paramName","slice","type","validation","params","properties","props","in","required","query","Array","isArray","propSchema","Object","entries","includes","queryParamsConfig","QUERY_PARAMS","filterable","join","items","style","explode","sortable","searchable","minimum","default","maximum","body","bodySchema","bodyName","ref","requestBody","content","fileUpload","FILE_UPLOAD","fieldName","mode","format","resp","String","status","converted","schemaName","finalSchema","undefined","defaultStatus","authName","security","scheme","bearerFormat","from","map","bearerAuth","BearerAuth","keys","Router","Logger","escapeHtml","str","replace","swaggerUIHtml","specUrl","title","safeTitle","safeUrl","JSON","stringify","redocHtml","log","Logger","for","SwaggerAdapter","name","options","afterStart","server","_container","addr","address","host","servers","length","url","port","description","wsAdapter","adapters","find","a","getStats","stats","namespace","Object","keys","namespaces","push","onRouteMount","controllerClass","mountPath","registerControllerForDocs","beforeMount","app","clearRegisteredRoutes","docsPath","redocPath","specPath","docsRouter","Router","use","_req","res","next","setHeader","join","get","spec","buildOpenAPISpec","json","type","send","swaggerUIHtml","info","title","redocHtml"]}
|