@moostjs/swagger 0.5.33 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,39 +1,55 @@
1
- import { Const, Controller, Moost, getMoostMate, useControllerContext, useEventLogger } from "moost";
1
+ import { Const, Controller, Moost, current, getMoostMate, useControllerContext, useLogger } from "moost";
2
2
  import { Get, HeaderHook, SetHeader, StatusHook, Url } from "@moostjs/event-http";
3
- import { THeaderHook, useSetHeaders } from "@wooksjs/event-http";
3
+ import { useResponse } from "@wooksjs/event-http";
4
4
  import { serveFile } from "@wooksjs/http-static";
5
5
  import Path from "path";
6
6
  import { getAbsoluteFSPath } from "swagger-ui-dist";
7
7
 
8
8
  //#region packages/swagger/src/swagger.mate.ts
9
- function getSwaggerMate() {
9
+ /** Returns the shared `Mate` instance extended with Swagger/OpenAPI metadata fields. */ function getSwaggerMate() {
10
10
  return getMoostMate();
11
11
  }
12
12
 
13
13
  //#endregion
14
14
  //#region packages/swagger/src/decorators.ts
15
- const SwaggerTag = (tag) => getSwaggerMate().decorate("swaggerTags", tag, true);
16
- const SwaggerExclude = () => getSwaggerMate().decorate("swaggerExclude", true);
17
- const SwaggerDescription = (descr) => getSwaggerMate().decorate("swaggerDescription", descr);
15
+ /** Adds an OpenAPI tag to a controller or handler for grouping in the Swagger UI. */ const SwaggerTag = (tag) => getSwaggerMate().decorate("swaggerTags", tag, true);
16
+ /** Excludes a controller or handler from the generated OpenAPI spec. */ const SwaggerExclude = () => getSwaggerMate().decorate("swaggerExclude", true);
17
+ /** Sets the OpenAPI description for a handler. */ const SwaggerDescription = (descr) => getSwaggerMate().decorate("swaggerDescription", descr);
18
18
  function SwaggerResponse(code, opts, example) {
19
19
  return getSwaggerMate().decorate((meta) => {
20
20
  let ex;
21
21
  if (example) ex = example;
22
22
  if (typeof code !== "number" && opts) ex = opts;
23
+ if (ex === void 0) {
24
+ ex = typeof code === "number" ? opts : code;
25
+ ex = ex?.example;
26
+ }
23
27
  meta.swaggerResponses = meta.swaggerResponses || {};
24
28
  const keyCode = typeof code === "number" ? code : 0;
25
29
  const opt = typeof code === "number" ? opts : code;
26
30
  const contentType = typeof opt.contentType === "string" ? opt.contentType : "*/*";
31
+ const description = typeof opt.description === "string" ? opt.description : void 0;
32
+ const headers = opt.headers;
27
33
  const response = ["object", "function"].includes(typeof opt.response) ? opt.response : opt;
28
- meta.swaggerResponses[keyCode] = meta.swaggerResponses[keyCode] || {};
29
- meta.swaggerResponses[keyCode][contentType] = {
34
+ meta.swaggerResponses[keyCode] = meta.swaggerResponses[keyCode] || { content: {} };
35
+ meta.swaggerResponses[keyCode].content[contentType] = {
30
36
  response,
31
37
  example: ex
32
38
  };
39
+ if (description) meta.swaggerResponses[keyCode].description = description;
40
+ if (headers) meta.swaggerResponses[keyCode].headers = {
41
+ ...meta.swaggerResponses[keyCode].headers,
42
+ ...headers
43
+ };
33
44
  return meta;
34
45
  });
35
46
  }
36
- function SwaggerRequestBody(opt) {
47
+ /**
48
+ * Defines the request body schema in the OpenAPI spec.
49
+ *
50
+ * @param opt - Schema definition. Use `{ response: MyDto }` for a typed schema,
51
+ * or `{ response: MyDto, contentType: 'multipart/form-data' }` for a specific content type.
52
+ */ function SwaggerRequestBody(opt) {
37
53
  return getSwaggerMate().decorate((meta) => {
38
54
  meta.swaggerRequestBody = meta.swaggerRequestBody || {};
39
55
  const contentType = typeof opt.contentType === "string" ? opt.contentType : "application/json";
@@ -42,12 +58,127 @@ function SwaggerRequestBody(opt) {
42
58
  return meta;
43
59
  });
44
60
  }
45
- function SwaggerParam(opts) {
61
+ /** Defines a parameter (query, path, header, cookie) in the OpenAPI spec. */ function SwaggerParam(opts) {
46
62
  return getSwaggerMate().decorate("swaggerParams", opts, true);
47
63
  }
48
- function SwaggerExample(example) {
64
+ /** Attaches an example value to a handler's OpenAPI documentation. */ function SwaggerExample(example) {
49
65
  return getSwaggerMate().decorate("swaggerExample", example);
50
66
  }
67
+ /** Marks a handler or controller as public, opting out of inherited security requirements. */ const SwaggerPublic = () => getSwaggerMate().decorate("swaggerPublic", true);
68
+ /** Marks a handler or controller as deprecated in the OpenAPI spec. */ const SwaggerDeprecated = () => getSwaggerMate().decorate("swaggerDeprecated", true);
69
+ /** Overrides the auto-generated operationId for an endpoint. */ const SwaggerOperationId = (id) => getSwaggerMate().decorate("swaggerOperationId", id);
70
+ /** Links an operation to external documentation. */ const SwaggerExternalDocs = (url, description) => getSwaggerMate().decorate("swaggerExternalDocs", {
71
+ url,
72
+ ...description ? { description } : {}
73
+ });
74
+ /**
75
+ * Attaches a security requirement to a handler or controller (OR semantics).
76
+ * Multiple calls add alternative requirements — any one suffices.
77
+ *
78
+ * @param schemeName - The name of the security scheme (must match a key in securitySchemes)
79
+ * @param scopes - OAuth2/OIDC scopes required (default: [])
80
+ */ function SwaggerSecurity(schemeName, scopes = []) {
81
+ return getSwaggerMate().decorate("swaggerSecurity", { [schemeName]: scopes }, true);
82
+ }
83
+ /**
84
+ * Attaches a combined security requirement (AND semantics).
85
+ * All schemes in the requirement must be satisfied simultaneously.
86
+ */ function SwaggerSecurityAll(requirement) {
87
+ return getSwaggerMate().decorate("swaggerSecurity", requirement, true);
88
+ }
89
+ function SwaggerLink(codeOrName, nameOrOptions, maybeOptions) {
90
+ const statusCode = typeof codeOrName === "number" ? codeOrName : 0;
91
+ const name = typeof codeOrName === "string" ? codeOrName : nameOrOptions;
92
+ const options = typeof codeOrName === "string" ? nameOrOptions : maybeOptions;
93
+ const config = {
94
+ statusCode,
95
+ name,
96
+ ..."operationId" in options && options.operationId ? { operationId: options.operationId } : {},
97
+ ..."operationRef" in options && options.operationRef ? { operationRef: options.operationRef } : {},
98
+ ..."handler" in options && options.handler ? { handler: options.handler } : {},
99
+ ...options.parameters ? { parameters: options.parameters } : {},
100
+ ...options.requestBody ? { requestBody: options.requestBody } : {},
101
+ ...options.description ? { description: options.description } : {},
102
+ ...options.server ? { server: options.server } : {}
103
+ };
104
+ return getSwaggerMate().decorate("swaggerLinks", config, true);
105
+ }
106
+ /**
107
+ * Documents an OpenAPI callback (webhook) on an operation.
108
+ * Describes a request your server sends to a client-provided URL.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * @SwaggerCallback('onEvent', {
113
+ * expression: '{$request.body#/callbackUrl}',
114
+ * requestBody: EventPayloadDto,
115
+ * description: 'Event notification sent to subscriber',
116
+ * })
117
+ * @Post('subscribe')
118
+ * subscribe() { ... }
119
+ * ```
120
+ */ function SwaggerCallback(name, options) {
121
+ const config = {
122
+ name,
123
+ expression: options.expression,
124
+ ...options.method ? { method: options.method } : {},
125
+ ...options.requestBody ? { requestBody: options.requestBody } : {},
126
+ ...options.contentType ? { contentType: options.contentType } : {},
127
+ ...options.description ? { description: options.description } : {},
128
+ ...options.responseStatus ? { responseStatus: options.responseStatus } : {},
129
+ ...options.responseDescription ? { responseDescription: options.responseDescription } : {}
130
+ };
131
+ return getSwaggerMate().decorate("swaggerCallbacks", config, true);
132
+ }
133
+
134
+ //#endregion
135
+ //#region packages/swagger/src/json-to-yaml.ts
136
+ const YAML_SPECIAL = /^[\s#!&*|>'{}[\],?:@`-]|[:#]\s|[\n\r]|\s$/;
137
+ function quoteString(str) {
138
+ if (str === "") return "''";
139
+ if (YAML_SPECIAL.test(str) || str === "true" || str === "false" || str === "null") return JSON.stringify(str);
140
+ const num = Number(str);
141
+ if (str.length > 0 && !Number.isNaN(num) && String(num) === str) return JSON.stringify(str);
142
+ return str;
143
+ }
144
+ function serializeValue(value, indent) {
145
+ if (value === null || value === void 0) return "null";
146
+ if (typeof value === "boolean") return value ? "true" : "false";
147
+ if (typeof value === "number") return Number.isFinite(value) ? String(value) : "null";
148
+ if (typeof value === "string") return quoteString(value);
149
+ const pad = " ".repeat(indent);
150
+ const childPad = " ".repeat(indent + 1);
151
+ if (Array.isArray(value)) {
152
+ if (value.length === 0) return "[]";
153
+ const lines = [];
154
+ for (const item of value) if (isObject(item) || Array.isArray(item)) {
155
+ const nested = serializeValue(item, indent + 1);
156
+ lines.push(`${pad}- ${nested.slice(childPad.length)}`);
157
+ } else lines.push(`${pad}- ${serializeValue(item, 0)}`);
158
+ return lines.join("\n");
159
+ }
160
+ if (isObject(value)) {
161
+ const entries = Object.entries(value);
162
+ if (entries.length === 0) return "{}";
163
+ const lines = [];
164
+ for (const [key, val] of entries) {
165
+ const yamlKey = quoteString(key);
166
+ if (isObject(val) || Array.isArray(val)) {
167
+ const nested = serializeValue(val, indent + 1);
168
+ if (nested === "[]" || nested === "{}") lines.push(`${pad}${yamlKey}: ${nested}`);
169
+ else lines.push(`${pad}${yamlKey}:\n${nested}`);
170
+ } else lines.push(`${pad}${yamlKey}: ${serializeValue(val, 0)}`);
171
+ }
172
+ return lines.join("\n");
173
+ }
174
+ return String(value);
175
+ }
176
+ function isObject(value) {
177
+ return typeof value === "object" && value !== null && !Array.isArray(value);
178
+ }
179
+ function jsonToYaml(value) {
180
+ return `${serializeValue(value, 0)}\n`;
181
+ }
51
182
 
52
183
  //#endregion
53
184
  //#region packages/swagger/src/mapping.ts
@@ -56,16 +187,26 @@ let schemaRefs = /* @__PURE__ */ new WeakMap();
56
187
  const nameToType = /* @__PURE__ */ new Map();
57
188
  function mapToSwaggerSpec(metadata, options, logger) {
58
189
  resetSchemaRegistry();
190
+ const is31 = options?.openapiVersion === "3.1";
191
+ const collectedSecuritySchemes = { ...options?.securitySchemes };
59
192
  const swaggerSpec = {
60
- openapi: "3.0.0",
193
+ openapi: is31 ? "3.1.0" : "3.0.0",
61
194
  info: {
62
195
  title: options?.title || "API Documentation",
63
- version: options?.version || "1.0.0"
196
+ ...options?.description ? { description: options.description } : {},
197
+ version: options?.version || "1.0.0",
198
+ ...options?.contact ? { contact: options.contact } : {},
199
+ ...options?.license ? { license: options.license } : {},
200
+ ...options?.termsOfService ? { termsOfService: options.termsOfService } : {}
64
201
  },
65
202
  paths: {},
66
203
  tags: [],
204
+ ...options?.servers?.length ? { servers: options.servers } : {},
205
+ ...options?.externalDocs ? { externalDocs: options.externalDocs } : {},
206
+ ...options?.security ? { security: options.security } : {},
67
207
  components: { schemas: globalSchemas }
68
208
  };
209
+ const deferredLinks = [];
69
210
  for (const controller of metadata) {
70
211
  const cmeta = controller.meta;
71
212
  if (cmeta?.swaggerExclude) continue;
@@ -77,13 +218,14 @@ function mapToSwaggerSpec(metadata, options, logger) {
77
218
  const uniqueParams = {};
78
219
  const handlerPath = handler.registeredAs[0].path;
79
220
  const handlerMethod = hh.method?.toLowerCase() || "get";
80
- const handlerDescription = hmeta?.description;
221
+ const handlerSummary = hmeta?.label;
222
+ const handlerDescription = hmeta?.swaggerDescription || hmeta?.description;
81
223
  const handlerTags = [...controllerTags, ...hmeta?.swaggerTags || []];
82
224
  if (!swaggerSpec.paths[handlerPath]) swaggerSpec.paths[handlerPath] = {};
83
225
  let responses;
84
- if (hmeta?.swaggerResponses) for (const [code, responseConfigs] of Object.entries(hmeta.swaggerResponses)) {
226
+ if (hmeta?.swaggerResponses) for (const [code, responseEntry] of Object.entries(hmeta.swaggerResponses)) {
85
227
  const newCode = code === "0" ? getDefaultStatusCode(handlerMethod) : code;
86
- for (const [contentType, conf] of Object.entries(responseConfigs)) {
228
+ for (const [contentType, conf] of Object.entries(responseEntry.content)) {
87
229
  const { response, example } = conf;
88
230
  const schema = resolveSwaggerSchemaFromConfig(response);
89
231
  if (schema) {
@@ -92,16 +234,35 @@ function mapToSwaggerSpec(metadata, options, logger) {
92
234
  ...schema,
93
235
  example
94
236
  } : schema;
95
- responses[newCode] = { content: { [contentType]: { schema: schemaWithExample } } };
237
+ if (!responses[newCode]) responses[newCode] = {
238
+ description: responseEntry.description || defaultStatusDescription(Number(newCode)),
239
+ content: {}
240
+ };
241
+ responses[newCode].content[contentType] = { schema: schemaWithExample };
242
+ }
243
+ }
244
+ if (responseEntry.headers) {
245
+ const resolvedHeaders = resolveResponseHeaders(responseEntry.headers);
246
+ if (resolvedHeaders) {
247
+ responses = responses || {};
248
+ if (!responses[newCode]) responses[newCode] = {
249
+ description: defaultStatusDescription(Number(newCode)),
250
+ content: {}
251
+ };
252
+ responses[newCode].headers = resolvedHeaders;
96
253
  }
97
254
  }
98
255
  }
99
- else if (hmeta?.returnType) {
256
+ const defaultCode = getDefaultStatusCode(handlerMethod);
257
+ if (!responses?.[defaultCode] && hmeta?.returnType) {
100
258
  const ensured = ensureSchema(hmeta.returnType);
101
259
  const schema = toSchemaOrRef(ensured);
102
260
  if (schema) {
103
261
  responses = responses || {};
104
- responses[getDefaultStatusCode(handlerMethod)] = { content: { "*/*": { schema } } };
262
+ responses[defaultCode] = {
263
+ description: defaultStatusDescription(Number(defaultCode)),
264
+ content: { "*/*": { schema } }
265
+ };
105
266
  }
106
267
  }
107
268
  let reqBodyRequired = true;
@@ -111,13 +272,39 @@ function mapToSwaggerSpec(metadata, options, logger) {
111
272
  if (schema) bodyContent[contentType] = { schema };
112
273
  }
113
274
  swaggerSpec.paths[handlerPath][handlerMethod] = {
114
- summary: handlerDescription,
115
- operationId: `${handlerMethod.toUpperCase()}_${handlerPath.replace(/\//g, "_").replace(/[{}]/g, "__").replace(/[^\dA-Za-z]/g, "_")}`,
275
+ summary: handlerSummary,
276
+ description: handlerDescription,
277
+ operationId: hmeta?.swaggerOperationId || hmeta?.id || `${handlerMethod.toUpperCase()}_${handlerPath.replaceAll(/\//g, "_").replaceAll(/[{}]/g, "__").replaceAll(/[^\dA-Za-z]/g, "_")}`,
116
278
  tags: handlerTags,
117
279
  parameters: [],
118
280
  responses
119
281
  };
120
282
  const endpointSpec = swaggerSpec.paths[handlerPath][handlerMethod];
283
+ if (hmeta?.swaggerDeprecated || cmeta?.swaggerDeprecated) endpointSpec.deprecated = true;
284
+ if (hmeta?.swaggerExternalDocs) endpointSpec.externalDocs = hmeta.swaggerExternalDocs;
285
+ const opSecurity = resolveOperationSecurity(cmeta, hmeta, collectedSecuritySchemes);
286
+ if (opSecurity !== void 0) endpointSpec.security = opSecurity;
287
+ if (hmeta?.swaggerLinks?.length) for (const linkConfig of hmeta.swaggerLinks) deferredLinks.push({
288
+ endpointSpec,
289
+ httpMethod: handlerMethod,
290
+ config: linkConfig
291
+ });
292
+ if (hmeta?.swaggerCallbacks?.length) {
293
+ endpointSpec.callbacks = endpointSpec.callbacks || {};
294
+ for (const cb of hmeta.swaggerCallbacks) {
295
+ const method = (cb.method || "post").toLowerCase();
296
+ const contentType = cb.contentType || "application/json";
297
+ const status = String(cb.responseStatus || 200);
298
+ const responseDesc = cb.responseDescription || "OK";
299
+ const pathItem = { responses: { [status]: { description: responseDesc } } };
300
+ if (cb.description) pathItem.description = cb.description;
301
+ if (cb.requestBody) {
302
+ const schema = resolveSwaggerSchemaFromConfig(cb.requestBody);
303
+ if (schema) pathItem.requestBody = { content: { [contentType]: { schema } } };
304
+ }
305
+ endpointSpec.callbacks[cb.name] = { [cb.expression]: { [method]: pathItem } };
306
+ }
307
+ }
121
308
  function addParam(param) {
122
309
  const key = `${param.in}//${param.name}`;
123
310
  if (uniqueParams[key]) {
@@ -156,8 +343,7 @@ function mapToSwaggerSpec(metadata, options, logger) {
156
343
  schema
157
344
  });
158
345
  }
159
- for (let i = 0; i < handler.meta.params.length; i++) {
160
- const paramMeta = handler.meta.params[i];
346
+ for (const paramMeta of handler.meta.params) {
161
347
  if (paramMeta.paramSource && ["QUERY_ITEM", "QUERY"].includes(paramMeta.paramSource)) {
162
348
  const ensured = ensureSchema(paramMeta.type);
163
349
  if (paramMeta.paramSource === "QUERY_ITEM") {
@@ -209,7 +395,7 @@ function mapToSwaggerSpec(metadata, options, logger) {
209
395
  }
210
396
  }
211
397
  const bodyEntries = Object.entries(bodyContent).filter((entry) => entry[1] && entry[1].schema !== void 0);
212
- if (bodyEntries.length) {
398
+ if (bodyEntries.length > 0) {
213
399
  const content = {};
214
400
  for (const [contentType, { schema }] of bodyEntries) content[contentType] = { schema };
215
401
  endpointSpec.requestBody = {
@@ -219,6 +405,54 @@ function mapToSwaggerSpec(metadata, options, logger) {
219
405
  }
220
406
  }
221
407
  }
408
+ if (deferredLinks.length > 0) {
409
+ const handlerOpIds = /* @__PURE__ */ new Map();
410
+ for (const controller of metadata) for (const handler of controller.handlers) {
411
+ const hh = handler.handler;
412
+ if (hh.type !== "HTTP" || handler.registeredAs.length === 0) continue;
413
+ const path = handler.registeredAs[0].path;
414
+ const method = hh.method?.toLowerCase() || "get";
415
+ const opId = swaggerSpec.paths[path]?.[method]?.operationId;
416
+ if (opId) {
417
+ if (!handlerOpIds.has(controller.type)) handlerOpIds.set(controller.type, /* @__PURE__ */ new Map());
418
+ const methodMap = handlerOpIds.get(controller.type);
419
+ methodMap?.set(handler.method, opId);
420
+ }
421
+ }
422
+ for (const { endpointSpec, httpMethod, config } of deferredLinks) {
423
+ const statusCode = config.statusCode === 0 ? String(getDefaultStatusCode(httpMethod)) : String(config.statusCode);
424
+ const link = {};
425
+ if (config.handler) {
426
+ const [ctrlClass, methodName] = config.handler;
427
+ const resolvedId = handlerOpIds.get(ctrlClass)?.get(methodName);
428
+ if (!resolvedId) continue;
429
+ link.operationId = resolvedId;
430
+ } else if (config.operationId) link.operationId = config.operationId;
431
+ else if (config.operationRef) link.operationRef = config.operationRef;
432
+ if (config.parameters) link.parameters = config.parameters;
433
+ if (config.requestBody) link.requestBody = config.requestBody;
434
+ if (config.description) link.description = config.description;
435
+ if (config.server) link.server = config.server;
436
+ if (!endpointSpec.responses) endpointSpec.responses = {};
437
+ if (!endpointSpec.responses[statusCode]) endpointSpec.responses[statusCode] = {
438
+ description: defaultStatusDescription(Number(statusCode)),
439
+ content: {}
440
+ };
441
+ const responseEntry = endpointSpec.responses[statusCode];
442
+ if (!responseEntry.links) responseEntry.links = {};
443
+ responseEntry.links[config.name] = link;
444
+ }
445
+ }
446
+ const manualTags = new Map((options?.tags || []).map((t) => [t.name, t]));
447
+ const discoveredNames = /* @__PURE__ */ new Set();
448
+ for (const methods of Object.values(swaggerSpec.paths)) for (const endpoint of Object.values(methods)) for (const tag of endpoint.tags) discoveredNames.add(tag);
449
+ for (const tag of manualTags.values()) swaggerSpec.tags.push(tag);
450
+ for (const name of discoveredNames) if (!manualTags.has(name)) swaggerSpec.tags.push({ name });
451
+ const ownedSchemas = {};
452
+ for (const [key, value] of Object.entries(globalSchemas)) ownedSchemas[key] = cloneSchema(value);
453
+ swaggerSpec.components.schemas = ownedSchemas;
454
+ if (Object.keys(collectedSecuritySchemes).length > 0) swaggerSpec.components.securitySchemes = collectedSecuritySchemes;
455
+ if (is31) transformSpecTo31(swaggerSpec);
222
456
  return swaggerSpec;
223
457
  }
224
458
  function resolveSwaggerSchemaFromConfig(type) {
@@ -226,6 +460,20 @@ function resolveSwaggerSchemaFromConfig(type) {
226
460
  const ensured = ensureSchema(type);
227
461
  return toSchemaOrRef(ensured);
228
462
  }
463
+ function resolveResponseHeaders(headers) {
464
+ const resolved = {};
465
+ let hasAny = false;
466
+ for (const [name, header] of Object.entries(headers)) {
467
+ const schema = resolveSwaggerSchemaFromConfig(header.type) || { type: "string" };
468
+ const entry = { schema };
469
+ if (header.description) entry.description = header.description;
470
+ if (header.required !== void 0) entry.required = header.required;
471
+ if (header.example !== void 0) entry.example = header.example;
472
+ resolved[name] = entry;
473
+ hasAny = true;
474
+ }
475
+ return hasAny ? resolved : void 0;
476
+ }
229
477
  function toSchemaOrRef(result) {
230
478
  if (!result) return void 0;
231
479
  if (result.ref) return { $ref: result.ref };
@@ -234,7 +482,7 @@ function toSchemaOrRef(result) {
234
482
  function inferBodyContentType(schema, resolved) {
235
483
  const target = resolved ?? resolveSchemaFromRef(schema);
236
484
  const schemaType = target?.type ?? schema.type;
237
- if (schemaType && [
485
+ if (typeof schemaType === "string" && [
238
486
  "string",
239
487
  "number",
240
488
  "integer",
@@ -400,22 +648,60 @@ function ensureComponentName(typeRef, schema, suggestedName) {
400
648
  schemaRefs.set(typeRef, candidate);
401
649
  applySwaggerMetadata(typeRef, schema);
402
650
  globalSchemas[candidate] = cloneSchema(schema);
651
+ hoistDefs(globalSchemas[candidate]);
403
652
  return candidate;
404
653
  }
654
+ /**
655
+ * When a schema has `$defs`, hoist each definition into `globalSchemas`
656
+ * (i.e. `#/components/schemas/`) and rewrite all `#/$defs/X` references
657
+ * throughout the schema tree to `#/components/schemas/X`.
658
+ * Removes the `$defs` property from the schema after hoisting.
659
+ */ function hoistDefs(schema) {
660
+ if (!schema.$defs) return;
661
+ for (const [name, def] of Object.entries(schema.$defs)) if (!globalSchemas[name]) {
662
+ globalSchemas[name] = cloneSchema(def);
663
+ hoistDefs(globalSchemas[name]);
664
+ }
665
+ delete schema.$defs;
666
+ rewriteDefsRefs(schema);
667
+ }
668
+ /** Recursively rewrite `$ref: '#/$defs/X'` → `$ref: '#/components/schemas/X'` */ function rewriteDefsRefs(schema) {
669
+ if (schema.$ref?.startsWith("#/$defs/")) schema.$ref = schema.$ref.replace("#/$defs/", "#/components/schemas/");
670
+ if (schema.discriminator?.mapping) {
671
+ for (const [key, value] of Object.entries(schema.discriminator.mapping)) if (value.startsWith("#/$defs/")) schema.discriminator.mapping[key] = value.replace("#/$defs/", "#/components/schemas/");
672
+ }
673
+ if (schema.items && !Array.isArray(schema.items)) rewriteDefsRefs(schema.items);
674
+ if (schema.properties) for (const prop of Object.values(schema.properties)) rewriteDefsRefs(prop);
675
+ for (const list of [
676
+ schema.allOf,
677
+ schema.anyOf,
678
+ schema.oneOf
679
+ ]) if (list) for (const item of list) rewriteDefsRefs(item);
680
+ if (typeof schema.additionalProperties === "object" && schema.additionalProperties) rewriteDefsRefs(schema.additionalProperties);
681
+ if (schema.not) rewriteDefsRefs(schema.not);
682
+ }
405
683
  function applySwaggerMetadata(typeRef, schema) {
406
684
  try {
407
685
  const mate = getSwaggerMate();
408
686
  const meta = mate.read(typeRef);
409
- if (!meta) return;
410
- if (meta.swaggerExample !== void 0 && schema.example === void 0) schema.example = meta.swaggerExample;
411
- const title = meta.label || meta.id;
412
- if (title && !schema.title) schema.title = title;
413
- if (meta.swaggerDescription && !schema.description) schema.description = meta.swaggerDescription;
414
- else if (meta.description && !schema.description) schema.description = meta.description;
687
+ if (meta) {
688
+ if (meta.swaggerExample !== void 0 && schema.example === void 0) schema.example = meta.swaggerExample;
689
+ const title = meta.label || meta.id;
690
+ if (title && !schema.title) schema.title = title;
691
+ if (meta.swaggerDescription && !schema.description) schema.description = meta.swaggerDescription;
692
+ else if (meta.description && !schema.description) schema.description = meta.description;
693
+ }
415
694
  } catch {}
695
+ if (schema.example === void 0) {
696
+ const exampleFn = typeRef.toExampleData;
697
+ if (typeof exampleFn === "function") {
698
+ const example = exampleFn.call(typeRef);
699
+ if (example !== void 0) schema.example = example;
700
+ }
701
+ }
416
702
  }
417
703
  function sanitizeComponentName(name) {
418
- const sanitized = name.replace(/[^A-Za-z0-9_.-]/g, "_");
704
+ const sanitized = name.replaceAll(/[^A-Za-z0-9_.-]/g, "_");
419
705
  return sanitized || "Schema";
420
706
  }
421
707
  function getTypeName(typeRef) {
@@ -498,6 +784,107 @@ function getDefaultStatusCode(httpMethod) {
498
784
  };
499
785
  return defaultStatusCodes[httpMethod.toUpperCase()] || 200;
500
786
  }
787
+ const STATUS_DESCRIPTIONS = {
788
+ 200: "OK",
789
+ 201: "Created",
790
+ 202: "Accepted",
791
+ 204: "No Content",
792
+ 301: "Moved Permanently",
793
+ 302: "Found",
794
+ 304: "Not Modified",
795
+ 400: "Bad Request",
796
+ 401: "Unauthorized",
797
+ 403: "Forbidden",
798
+ 404: "Not Found",
799
+ 405: "Method Not Allowed",
800
+ 409: "Conflict",
801
+ 410: "Gone",
802
+ 415: "Unsupported Media Type",
803
+ 422: "Unprocessable Entity",
804
+ 429: "Too Many Requests",
805
+ 500: "Internal Server Error",
806
+ 502: "Bad Gateway",
807
+ 503: "Service Unavailable"
808
+ };
809
+ function defaultStatusDescription(code) {
810
+ return STATUS_DESCRIPTIONS[code] || "Response";
811
+ }
812
+ function transformSpecTo31(spec) {
813
+ for (const schema of Object.values(spec.components.schemas)) convertSchemaTo31(schema);
814
+ for (const methods of Object.values(spec.paths)) for (const endpoint of Object.values(methods)) {
815
+ for (const param of endpoint.parameters) convertSchemaTo31(param.schema);
816
+ if (endpoint.responses) for (const response of Object.values(endpoint.responses)) {
817
+ for (const media of Object.values(response.content)) convertSchemaTo31(media.schema);
818
+ if (response.headers) for (const header of Object.values(response.headers)) convertSchemaTo31(header.schema);
819
+ }
820
+ if (endpoint.requestBody) for (const media of Object.values(endpoint.requestBody.content)) convertSchemaTo31(media.schema);
821
+ }
822
+ }
823
+ function convertSchemaTo31(schema) {
824
+ if (schema.nullable) {
825
+ delete schema.nullable;
826
+ if (typeof schema.type === "string") schema.type = [schema.type, "null"];
827
+ else if (Array.isArray(schema.type)) {
828
+ if (!schema.type.includes("null")) schema.type.push("null");
829
+ } else schema.type = "null";
830
+ }
831
+ if (schema.properties) for (const prop of Object.values(schema.properties)) convertSchemaTo31(prop);
832
+ if (schema.items) if (Array.isArray(schema.items)) for (const item of schema.items) convertSchemaTo31(item);
833
+ else convertSchemaTo31(schema.items);
834
+ if (schema.allOf) for (const sub of schema.allOf) convertSchemaTo31(sub);
835
+ if (schema.anyOf) for (const sub of schema.anyOf) convertSchemaTo31(sub);
836
+ if (schema.oneOf) for (const sub of schema.oneOf) convertSchemaTo31(sub);
837
+ if (schema.not) convertSchemaTo31(schema.not);
838
+ if (typeof schema.additionalProperties === "object" && schema.additionalProperties) convertSchemaTo31(schema.additionalProperties);
839
+ }
840
+ function collectSchemesFromTransports(transports, schemes) {
841
+ if (transports.bearer) schemes.bearerAuth = {
842
+ type: "http",
843
+ scheme: "bearer",
844
+ ...transports.bearer.format ? { bearerFormat: transports.bearer.format } : {},
845
+ ...transports.bearer.description ? { description: transports.bearer.description } : {}
846
+ };
847
+ if (transports.basic) schemes.basicAuth = {
848
+ type: "http",
849
+ scheme: "basic",
850
+ ...transports.basic.description ? { description: transports.basic.description } : {}
851
+ };
852
+ if (transports.apiKey) schemes.apiKeyAuth = {
853
+ type: "apiKey",
854
+ name: transports.apiKey.name,
855
+ in: transports.apiKey.in,
856
+ ...transports.apiKey.description ? { description: transports.apiKey.description } : {}
857
+ };
858
+ if (transports.cookie) schemes.cookieAuth = {
859
+ type: "apiKey",
860
+ name: transports.cookie.name,
861
+ in: "cookie",
862
+ ...transports.cookie.description ? { description: transports.cookie.description } : {}
863
+ };
864
+ }
865
+ function transportsToSecurityRequirement(transports) {
866
+ const requirements = [];
867
+ if (transports.bearer) requirements.push({ bearerAuth: [] });
868
+ if (transports.basic) requirements.push({ basicAuth: [] });
869
+ if (transports.apiKey) requirements.push({ apiKeyAuth: [] });
870
+ if (transports.cookie) requirements.push({ cookieAuth: [] });
871
+ return requirements;
872
+ }
873
+ function resolveOperationSecurity(cmeta, hmeta, schemes) {
874
+ if (hmeta?.swaggerPublic) return [];
875
+ if (hmeta?.swaggerSecurity?.length) return hmeta.swaggerSecurity;
876
+ if (hmeta?.authTransports) {
877
+ collectSchemesFromTransports(hmeta.authTransports, schemes);
878
+ return transportsToSecurityRequirement(hmeta.authTransports);
879
+ }
880
+ if (cmeta?.swaggerPublic) return [];
881
+ if (cmeta?.swaggerSecurity?.length) return cmeta.swaggerSecurity;
882
+ if (cmeta?.authTransports) {
883
+ collectSchemesFromTransports(cmeta.authTransports, schemes);
884
+ return transportsToSecurityRequirement(cmeta.authTransports);
885
+ }
886
+ return void 0;
887
+ }
501
888
 
502
889
  //#endregion
503
890
  //#region packages/swagger/src/swagger.controller.ts
@@ -526,13 +913,10 @@ function _ts_param(paramIndex, decorator) {
526
913
  };
527
914
  }
528
915
  var SwaggerController = class {
529
- "processCors"() {
530
- if (this.opts.cors) {
531
- const { enableCors } = useSetHeaders();
532
- enableCors(this.opts.cors === true ? void 0 : this.opts.cors);
533
- }
916
+ processCors() {
917
+ if (this.opts.cors) useResponse().enableCors(this.opts.cors === true ? void 0 : this.opts.cors);
534
918
  }
535
- "serveIndex"(url, location, status) {
919
+ serveIndex(url, location, status) {
536
920
  this.processCors();
537
921
  if (!url.endsWith("index.html") && !url.endsWith("/")) {
538
922
  status.value = 302;
@@ -576,21 +960,30 @@ var SwaggerController = class {
576
960
  });
577
961
  };`;
578
962
  }
579
- async "spec.json"() {
580
- this.processCors();
581
- const logger = useEventLogger("@moostjs/zod");
963
+ async resolveSpec() {
582
964
  if (!this.spec) {
583
- const { instantiate } = useControllerContext();
965
+ const ctx = current();
966
+ const l = useLogger(ctx);
967
+ const logger = typeof l.topic === "function" ? l.topic("@moostjs/swagger") : l;
968
+ const { instantiate } = useControllerContext(ctx);
584
969
  const moost = await instantiate(Moost);
585
970
  this.spec = mapToSwaggerSpec(moost.getControllersOverview(), this.opts, logger);
586
971
  }
587
972
  return this.spec;
588
973
  }
589
- "files"(url) {
974
+ async "spec.json"() {
975
+ this.processCors();
976
+ return this.resolveSpec();
977
+ }
978
+ async "spec.yaml"() {
979
+ this.processCors();
980
+ return jsonToYaml(await this.resolveSpec());
981
+ }
982
+ files(url) {
590
983
  this.processCors();
591
984
  return this.serve(url.split("/").pop());
592
985
  }
593
- "serve"(path) {
986
+ serve(path) {
594
987
  return serveFile(path, {
595
988
  baseDir: this.assetPath,
596
989
  cacheControl: {
@@ -636,6 +1029,13 @@ _ts_decorate([
636
1029
  _ts_metadata("design:paramtypes", []),
637
1030
  _ts_metadata("design:returntype", Promise)
638
1031
  ], SwaggerController.prototype, "spec.json", null);
1032
+ _ts_decorate([
1033
+ Get(),
1034
+ SetHeader("content-type", "text/yaml"),
1035
+ _ts_metadata("design:type", Function),
1036
+ _ts_metadata("design:paramtypes", []),
1037
+ _ts_metadata("design:returntype", Promise)
1038
+ ], SwaggerController.prototype, "spec.yaml", null);
639
1039
  _ts_decorate([
640
1040
  Get("swagger-ui-bundle.*(js|js\\.map)"),
641
1041
  Get("swagger-ui-standalone-preset.*(js|js\\.map)"),
@@ -655,4 +1055,4 @@ SwaggerController = _ts_decorate([
655
1055
  ], SwaggerController);
656
1056
 
657
1057
  //#endregion
658
- export { SwaggerController, SwaggerDescription, SwaggerExample, SwaggerExclude, SwaggerParam, SwaggerRequestBody, SwaggerResponse, SwaggerTag, getSwaggerMate };
1058
+ export { SwaggerCallback, SwaggerController, SwaggerDeprecated, SwaggerDescription, SwaggerExample, SwaggerExclude, SwaggerExternalDocs, SwaggerLink, SwaggerOperationId, SwaggerParam, SwaggerPublic, SwaggerRequestBody, SwaggerResponse, SwaggerSecurity, SwaggerSecurityAll, SwaggerTag, getSwaggerMate };