nestjs-openapi 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1529 @@
1
+ import { Schema, Option, Effect } from 'effect';
2
+ import { ts, Node } from 'ts-morph';
3
+
4
+ class ProjectInitError extends Schema.TaggedError()(
5
+ "ProjectInitError",
6
+ {
7
+ tsconfig: Schema.String,
8
+ message: Schema.String,
9
+ cause: Schema.optional(Schema.Unknown)
10
+ }
11
+ ) {
12
+ static create(tsconfig, cause) {
13
+ return new ProjectInitError({
14
+ tsconfig,
15
+ message: `Failed to initialize project with tsconfig: ${tsconfig}`,
16
+ cause
17
+ });
18
+ }
19
+ }
20
+ class EntryNotFoundError extends Schema.TaggedError()(
21
+ "EntryNotFoundError",
22
+ {
23
+ entry: Schema.String,
24
+ className: Schema.String,
25
+ message: Schema.String
26
+ }
27
+ ) {
28
+ static fileNotFound(entry) {
29
+ return new EntryNotFoundError({
30
+ entry,
31
+ className: "AppModule",
32
+ message: `Source file not found: ${entry}`
33
+ });
34
+ }
35
+ static classNotFound(entry, className) {
36
+ return new EntryNotFoundError({
37
+ entry,
38
+ className,
39
+ message: `Entry class '${className}' not found in ${entry}`
40
+ });
41
+ }
42
+ }
43
+ class ConfigNotFoundError extends Schema.TaggedError()(
44
+ "ConfigNotFoundError",
45
+ {
46
+ path: Schema.optional(Schema.String),
47
+ searchDir: Schema.optional(Schema.String),
48
+ message: Schema.String
49
+ }
50
+ ) {
51
+ static notFound(searchDir) {
52
+ return new ConfigNotFoundError({
53
+ searchDir,
54
+ message: "No configuration file found. Create an openapi.config.ts file or specify a path."
55
+ });
56
+ }
57
+ static pathNotFound(path) {
58
+ return new ConfigNotFoundError({
59
+ path,
60
+ message: `Configuration file not found: ${path}`
61
+ });
62
+ }
63
+ }
64
+ class ConfigLoadError extends Schema.TaggedError()(
65
+ "ConfigLoadError",
66
+ {
67
+ path: Schema.String,
68
+ message: Schema.String,
69
+ cause: Schema.optional(Schema.Unknown)
70
+ }
71
+ ) {
72
+ static importFailed(path, cause) {
73
+ return new ConfigLoadError({
74
+ path,
75
+ message: `Failed to load configuration from ${path}`,
76
+ cause
77
+ });
78
+ }
79
+ static noExport(path) {
80
+ return new ConfigLoadError({
81
+ path,
82
+ message: `Configuration file must export a default config or named 'config' export: ${path}`
83
+ });
84
+ }
85
+ }
86
+ class ConfigValidationError extends Schema.TaggedError()(
87
+ "ConfigValidationError",
88
+ {
89
+ path: Schema.String,
90
+ message: Schema.String,
91
+ issues: Schema.Array(Schema.String)
92
+ }
93
+ ) {
94
+ static fromIssues(path, issues) {
95
+ const issuesList = issues.length > 0 ? `
96
+ ${issues.join("\n ")}` : "";
97
+ return new ConfigValidationError({
98
+ path,
99
+ message: `Configuration validation failed: ${path}${issuesList}`,
100
+ issues: [...issues]
101
+ });
102
+ }
103
+ }
104
+ class InvalidMethodError extends Schema.TaggedError()(
105
+ "InvalidMethodError",
106
+ {
107
+ controllerName: Schema.String,
108
+ methodName: Schema.String,
109
+ message: Schema.String
110
+ }
111
+ ) {
112
+ static create(controllerName, methodName, reason) {
113
+ return new InvalidMethodError({
114
+ controllerName,
115
+ methodName,
116
+ message: `Invalid method ${controllerName}.${methodName}: ${reason}`
117
+ });
118
+ }
119
+ }
120
+
121
+ const resolveClassFromSymbol = (sym) => {
122
+ const target = sym.getAliasedSymbol?.() ?? sym;
123
+ const declarations = target.getDeclarations() ?? [];
124
+ const classDecl = declarations.find(
125
+ (d) => d.getKind?.() === ts.SyntaxKind.ClassDeclaration
126
+ );
127
+ return Option.fromNullable(classDecl);
128
+ };
129
+ const getArrayInitializer = (objectLiteral, propertyName) => {
130
+ const initializer = objectLiteral.getProperty(propertyName)?.asKind(ts.SyntaxKind.PropertyAssignment)?.getInitializer();
131
+ return Option.fromNullable(initializer);
132
+ };
133
+ const getStringLiteralValue = (expr) => {
134
+ if (!expr) return Option.none();
135
+ const stringLit = expr.asKind?.(ts.SyntaxKind.StringLiteral);
136
+ return Option.fromNullable(stringLit?.getLiteralValue());
137
+ };
138
+ const getSymbolFromIdentifier = (expr) => {
139
+ if (!expr) return Option.none();
140
+ const id = expr.asKind?.(ts.SyntaxKind.Identifier);
141
+ const symbol = id?.getSymbol() ?? id?.getDefinitionNodes()[0]?.getSymbol();
142
+ return Option.fromNullable(symbol);
143
+ };
144
+
145
+ const isModuleClass = (cls) => cls?.getDecorators().some((d) => d.getName() === "Module") ?? false;
146
+ const getModuleDecoratorArg = (cls) => {
147
+ const decorator = cls.getDecorators().find((d) => d.getName() === "Module");
148
+ const arg = decorator?.getArguments()?.[0]?.asKind?.(ts.SyntaxKind.ObjectLiteralExpression);
149
+ return Option.fromNullable(arg);
150
+ };
151
+ const resolveClassFromExpression = (expr) => {
152
+ if (!expr) return Option.none();
153
+ const id = expr.asKind?.(ts.SyntaxKind.Identifier);
154
+ const symbol = id?.getSymbol() ?? id?.getDefinitionNodes()[0]?.getSymbol();
155
+ if (symbol) return resolveClassFromSymbol(symbol);
156
+ const call = expr.asKind?.(ts.SyntaxKind.CallExpression);
157
+ if (call && call.getExpression().getText().endsWith("forwardRef")) {
158
+ const args = call.getArguments();
159
+ if (args.length === 0) return Option.none();
160
+ const arrow = args[0]?.asKind(ts.SyntaxKind.ArrowFunction);
161
+ if (!arrow) return Option.none();
162
+ const body = arrow.getBody();
163
+ const bodyId = body.asKind?.(ts.SyntaxKind.Identifier);
164
+ if (bodyId) return resolveClassFromExpression(bodyId);
165
+ const block = body.asKind?.(ts.SyntaxKind.Block);
166
+ const retExpr = block?.getStatements().find((s) => s.getKind() === ts.SyntaxKind.ReturnStatement)?.asKind(ts.SyntaxKind.ReturnStatement)?.getExpression();
167
+ return resolveClassFromExpression(retExpr);
168
+ }
169
+ const pae = expr.asKind?.(ts.SyntaxKind.PropertyAccessExpression);
170
+ if (pae) return resolveClassFromExpression(pae.getNameNode());
171
+ return Option.none();
172
+ };
173
+ const resolveArrayOfClasses = (propInit) => {
174
+ const arr = propInit?.asKind?.(ts.SyntaxKind.ArrayLiteralExpression);
175
+ if (!arr) return [];
176
+ return arr.getElements().flatMap((element) => {
177
+ if (element.isKind(ts.SyntaxKind.SpreadElement)) return [];
178
+ const candidate = resolveClassFromExpression(
179
+ element
180
+ );
181
+ return Option.isSome(candidate) ? [candidate.value] : [];
182
+ });
183
+ };
184
+ const getModuleMetadata = (mod) => {
185
+ const modArgOpt = getModuleDecoratorArg(mod);
186
+ if (Option.isNone(modArgOpt)) {
187
+ return { controllers: [], imports: [] };
188
+ }
189
+ const obj = modArgOpt.value;
190
+ const controllersInit = Option.getOrUndefined(getArrayInitializer(obj, "controllers")) ?? Option.getOrUndefined(getArrayInitializer(obj, "controller"));
191
+ const importsInit = Option.getOrUndefined(
192
+ getArrayInitializer(obj, "imports")
193
+ );
194
+ const controllers = resolveArrayOfClasses(controllersInit).filter(
195
+ (cls) => cls.getName() !== void 0
196
+ );
197
+ const imports = resolveArrayOfClasses(importsInit).filter(isModuleClass);
198
+ return { controllers, imports };
199
+ };
200
+
201
+ const moduleKey = (mod) => `${mod.getSourceFile().getFilePath()}::${mod.getName() ?? "<anonymous>"}`;
202
+ const getModules = Effect.fn("Modules.getModules")(function* (root) {
203
+ if (!isModuleClass(root)) {
204
+ yield* Effect.logWarning("Root is not a NestJS module").pipe(
205
+ Effect.annotateLogs({
206
+ className: root.getName() ?? "<anonymous>",
207
+ file: root.getSourceFile().getFilePath()
208
+ })
209
+ );
210
+ return [];
211
+ }
212
+ yield* Effect.logDebug("Starting module traversal").pipe(
213
+ Effect.annotateLogs({
214
+ root: root.getName() ?? "<anonymous>",
215
+ file: root.getSourceFile().getFilePath()
216
+ })
217
+ );
218
+ const results = [];
219
+ const visited = /* @__PURE__ */ new Set();
220
+ const stack = [root];
221
+ while (stack.length > 0) {
222
+ const mod = stack.pop();
223
+ const key = moduleKey(mod);
224
+ if (visited.has(key)) continue;
225
+ visited.add(key);
226
+ const { controllers, imports } = getModuleMetadata(mod);
227
+ if (controllers.length > 0) {
228
+ results.push({ declaration: mod, controllers });
229
+ }
230
+ stack.push(...imports);
231
+ }
232
+ yield* Effect.logDebug("Module traversal complete").pipe(
233
+ Effect.annotateLogs({
234
+ modulesWithControllers: results.length,
235
+ totalVisited: visited.size
236
+ })
237
+ );
238
+ return results;
239
+ });
240
+ const getAllControllers = Effect.fn("Modules.getAllControllers")(
241
+ function* (root) {
242
+ const modules = yield* getModules(root);
243
+ return modules.flatMap((m) => m.controllers);
244
+ }
245
+ );
246
+
247
+ const HTTP_DECORATORS = /* @__PURE__ */ new Set([
248
+ "Get",
249
+ "Post",
250
+ "Put",
251
+ "Patch",
252
+ "Delete",
253
+ "Options",
254
+ "Head",
255
+ "All"
256
+ ]);
257
+ const normalizePath = (path) => {
258
+ if (!path) return "/";
259
+ let out = path.startsWith("/") ? path : `/${path}`;
260
+ if (out !== "/" && out.endsWith("/")) out = out.slice(0, -1);
261
+ return out;
262
+ };
263
+ const getControllerPrefix = (controller) => {
264
+ const decorator = controller.getDecorators().find((d) => d.getName() === "Controller");
265
+ if (!decorator) return "/";
266
+ const arg = decorator.getArguments()[0];
267
+ const value = arg?.asKind?.(ts.SyntaxKind.StringLiteral)?.getLiteralValue();
268
+ return value ? normalizePath(value) : "/";
269
+ };
270
+ const getControllerName = (controller) => controller.getName() ?? "<anonymous>";
271
+ const isHttpMethod = (method) => method.getDecorators().some((d) => HTTP_DECORATORS.has(d.getName()));
272
+ const getHttpMethods = (controller) => controller.getMethods().filter(isHttpMethod);
273
+ const getDecoratorName$1 = (decorator) => {
274
+ const expr = decorator.getExpression();
275
+ if (expr.getKind() === ts.SyntaxKind.CallExpression) {
276
+ const call = expr.asKindOrThrow(ts.SyntaxKind.CallExpression);
277
+ const exprText = call.getExpression().getText();
278
+ return exprText.split(".").pop();
279
+ }
280
+ return expr.getText().split(".").pop();
281
+ };
282
+ const getControllerTags = (controller) => {
283
+ const apiTagsDecorator = controller.getDecorators().find((d) => getDecoratorName$1(d) === "ApiTags");
284
+ if (!apiTagsDecorator) {
285
+ const name = getControllerName(controller);
286
+ return [name.replace(/Controller$/i, "")];
287
+ }
288
+ const tags = apiTagsDecorator.getArguments().flatMap((arg) => {
289
+ const stringLit = arg.asKind?.(ts.SyntaxKind.StringLiteral);
290
+ return stringLit ? [stringLit.getLiteralValue()] : [];
291
+ });
292
+ return tags.length > 0 ? tags : [
293
+ // Keep PascalCase, just remove 'Controller' suffix
294
+ getControllerName(controller).replace(/Controller$/i, "")
295
+ ];
296
+ };
297
+ const isHttpDecorator = (decorator) => HTTP_DECORATORS.has(decorator.getName());
298
+ const getHttpDecorator = (method) => method.getDecorators().find((d) => HTTP_DECORATORS.has(d.getName()));
299
+
300
+ const DEFAULT_SCHEME_NAMES = {
301
+ ApiBearerAuth: "bearer",
302
+ ApiBasicAuth: "basic",
303
+ ApiOAuth2: "oauth2",
304
+ ApiCookieAuth: "cookie"
305
+ };
306
+ const SECURITY_DECORATORS = /* @__PURE__ */ new Set([
307
+ "ApiBearerAuth",
308
+ "ApiBasicAuth",
309
+ "ApiOAuth2",
310
+ "ApiSecurity",
311
+ "ApiCookieAuth"
312
+ ]);
313
+ const extractFirstStringArg = (decorator) => {
314
+ const args = decorator.getArguments();
315
+ if (args.length === 0) return void 0;
316
+ const firstArg = args[0];
317
+ const stringLit = firstArg.asKind?.(ts.SyntaxKind.StringLiteral);
318
+ return stringLit?.getLiteralValue();
319
+ };
320
+ const extractStringArray = (decorator) => {
321
+ const args = decorator.getArguments();
322
+ if (args.length === 0) return [];
323
+ const firstArg = args[0];
324
+ const arrayLit = firstArg.asKind?.(ts.SyntaxKind.ArrayLiteralExpression);
325
+ if (!arrayLit) return [];
326
+ const scopes = [];
327
+ for (const element of arrayLit.getElements()) {
328
+ const stringLit = element.asKind?.(ts.SyntaxKind.StringLiteral);
329
+ if (stringLit) {
330
+ scopes.push(stringLit.getLiteralValue());
331
+ }
332
+ }
333
+ return scopes;
334
+ };
335
+ const extractSecondStringArg = (decorator) => {
336
+ const args = decorator.getArguments();
337
+ if (args.length < 2) return void 0;
338
+ const secondArg = args[1];
339
+ const stringLit = secondArg.asKind?.(ts.SyntaxKind.StringLiteral);
340
+ return stringLit?.getLiteralValue();
341
+ };
342
+ const parseSecurityDecorator = (decorator) => {
343
+ const decoratorName = getDecoratorName$1(decorator);
344
+ if (!SECURITY_DECORATORS.has(decoratorName)) {
345
+ return void 0;
346
+ }
347
+ switch (decoratorName) {
348
+ case "ApiBearerAuth":
349
+ case "ApiBasicAuth":
350
+ case "ApiCookieAuth": {
351
+ const schemeName = extractFirstStringArg(decorator) ?? DEFAULT_SCHEME_NAMES[decoratorName];
352
+ return { schemeName, scopes: [] };
353
+ }
354
+ case "ApiOAuth2": {
355
+ const scopes = extractStringArray(decorator);
356
+ const schemeName = extractSecondStringArg(decorator) ?? DEFAULT_SCHEME_NAMES[decoratorName];
357
+ return { schemeName, scopes: [...scopes] };
358
+ }
359
+ case "ApiSecurity": {
360
+ const schemeName = extractFirstStringArg(decorator);
361
+ if (!schemeName) return void 0;
362
+ const args = decorator.getArguments();
363
+ const scopes = [];
364
+ if (args.length >= 2) {
365
+ const secondArg = args[1];
366
+ const arrayLit = secondArg.asKind?.(
367
+ ts.SyntaxKind.ArrayLiteralExpression
368
+ );
369
+ if (arrayLit) {
370
+ for (const element of arrayLit.getElements()) {
371
+ const stringLit = element.asKind?.(ts.SyntaxKind.StringLiteral);
372
+ if (stringLit) {
373
+ scopes.push(stringLit.getLiteralValue());
374
+ }
375
+ }
376
+ }
377
+ }
378
+ return { schemeName, scopes };
379
+ }
380
+ default:
381
+ return void 0;
382
+ }
383
+ };
384
+ const extractSecurityFromDecorators = (decorators) => {
385
+ const requirements = [];
386
+ for (const decorator of decorators) {
387
+ const requirement = parseSecurityDecorator(decorator);
388
+ if (requirement) {
389
+ requirements.push(requirement);
390
+ }
391
+ }
392
+ return requirements;
393
+ };
394
+ const extractControllerSecurity = (controller) => extractSecurityFromDecorators(controller.getDecorators());
395
+ const extractMethodSecurity = (method) => extractSecurityFromDecorators(method.getDecorators());
396
+ const hasMethodSecurityDecorators = (method) => method.getDecorators().some((d) => SECURITY_DECORATORS.has(getDecoratorName$1(d)));
397
+ const combineSecurityRequirements = (controllerSecurity, methodSecurity, hasMethodDecorators) => {
398
+ if (hasMethodDecorators) {
399
+ return methodSecurity;
400
+ }
401
+ return controllerSecurity;
402
+ };
403
+
404
+ const getDecoratorName = (decorator) => {
405
+ const callExpr = decorator.getCallExpression();
406
+ if (!callExpr) {
407
+ const nameNode = decorator.getName();
408
+ return Option.fromNullable(nameNode);
409
+ }
410
+ const expr = callExpr.getExpression();
411
+ if (expr.getKind() === ts.SyntaxKind.Identifier) {
412
+ return Option.some(expr.getText());
413
+ }
414
+ return Option.none();
415
+ };
416
+ const DECORATOR_MAPPINGS = {
417
+ // Type validators
418
+ IsString: () => ({ type: "string" }),
419
+ IsNumber: () => ({ type: "number" }),
420
+ IsInt: () => ({ type: "integer" }),
421
+ IsBoolean: () => ({ type: "boolean" }),
422
+ IsArray: () => ({ type: "array" }),
423
+ IsObject: () => ({ type: "object" }),
424
+ IsDate: () => ({ type: "string", format: "date-time" }),
425
+ // String format validators
426
+ IsEmail: () => ({ format: "email" }),
427
+ IsUrl: () => ({ format: "uri" }),
428
+ IsUUID: () => ({ format: "uuid" }),
429
+ IsDateString: () => ({ format: "date-time" }),
430
+ IsISO8601: () => ({ format: "date-time" }),
431
+ IsPhoneNumber: () => ({ format: "phone" }),
432
+ IsCreditCard: () => ({ format: "credit-card" }),
433
+ IsIP: () => ({ format: "ipv4" }),
434
+ IsJSON: () => ({ format: "json" }),
435
+ // String length validators
436
+ MinLength: (args) => ({ minLength: parseNumber(args[0]) }),
437
+ MaxLength: (args) => ({ maxLength: parseNumber(args[0]) }),
438
+ Length: (args) => ({
439
+ minLength: parseNumber(args[0]),
440
+ maxLength: parseNumber(args[1]) ?? parseNumber(args[0])
441
+ }),
442
+ // String pattern validators
443
+ Matches: (args) => {
444
+ const pattern = args[0];
445
+ if (pattern) {
446
+ const cleanPattern = pattern.replace(/^\/(.*)\/[gimsuvy]*$/, "$1");
447
+ return { pattern: cleanPattern };
448
+ }
449
+ return {};
450
+ },
451
+ // Number validators
452
+ Min: (args) => ({ minimum: parseNumber(args[0]) }),
453
+ Max: (args) => ({ maximum: parseNumber(args[0]) }),
454
+ IsPositive: () => ({ exclusiveMinimum: 0 }),
455
+ IsNegative: () => ({ exclusiveMaximum: 0 }),
456
+ // Array validators
457
+ ArrayMinSize: (args) => ({ minItems: parseNumber(args[0]) }),
458
+ ArrayMaxSize: (args) => ({ maxItems: parseNumber(args[0]) }),
459
+ // Enum validator - static analysis can't easily extract enum values
460
+ IsEnum: () => ({}),
461
+ // Other validators (these don't map directly to OpenAPI but inform the schema)
462
+ IsOptional: () => ({}),
463
+ // Handled separately via required array
464
+ IsNotEmpty: () => ({ minLength: 1 }),
465
+ IsDefined: () => ({}),
466
+ IsEmpty: () => ({}),
467
+ // Nested/type validators
468
+ ValidateNested: () => ({}),
469
+ Type: () => ({})
470
+ };
471
+ const parseNumber = (value) => {
472
+ if (value === void 0) return void 0;
473
+ const num = Number(value);
474
+ return isNaN(num) ? void 0 : num;
475
+ };
476
+ const getDecoratorArgs = (decorator) => {
477
+ const callExpr = decorator.getCallExpression();
478
+ if (!callExpr) return [];
479
+ return callExpr.getArguments().map((arg) => arg.getText());
480
+ };
481
+ const extractEnumValues = (enumDecl) => {
482
+ const values = [];
483
+ let currentValue = 0;
484
+ for (const member of enumDecl.getMembers()) {
485
+ const initializer = member.getInitializer();
486
+ if (initializer) {
487
+ if (Node.isStringLiteral(initializer)) {
488
+ values.push(initializer.getLiteralValue());
489
+ } else if (Node.isNumericLiteral(initializer)) {
490
+ const numValue = initializer.getLiteralValue();
491
+ values.push(numValue);
492
+ currentValue = numValue + 1;
493
+ } else {
494
+ const text = initializer.getText();
495
+ const num = Number(text);
496
+ if (!isNaN(num)) {
497
+ values.push(num);
498
+ currentValue = num + 1;
499
+ } else {
500
+ values.push(text);
501
+ }
502
+ }
503
+ } else {
504
+ values.push(currentValue);
505
+ currentValue++;
506
+ }
507
+ }
508
+ return values;
509
+ };
510
+ const resolveEnumFromDecorator = (decorator) => {
511
+ const callExpr = decorator.getCallExpression();
512
+ if (!callExpr) return void 0;
513
+ const args = callExpr.getArguments();
514
+ if (args.length === 0) return void 0;
515
+ const firstArg = args[0];
516
+ if (!Node.isIdentifier(firstArg)) return void 0;
517
+ const symbol = firstArg.getSymbol();
518
+ if (!symbol) return void 0;
519
+ const declarations = symbol.getDeclarations();
520
+ if (!declarations || declarations.length === 0) return void 0;
521
+ for (const decl of declarations) {
522
+ if (Node.isEnumDeclaration(decl)) {
523
+ return extractEnumValues(decl);
524
+ }
525
+ }
526
+ for (const decl of declarations) {
527
+ if (Node.isImportSpecifier(decl)) {
528
+ const localSymbol = decl.getSymbol();
529
+ const aliasedSymbol = localSymbol?.getAliasedSymbol();
530
+ if (aliasedSymbol) {
531
+ const aliasedDecls = aliasedSymbol.getDeclarations();
532
+ for (const aliasedDecl of aliasedDecls ?? []) {
533
+ if (Node.isEnumDeclaration(aliasedDecl)) {
534
+ return extractEnumValues(aliasedDecl);
535
+ }
536
+ }
537
+ }
538
+ }
539
+ }
540
+ return void 0;
541
+ };
542
+ const extractPropertyConstraints = (property) => {
543
+ const constraints = {};
544
+ for (const decorator of property.getDecorators()) {
545
+ const nameOpt = getDecoratorName(decorator);
546
+ if (Option.isNone(nameOpt)) continue;
547
+ if (nameOpt.value === "IsEnum") {
548
+ const enumValues = resolveEnumFromDecorator(decorator);
549
+ if (enumValues && enumValues.length > 0) {
550
+ Object.assign(constraints, { enum: enumValues });
551
+ }
552
+ continue;
553
+ }
554
+ const mapper = DECORATOR_MAPPINGS[nameOpt.value];
555
+ if (!mapper) continue;
556
+ Object.assign(constraints, mapper(getDecoratorArgs(decorator)));
557
+ }
558
+ return constraints;
559
+ };
560
+ const isPropertyOptional = (property) => {
561
+ const decorators = property.getDecorators();
562
+ return decorators.some((d) => {
563
+ const nameOpt = getDecoratorName(d);
564
+ return Option.isSome(nameOpt) && nameOpt.value === "IsOptional";
565
+ });
566
+ };
567
+ const extractPropertyValidationInfo = (property) => {
568
+ const constraints = {};
569
+ let isOptional = false;
570
+ for (const decorator of property.getDecorators()) {
571
+ const nameOpt = getDecoratorName(decorator);
572
+ if (Option.isNone(nameOpt)) continue;
573
+ const name = nameOpt.value;
574
+ if (name === "IsOptional") {
575
+ isOptional = true;
576
+ continue;
577
+ }
578
+ if (name === "IsEnum") {
579
+ const enumValues = resolveEnumFromDecorator(decorator);
580
+ if (enumValues && enumValues.length > 0) {
581
+ Object.assign(constraints, { enum: enumValues });
582
+ }
583
+ continue;
584
+ }
585
+ const mapper = DECORATOR_MAPPINGS[name];
586
+ if (!mapper) continue;
587
+ Object.assign(constraints, mapper(getDecoratorArgs(decorator)));
588
+ }
589
+ return { isOptional, constraints };
590
+ };
591
+ const extractClassConstraints = (classDecl) => {
592
+ const result = {};
593
+ const properties = classDecl.getProperties();
594
+ for (const property of properties) {
595
+ const name = property.getName();
596
+ const constraints = extractPropertyConstraints(property);
597
+ if (Object.keys(constraints).length > 0) {
598
+ result[name] = constraints;
599
+ }
600
+ }
601
+ return result;
602
+ };
603
+ const getRequiredProperties = (classDecl) => {
604
+ const properties = classDecl.getProperties();
605
+ const required = [];
606
+ for (const property of properties) {
607
+ if (!isPropertyOptional(property)) {
608
+ required.push(property.getName());
609
+ }
610
+ }
611
+ return required;
612
+ };
613
+ const applyConstraintsToSchema = (schema, propertyConstraints, requiredProperties) => {
614
+ const updated = { ...schema };
615
+ if (schema.properties && Object.keys(propertyConstraints).length > 0) {
616
+ const updatedProperties = {};
617
+ for (const [propName, propSchema] of Object.entries(schema.properties)) {
618
+ const constraints = propertyConstraints[propName];
619
+ if (constraints) {
620
+ updatedProperties[propName] = {
621
+ ...propSchema,
622
+ ...constraints
623
+ };
624
+ } else {
625
+ updatedProperties[propName] = propSchema;
626
+ }
627
+ }
628
+ updated.properties = updatedProperties;
629
+ }
630
+ if (requiredProperties && requiredProperties.length > 0) {
631
+ const existingRequired = new Set(schema.required ?? []);
632
+ for (const prop of requiredProperties) {
633
+ existingRequired.add(prop);
634
+ }
635
+ updated.required = [...existingRequired];
636
+ }
637
+ return updated;
638
+ };
639
+ const mergeValidationConstraints = (schemas, classConstraints, classRequired) => {
640
+ const updatedDefinitions = {};
641
+ for (const [name, schema] of Object.entries(schemas.definitions)) {
642
+ const constraints = classConstraints.get(name);
643
+ const required = classRequired.get(name);
644
+ if (constraints || required) {
645
+ updatedDefinitions[name] = applyConstraintsToSchema(
646
+ schema,
647
+ constraints ?? {},
648
+ required
649
+ );
650
+ } else {
651
+ updatedDefinitions[name] = schema;
652
+ }
653
+ }
654
+ return { definitions: updatedDefinitions };
655
+ };
656
+
657
+ const HTTP_METHOD_MAP = {
658
+ Get: "GET",
659
+ Post: "POST",
660
+ Put: "PUT",
661
+ Patch: "PATCH",
662
+ Delete: "DELETE",
663
+ Options: "OPTIONS",
664
+ Head: "HEAD",
665
+ All: "ALL"
666
+ };
667
+ const PARAMETER_DECORATOR_MAP = {
668
+ Param: "path",
669
+ Query: "query",
670
+ Body: "body",
671
+ Headers: "header"
672
+ };
673
+ const PRIMITIVE_TYPES = /* @__PURE__ */ new Set([
674
+ "string",
675
+ "number",
676
+ "boolean",
677
+ "any",
678
+ "unknown",
679
+ "void",
680
+ "null",
681
+ "undefined",
682
+ "never",
683
+ "object",
684
+ "String",
685
+ "Number",
686
+ "Boolean",
687
+ "Date"
688
+ ]);
689
+ const buildFullPath = (controllerPrefix, methodPath) => {
690
+ const prefix = controllerPrefix.replace(/\/+$/, "");
691
+ const normalizedPath = methodPath.replace(/^\/+/, "");
692
+ if (!prefix && !normalizedPath) return "/";
693
+ if (!normalizedPath) return prefix || "/";
694
+ if (!prefix) return `/${normalizedPath}`;
695
+ return `${prefix}/${normalizedPath}`.replace(/\/+/g, "/");
696
+ };
697
+ const getRoutePath = (decorator) => {
698
+ const arg = decorator.getArguments()[0];
699
+ const stringLit = arg?.asKind?.(ts.SyntaxKind.StringLiteral);
700
+ return normalizePath(stringLit?.getLiteralValue() ?? "/");
701
+ };
702
+ const parseTypeText = (text) => {
703
+ const trimmed = text.trim();
704
+ return trimmed.startsWith("{") && trimmed.endsWith("}") ? { type: Option.none(), inline: Option.some(trimmed) } : { type: Option.some(trimmed), inline: Option.none() };
705
+ };
706
+ const getReturnTypeInfo = (method) => {
707
+ const returnType = method.getReturnType();
708
+ const awaited = returnType.getAwaitedType?.() ?? returnType;
709
+ const symbol = awaited.getSymbol?.();
710
+ const getOriginalTypeName = () => {
711
+ if (!symbol) return null;
712
+ const aliased = symbol.getAliasedSymbol?.();
713
+ if (aliased) {
714
+ const name = aliased.getName();
715
+ if (name && !name.startsWith("__")) return name;
716
+ }
717
+ return null;
718
+ };
719
+ let text = getOriginalTypeName() ?? awaited.getText(method);
720
+ const promiseMatch = text.match(/^Promise<(.+)>$/);
721
+ if (promiseMatch) text = promiseMatch[1].trim();
722
+ text = text.replace(/\bimport\([^)]*\)\./g, "");
723
+ const resolveFilePath = () => {
724
+ if (symbol) {
725
+ const decls = symbol.getDeclarations();
726
+ if (decls && decls.length > 0) {
727
+ return Option.some(decls[0].getSourceFile().getFilePath());
728
+ }
729
+ }
730
+ return Option.none();
731
+ };
732
+ if (text.endsWith("[]")) {
733
+ const baseType = text.slice(0, -2);
734
+ const base = parseTypeText(baseType);
735
+ return {
736
+ ...base,
737
+ container: Option.some("array"),
738
+ filePath: Option.isSome(base.type) ? resolveFilePath() : Option.none()
739
+ };
740
+ }
741
+ const arrayMatch = text.match(/^(?:Readonly)?Array<(.+)>$/);
742
+ if (arrayMatch) {
743
+ const base = parseTypeText(arrayMatch[1]);
744
+ return {
745
+ ...base,
746
+ container: Option.some("array"),
747
+ filePath: Option.isSome(base.type) ? resolveFilePath() : Option.none()
748
+ };
749
+ }
750
+ const parsed = parseTypeText(text);
751
+ return {
752
+ ...parsed,
753
+ container: Option.none(),
754
+ filePath: Option.isSome(parsed.type) ? resolveFilePath() : Option.none()
755
+ };
756
+ };
757
+ const extractDescriptionFromDecorator = (decorator) => {
758
+ for (const arg of decorator.getArguments()) {
759
+ const objLit = arg.asKind?.(ts.SyntaxKind.ObjectLiteralExpression);
760
+ if (!objLit) continue;
761
+ const descProperty = objLit.getProperty("description");
762
+ if (!descProperty) continue;
763
+ const propAssignment = descProperty.asKind?.(
764
+ ts.SyntaxKind.PropertyAssignment
765
+ );
766
+ if (!propAssignment) continue;
767
+ const initializer = propAssignment.getInitializer();
768
+ const stringLit = initializer?.asKind?.(ts.SyntaxKind.StringLiteral);
769
+ if (!stringLit) continue;
770
+ return Option.some(stringLit.getLiteralValue());
771
+ }
772
+ return Option.none();
773
+ };
774
+ const extractDescriptionByName = (decorator, paramName) => {
775
+ for (const arg of decorator.getArguments()) {
776
+ const objLit = arg.asKind?.(ts.SyntaxKind.ObjectLiteralExpression);
777
+ if (!objLit) continue;
778
+ const nameProperty = objLit.getProperty("name");
779
+ if (!nameProperty) continue;
780
+ const namePropAssignment = nameProperty.asKind?.(
781
+ ts.SyntaxKind.PropertyAssignment
782
+ );
783
+ if (!namePropAssignment) continue;
784
+ const nameInitializer = namePropAssignment.getInitializer();
785
+ const nameStringLit = nameInitializer?.asKind?.(
786
+ ts.SyntaxKind.StringLiteral
787
+ );
788
+ if (nameStringLit?.getLiteralValue() !== paramName) continue;
789
+ const descProperty = objLit.getProperty("description");
790
+ if (!descProperty) continue;
791
+ const descPropAssignment = descProperty.asKind?.(
792
+ ts.SyntaxKind.PropertyAssignment
793
+ );
794
+ if (!descPropAssignment) continue;
795
+ const descInitializer = descPropAssignment.getInitializer();
796
+ const descStringLit = descInitializer?.asKind?.(
797
+ ts.SyntaxKind.StringLiteral
798
+ );
799
+ if (!descStringLit) continue;
800
+ return Option.some(descStringLit.getLiteralValue());
801
+ }
802
+ return Option.none();
803
+ };
804
+ const extractParameterDescription = (method, param, paramName, paramLocation) => {
805
+ const apiDecoratorNames = ["ApiQuery", "ApiParam", "ApiBody", "ApiHeader"];
806
+ for (const decorator of param.getDecorators()) {
807
+ const decoratorName = getDecoratorName$1(decorator);
808
+ if (apiDecoratorNames.includes(decoratorName)) {
809
+ const desc = extractDescriptionFromDecorator(decorator);
810
+ if (Option.isSome(desc)) return desc;
811
+ }
812
+ }
813
+ const decoratorMap = {
814
+ query: "ApiQuery",
815
+ path: "ApiParam",
816
+ header: "ApiHeader",
817
+ body: "ApiBody"
818
+ };
819
+ for (const decorator of method.getDecorators()) {
820
+ const decoratorName = getDecoratorName$1(decorator);
821
+ if (decoratorName === decoratorMap[paramLocation]) {
822
+ const desc = extractDescriptionByName(decorator, paramName);
823
+ if (Option.isSome(desc)) return desc;
824
+ }
825
+ }
826
+ return Option.none();
827
+ };
828
+ const BUILT_IN_TYPES = /* @__PURE__ */ new Set([
829
+ "Array",
830
+ "ReadonlyArray",
831
+ "Promise",
832
+ "Map",
833
+ "Set",
834
+ "WeakMap",
835
+ "WeakSet",
836
+ "Record",
837
+ "Partial",
838
+ "Required",
839
+ "Pick",
840
+ "Omit",
841
+ "Exclude",
842
+ "Extract",
843
+ "NonNullable",
844
+ "ReturnType",
845
+ "InstanceType",
846
+ "Parameters"
847
+ ]);
848
+ const isExpandableType = (typeName) => {
849
+ if (PRIMITIVE_TYPES.has(typeName)) return false;
850
+ if (BUILT_IN_TYPES.has(typeName.split("<")[0])) return false;
851
+ if (typeName.includes(" | ") || typeName.includes(" & ")) return false;
852
+ if (typeName.endsWith("[]")) return false;
853
+ return /^[A-Z][a-zA-Z0-9]*$/.test(typeName);
854
+ };
855
+ const tsTypeToString = (typeText) => {
856
+ const trimmed = typeText.trim();
857
+ if (trimmed === "string" || trimmed === "String") return "string";
858
+ if (trimmed === "number" || trimmed === "Number") return "number";
859
+ if (trimmed === "boolean" || trimmed === "Boolean") return "boolean";
860
+ if (trimmed === "Date") return "Date";
861
+ if (trimmed.includes(" | undefined")) {
862
+ return trimmed.replace(" | undefined", "").trim();
863
+ }
864
+ return trimmed;
865
+ };
866
+ const expandQueryDtoProperties = (method, param, paramType) => {
867
+ const expandedParams = [];
868
+ const properties = paramType.getProperties();
869
+ for (const prop of properties) {
870
+ const propName = prop.getName();
871
+ if (propName.startsWith("_")) continue;
872
+ const declarations = prop.getDeclarations();
873
+ let propTypeText = "unknown";
874
+ let isOptional = false;
875
+ let constraints = void 0;
876
+ if (declarations.length > 0) {
877
+ const decl = declarations[0];
878
+ const propDecl = decl.asKind?.(ts.SyntaxKind.PropertyDeclaration);
879
+ const propSig = decl.asKind?.(ts.SyntaxKind.PropertySignature);
880
+ if (propDecl) {
881
+ propTypeText = tsTypeToString(propDecl.getType().getText(propDecl));
882
+ const validationInfo = extractPropertyValidationInfo(propDecl);
883
+ isOptional = propDecl.hasQuestionToken() || propDecl.hasInitializer?.() || validationInfo.isOptional;
884
+ if (Object.keys(validationInfo.constraints).length > 0) {
885
+ constraints = validationInfo.constraints;
886
+ }
887
+ } else if (propSig) {
888
+ propTypeText = tsTypeToString(propSig.getType().getText(propSig));
889
+ isOptional = propSig.hasQuestionToken();
890
+ }
891
+ }
892
+ const description = extractDescriptionByName(
893
+ method.getDecorators().find((d) => getDecoratorName$1(d) === "ApiQuery") ?? param.getDecorators()[0],
894
+ propName
895
+ );
896
+ expandedParams.push({
897
+ name: propName,
898
+ location: "query",
899
+ tsType: propTypeText,
900
+ required: !isOptional,
901
+ description,
902
+ constraints
903
+ });
904
+ }
905
+ return expandedParams;
906
+ };
907
+ const extractParameters = (method, options = {}) => method.getParameters().reduce((params, param) => {
908
+ const relevantDecorator = param.getDecorators().find((d) => d.getName() in PARAMETER_DECORATOR_MAP);
909
+ if (!relevantDecorator) return params;
910
+ const decoratorName = relevantDecorator.getName();
911
+ const args = relevantDecorator.getArguments();
912
+ let paramName = param.getName();
913
+ let hasExplicitName = false;
914
+ for (const arg of args) {
915
+ const stringLit = arg.asKind?.(ts.SyntaxKind.StringLiteral);
916
+ if (stringLit) {
917
+ paramName = stringLit.getLiteralValue();
918
+ hasExplicitName = true;
919
+ break;
920
+ }
921
+ }
922
+ const location = PARAMETER_DECORATOR_MAP[decoratorName] ?? "query";
923
+ const paramType = param.getType();
924
+ let tsType = paramType.getText(param);
925
+ const symbol = paramType.getSymbol?.();
926
+ if (symbol) {
927
+ const symbolName = symbol.getName();
928
+ if (symbolName && !symbolName.startsWith("__") && !BUILT_IN_TYPES.has(symbolName)) {
929
+ tsType = symbolName;
930
+ }
931
+ }
932
+ tsType = tsType.replace(/\bimport\([^)]*\)\./g, "");
933
+ if (decoratorName === "Query" && !hasExplicitName && options.query?.style !== "ref" && isExpandableType(tsType)) {
934
+ const expandedParams = expandQueryDtoProperties(method, param, paramType);
935
+ if (expandedParams.length > 0) {
936
+ params.push(...expandedParams);
937
+ return params;
938
+ }
939
+ }
940
+ const isOptional = param.hasQuestionToken() || param.hasInitializer();
941
+ const description = extractParameterDescription(
942
+ method,
943
+ param,
944
+ paramName,
945
+ location
946
+ );
947
+ params.push({
948
+ name: paramName,
949
+ location,
950
+ tsType,
951
+ required: !isOptional,
952
+ description
953
+ });
954
+ return params;
955
+ }, []);
956
+ const extractDecoratorNames = (method) => method.getDecorators().map((d) => getDecoratorName$1(d));
957
+ const extractStringArguments = (decorator) => {
958
+ const args = decorator.getArguments();
959
+ const results = [];
960
+ for (const arg of args) {
961
+ const stringLit = arg.asKind?.(ts.SyntaxKind.StringLiteral);
962
+ if (stringLit) {
963
+ results.push(stringLit.getLiteralValue());
964
+ }
965
+ }
966
+ return results;
967
+ };
968
+ const extractApiConsumes = (method) => {
969
+ for (const decorator of method.getDecorators()) {
970
+ const decoratorName = getDecoratorName$1(decorator);
971
+ if (decoratorName === "ApiConsumes") {
972
+ return extractStringArguments(decorator);
973
+ }
974
+ }
975
+ return [];
976
+ };
977
+ const extractApiProduces = (method) => {
978
+ for (const decorator of method.getDecorators()) {
979
+ const decoratorName = getDecoratorName$1(decorator);
980
+ if (decoratorName === "ApiProduces") {
981
+ return extractStringArguments(decorator);
982
+ }
983
+ }
984
+ return [];
985
+ };
986
+ const extractStringPropertyFromObjectLiteral = (decorator, propertyName) => {
987
+ const arg = decorator.getArguments()[0];
988
+ const objLit = arg?.asKind?.(ts.SyntaxKind.ObjectLiteralExpression);
989
+ if (!objLit) return Option.none();
990
+ const property = objLit.getProperty(propertyName);
991
+ if (!property) return Option.none();
992
+ const propAssignment = property.asKind?.(ts.SyntaxKind.PropertyAssignment);
993
+ if (!propAssignment) return Option.none();
994
+ const initializer = propAssignment.getInitializer();
995
+ const stringLit = initializer?.asKind?.(ts.SyntaxKind.StringLiteral);
996
+ if (stringLit) {
997
+ return Option.some(stringLit.getLiteralValue());
998
+ }
999
+ return Option.none();
1000
+ };
1001
+ const extractBooleanPropertyFromDecorator = (decorator, propertyName) => {
1002
+ const arg = decorator.getArguments()[0];
1003
+ const objLit = arg?.asKind?.(ts.SyntaxKind.ObjectLiteralExpression);
1004
+ if (!objLit) return Option.none();
1005
+ const property = objLit.getProperty(propertyName);
1006
+ if (!property) return Option.none();
1007
+ const propAssignment = property.asKind?.(ts.SyntaxKind.PropertyAssignment);
1008
+ if (!propAssignment) return Option.none();
1009
+ const initializer = propAssignment.getInitializer();
1010
+ if (initializer?.getKind() === ts.SyntaxKind.TrueKeyword) {
1011
+ return Option.some(true);
1012
+ }
1013
+ if (initializer?.getKind() === ts.SyntaxKind.FalseKeyword) {
1014
+ return Option.some(false);
1015
+ }
1016
+ return Option.none();
1017
+ };
1018
+ const HTTP_STATUS_MAP = {
1019
+ CONTINUE: 100,
1020
+ SWITCHING_PROTOCOLS: 101,
1021
+ PROCESSING: 102,
1022
+ EARLYHINTS: 103,
1023
+ OK: 200,
1024
+ CREATED: 201,
1025
+ ACCEPTED: 202,
1026
+ NON_AUTHORITATIVE_INFORMATION: 203,
1027
+ NO_CONTENT: 204,
1028
+ RESET_CONTENT: 205,
1029
+ PARTIAL_CONTENT: 206,
1030
+ AMBIGUOUS: 300,
1031
+ MOVED_PERMANENTLY: 301,
1032
+ FOUND: 302,
1033
+ SEE_OTHER: 303,
1034
+ NOT_MODIFIED: 304,
1035
+ TEMPORARY_REDIRECT: 307,
1036
+ PERMANENT_REDIRECT: 308,
1037
+ BAD_REQUEST: 400,
1038
+ UNAUTHORIZED: 401,
1039
+ PAYMENT_REQUIRED: 402,
1040
+ FORBIDDEN: 403,
1041
+ NOT_FOUND: 404,
1042
+ METHOD_NOT_ALLOWED: 405,
1043
+ NOT_ACCEPTABLE: 406,
1044
+ PROXY_AUTHENTICATION_REQUIRED: 407,
1045
+ REQUEST_TIMEOUT: 408,
1046
+ CONFLICT: 409,
1047
+ GONE: 410,
1048
+ LENGTH_REQUIRED: 411,
1049
+ PRECONDITION_FAILED: 412,
1050
+ PAYLOAD_TOO_LARGE: 413,
1051
+ URI_TOO_LONG: 414,
1052
+ UNSUPPORTED_MEDIA_TYPE: 415,
1053
+ REQUESTED_RANGE_NOT_SATISFIABLE: 416,
1054
+ EXPECTATION_FAILED: 417,
1055
+ I_AM_A_TEAPOT: 418,
1056
+ MISDIRECTED: 421,
1057
+ UNPROCESSABLE_ENTITY: 422,
1058
+ FAILED_DEPENDENCY: 424,
1059
+ PRECONDITION_REQUIRED: 428,
1060
+ TOO_MANY_REQUESTS: 429,
1061
+ INTERNAL_SERVER_ERROR: 500,
1062
+ NOT_IMPLEMENTED: 501,
1063
+ BAD_GATEWAY: 502,
1064
+ SERVICE_UNAVAILABLE: 503,
1065
+ GATEWAY_TIMEOUT: 504,
1066
+ HTTP_VERSION_NOT_SUPPORTED: 505
1067
+ };
1068
+ const extractNumberPropertyFromDecorator = (decorator, propertyName) => {
1069
+ const arg = decorator.getArguments()[0];
1070
+ const objLit = arg?.asKind?.(ts.SyntaxKind.ObjectLiteralExpression);
1071
+ if (!objLit) return Option.none();
1072
+ const property = objLit.getProperty(propertyName);
1073
+ if (!property) return Option.none();
1074
+ const propAssignment = property.asKind?.(ts.SyntaxKind.PropertyAssignment);
1075
+ if (!propAssignment) return Option.none();
1076
+ const initializer = propAssignment.getInitializer();
1077
+ const numLit = initializer?.asKind?.(ts.SyntaxKind.NumericLiteral);
1078
+ if (numLit) {
1079
+ return Option.some(Number(numLit.getLiteralValue()));
1080
+ }
1081
+ const propAccess = initializer?.asKind?.(
1082
+ ts.SyntaxKind.PropertyAccessExpression
1083
+ );
1084
+ if (propAccess) {
1085
+ const statusName = propAccess.getName();
1086
+ if (statusName && HTTP_STATUS_MAP[statusName] !== void 0) {
1087
+ return Option.some(HTTP_STATUS_MAP[statusName]);
1088
+ }
1089
+ }
1090
+ return Option.none();
1091
+ };
1092
+ const extractHttpCode = (method) => {
1093
+ for (const decorator of method.getDecorators()) {
1094
+ const decoratorName = getDecoratorName$1(decorator);
1095
+ if (decoratorName === "HttpCode") {
1096
+ const arg = decorator.getArguments()[0];
1097
+ if (!arg) continue;
1098
+ const numLit = arg.asKind?.(ts.SyntaxKind.NumericLiteral);
1099
+ if (numLit) {
1100
+ return Option.some(Number(numLit.getLiteralValue()));
1101
+ }
1102
+ const propAccess = arg.asKind?.(ts.SyntaxKind.PropertyAccessExpression);
1103
+ if (propAccess) {
1104
+ const statusName = propAccess.getName();
1105
+ if (statusName && HTTP_STATUS_MAP[statusName] !== void 0) {
1106
+ return Option.some(HTTP_STATUS_MAP[statusName]);
1107
+ }
1108
+ }
1109
+ }
1110
+ }
1111
+ return Option.none();
1112
+ };
1113
+ const extractResponseType = (decorator) => {
1114
+ const arg = decorator.getArguments()[0];
1115
+ const objLit = arg?.asKind?.(ts.SyntaxKind.ObjectLiteralExpression);
1116
+ if (!objLit) return { type: Option.none(), isArray: false };
1117
+ const typeProperty = objLit.getProperty("type");
1118
+ if (!typeProperty) return { type: Option.none(), isArray: false };
1119
+ const propAssignment = typeProperty.asKind?.(
1120
+ ts.SyntaxKind.PropertyAssignment
1121
+ );
1122
+ if (!propAssignment) return { type: Option.none(), isArray: false };
1123
+ const initializer = propAssignment.getInitializer();
1124
+ if (!initializer) return { type: Option.none(), isArray: false };
1125
+ const arrayLit = initializer.asKind?.(ts.SyntaxKind.ArrayLiteralExpression);
1126
+ if (arrayLit) {
1127
+ const elements = arrayLit.getElements();
1128
+ if (elements.length > 0) {
1129
+ const firstElement = elements[0];
1130
+ const identifier2 = firstElement.asKind?.(ts.SyntaxKind.Identifier);
1131
+ if (identifier2) {
1132
+ return { type: Option.some(identifier2.getText()), isArray: true };
1133
+ }
1134
+ }
1135
+ return { type: Option.none(), isArray: true };
1136
+ }
1137
+ const identifier = initializer.asKind?.(ts.SyntaxKind.Identifier);
1138
+ if (identifier) {
1139
+ return { type: Option.some(identifier.getText()), isArray: false };
1140
+ }
1141
+ return { type: Option.none(), isArray: false };
1142
+ };
1143
+ const extractApiResponses = (method) => {
1144
+ const responses = [];
1145
+ for (const decorator of method.getDecorators()) {
1146
+ const decoratorName = getDecoratorName$1(decorator);
1147
+ if (decoratorName === "ApiResponse") {
1148
+ const statusCode = extractNumberPropertyFromDecorator(
1149
+ decorator,
1150
+ "status"
1151
+ );
1152
+ if (Option.isNone(statusCode)) continue;
1153
+ const description = extractStringPropertyFromObjectLiteral(
1154
+ decorator,
1155
+ "description"
1156
+ );
1157
+ const { type, isArray } = extractResponseType(decorator);
1158
+ responses.push({
1159
+ statusCode: statusCode.value,
1160
+ description,
1161
+ type,
1162
+ isArray
1163
+ });
1164
+ }
1165
+ }
1166
+ return responses;
1167
+ };
1168
+ const extractApiOperationMetadata = (method) => {
1169
+ for (const decorator of method.getDecorators()) {
1170
+ const decoratorName = getDecoratorName$1(decorator);
1171
+ if (decoratorName === "ApiOperation") {
1172
+ return {
1173
+ summary: extractStringPropertyFromObjectLiteral(decorator, "summary"),
1174
+ description: extractStringPropertyFromObjectLiteral(
1175
+ decorator,
1176
+ "description"
1177
+ ),
1178
+ operationId: extractStringPropertyFromObjectLiteral(
1179
+ decorator,
1180
+ "operationId"
1181
+ ),
1182
+ deprecated: extractBooleanPropertyFromDecorator(
1183
+ decorator,
1184
+ "deprecated"
1185
+ )
1186
+ };
1187
+ }
1188
+ }
1189
+ return {
1190
+ summary: Option.none(),
1191
+ description: Option.none(),
1192
+ operationId: Option.none(),
1193
+ deprecated: Option.none()
1194
+ };
1195
+ };
1196
+ const getMethodInfo = (controller, method, options = {}) => {
1197
+ const httpDecorator = getHttpDecorator(method);
1198
+ if (!httpDecorator) return Option.none();
1199
+ const decoratorName = httpDecorator.getName();
1200
+ const httpMethod = HTTP_METHOD_MAP[decoratorName];
1201
+ if (!httpMethod) return Option.none();
1202
+ const controllerPrefix = getControllerPrefix(controller);
1203
+ const methodPath = getRoutePath(httpDecorator);
1204
+ const fullPath = buildFullPath(controllerPrefix, methodPath);
1205
+ const controllerSecurity = extractControllerSecurity(controller);
1206
+ const methodSecurity = extractMethodSecurity(method);
1207
+ const hasMethodSecurity = hasMethodSecurityDecorators(method);
1208
+ const security = combineSecurityRequirements(
1209
+ controllerSecurity,
1210
+ methodSecurity,
1211
+ hasMethodSecurity
1212
+ );
1213
+ return Option.some({
1214
+ httpMethod,
1215
+ path: fullPath,
1216
+ methodName: method.getName(),
1217
+ controllerName: getControllerName(controller),
1218
+ controllerTags: [...getControllerTags(controller)],
1219
+ returnType: getReturnTypeInfo(method),
1220
+ parameters: extractParameters(method, options),
1221
+ decorators: [...extractDecoratorNames(method)],
1222
+ operation: extractApiOperationMetadata(method),
1223
+ responses: [...extractApiResponses(method)],
1224
+ httpCode: extractHttpCode(method),
1225
+ consumes: [...extractApiConsumes(method)],
1226
+ produces: [...extractApiProduces(method)],
1227
+ security: [...security]
1228
+ });
1229
+ };
1230
+ const getControllerMethodInfos = (controller, options = {}) => controller.getMethods().map((method) => getMethodInfo(controller, method, options)).filter(Option.isSome).map((opt) => opt.value);
1231
+
1232
+ const buildSecurityRequirements = (requirements) => {
1233
+ if (requirements.length === 0) return void 0;
1234
+ const combined = {};
1235
+ for (const req of requirements) {
1236
+ combined[req.schemeName] = [...req.scopes];
1237
+ }
1238
+ return [combined];
1239
+ };
1240
+ const getRequestContentTypes = (methodInfo) => methodInfo.consumes.length > 0 ? methodInfo.consumes : ["application/json"];
1241
+ const getResponseContentTypes = (methodInfo) => methodInfo.produces.length > 0 ? methodInfo.produces : ["application/json"];
1242
+ const buildContentObject = (contentTypes, schema) => Object.fromEntries(contentTypes.map((type) => [type, { schema }]));
1243
+ const parseInlineObjectType = (typeStr) => {
1244
+ const trimmed = typeStr.trim();
1245
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
1246
+ return null;
1247
+ }
1248
+ const content = trimmed.slice(1, -1).trim();
1249
+ if (!content) {
1250
+ return { properties: {}, required: [] };
1251
+ }
1252
+ const properties = {};
1253
+ const required = [];
1254
+ const parts = [];
1255
+ let current = "";
1256
+ let braceDepth = 0;
1257
+ for (const char of content) {
1258
+ if (char === "{") {
1259
+ braceDepth++;
1260
+ current += char;
1261
+ } else if (char === "}") {
1262
+ braceDepth--;
1263
+ current += char;
1264
+ } else if ((char === ";" || char === ",") && braceDepth === 0) {
1265
+ if (current.trim()) {
1266
+ parts.push(current.trim());
1267
+ }
1268
+ current = "";
1269
+ } else {
1270
+ current += char;
1271
+ }
1272
+ }
1273
+ if (current.trim()) {
1274
+ parts.push(current.trim());
1275
+ }
1276
+ for (const part of parts) {
1277
+ const colonIndex = part.indexOf(":");
1278
+ if (colonIndex === -1) continue;
1279
+ let propName = part.slice(0, colonIndex).trim();
1280
+ const propType = part.slice(colonIndex + 1).trim();
1281
+ const isOptional = propName.endsWith("?");
1282
+ if (isOptional) {
1283
+ propName = propName.slice(0, -1).trim();
1284
+ }
1285
+ if (!propName || !propType) continue;
1286
+ properties[propName] = tsTypeToOpenApiSchema(propType);
1287
+ if (!isOptional) {
1288
+ required.push(propName);
1289
+ }
1290
+ }
1291
+ return { properties, required };
1292
+ };
1293
+ const tsTypeToOpenApiSchema = (tsType) => {
1294
+ const trimmed = tsType.trim();
1295
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
1296
+ const parsed = parseInlineObjectType(trimmed);
1297
+ if (parsed) {
1298
+ const schema = {
1299
+ type: "object",
1300
+ properties: parsed.properties
1301
+ };
1302
+ if (parsed.required.length > 0) {
1303
+ return { ...schema, required: parsed.required };
1304
+ }
1305
+ return schema;
1306
+ }
1307
+ }
1308
+ if (trimmed.includes(" | ")) {
1309
+ const types = trimmed.split(" | ").map((t) => t.trim());
1310
+ return {
1311
+ oneOf: types.map((type) => tsTypeToOpenApiSchema(type))
1312
+ };
1313
+ }
1314
+ switch (trimmed.toLowerCase()) {
1315
+ case "string":
1316
+ return { type: "string" };
1317
+ case "number":
1318
+ return { type: "number" };
1319
+ case "boolean":
1320
+ return { type: "boolean" };
1321
+ case "date":
1322
+ return { type: "string", format: "date-time" };
1323
+ case "void":
1324
+ case "undefined":
1325
+ case "never":
1326
+ case "null":
1327
+ return { type: "object" };
1328
+ case "unknown":
1329
+ case "any":
1330
+ return { type: "object" };
1331
+ }
1332
+ if (trimmed === "StreamableFile" || trimmed === "Buffer" || trimmed === "Readable" || trimmed === "ReadableStream") {
1333
+ return { type: "string", format: "binary" };
1334
+ }
1335
+ if (trimmed.endsWith("[]")) {
1336
+ const itemType = trimmed.slice(0, -2);
1337
+ return {
1338
+ type: "array",
1339
+ items: tsTypeToOpenApiSchema(itemType)
1340
+ };
1341
+ }
1342
+ const recordMatch = trimmed.match(/^Record<string,\s*(.+)>$/);
1343
+ if (recordMatch) {
1344
+ return {
1345
+ type: "object"
1346
+ };
1347
+ }
1348
+ if (trimmed.match(/^[A-Z][a-zA-Z0-9]*(<[^>]+>)?$/)) {
1349
+ return { $ref: `#/components/schemas/${trimmed}` };
1350
+ }
1351
+ return { type: "object" };
1352
+ };
1353
+ const getParameterLocation = (location) => {
1354
+ switch (location) {
1355
+ case "path":
1356
+ return "path";
1357
+ case "header":
1358
+ return "header";
1359
+ case "cookie":
1360
+ return "cookie";
1361
+ default:
1362
+ return "query";
1363
+ }
1364
+ };
1365
+ const transformParameter = (param) => {
1366
+ const baseSchema = tsTypeToOpenApiSchema(param.tsType);
1367
+ const schema = param.constraints ? { ...baseSchema, ...param.constraints } : baseSchema;
1368
+ return {
1369
+ name: param.name,
1370
+ in: getParameterLocation(param.location),
1371
+ description: Option.getOrElse(
1372
+ param.description,
1373
+ () => `${param.location} parameter: ${param.name}`
1374
+ ),
1375
+ required: param.location === "path" ? true : param.required,
1376
+ schema
1377
+ };
1378
+ };
1379
+ const buildResponseSchema = (returnType) => {
1380
+ if (Option.isSome(returnType.container) && returnType.container.value === "array") {
1381
+ if (Option.isSome(returnType.inline)) {
1382
+ return {
1383
+ type: "array",
1384
+ items: tsTypeToOpenApiSchema(returnType.inline.value)
1385
+ };
1386
+ }
1387
+ return {
1388
+ type: "array",
1389
+ items: Option.isSome(returnType.type) ? tsTypeToOpenApiSchema(returnType.type.value) : { type: "object" }
1390
+ };
1391
+ }
1392
+ if (Option.isSome(returnType.type)) {
1393
+ return tsTypeToOpenApiSchema(returnType.type.value);
1394
+ }
1395
+ if (Option.isSome(returnType.inline)) {
1396
+ return tsTypeToOpenApiSchema(returnType.inline.value);
1397
+ }
1398
+ return { type: "string" };
1399
+ };
1400
+ const buildResponseSchemaFromMetadata = (response) => {
1401
+ if (Option.isNone(response.type)) return void 0;
1402
+ const typeName = response.type.value;
1403
+ if (response.isArray) {
1404
+ return {
1405
+ type: "array",
1406
+ items: tsTypeToOpenApiSchema(typeName)
1407
+ };
1408
+ }
1409
+ return tsTypeToOpenApiSchema(typeName);
1410
+ };
1411
+ const getDefaultSuccessCode = (methodInfo) => {
1412
+ if (Option.isSome(methodInfo.httpCode)) {
1413
+ return methodInfo.httpCode.value;
1414
+ }
1415
+ return methodInfo.httpMethod === "POST" ? 201 : 200;
1416
+ };
1417
+ const hasMeaningfulReturnType = (returnType) => {
1418
+ if (Option.isSome(returnType.type)) {
1419
+ const typeName = returnType.type.value.toLowerCase();
1420
+ if (["void", "undefined", "never"].includes(typeName)) {
1421
+ return false;
1422
+ }
1423
+ return true;
1424
+ }
1425
+ return Option.isSome(returnType.inline);
1426
+ };
1427
+ const isSuccessWithContent = (statusCode) => statusCode >= 200 && statusCode < 300 && statusCode !== 204;
1428
+ const buildResponseEntry = (response, returnType, hasReturnType, contentTypes) => {
1429
+ const schema = buildResponseSchemaFromMetadata(response) ?? (hasReturnType && isSuccessWithContent(response.statusCode) ? buildResponseSchema(returnType) : void 0);
1430
+ const description = Option.getOrElse(response.description, () => "");
1431
+ if (!schema) return { description };
1432
+ return { description, content: buildContentObject(contentTypes, schema) };
1433
+ };
1434
+ const buildResponses = (methodInfo) => {
1435
+ const contentTypes = getResponseContentTypes(methodInfo);
1436
+ const returnType = methodInfo.returnType;
1437
+ const hasReturnType = hasMeaningfulReturnType(returnType);
1438
+ const statusCode = getDefaultSuccessCode(methodInfo);
1439
+ if (methodInfo.responses.length === 0) {
1440
+ if (!hasReturnType) {
1441
+ return { [statusCode.toString()]: { description: "" } };
1442
+ }
1443
+ return {
1444
+ [statusCode.toString()]: {
1445
+ description: "",
1446
+ content: buildContentObject(
1447
+ contentTypes,
1448
+ buildResponseSchema(returnType)
1449
+ )
1450
+ }
1451
+ };
1452
+ }
1453
+ const result = {};
1454
+ const hasSuccessResponse = methodInfo.responses.some(
1455
+ (r) => r.statusCode >= 200 && r.statusCode < 300
1456
+ );
1457
+ if (!hasSuccessResponse && hasReturnType) {
1458
+ result[statusCode.toString()] = {
1459
+ description: "",
1460
+ content: buildContentObject(
1461
+ contentTypes,
1462
+ buildResponseSchema(returnType)
1463
+ )
1464
+ };
1465
+ }
1466
+ for (const response of methodInfo.responses) {
1467
+ result[response.statusCode.toString()] = buildResponseEntry(
1468
+ response,
1469
+ returnType,
1470
+ hasReturnType,
1471
+ contentTypes
1472
+ );
1473
+ }
1474
+ return result;
1475
+ };
1476
+ const buildOpenApiPath = (path) => path.replace(/:([^/]+)/g, "{$1}") || "/";
1477
+ const transformMethod = (methodInfo) => {
1478
+ const path = buildOpenApiPath(methodInfo.path);
1479
+ const method = methodInfo.httpMethod.toLowerCase();
1480
+ const bodyParams = methodInfo.parameters.filter((p) => p.location === "body");
1481
+ const nonBodyParams = methodInfo.parameters.filter(
1482
+ (p) => p.location !== "body"
1483
+ );
1484
+ const parameters = nonBodyParams.map(transformParameter);
1485
+ const requestContentTypes = getRequestContentTypes(methodInfo);
1486
+ const requestBody = bodyParams.length > 0 ? {
1487
+ description: `Request body parameter: ${bodyParams[0].name}`,
1488
+ required: bodyParams[0].required,
1489
+ content: buildContentObject(
1490
+ requestContentTypes,
1491
+ tsTypeToOpenApiSchema(bodyParams[0].tsType)
1492
+ )
1493
+ } : void 0;
1494
+ const operationId = Option.getOrElse(
1495
+ methodInfo.operation.operationId,
1496
+ () => `${methodInfo.controllerName}_${methodInfo.methodName}`
1497
+ );
1498
+ const summary = Option.getOrUndefined(methodInfo.operation.summary);
1499
+ const description = Option.getOrUndefined(methodInfo.operation.description);
1500
+ const deprecated = Option.getOrUndefined(methodInfo.operation.deprecated);
1501
+ const security = buildSecurityRequirements(methodInfo.security);
1502
+ const operation = {
1503
+ operationId,
1504
+ // Always include parameters array (even if empty) to match NestJS Swagger
1505
+ parameters,
1506
+ ...requestBody ? { requestBody } : {},
1507
+ responses: buildResponses(methodInfo),
1508
+ ...summary !== void 0 ? { summary } : {},
1509
+ ...description !== void 0 ? { description } : {},
1510
+ ...deprecated !== void 0 ? { deprecated } : {},
1511
+ tags: methodInfo.controllerTags.length > 0 ? [...methodInfo.controllerTags] : void 0,
1512
+ ...security !== void 0 ? { security } : {}
1513
+ };
1514
+ return {
1515
+ [path]: {
1516
+ [method]: operation
1517
+ }
1518
+ };
1519
+ };
1520
+ const transformMethods = (methodInfos) => methodInfos.reduce((acc, methodInfo) => {
1521
+ const endpoint = transformMethod(methodInfo);
1522
+ for (const path in endpoint) {
1523
+ if (!acc[path]) acc[path] = {};
1524
+ Object.assign(acc[path], endpoint[path]);
1525
+ }
1526
+ return acc;
1527
+ }, {});
1528
+
1529
+ export { extractClassConstraints as A, getRequiredProperties as B, ConfigNotFoundError as C, mergeValidationConstraints as D, EntryNotFoundError as E, InvalidMethodError as I, ProjectInitError as P, ConfigLoadError as a, ConfigValidationError as b, getAllControllers as c, getControllerPrefix as d, getControllerName as e, getHttpMethods as f, getModules as g, getDecoratorName$1 as h, isHttpMethod as i, getControllerTags as j, getHttpDecorator as k, isHttpDecorator as l, getMethodInfo as m, normalizePath as n, getControllerMethodInfos as o, transformMethods as p, getArrayInitializer as q, resolveClassFromSymbol as r, getStringLiteralValue as s, transformMethod as t, getSymbolFromIdentifier as u, isModuleClass as v, getModuleDecoratorArg as w, resolveClassFromExpression as x, resolveArrayOfClasses as y, getModuleMetadata as z };