@moostjs/swagger 0.5.33 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -29,34 +29,50 @@ const path = __toESM(require("path"));
29
29
  const swagger_ui_dist = __toESM(require("swagger-ui-dist"));
30
30
 
31
31
  //#region packages/swagger/src/swagger.mate.ts
32
- function getSwaggerMate() {
32
+ /** Returns the shared `Mate` instance extended with Swagger/OpenAPI metadata fields. */ function getSwaggerMate() {
33
33
  return (0, moost.getMoostMate)();
34
34
  }
35
35
 
36
36
  //#endregion
37
37
  //#region packages/swagger/src/decorators.ts
38
- const SwaggerTag = (tag) => getSwaggerMate().decorate("swaggerTags", tag, true);
39
- const SwaggerExclude = () => getSwaggerMate().decorate("swaggerExclude", true);
40
- const SwaggerDescription = (descr) => getSwaggerMate().decorate("swaggerDescription", descr);
38
+ /** Adds an OpenAPI tag to a controller or handler for grouping in the Swagger UI. */ const SwaggerTag = (tag) => getSwaggerMate().decorate("swaggerTags", tag, true);
39
+ /** Excludes a controller or handler from the generated OpenAPI spec. */ const SwaggerExclude = () => getSwaggerMate().decorate("swaggerExclude", true);
40
+ /** Sets the OpenAPI description for a handler. */ const SwaggerDescription = (descr) => getSwaggerMate().decorate("swaggerDescription", descr);
41
41
  function SwaggerResponse(code, opts, example) {
42
42
  return getSwaggerMate().decorate((meta) => {
43
43
  let ex;
44
44
  if (example) ex = example;
45
45
  if (typeof code !== "number" && opts) ex = opts;
46
+ if (ex === void 0) {
47
+ ex = typeof code === "number" ? opts : code;
48
+ ex = ex?.example;
49
+ }
46
50
  meta.swaggerResponses = meta.swaggerResponses || {};
47
51
  const keyCode = typeof code === "number" ? code : 0;
48
52
  const opt = typeof code === "number" ? opts : code;
49
53
  const contentType = typeof opt.contentType === "string" ? opt.contentType : "*/*";
54
+ const description = typeof opt.description === "string" ? opt.description : void 0;
55
+ const headers = opt.headers;
50
56
  const response = ["object", "function"].includes(typeof opt.response) ? opt.response : opt;
51
- meta.swaggerResponses[keyCode] = meta.swaggerResponses[keyCode] || {};
52
- meta.swaggerResponses[keyCode][contentType] = {
57
+ meta.swaggerResponses[keyCode] = meta.swaggerResponses[keyCode] || { content: {} };
58
+ meta.swaggerResponses[keyCode].content[contentType] = {
53
59
  response,
54
60
  example: ex
55
61
  };
62
+ if (description) meta.swaggerResponses[keyCode].description = description;
63
+ if (headers) meta.swaggerResponses[keyCode].headers = {
64
+ ...meta.swaggerResponses[keyCode].headers,
65
+ ...headers
66
+ };
56
67
  return meta;
57
68
  });
58
69
  }
59
- function SwaggerRequestBody(opt) {
70
+ /**
71
+ * Defines the request body schema in the OpenAPI spec.
72
+ *
73
+ * @param opt - Schema definition. Use `{ response: MyDto }` for a typed schema,
74
+ * or `{ response: MyDto, contentType: 'multipart/form-data' }` for a specific content type.
75
+ */ function SwaggerRequestBody(opt) {
60
76
  return getSwaggerMate().decorate((meta) => {
61
77
  meta.swaggerRequestBody = meta.swaggerRequestBody || {};
62
78
  const contentType = typeof opt.contentType === "string" ? opt.contentType : "application/json";
@@ -65,12 +81,127 @@ function SwaggerRequestBody(opt) {
65
81
  return meta;
66
82
  });
67
83
  }
68
- function SwaggerParam(opts) {
84
+ /** Defines a parameter (query, path, header, cookie) in the OpenAPI spec. */ function SwaggerParam(opts) {
69
85
  return getSwaggerMate().decorate("swaggerParams", opts, true);
70
86
  }
71
- function SwaggerExample(example) {
87
+ /** Attaches an example value to a handler's OpenAPI documentation. */ function SwaggerExample(example) {
72
88
  return getSwaggerMate().decorate("swaggerExample", example);
73
89
  }
90
+ /** Marks a handler or controller as public, opting out of inherited security requirements. */ const SwaggerPublic = () => getSwaggerMate().decorate("swaggerPublic", true);
91
+ /** Marks a handler or controller as deprecated in the OpenAPI spec. */ const SwaggerDeprecated = () => getSwaggerMate().decorate("swaggerDeprecated", true);
92
+ /** Overrides the auto-generated operationId for an endpoint. */ const SwaggerOperationId = (id) => getSwaggerMate().decorate("swaggerOperationId", id);
93
+ /** Links an operation to external documentation. */ const SwaggerExternalDocs = (url, description) => getSwaggerMate().decorate("swaggerExternalDocs", {
94
+ url,
95
+ ...description ? { description } : {}
96
+ });
97
+ /**
98
+ * Attaches a security requirement to a handler or controller (OR semantics).
99
+ * Multiple calls add alternative requirements — any one suffices.
100
+ *
101
+ * @param schemeName - The name of the security scheme (must match a key in securitySchemes)
102
+ * @param scopes - OAuth2/OIDC scopes required (default: [])
103
+ */ function SwaggerSecurity(schemeName, scopes = []) {
104
+ return getSwaggerMate().decorate("swaggerSecurity", { [schemeName]: scopes }, true);
105
+ }
106
+ /**
107
+ * Attaches a combined security requirement (AND semantics).
108
+ * All schemes in the requirement must be satisfied simultaneously.
109
+ */ function SwaggerSecurityAll(requirement) {
110
+ return getSwaggerMate().decorate("swaggerSecurity", requirement, true);
111
+ }
112
+ function SwaggerLink(codeOrName, nameOrOptions, maybeOptions) {
113
+ const statusCode = typeof codeOrName === "number" ? codeOrName : 0;
114
+ const name = typeof codeOrName === "string" ? codeOrName : nameOrOptions;
115
+ const options = typeof codeOrName === "string" ? nameOrOptions : maybeOptions;
116
+ const config = {
117
+ statusCode,
118
+ name,
119
+ ..."operationId" in options && options.operationId ? { operationId: options.operationId } : {},
120
+ ..."operationRef" in options && options.operationRef ? { operationRef: options.operationRef } : {},
121
+ ..."handler" in options && options.handler ? { handler: options.handler } : {},
122
+ ...options.parameters ? { parameters: options.parameters } : {},
123
+ ...options.requestBody ? { requestBody: options.requestBody } : {},
124
+ ...options.description ? { description: options.description } : {},
125
+ ...options.server ? { server: options.server } : {}
126
+ };
127
+ return getSwaggerMate().decorate("swaggerLinks", config, true);
128
+ }
129
+ /**
130
+ * Documents an OpenAPI callback (webhook) on an operation.
131
+ * Describes a request your server sends to a client-provided URL.
132
+ *
133
+ * @example
134
+ * ```ts
135
+ * @SwaggerCallback('onEvent', {
136
+ * expression: '{$request.body#/callbackUrl}',
137
+ * requestBody: EventPayloadDto,
138
+ * description: 'Event notification sent to subscriber',
139
+ * })
140
+ * @Post('subscribe')
141
+ * subscribe() { ... }
142
+ * ```
143
+ */ function SwaggerCallback(name, options) {
144
+ const config = {
145
+ name,
146
+ expression: options.expression,
147
+ ...options.method ? { method: options.method } : {},
148
+ ...options.requestBody ? { requestBody: options.requestBody } : {},
149
+ ...options.contentType ? { contentType: options.contentType } : {},
150
+ ...options.description ? { description: options.description } : {},
151
+ ...options.responseStatus ? { responseStatus: options.responseStatus } : {},
152
+ ...options.responseDescription ? { responseDescription: options.responseDescription } : {}
153
+ };
154
+ return getSwaggerMate().decorate("swaggerCallbacks", config, true);
155
+ }
156
+
157
+ //#endregion
158
+ //#region packages/swagger/src/json-to-yaml.ts
159
+ const YAML_SPECIAL = /^[\s#!&*|>'{}[\],?:@`-]|[:#]\s|[\n\r]|\s$/;
160
+ function quoteString(str) {
161
+ if (str === "") return "''";
162
+ if (YAML_SPECIAL.test(str) || str === "true" || str === "false" || str === "null") return JSON.stringify(str);
163
+ const num = Number(str);
164
+ if (str.length > 0 && !Number.isNaN(num) && String(num) === str) return JSON.stringify(str);
165
+ return str;
166
+ }
167
+ function serializeValue(value, indent) {
168
+ if (value === null || value === void 0) return "null";
169
+ if (typeof value === "boolean") return value ? "true" : "false";
170
+ if (typeof value === "number") return Number.isFinite(value) ? String(value) : "null";
171
+ if (typeof value === "string") return quoteString(value);
172
+ const pad = " ".repeat(indent);
173
+ const childPad = " ".repeat(indent + 1);
174
+ if (Array.isArray(value)) {
175
+ if (value.length === 0) return "[]";
176
+ const lines = [];
177
+ for (const item of value) if (isObject(item) || Array.isArray(item)) {
178
+ const nested = serializeValue(item, indent + 1);
179
+ lines.push(`${pad}- ${nested.slice(childPad.length)}`);
180
+ } else lines.push(`${pad}- ${serializeValue(item, 0)}`);
181
+ return lines.join("\n");
182
+ }
183
+ if (isObject(value)) {
184
+ const entries = Object.entries(value);
185
+ if (entries.length === 0) return "{}";
186
+ const lines = [];
187
+ for (const [key, val] of entries) {
188
+ const yamlKey = quoteString(key);
189
+ if (isObject(val) || Array.isArray(val)) {
190
+ const nested = serializeValue(val, indent + 1);
191
+ if (nested === "[]" || nested === "{}") lines.push(`${pad}${yamlKey}: ${nested}`);
192
+ else lines.push(`${pad}${yamlKey}:\n${nested}`);
193
+ } else lines.push(`${pad}${yamlKey}: ${serializeValue(val, 0)}`);
194
+ }
195
+ return lines.join("\n");
196
+ }
197
+ return String(value);
198
+ }
199
+ function isObject(value) {
200
+ return typeof value === "object" && value !== null && !Array.isArray(value);
201
+ }
202
+ function jsonToYaml(value) {
203
+ return `${serializeValue(value, 0)}\n`;
204
+ }
74
205
 
75
206
  //#endregion
76
207
  //#region packages/swagger/src/mapping.ts
@@ -79,16 +210,26 @@ let schemaRefs = /* @__PURE__ */ new WeakMap();
79
210
  const nameToType = /* @__PURE__ */ new Map();
80
211
  function mapToSwaggerSpec(metadata, options, logger) {
81
212
  resetSchemaRegistry();
213
+ const is31 = options?.openapiVersion === "3.1";
214
+ const collectedSecuritySchemes = { ...options?.securitySchemes };
82
215
  const swaggerSpec = {
83
- openapi: "3.0.0",
216
+ openapi: is31 ? "3.1.0" : "3.0.0",
84
217
  info: {
85
218
  title: options?.title || "API Documentation",
86
- version: options?.version || "1.0.0"
219
+ ...options?.description ? { description: options.description } : {},
220
+ version: options?.version || "1.0.0",
221
+ ...options?.contact ? { contact: options.contact } : {},
222
+ ...options?.license ? { license: options.license } : {},
223
+ ...options?.termsOfService ? { termsOfService: options.termsOfService } : {}
87
224
  },
88
225
  paths: {},
89
226
  tags: [],
227
+ ...options?.servers?.length ? { servers: options.servers } : {},
228
+ ...options?.externalDocs ? { externalDocs: options.externalDocs } : {},
229
+ ...options?.security ? { security: options.security } : {},
90
230
  components: { schemas: globalSchemas }
91
231
  };
232
+ const deferredLinks = [];
92
233
  for (const controller of metadata) {
93
234
  const cmeta = controller.meta;
94
235
  if (cmeta?.swaggerExclude) continue;
@@ -100,13 +241,14 @@ function mapToSwaggerSpec(metadata, options, logger) {
100
241
  const uniqueParams = {};
101
242
  const handlerPath = handler.registeredAs[0].path;
102
243
  const handlerMethod = hh.method?.toLowerCase() || "get";
103
- const handlerDescription = hmeta?.description;
244
+ const handlerSummary = hmeta?.label;
245
+ const handlerDescription = hmeta?.swaggerDescription || hmeta?.description;
104
246
  const handlerTags = [...controllerTags, ...hmeta?.swaggerTags || []];
105
247
  if (!swaggerSpec.paths[handlerPath]) swaggerSpec.paths[handlerPath] = {};
106
248
  let responses;
107
- if (hmeta?.swaggerResponses) for (const [code, responseConfigs] of Object.entries(hmeta.swaggerResponses)) {
249
+ if (hmeta?.swaggerResponses) for (const [code, responseEntry] of Object.entries(hmeta.swaggerResponses)) {
108
250
  const newCode = code === "0" ? getDefaultStatusCode(handlerMethod) : code;
109
- for (const [contentType, conf] of Object.entries(responseConfigs)) {
251
+ for (const [contentType, conf] of Object.entries(responseEntry.content)) {
110
252
  const { response, example } = conf;
111
253
  const schema = resolveSwaggerSchemaFromConfig(response);
112
254
  if (schema) {
@@ -115,16 +257,35 @@ function mapToSwaggerSpec(metadata, options, logger) {
115
257
  ...schema,
116
258
  example
117
259
  } : schema;
118
- responses[newCode] = { content: { [contentType]: { schema: schemaWithExample } } };
260
+ if (!responses[newCode]) responses[newCode] = {
261
+ description: responseEntry.description || defaultStatusDescription(Number(newCode)),
262
+ content: {}
263
+ };
264
+ responses[newCode].content[contentType] = { schema: schemaWithExample };
265
+ }
266
+ }
267
+ if (responseEntry.headers) {
268
+ const resolvedHeaders = resolveResponseHeaders(responseEntry.headers);
269
+ if (resolvedHeaders) {
270
+ responses = responses || {};
271
+ if (!responses[newCode]) responses[newCode] = {
272
+ description: defaultStatusDescription(Number(newCode)),
273
+ content: {}
274
+ };
275
+ responses[newCode].headers = resolvedHeaders;
119
276
  }
120
277
  }
121
278
  }
122
- else if (hmeta?.returnType) {
279
+ const defaultCode = getDefaultStatusCode(handlerMethod);
280
+ if (!responses?.[defaultCode] && hmeta?.returnType) {
123
281
  const ensured = ensureSchema(hmeta.returnType);
124
282
  const schema = toSchemaOrRef(ensured);
125
283
  if (schema) {
126
284
  responses = responses || {};
127
- responses[getDefaultStatusCode(handlerMethod)] = { content: { "*/*": { schema } } };
285
+ responses[defaultCode] = {
286
+ description: defaultStatusDescription(Number(defaultCode)),
287
+ content: { "*/*": { schema } }
288
+ };
128
289
  }
129
290
  }
130
291
  let reqBodyRequired = true;
@@ -134,13 +295,39 @@ function mapToSwaggerSpec(metadata, options, logger) {
134
295
  if (schema) bodyContent[contentType] = { schema };
135
296
  }
136
297
  swaggerSpec.paths[handlerPath][handlerMethod] = {
137
- summary: handlerDescription,
138
- operationId: `${handlerMethod.toUpperCase()}_${handlerPath.replace(/\//g, "_").replace(/[{}]/g, "__").replace(/[^\dA-Za-z]/g, "_")}`,
298
+ summary: handlerSummary,
299
+ description: handlerDescription,
300
+ operationId: hmeta?.swaggerOperationId || hmeta?.id || `${handlerMethod.toUpperCase()}_${handlerPath.replaceAll(/\//g, "_").replaceAll(/[{}]/g, "__").replaceAll(/[^\dA-Za-z]/g, "_")}`,
139
301
  tags: handlerTags,
140
302
  parameters: [],
141
303
  responses
142
304
  };
143
305
  const endpointSpec = swaggerSpec.paths[handlerPath][handlerMethod];
306
+ if (hmeta?.swaggerDeprecated || cmeta?.swaggerDeprecated) endpointSpec.deprecated = true;
307
+ if (hmeta?.swaggerExternalDocs) endpointSpec.externalDocs = hmeta.swaggerExternalDocs;
308
+ const opSecurity = resolveOperationSecurity(cmeta, hmeta, collectedSecuritySchemes);
309
+ if (opSecurity !== void 0) endpointSpec.security = opSecurity;
310
+ if (hmeta?.swaggerLinks?.length) for (const linkConfig of hmeta.swaggerLinks) deferredLinks.push({
311
+ endpointSpec,
312
+ httpMethod: handlerMethod,
313
+ config: linkConfig
314
+ });
315
+ if (hmeta?.swaggerCallbacks?.length) {
316
+ endpointSpec.callbacks = endpointSpec.callbacks || {};
317
+ for (const cb of hmeta.swaggerCallbacks) {
318
+ const method = (cb.method || "post").toLowerCase();
319
+ const contentType = cb.contentType || "application/json";
320
+ const status = String(cb.responseStatus || 200);
321
+ const responseDesc = cb.responseDescription || "OK";
322
+ const pathItem = { responses: { [status]: { description: responseDesc } } };
323
+ if (cb.description) pathItem.description = cb.description;
324
+ if (cb.requestBody) {
325
+ const schema = resolveSwaggerSchemaFromConfig(cb.requestBody);
326
+ if (schema) pathItem.requestBody = { content: { [contentType]: { schema } } };
327
+ }
328
+ endpointSpec.callbacks[cb.name] = { [cb.expression]: { [method]: pathItem } };
329
+ }
330
+ }
144
331
  function addParam(param) {
145
332
  const key = `${param.in}//${param.name}`;
146
333
  if (uniqueParams[key]) {
@@ -179,8 +366,7 @@ function mapToSwaggerSpec(metadata, options, logger) {
179
366
  schema
180
367
  });
181
368
  }
182
- for (let i = 0; i < handler.meta.params.length; i++) {
183
- const paramMeta = handler.meta.params[i];
369
+ for (const paramMeta of handler.meta.params) {
184
370
  if (paramMeta.paramSource && ["QUERY_ITEM", "QUERY"].includes(paramMeta.paramSource)) {
185
371
  const ensured = ensureSchema(paramMeta.type);
186
372
  if (paramMeta.paramSource === "QUERY_ITEM") {
@@ -232,7 +418,7 @@ function mapToSwaggerSpec(metadata, options, logger) {
232
418
  }
233
419
  }
234
420
  const bodyEntries = Object.entries(bodyContent).filter((entry) => entry[1] && entry[1].schema !== void 0);
235
- if (bodyEntries.length) {
421
+ if (bodyEntries.length > 0) {
236
422
  const content = {};
237
423
  for (const [contentType, { schema }] of bodyEntries) content[contentType] = { schema };
238
424
  endpointSpec.requestBody = {
@@ -242,6 +428,54 @@ function mapToSwaggerSpec(metadata, options, logger) {
242
428
  }
243
429
  }
244
430
  }
431
+ if (deferredLinks.length > 0) {
432
+ const handlerOpIds = /* @__PURE__ */ new Map();
433
+ for (const controller of metadata) for (const handler of controller.handlers) {
434
+ const hh = handler.handler;
435
+ if (hh.type !== "HTTP" || handler.registeredAs.length === 0) continue;
436
+ const path$1 = handler.registeredAs[0].path;
437
+ const method = hh.method?.toLowerCase() || "get";
438
+ const opId = swaggerSpec.paths[path$1]?.[method]?.operationId;
439
+ if (opId) {
440
+ if (!handlerOpIds.has(controller.type)) handlerOpIds.set(controller.type, /* @__PURE__ */ new Map());
441
+ const methodMap = handlerOpIds.get(controller.type);
442
+ methodMap?.set(handler.method, opId);
443
+ }
444
+ }
445
+ for (const { endpointSpec, httpMethod, config } of deferredLinks) {
446
+ const statusCode = config.statusCode === 0 ? String(getDefaultStatusCode(httpMethod)) : String(config.statusCode);
447
+ const link = {};
448
+ if (config.handler) {
449
+ const [ctrlClass, methodName] = config.handler;
450
+ const resolvedId = handlerOpIds.get(ctrlClass)?.get(methodName);
451
+ if (!resolvedId) continue;
452
+ link.operationId = resolvedId;
453
+ } else if (config.operationId) link.operationId = config.operationId;
454
+ else if (config.operationRef) link.operationRef = config.operationRef;
455
+ if (config.parameters) link.parameters = config.parameters;
456
+ if (config.requestBody) link.requestBody = config.requestBody;
457
+ if (config.description) link.description = config.description;
458
+ if (config.server) link.server = config.server;
459
+ if (!endpointSpec.responses) endpointSpec.responses = {};
460
+ if (!endpointSpec.responses[statusCode]) endpointSpec.responses[statusCode] = {
461
+ description: defaultStatusDescription(Number(statusCode)),
462
+ content: {}
463
+ };
464
+ const responseEntry = endpointSpec.responses[statusCode];
465
+ if (!responseEntry.links) responseEntry.links = {};
466
+ responseEntry.links[config.name] = link;
467
+ }
468
+ }
469
+ const manualTags = new Map((options?.tags || []).map((t) => [t.name, t]));
470
+ const discoveredNames = /* @__PURE__ */ new Set();
471
+ for (const methods of Object.values(swaggerSpec.paths)) for (const endpoint of Object.values(methods)) for (const tag of endpoint.tags) discoveredNames.add(tag);
472
+ for (const tag of manualTags.values()) swaggerSpec.tags.push(tag);
473
+ for (const name of discoveredNames) if (!manualTags.has(name)) swaggerSpec.tags.push({ name });
474
+ const ownedSchemas = {};
475
+ for (const [key, value] of Object.entries(globalSchemas)) ownedSchemas[key] = cloneSchema(value);
476
+ swaggerSpec.components.schemas = ownedSchemas;
477
+ if (Object.keys(collectedSecuritySchemes).length > 0) swaggerSpec.components.securitySchemes = collectedSecuritySchemes;
478
+ if (is31) transformSpecTo31(swaggerSpec);
245
479
  return swaggerSpec;
246
480
  }
247
481
  function resolveSwaggerSchemaFromConfig(type) {
@@ -249,6 +483,20 @@ function resolveSwaggerSchemaFromConfig(type) {
249
483
  const ensured = ensureSchema(type);
250
484
  return toSchemaOrRef(ensured);
251
485
  }
486
+ function resolveResponseHeaders(headers) {
487
+ const resolved = {};
488
+ let hasAny = false;
489
+ for (const [name, header] of Object.entries(headers)) {
490
+ const schema = resolveSwaggerSchemaFromConfig(header.type) || { type: "string" };
491
+ const entry = { schema };
492
+ if (header.description) entry.description = header.description;
493
+ if (header.required !== void 0) entry.required = header.required;
494
+ if (header.example !== void 0) entry.example = header.example;
495
+ resolved[name] = entry;
496
+ hasAny = true;
497
+ }
498
+ return hasAny ? resolved : void 0;
499
+ }
252
500
  function toSchemaOrRef(result) {
253
501
  if (!result) return void 0;
254
502
  if (result.ref) return { $ref: result.ref };
@@ -257,7 +505,7 @@ function toSchemaOrRef(result) {
257
505
  function inferBodyContentType(schema, resolved) {
258
506
  const target = resolved ?? resolveSchemaFromRef(schema);
259
507
  const schemaType = target?.type ?? schema.type;
260
- if (schemaType && [
508
+ if (typeof schemaType === "string" && [
261
509
  "string",
262
510
  "number",
263
511
  "integer",
@@ -423,22 +671,60 @@ function ensureComponentName(typeRef, schema, suggestedName) {
423
671
  schemaRefs.set(typeRef, candidate);
424
672
  applySwaggerMetadata(typeRef, schema);
425
673
  globalSchemas[candidate] = cloneSchema(schema);
674
+ hoistDefs(globalSchemas[candidate]);
426
675
  return candidate;
427
676
  }
677
+ /**
678
+ * When a schema has `$defs`, hoist each definition into `globalSchemas`
679
+ * (i.e. `#/components/schemas/`) and rewrite all `#/$defs/X` references
680
+ * throughout the schema tree to `#/components/schemas/X`.
681
+ * Removes the `$defs` property from the schema after hoisting.
682
+ */ function hoistDefs(schema) {
683
+ if (!schema.$defs) return;
684
+ for (const [name, def] of Object.entries(schema.$defs)) if (!globalSchemas[name]) {
685
+ globalSchemas[name] = cloneSchema(def);
686
+ hoistDefs(globalSchemas[name]);
687
+ }
688
+ delete schema.$defs;
689
+ rewriteDefsRefs(schema);
690
+ }
691
+ /** Recursively rewrite `$ref: '#/$defs/X'` → `$ref: '#/components/schemas/X'` */ function rewriteDefsRefs(schema) {
692
+ if (schema.$ref?.startsWith("#/$defs/")) schema.$ref = schema.$ref.replace("#/$defs/", "#/components/schemas/");
693
+ if (schema.discriminator?.mapping) {
694
+ for (const [key, value] of Object.entries(schema.discriminator.mapping)) if (value.startsWith("#/$defs/")) schema.discriminator.mapping[key] = value.replace("#/$defs/", "#/components/schemas/");
695
+ }
696
+ if (schema.items && !Array.isArray(schema.items)) rewriteDefsRefs(schema.items);
697
+ if (schema.properties) for (const prop of Object.values(schema.properties)) rewriteDefsRefs(prop);
698
+ for (const list of [
699
+ schema.allOf,
700
+ schema.anyOf,
701
+ schema.oneOf
702
+ ]) if (list) for (const item of list) rewriteDefsRefs(item);
703
+ if (typeof schema.additionalProperties === "object" && schema.additionalProperties) rewriteDefsRefs(schema.additionalProperties);
704
+ if (schema.not) rewriteDefsRefs(schema.not);
705
+ }
428
706
  function applySwaggerMetadata(typeRef, schema) {
429
707
  try {
430
708
  const mate = getSwaggerMate();
431
709
  const meta = mate.read(typeRef);
432
- if (!meta) return;
433
- if (meta.swaggerExample !== void 0 && schema.example === void 0) schema.example = meta.swaggerExample;
434
- const title = meta.label || meta.id;
435
- if (title && !schema.title) schema.title = title;
436
- if (meta.swaggerDescription && !schema.description) schema.description = meta.swaggerDescription;
437
- else if (meta.description && !schema.description) schema.description = meta.description;
710
+ if (meta) {
711
+ if (meta.swaggerExample !== void 0 && schema.example === void 0) schema.example = meta.swaggerExample;
712
+ const title = meta.label || meta.id;
713
+ if (title && !schema.title) schema.title = title;
714
+ if (meta.swaggerDescription && !schema.description) schema.description = meta.swaggerDescription;
715
+ else if (meta.description && !schema.description) schema.description = meta.description;
716
+ }
438
717
  } catch {}
718
+ if (schema.example === void 0) {
719
+ const exampleFn = typeRef.toExampleData;
720
+ if (typeof exampleFn === "function") {
721
+ const example = exampleFn.call(typeRef);
722
+ if (example !== void 0) schema.example = example;
723
+ }
724
+ }
439
725
  }
440
726
  function sanitizeComponentName(name) {
441
- const sanitized = name.replace(/[^A-Za-z0-9_.-]/g, "_");
727
+ const sanitized = name.replaceAll(/[^A-Za-z0-9_.-]/g, "_");
442
728
  return sanitized || "Schema";
443
729
  }
444
730
  function getTypeName(typeRef) {
@@ -521,6 +807,107 @@ function getDefaultStatusCode(httpMethod) {
521
807
  };
522
808
  return defaultStatusCodes[httpMethod.toUpperCase()] || 200;
523
809
  }
810
+ const STATUS_DESCRIPTIONS = {
811
+ 200: "OK",
812
+ 201: "Created",
813
+ 202: "Accepted",
814
+ 204: "No Content",
815
+ 301: "Moved Permanently",
816
+ 302: "Found",
817
+ 304: "Not Modified",
818
+ 400: "Bad Request",
819
+ 401: "Unauthorized",
820
+ 403: "Forbidden",
821
+ 404: "Not Found",
822
+ 405: "Method Not Allowed",
823
+ 409: "Conflict",
824
+ 410: "Gone",
825
+ 415: "Unsupported Media Type",
826
+ 422: "Unprocessable Entity",
827
+ 429: "Too Many Requests",
828
+ 500: "Internal Server Error",
829
+ 502: "Bad Gateway",
830
+ 503: "Service Unavailable"
831
+ };
832
+ function defaultStatusDescription(code) {
833
+ return STATUS_DESCRIPTIONS[code] || "Response";
834
+ }
835
+ function transformSpecTo31(spec) {
836
+ for (const schema of Object.values(spec.components.schemas)) convertSchemaTo31(schema);
837
+ for (const methods of Object.values(spec.paths)) for (const endpoint of Object.values(methods)) {
838
+ for (const param of endpoint.parameters) convertSchemaTo31(param.schema);
839
+ if (endpoint.responses) for (const response of Object.values(endpoint.responses)) {
840
+ for (const media of Object.values(response.content)) convertSchemaTo31(media.schema);
841
+ if (response.headers) for (const header of Object.values(response.headers)) convertSchemaTo31(header.schema);
842
+ }
843
+ if (endpoint.requestBody) for (const media of Object.values(endpoint.requestBody.content)) convertSchemaTo31(media.schema);
844
+ }
845
+ }
846
+ function convertSchemaTo31(schema) {
847
+ if (schema.nullable) {
848
+ delete schema.nullable;
849
+ if (typeof schema.type === "string") schema.type = [schema.type, "null"];
850
+ else if (Array.isArray(schema.type)) {
851
+ if (!schema.type.includes("null")) schema.type.push("null");
852
+ } else schema.type = "null";
853
+ }
854
+ if (schema.properties) for (const prop of Object.values(schema.properties)) convertSchemaTo31(prop);
855
+ if (schema.items) if (Array.isArray(schema.items)) for (const item of schema.items) convertSchemaTo31(item);
856
+ else convertSchemaTo31(schema.items);
857
+ if (schema.allOf) for (const sub of schema.allOf) convertSchemaTo31(sub);
858
+ if (schema.anyOf) for (const sub of schema.anyOf) convertSchemaTo31(sub);
859
+ if (schema.oneOf) for (const sub of schema.oneOf) convertSchemaTo31(sub);
860
+ if (schema.not) convertSchemaTo31(schema.not);
861
+ if (typeof schema.additionalProperties === "object" && schema.additionalProperties) convertSchemaTo31(schema.additionalProperties);
862
+ }
863
+ function collectSchemesFromTransports(transports, schemes) {
864
+ if (transports.bearer) schemes.bearerAuth = {
865
+ type: "http",
866
+ scheme: "bearer",
867
+ ...transports.bearer.format ? { bearerFormat: transports.bearer.format } : {},
868
+ ...transports.bearer.description ? { description: transports.bearer.description } : {}
869
+ };
870
+ if (transports.basic) schemes.basicAuth = {
871
+ type: "http",
872
+ scheme: "basic",
873
+ ...transports.basic.description ? { description: transports.basic.description } : {}
874
+ };
875
+ if (transports.apiKey) schemes.apiKeyAuth = {
876
+ type: "apiKey",
877
+ name: transports.apiKey.name,
878
+ in: transports.apiKey.in,
879
+ ...transports.apiKey.description ? { description: transports.apiKey.description } : {}
880
+ };
881
+ if (transports.cookie) schemes.cookieAuth = {
882
+ type: "apiKey",
883
+ name: transports.cookie.name,
884
+ in: "cookie",
885
+ ...transports.cookie.description ? { description: transports.cookie.description } : {}
886
+ };
887
+ }
888
+ function transportsToSecurityRequirement(transports) {
889
+ const requirements = [];
890
+ if (transports.bearer) requirements.push({ bearerAuth: [] });
891
+ if (transports.basic) requirements.push({ basicAuth: [] });
892
+ if (transports.apiKey) requirements.push({ apiKeyAuth: [] });
893
+ if (transports.cookie) requirements.push({ cookieAuth: [] });
894
+ return requirements;
895
+ }
896
+ function resolveOperationSecurity(cmeta, hmeta, schemes) {
897
+ if (hmeta?.swaggerPublic) return [];
898
+ if (hmeta?.swaggerSecurity?.length) return hmeta.swaggerSecurity;
899
+ if (hmeta?.authTransports) {
900
+ collectSchemesFromTransports(hmeta.authTransports, schemes);
901
+ return transportsToSecurityRequirement(hmeta.authTransports);
902
+ }
903
+ if (cmeta?.swaggerPublic) return [];
904
+ if (cmeta?.swaggerSecurity?.length) return cmeta.swaggerSecurity;
905
+ if (cmeta?.authTransports) {
906
+ collectSchemesFromTransports(cmeta.authTransports, schemes);
907
+ return transportsToSecurityRequirement(cmeta.authTransports);
908
+ }
909
+ return void 0;
910
+ }
524
911
 
525
912
  //#endregion
526
913
  //#region packages/swagger/src/swagger.controller.ts
@@ -549,13 +936,10 @@ function _ts_param(paramIndex, decorator) {
549
936
  };
550
937
  }
551
938
  var SwaggerController = class {
552
- "processCors"() {
553
- if (this.opts.cors) {
554
- const { enableCors } = (0, __wooksjs_event_http.useSetHeaders)();
555
- enableCors(this.opts.cors === true ? void 0 : this.opts.cors);
556
- }
939
+ processCors() {
940
+ if (this.opts.cors) (0, __wooksjs_event_http.useResponse)().enableCors(this.opts.cors === true ? void 0 : this.opts.cors);
557
941
  }
558
- "serveIndex"(url, location, status) {
942
+ serveIndex(url, location, status) {
559
943
  this.processCors();
560
944
  if (!url.endsWith("index.html") && !url.endsWith("/")) {
561
945
  status.value = 302;
@@ -599,21 +983,30 @@ var SwaggerController = class {
599
983
  });
600
984
  };`;
601
985
  }
602
- async "spec.json"() {
603
- this.processCors();
604
- const logger = (0, moost.useEventLogger)("@moostjs/zod");
986
+ async resolveSpec() {
605
987
  if (!this.spec) {
606
- const { instantiate } = (0, moost.useControllerContext)();
988
+ const ctx = (0, moost.current)();
989
+ const l = (0, moost.useLogger)(ctx);
990
+ const logger = typeof l.topic === "function" ? l.topic("@moostjs/swagger") : l;
991
+ const { instantiate } = (0, moost.useControllerContext)(ctx);
607
992
  const moost$1 = await instantiate(moost.Moost);
608
993
  this.spec = mapToSwaggerSpec(moost$1.getControllersOverview(), this.opts, logger);
609
994
  }
610
995
  return this.spec;
611
996
  }
612
- "files"(url) {
997
+ async "spec.json"() {
998
+ this.processCors();
999
+ return this.resolveSpec();
1000
+ }
1001
+ async "spec.yaml"() {
1002
+ this.processCors();
1003
+ return jsonToYaml(await this.resolveSpec());
1004
+ }
1005
+ files(url) {
613
1006
  this.processCors();
614
1007
  return this.serve(url.split("/").pop());
615
1008
  }
616
- "serve"(path$1) {
1009
+ serve(path$1) {
617
1010
  return (0, __wooksjs_http_static.serveFile)(path$1, {
618
1011
  baseDir: this.assetPath,
619
1012
  cacheControl: {
@@ -641,7 +1034,7 @@ _ts_decorate([
641
1034
  _ts_metadata("design:type", Function),
642
1035
  _ts_metadata("design:paramtypes", [
643
1036
  String,
644
- typeof __wooksjs_event_http.THeaderHook === "undefined" ? Object : __wooksjs_event_http.THeaderHook,
1037
+ typeof THeaderHook === "undefined" ? Object : THeaderHook,
645
1038
  typeof TStatusHook === "undefined" ? Object : TStatusHook
646
1039
  ]),
647
1040
  _ts_metadata("design:returntype", void 0)
@@ -659,6 +1052,13 @@ _ts_decorate([
659
1052
  _ts_metadata("design:paramtypes", []),
660
1053
  _ts_metadata("design:returntype", Promise)
661
1054
  ], SwaggerController.prototype, "spec.json", null);
1055
+ _ts_decorate([
1056
+ (0, __moostjs_event_http.Get)(),
1057
+ (0, __moostjs_event_http.SetHeader)("content-type", "text/yaml"),
1058
+ _ts_metadata("design:type", Function),
1059
+ _ts_metadata("design:paramtypes", []),
1060
+ _ts_metadata("design:returntype", Promise)
1061
+ ], SwaggerController.prototype, "spec.yaml", null);
662
1062
  _ts_decorate([
663
1063
  (0, __moostjs_event_http.Get)("swagger-ui-bundle.*(js|js\\.map)"),
664
1064
  (0, __moostjs_event_http.Get)("swagger-ui-standalone-preset.*(js|js\\.map)"),
@@ -678,17 +1078,25 @@ SwaggerController = _ts_decorate([
678
1078
  ], SwaggerController);
679
1079
 
680
1080
  //#endregion
1081
+ exports.SwaggerCallback = SwaggerCallback;
681
1082
  Object.defineProperty(exports, 'SwaggerController', {
682
1083
  enumerable: true,
683
1084
  get: function () {
684
1085
  return SwaggerController;
685
1086
  }
686
1087
  });
1088
+ exports.SwaggerDeprecated = SwaggerDeprecated;
687
1089
  exports.SwaggerDescription = SwaggerDescription;
688
1090
  exports.SwaggerExample = SwaggerExample;
689
1091
  exports.SwaggerExclude = SwaggerExclude;
1092
+ exports.SwaggerExternalDocs = SwaggerExternalDocs;
1093
+ exports.SwaggerLink = SwaggerLink;
1094
+ exports.SwaggerOperationId = SwaggerOperationId;
690
1095
  exports.SwaggerParam = SwaggerParam;
1096
+ exports.SwaggerPublic = SwaggerPublic;
691
1097
  exports.SwaggerRequestBody = SwaggerRequestBody;
692
1098
  exports.SwaggerResponse = SwaggerResponse;
1099
+ exports.SwaggerSecurity = SwaggerSecurity;
1100
+ exports.SwaggerSecurityAll = SwaggerSecurityAll;
693
1101
  exports.SwaggerTag = SwaggerTag;
694
1102
  exports.getSwaggerMate = getSwaggerMate;