@dudousxd/nestjs-codegen 0.15.0 → 0.16.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/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # @dudousxd/nestjs-codegen
2
2
 
3
+ ## 0.16.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Discover routes on mixin (factory-produced) controllers.
8
+
9
+ A `@Controller` class whose heritage clause is a factory call now contributes the
10
+ decorated methods of the class that factory returns:
11
+
12
+ ```ts
13
+ @Controller("base/util/search")
14
+ export class SearchUtilController extends createTableController(Util, {
15
+ dto: UtilDTO,
16
+ }) {}
17
+ ```
18
+
19
+ NestJS already routes inherited methods at runtime via the prototype chain; the
20
+ static discovery pass now follows the same link. Each such route carries a new
21
+ `controllerRef.mixin` binding recording the factory and its call-site class
22
+ arguments — the decorator arguments _inside_ a factory reference its own
23
+ parameters, so the concrete entity is only knowable from the call site.
24
+
25
+ That binding drives two things:
26
+
27
+ - **`filterFields`** are derived from the call-site entity, so
28
+ `@ApplyFilter(GeneratedFilter)` works even though the filter class is declared
29
+ inside the factory and its `@Filterable({ entity })` names a parameter. This is
30
+ also what flips these routes from mutation to query in the emitted client.
31
+ - **Response types** are instantiated against the derived class, so
32
+ `Paginated<D>` resolves to the concrete `D`. This needs the type checker, which
33
+ needs lib files — the discovery Project sets `skipLoadingLibFiles` for
34
+ cold-start speed, so mixin response types resolve through a second Project
35
+ built lazily on the first mixin controller.
36
+
37
+ **Behaviour change:** `joinPaths` now always emits a leading slash.
38
+ `@Controller('items')` + `@Post(':id')` previously produced `items/:id` while
39
+ `@Controller('items')` alone produced `/items` — the prefix+suffix branch was the
40
+ only one that did not add it. The client's `buildUrl()` normalises before
41
+ requesting, so no URL was ever broken by this, but the raw value reaches the
42
+ emitted `ROUTES` map and the OpenAPI export, where a path without a leading slash
43
+ is invalid.
44
+
3
45
  ## 0.15.0
4
46
 
5
47
  ### Minor Changes
package/dist/cli/main.cjs CHANGED
@@ -2442,7 +2442,7 @@ var import_chokidar = __toESM(require("chokidar"), 1);
2442
2442
  // src/discovery/contracts-fast.ts
2443
2443
  var import_node_path15 = require("path");
2444
2444
  var import_fast_glob3 = __toESM(require("fast-glob"), 1);
2445
- var import_ts_morph9 = require("ts-morph");
2445
+ var import_ts_morph10 = require("ts-morph");
2446
2446
 
2447
2447
  // src/discovery/dto-type-resolver.ts
2448
2448
  var import_ts_morph7 = require("ts-morph");
@@ -3421,6 +3421,12 @@ function toFilterFieldType(name, r) {
3421
3421
  }
3422
3422
 
3423
3423
  // src/discovery/filter-for.ts
3424
+ function resolveMixinEntityClass(mixin, project) {
3425
+ const entityArg = mixin?.classArgs[0];
3426
+ if (!entityArg) return void 0;
3427
+ const file = project.getSourceFile(entityArg.filePath) ?? project.addSourceFileAtPathIfExists(entityArg.filePath);
3428
+ return file?.getClass(entityArg.name);
3429
+ }
3424
3430
  function classifyFilterForHint(typeInit) {
3425
3431
  if (import_ts_morph6.Node.isStringLiteral(typeInit)) {
3426
3432
  switch (typeInit.getLiteralValue()) {
@@ -3494,7 +3500,9 @@ function extractFilterForHints(classDecl, project) {
3494
3500
  }
3495
3501
  return hints;
3496
3502
  }
3497
- function extractApplyFilterInfo(method, sourceFile, project) {
3503
+ function extractApplyFilterInfo(method, sourceFile, project, mixin) {
3504
+ const declFile = method.getSourceFile();
3505
+ const mixinEntity = resolveMixinEntityClass(mixin, project);
3498
3506
  for (const param of method.getParameters()) {
3499
3507
  const filterDecorator = param.getDecorators().find((d) => d.getName() === "ApplyFilter");
3500
3508
  if (!filterDecorator) continue;
@@ -3514,12 +3522,12 @@ function extractApplyFilterInfo(method, sourceFile, project) {
3514
3522
  }
3515
3523
  }
3516
3524
  const filterClassName = filterClassArg.getText();
3517
- const resolved = findType(filterClassName, sourceFile, project);
3518
- if (resolved && resolved.kind === "class") {
3519
- const classDecl = resolved.decl;
3525
+ const resolved = findType(filterClassName, declFile, project);
3526
+ const classDecl = resolved?.kind === "class" ? resolved.decl : resolveLocalClassDeclaration(filterClassArg);
3527
+ if (classDecl) {
3520
3528
  let fieldTypes = extractClassPropertyTypes(classDecl, project);
3521
3529
  if (fieldTypes.length === 0) {
3522
- fieldTypes = extractFilterableEntityFields(classDecl, project);
3530
+ fieldTypes = extractFilterableEntityFields(classDecl, project, mixinEntity);
3523
3531
  }
3524
3532
  const filterForHints = extractFilterForHints(classDecl, project);
3525
3533
  if (filterForHints.size > 0) {
@@ -3604,22 +3612,29 @@ function extractClassPropertyTypes(classDecl, project) {
3604
3612
  }
3605
3613
  return fields;
3606
3614
  }
3607
- function extractFilterableEntityFields(filterClass, project) {
3615
+ function resolveLocalClassDeclaration(identifier) {
3616
+ if (!import_ts_morph6.Node.isIdentifier(identifier)) return void 0;
3617
+ return identifier.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => n !== void 0 && import_ts_morph6.Node.isClassDeclaration(n));
3618
+ }
3619
+ function resolveDeclaredEntity(optionsArg, filterClass, project) {
3620
+ if (!import_ts_morph6.Node.isObjectLiteralExpression(optionsArg)) return void 0;
3621
+ const entityProp = optionsArg.getProperty("entity");
3622
+ if (!entityProp || !import_ts_morph6.Node.isPropertyAssignment(entityProp)) return void 0;
3623
+ const entityInit = entityProp.getInitializer();
3624
+ if (!entityInit || !import_ts_morph6.Node.isIdentifier(entityInit)) return void 0;
3625
+ const resolved = findType(entityInit.getText(), filterClass.getSourceFile(), project);
3626
+ if (!resolved || resolved.kind !== "class") return void 0;
3627
+ return resolved.decl;
3628
+ }
3629
+ function extractFilterableEntityFields(filterClass, project, entityOverride) {
3608
3630
  const filterableDecorator = filterClass.getDecorators().find((d) => d.getName() === "Filterable");
3609
3631
  if (!filterableDecorator) return [];
3610
3632
  const args = filterableDecorator.getArguments();
3611
3633
  if (args.length === 0) return [];
3612
3634
  const optionsArg = args[0];
3613
3635
  if (!import_ts_morph6.Node.isObjectLiteralExpression(optionsArg)) return [];
3614
- const entityProp = optionsArg.getProperty("entity");
3615
- if (!entityProp || !import_ts_morph6.Node.isPropertyAssignment(entityProp)) return [];
3616
- const entityInit = entityProp.getInitializer();
3617
- if (!entityInit || !import_ts_morph6.Node.isIdentifier(entityInit)) return [];
3618
- const entityName = entityInit.getText();
3619
- const filterSourceFile = filterClass.getSourceFile();
3620
- const resolvedEntity = findType(entityName, filterSourceFile, project);
3621
- if (!resolvedEntity || resolvedEntity.kind !== "class") return [];
3622
- const entityDecl = resolvedEntity.decl;
3636
+ const entityDecl = entityOverride ?? resolveDeclaredEntity(optionsArg, filterClass, project);
3637
+ if (!entityDecl) return [];
3623
3638
  const fields = collectEntityFields(
3624
3639
  entityDecl,
3625
3640
  entityDecl.getSourceFile(),
@@ -4044,9 +4059,9 @@ function unwrapNamedContainer(node, names) {
4044
4059
  }
4045
4060
  return node;
4046
4061
  }
4047
- function extractDtoContract(method, sourceFile, project) {
4062
+ function extractDtoContract(method, sourceFile, project, mixin) {
4048
4063
  let body = extractBodyType(method, sourceFile, project);
4049
- const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
4064
+ const filterInfo = extractApplyFilterInfo(method, sourceFile, project, mixin);
4050
4065
  const query = extractQueryType(method, sourceFile, project);
4051
4066
  const uploads = extractUploadedFiles(method);
4052
4067
  const multipartBody = uploads.fields ? `{ ${uploads.fields} }` : null;
@@ -4156,12 +4171,94 @@ function resolveParamClass(method, decoratorName, sourceFile, project) {
4156
4171
  return null;
4157
4172
  }
4158
4173
 
4159
- // src/discovery/zod-ast-to-ts.ts
4174
+ // src/discovery/heritage.ts
4160
4175
  var import_ts_morph8 = require("ts-morph");
4176
+ function unwrapExpression(node) {
4177
+ let current = node;
4178
+ while (import_ts_morph8.Node.isAsExpression(current) || import_ts_morph8.Node.isSatisfiesExpression(current) || import_ts_morph8.Node.isParenthesizedExpression(current) || import_ts_morph8.Node.isTypeAssertion(current)) {
4179
+ current = current.getExpression();
4180
+ }
4181
+ return current;
4182
+ }
4183
+ function resolveReturnedClass(factoryDecl) {
4184
+ const body = factoryDecl.getBody();
4185
+ if (!body) return void 0;
4186
+ const returns = body.getDescendants().filter((n) => import_ts_morph8.Node.isReturnStatement(n));
4187
+ for (const ret of returns) {
4188
+ const expr = ret.getExpression();
4189
+ if (!expr) continue;
4190
+ const inner = unwrapExpression(expr);
4191
+ if (import_ts_morph8.Node.isClassExpression(inner)) {
4192
+ return inner;
4193
+ }
4194
+ if (import_ts_morph8.Node.isIdentifier(inner)) {
4195
+ const name = inner.getText();
4196
+ const decl = body.getDescendants().find((n) => import_ts_morph8.Node.isClassDeclaration(n) && n.getName() === name);
4197
+ if (decl) return decl;
4198
+ }
4199
+ }
4200
+ return void 0;
4201
+ }
4202
+ function resolveInheritedMethods(cls) {
4203
+ const expr = cls.getExtends()?.getExpression();
4204
+ if (!expr || !import_ts_morph8.Node.isCallExpression(expr)) return void 0;
4205
+ const callee = expr.getExpression();
4206
+ if (!import_ts_morph8.Node.isIdentifier(callee)) return void 0;
4207
+ const factoryDecl = callee.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => import_ts_morph8.Node.isFunctionDeclaration(n));
4208
+ if (!factoryDecl) return void 0;
4209
+ const returnedClass = resolveReturnedClass(factoryDecl);
4210
+ if (!returnedClass) return void 0;
4211
+ const classArgs = [];
4212
+ for (const arg of expr.getArguments()) {
4213
+ if (!import_ts_morph8.Node.isIdentifier(arg)) continue;
4214
+ const decl = arg.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => import_ts_morph8.Node.isClassDeclaration(n));
4215
+ if (decl) {
4216
+ classArgs.push({
4217
+ name: decl.getName() ?? arg.getText(),
4218
+ filePath: decl.getSourceFile().getFilePath()
4219
+ });
4220
+ }
4221
+ }
4222
+ return {
4223
+ methods: returnedClass.getMethods(),
4224
+ factoryName: callee.getText(),
4225
+ factoryFilePath: factoryDecl.getSourceFile().getFilePath(),
4226
+ classArgs
4227
+ };
4228
+ }
4229
+ var mixinTypeProject;
4230
+ function getMixinTypeProject() {
4231
+ mixinTypeProject ??= new import_ts_morph8.Project({
4232
+ skipAddingFilesFromTsConfig: true,
4233
+ compilerOptions: { strict: true }
4234
+ });
4235
+ return mixinTypeProject;
4236
+ }
4237
+ function clearMixinTypeProject() {
4238
+ mixinTypeProject = void 0;
4239
+ }
4240
+ function resolveInstantiatedReturnType(cls, methodName) {
4241
+ const filePath = cls.getSourceFile().getFilePath();
4242
+ const className = cls.getName();
4243
+ if (!className) return void 0;
4244
+ const project = getMixinTypeProject();
4245
+ const sourceFile = project.getSourceFile(filePath) ?? project.addSourceFileAtPathIfExists(filePath);
4246
+ const typedCls = sourceFile?.getClass(className);
4247
+ if (!typedCls) return void 0;
4248
+ const prop = typedCls.getType().getProperty(methodName);
4249
+ if (!prop) return void 0;
4250
+ const returnType = prop.getTypeAtLocation(typedCls).getCallSignatures()[0]?.getReturnType();
4251
+ if (!returnType) return void 0;
4252
+ const unwrapped = returnType.getSymbol()?.getName() === "Promise" ? returnType.getTypeArguments()[0] ?? returnType : returnType;
4253
+ return unwrapped.getText(typedCls);
4254
+ }
4255
+
4256
+ // src/discovery/zod-ast-to-ts.ts
4257
+ var import_ts_morph9 = require("ts-morph");
4161
4258
  function zodAstToTs(node) {
4162
- if (!import_ts_morph8.Node.isCallExpression(node)) return "unknown";
4259
+ if (!import_ts_morph9.Node.isCallExpression(node)) return "unknown";
4163
4260
  const expr = node.getExpression();
4164
- if (import_ts_morph8.Node.isPropertyAccessExpression(expr)) {
4261
+ if (import_ts_morph9.Node.isPropertyAccessExpression(expr)) {
4165
4262
  const methodName = expr.getName();
4166
4263
  const receiver = expr.getExpression();
4167
4264
  if (methodName === "optional") {
@@ -4185,17 +4282,17 @@ function zodAstToTs(node) {
4185
4282
  case "literal": {
4186
4283
  const lit = args[0];
4187
4284
  if (!lit) return "unknown";
4188
- if (import_ts_morph8.Node.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
4189
- if (import_ts_morph8.Node.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
4190
- if (lit.getKind() === import_ts_morph8.SyntaxKind.TrueKeyword) return "true";
4191
- if (lit.getKind() === import_ts_morph8.SyntaxKind.FalseKeyword) return "false";
4285
+ if (import_ts_morph9.Node.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
4286
+ if (import_ts_morph9.Node.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
4287
+ if (lit.getKind() === import_ts_morph9.SyntaxKind.TrueKeyword) return "true";
4288
+ if (lit.getKind() === import_ts_morph9.SyntaxKind.FalseKeyword) return "false";
4192
4289
  return "unknown";
4193
4290
  }
4194
4291
  case "enum": {
4195
4292
  const arrArg = args[0];
4196
- if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4293
+ if (!arrArg || !import_ts_morph9.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4197
4294
  const members = arrArg.getElements().map(
4198
- (el) => import_ts_morph8.Node.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
4295
+ (el) => import_ts_morph9.Node.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
4199
4296
  );
4200
4297
  return members.join(" | ");
4201
4298
  }
@@ -4206,10 +4303,10 @@ function zodAstToTs(node) {
4206
4303
  }
4207
4304
  case "object": {
4208
4305
  const objArg = args[0];
4209
- if (!objArg || !import_ts_morph8.Node.isObjectLiteralExpression(objArg)) return "unknown";
4306
+ if (!objArg || !import_ts_morph9.Node.isObjectLiteralExpression(objArg)) return "unknown";
4210
4307
  const lines = [];
4211
4308
  for (const prop of objArg.getProperties()) {
4212
- if (!import_ts_morph8.Node.isPropertyAssignment(prop)) continue;
4309
+ if (!import_ts_morph9.Node.isPropertyAssignment(prop)) continue;
4213
4310
  const key = prop.getName();
4214
4311
  const valNode = prop.getInitializer();
4215
4312
  if (!valNode) continue;
@@ -4221,7 +4318,7 @@ function zodAstToTs(node) {
4221
4318
  }
4222
4319
  case "union": {
4223
4320
  const arrArg = args[0];
4224
- if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4321
+ if (!arrArg || !import_ts_morph9.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4225
4322
  return arrArg.getElements().map(zodAstToTs).join(" | ");
4226
4323
  }
4227
4324
  case "record": {
@@ -4231,7 +4328,7 @@ function zodAstToTs(node) {
4231
4328
  }
4232
4329
  case "tuple": {
4233
4330
  const arrArg = args[0];
4234
- if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4331
+ if (!arrArg || !import_ts_morph9.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4235
4332
  return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
4236
4333
  }
4237
4334
  default:
@@ -4241,18 +4338,18 @@ function zodAstToTs(node) {
4241
4338
  return "unknown";
4242
4339
  }
4243
4340
  function isOptionalChain(node) {
4244
- if (!import_ts_morph8.Node.isCallExpression(node)) return false;
4341
+ if (!import_ts_morph9.Node.isCallExpression(node)) return false;
4245
4342
  const expr = node.getExpression();
4246
- return import_ts_morph8.Node.isPropertyAccessExpression(expr) && expr.getName() === "optional";
4343
+ return import_ts_morph9.Node.isPropertyAccessExpression(expr) && expr.getName() === "optional";
4247
4344
  }
4248
4345
  function parseDefineContractCall(callExpr) {
4249
- if (!import_ts_morph8.Node.isCallExpression(callExpr)) return null;
4346
+ if (!import_ts_morph9.Node.isCallExpression(callExpr)) return null;
4250
4347
  const callee = callExpr.getExpression();
4251
- const calleeName = import_ts_morph8.Node.isIdentifier(callee) ? callee.getText() : import_ts_morph8.Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
4348
+ const calleeName = import_ts_morph9.Node.isIdentifier(callee) ? callee.getText() : import_ts_morph9.Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
4252
4349
  if (calleeName !== "defineContract") return null;
4253
4350
  const args = callExpr.getArguments();
4254
4351
  const optsArg = args[0];
4255
- if (!optsArg || !import_ts_morph8.Node.isObjectLiteralExpression(optsArg)) return null;
4352
+ if (!optsArg || !import_ts_morph9.Node.isObjectLiteralExpression(optsArg)) return null;
4256
4353
  let query = null;
4257
4354
  let body = null;
4258
4355
  let response = "unknown";
@@ -4260,7 +4357,7 @@ function parseDefineContractCall(callExpr) {
4260
4357
  let bodyZodText = null;
4261
4358
  let queryZodText = null;
4262
4359
  for (const prop of optsArg.getProperties()) {
4263
- if (!import_ts_morph8.Node.isPropertyAssignment(prop)) continue;
4360
+ if (!import_ts_morph9.Node.isPropertyAssignment(prop)) continue;
4264
4361
  const propName = prop.getName();
4265
4362
  const val = prop.getInitializer();
4266
4363
  if (!val) continue;
@@ -4289,6 +4386,7 @@ async function discoverContractsFast(opts) {
4289
4386
  project.addSourceFileAtPath(f);
4290
4387
  }
4291
4388
  bindDiscoveryContext(project, cwd, tsconfigPath);
4389
+ clearMixinTypeProject();
4292
4390
  return extractAllRoutes(project);
4293
4391
  }
4294
4392
  function resolveTsconfigPath(cwd, tsconfig) {
@@ -4296,14 +4394,14 @@ function resolveTsconfigPath(cwd, tsconfig) {
4296
4394
  }
4297
4395
  function createDiscoveryProject(tsconfigPath) {
4298
4396
  try {
4299
- return new import_ts_morph9.Project({
4397
+ return new import_ts_morph10.Project({
4300
4398
  tsConfigFilePath: tsconfigPath,
4301
4399
  skipAddingFilesFromTsConfig: true,
4302
4400
  skipLoadingLibFiles: true,
4303
4401
  skipFileDependencyResolution: true
4304
4402
  });
4305
4403
  } catch {
4306
- return new import_ts_morph9.Project({
4404
+ return new import_ts_morph10.Project({
4307
4405
  skipAddingFilesFromTsConfig: true,
4308
4406
  skipLoadingLibFiles: true,
4309
4407
  skipFileDependencyResolution: true,
@@ -4410,15 +4508,16 @@ var PersistentDiscovery = class _PersistentDiscovery {
4410
4508
  runExtraction() {
4411
4509
  clearTypeResolutionCaches(this.project);
4412
4510
  clearEnumCache(this.project);
4511
+ clearMixinTypeProject();
4413
4512
  return extractRoutesFrom(this.project, this.controllerPaths);
4414
4513
  }
4415
4514
  };
4416
4515
  function decoratorStringArg(decoratorExpr) {
4417
4516
  if (!decoratorExpr) return void 0;
4418
- if (import_ts_morph9.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
4419
- if (import_ts_morph9.Node.isArrayLiteralExpression(decoratorExpr)) {
4517
+ if (import_ts_morph10.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
4518
+ if (import_ts_morph10.Node.isArrayLiteralExpression(decoratorExpr)) {
4420
4519
  const first = decoratorExpr.getElements()[0];
4421
- if (first && import_ts_morph9.Node.isStringLiteral(first)) return first.getLiteralValue();
4520
+ if (first && import_ts_morph10.Node.isStringLiteral(first)) return first.getLiteralValue();
4422
4521
  }
4423
4522
  return void 0;
4424
4523
  }
@@ -4440,7 +4539,8 @@ function joinPaths(prefix, suffix) {
4440
4539
  if (!prefix && !suffix) return "/";
4441
4540
  if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
4442
4541
  if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
4443
- const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
4542
+ const withLeadingSlash = prefix.startsWith("/") ? prefix : `/${prefix}`;
4543
+ const p = withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
4444
4544
  const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
4445
4545
  const combined = p + s;
4446
4546
  return combined === "" ? "/" : combined;
@@ -4494,7 +4594,8 @@ function buildRoute(args) {
4494
4594
  methodAs,
4495
4595
  sourceFile,
4496
4596
  seenNames,
4497
- contractSource
4597
+ contractSource,
4598
+ mixin
4498
4599
  } = args;
4499
4600
  const routeName = resolveRouteName(className, methodName, classAs, methodAs);
4500
4601
  const qualifiedRef = `${className}.${methodName}`;
@@ -4510,7 +4611,12 @@ function buildRoute(args) {
4510
4611
  path: combinedPath,
4511
4612
  name: routeName,
4512
4613
  params: extractParams(combinedPath),
4513
- controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
4614
+ controllerRef: {
4615
+ className,
4616
+ methodName,
4617
+ filePath: sourceFile.getFilePath(),
4618
+ ...mixin ? { mixin } : {}
4619
+ },
4514
4620
  contract: { contractSource }
4515
4621
  };
4516
4622
  }
@@ -4524,16 +4630,17 @@ function extractContractRoute(args) {
4524
4630
  className,
4525
4631
  sourceFile,
4526
4632
  project,
4527
- seenNames
4633
+ seenNames,
4634
+ mixin
4528
4635
  } = args;
4529
4636
  const firstDecoratorArg = applyContractDecorator.getArguments()[0];
4530
4637
  if (!firstDecoratorArg) return null;
4531
4638
  let contractDef = null;
4532
4639
  let bodyZodRef = null;
4533
4640
  let queryZodRef = null;
4534
- if (import_ts_morph9.Node.isCallExpression(firstDecoratorArg)) {
4641
+ if (import_ts_morph10.Node.isCallExpression(firstDecoratorArg)) {
4535
4642
  contractDef = parseDefineContractCall(firstDecoratorArg);
4536
- } else if (import_ts_morph9.Node.isIdentifier(firstDecoratorArg)) {
4643
+ } else if (import_ts_morph10.Node.isIdentifier(firstDecoratorArg)) {
4537
4644
  const identName = firstDecoratorArg.getText();
4538
4645
  const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
4539
4646
  if (!resolvedVar) {
@@ -4576,6 +4683,7 @@ function extractContractRoute(args) {
4576
4683
  methodAs,
4577
4684
  sourceFile,
4578
4685
  seenNames,
4686
+ mixin,
4579
4687
  contractSource: {
4580
4688
  query: contractDef.query,
4581
4689
  body: contractDef.body,
@@ -4592,13 +4700,14 @@ function extractContractRoute(args) {
4592
4700
  });
4593
4701
  }
4594
4702
  function extractDtoRoute(args) {
4595
- const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
4703
+ const { cls, method, verb, prefix, className, sourceFile, project, seenNames, mixin } = args;
4596
4704
  if (!verb) return null;
4597
4705
  const combined = joinPaths(prefix, verb.handlerPath);
4598
4706
  const methodName = method.getName();
4599
4707
  const classAs = readAsDecorator(cls, `class ${className}`);
4600
4708
  const methodAs = readAsDecorator(method, `${className}.${methodName}`);
4601
- const dtoContract = extractDtoContract(method, sourceFile, project);
4709
+ const dtoContract = extractDtoContract(method, sourceFile, project, mixin);
4710
+ const mixinResponse = mixin ? resolveInstantiatedReturnType(cls, methodName) : void 0;
4602
4711
  return buildRoute({
4603
4712
  className,
4604
4713
  methodName,
@@ -4608,10 +4717,11 @@ function extractDtoRoute(args) {
4608
4717
  methodAs,
4609
4718
  sourceFile,
4610
4719
  seenNames,
4720
+ mixin,
4611
4721
  contractSource: {
4612
4722
  query: dtoContract?.query ?? null,
4613
4723
  body: dtoContract?.body ?? null,
4614
- response: dtoContract?.response ?? "unknown",
4724
+ response: mixinResponse ?? dtoContract?.response ?? "unknown",
4615
4725
  error: dtoContract?.error ?? null,
4616
4726
  queryRef: dtoContract?.queryRef ?? null,
4617
4727
  bodyRef: dtoContract?.bodyRef ?? null,
@@ -4640,7 +4750,17 @@ function extractFromSourceFile(sourceFile, project) {
4640
4750
  const firstArg2 = controllerDecorator.getArguments()[0];
4641
4751
  const prefix = decoratorStringArg(firstArg2) ?? "";
4642
4752
  const className = cls.getName() ?? "Unknown";
4643
- for (const method of cls.getMethods()) {
4753
+ const inherited = resolveInheritedMethods(cls);
4754
+ const mixinBinding = inherited ? {
4755
+ factoryName: inherited.factoryName,
4756
+ factoryFilePath: inherited.factoryFilePath,
4757
+ classArgs: inherited.classArgs
4758
+ } : void 0;
4759
+ const methods = [
4760
+ ...cls.getMethods().map((method) => ({ method })),
4761
+ ...(inherited?.methods ?? []).map((method) => ({ method, mixin: mixinBinding }))
4762
+ ];
4763
+ for (const { method, mixin } of methods) {
4644
4764
  const verb = resolveVerb(method);
4645
4765
  const applyContractDecorator = method.getDecorator("ApplyContract");
4646
4766
  const route = applyContractDecorator ? extractContractRoute({
@@ -4652,7 +4772,8 @@ function extractFromSourceFile(sourceFile, project) {
4652
4772
  className,
4653
4773
  sourceFile,
4654
4774
  project,
4655
- seenNames
4775
+ seenNames,
4776
+ mixin
4656
4777
  }) : extractDtoRoute({
4657
4778
  cls,
4658
4779
  method,
@@ -4661,7 +4782,8 @@ function extractFromSourceFile(sourceFile, project) {
4661
4782
  className,
4662
4783
  sourceFile,
4663
4784
  project,
4664
- seenNames
4785
+ seenNames,
4786
+ mixin
4665
4787
  });
4666
4788
  if (route) routes.push(route);
4667
4789
  }
@@ -4862,7 +4984,7 @@ async function watch(config, onChange, options = {}) {
4862
4984
  }
4863
4985
 
4864
4986
  // src/index.ts
4865
- var VERSION = "0.15.0";
4987
+ var VERSION = "0.16.0";
4866
4988
 
4867
4989
  // src/cli/codegen.ts
4868
4990
  async function runCodegen(opts = {}) {