@dudousxd/nestjs-codegen 0.14.2 → 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/dist/cli/main.js CHANGED
@@ -741,6 +741,9 @@ function buildErrorType(c) {
741
741
  }
742
742
  return c.contractSource.error ?? "unknown";
743
743
  }
744
+ function filterFieldLiterals(fields) {
745
+ return fields?.length ? fields.map((f) => JSON.stringify(f)) : [];
746
+ }
744
747
  function emitRouterTypeBlock(tree, indent, outDir, serialization) {
745
748
  const pad = " ".repeat(indent);
746
749
  const lines = [];
@@ -767,7 +770,8 @@ function emitRouterTypeBlock(tree, indent, outDir, serialization) {
767
770
  const params = buildParamsType(c.params);
768
771
  const safeMethod = JSON.stringify(method);
769
772
  const safeUrl = JSON.stringify(c.path);
770
- const filterFields = c.contractSource.filterFields?.length ? c.contractSource.filterFields.map((f) => JSON.stringify(f)).join(" | ") : "never";
773
+ const filterLiterals = filterFieldLiterals(c.contractSource.filterFields);
774
+ const filterFields = filterLiterals.length ? filterLiterals.join(" | ") : "never";
771
775
  const stream = c.contractSource.stream ? "true" : "false";
772
776
  const binary = c.contractSource.binaryResponse ? "true" : "false";
773
777
  lines.push(
@@ -807,6 +811,7 @@ function buildRequestModel(c) {
807
811
  const TA = buildRouterTypeAccess(c.name);
808
812
  const withParams = hasPathParams(c.params);
809
813
  const { isGet, isQuery, hasBody, hasQuery } = requestShape(c.route);
814
+ const filterLiterals = filterFieldLiterals(c.contractSource.filterFields);
810
815
  const fields = [];
811
816
  if (withParams) fields.push(`params: ${TA}['params']`);
812
817
  if (hasQuery) fields.push(`query?: ${TA}['query']`);
@@ -836,7 +841,12 @@ function buildRequestModel(c) {
836
841
  // (`[name]` rather than `[name, undefined]`) so the bare `.queryKey()` is a
837
842
  // clean prefix that partial-matches every parametrized variant — making it
838
843
  // directly usable for `invalidateQueries`.
839
- queryKeyExpr: `(input === undefined ? [${flat}] as const : [${flat}, input] as const)`
844
+ queryKeyExpr: `(input === undefined ? [${flat}] as const : [${flat}, input] as const)`,
845
+ // Runtime counterpart to the type-level `filterFields` union: the same
846
+ // discovered field list, emitted as a literal `[...] as const` so apps can
847
+ // validate a dynamic/user-supplied field string with `isFilterField(...)`
848
+ // instead of casting. Omitted for routes with no filter.
849
+ ...filterLiterals.length ? { filterFieldsExpr: `[${filterLiterals.join(", ")}] as const` } : {}
840
850
  };
841
851
  }
842
852
  function renderFetcherRequest(req, binaryResponse) {
@@ -871,9 +881,24 @@ function emitReqHelper() {
871
881
  ""
872
882
  ];
873
883
  }
884
+ function emitFilterFieldGuard() {
885
+ return [
886
+ "/** Runtime guard: narrows `value` to one of the leaf's `filterFields` (a `readonly K[] as const`), so a dynamic field string can be passed to `.where()` without a cast. */",
887
+ "export function isFilterField<const K extends string>(",
888
+ " fields: readonly K[],",
889
+ " value: string,",
890
+ "): value is K {",
891
+ " return (fields as readonly string[]).includes(value);",
892
+ "}",
893
+ ""
894
+ ];
895
+ }
874
896
  function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
875
897
  const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
876
898
  lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
899
+ if (req.filterFieldsExpr) {
900
+ lines.push(`${pad} filterFields: ${req.filterFieldsExpr},`);
901
+ }
877
902
  if (streamExpr) {
878
903
  lines.push(`${pad} stream: () => ${streamExpr},`);
879
904
  }
@@ -1145,6 +1170,9 @@ function buildApiFile(routes, outDir, opts = {}) {
1145
1170
  lines.push("};");
1146
1171
  lines.push("");
1147
1172
  lines.push(...emitReqHelper());
1173
+ if (contracted.some((r) => r.contract?.contractSource.filterFields?.length)) {
1174
+ lines.push(...emitFilterFieldGuard());
1175
+ }
1148
1176
  lines.push("export function createApi(fetcher: Fetcher) {");
1149
1177
  lines.push(" return {");
1150
1178
  lines.push(
@@ -2381,8 +2409,8 @@ import chokidar from "chokidar";
2381
2409
  import { join as join14, resolve as resolve3 } from "path";
2382
2410
  import fg3 from "fast-glob";
2383
2411
  import {
2384
- Node as Node8,
2385
- Project as Project3
2412
+ Node as Node9,
2413
+ Project as Project4
2386
2414
  } from "ts-morph";
2387
2415
 
2388
2416
  // src/discovery/dto-type-resolver.ts
@@ -3374,6 +3402,12 @@ function toFilterFieldType(name, r) {
3374
3402
  }
3375
3403
 
3376
3404
  // src/discovery/filter-for.ts
3405
+ function resolveMixinEntityClass(mixin, project) {
3406
+ const entityArg = mixin?.classArgs[0];
3407
+ if (!entityArg) return void 0;
3408
+ const file = project.getSourceFile(entityArg.filePath) ?? project.addSourceFileAtPathIfExists(entityArg.filePath);
3409
+ return file?.getClass(entityArg.name);
3410
+ }
3377
3411
  function classifyFilterForHint(typeInit) {
3378
3412
  if (Node5.isStringLiteral(typeInit)) {
3379
3413
  switch (typeInit.getLiteralValue()) {
@@ -3447,7 +3481,9 @@ function extractFilterForHints(classDecl, project) {
3447
3481
  }
3448
3482
  return hints;
3449
3483
  }
3450
- function extractApplyFilterInfo(method, sourceFile, project) {
3484
+ function extractApplyFilterInfo(method, sourceFile, project, mixin) {
3485
+ const declFile = method.getSourceFile();
3486
+ const mixinEntity = resolveMixinEntityClass(mixin, project);
3451
3487
  for (const param of method.getParameters()) {
3452
3488
  const filterDecorator = param.getDecorators().find((d) => d.getName() === "ApplyFilter");
3453
3489
  if (!filterDecorator) continue;
@@ -3467,12 +3503,12 @@ function extractApplyFilterInfo(method, sourceFile, project) {
3467
3503
  }
3468
3504
  }
3469
3505
  const filterClassName = filterClassArg.getText();
3470
- const resolved = findType(filterClassName, sourceFile, project);
3471
- if (resolved && resolved.kind === "class") {
3472
- const classDecl = resolved.decl;
3506
+ const resolved = findType(filterClassName, declFile, project);
3507
+ const classDecl = resolved?.kind === "class" ? resolved.decl : resolveLocalClassDeclaration(filterClassArg);
3508
+ if (classDecl) {
3473
3509
  let fieldTypes = extractClassPropertyTypes(classDecl, project);
3474
3510
  if (fieldTypes.length === 0) {
3475
- fieldTypes = extractFilterableEntityFields(classDecl, project);
3511
+ fieldTypes = extractFilterableEntityFields(classDecl, project, mixinEntity);
3476
3512
  }
3477
3513
  const filterForHints = extractFilterForHints(classDecl, project);
3478
3514
  if (filterForHints.size > 0) {
@@ -3557,22 +3593,29 @@ function extractClassPropertyTypes(classDecl, project) {
3557
3593
  }
3558
3594
  return fields;
3559
3595
  }
3560
- function extractFilterableEntityFields(filterClass, project) {
3596
+ function resolveLocalClassDeclaration(identifier) {
3597
+ if (!Node5.isIdentifier(identifier)) return void 0;
3598
+ return identifier.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => n !== void 0 && Node5.isClassDeclaration(n));
3599
+ }
3600
+ function resolveDeclaredEntity(optionsArg, filterClass, project) {
3601
+ if (!Node5.isObjectLiteralExpression(optionsArg)) return void 0;
3602
+ const entityProp = optionsArg.getProperty("entity");
3603
+ if (!entityProp || !Node5.isPropertyAssignment(entityProp)) return void 0;
3604
+ const entityInit = entityProp.getInitializer();
3605
+ if (!entityInit || !Node5.isIdentifier(entityInit)) return void 0;
3606
+ const resolved = findType(entityInit.getText(), filterClass.getSourceFile(), project);
3607
+ if (!resolved || resolved.kind !== "class") return void 0;
3608
+ return resolved.decl;
3609
+ }
3610
+ function extractFilterableEntityFields(filterClass, project, entityOverride) {
3561
3611
  const filterableDecorator = filterClass.getDecorators().find((d) => d.getName() === "Filterable");
3562
3612
  if (!filterableDecorator) return [];
3563
3613
  const args = filterableDecorator.getArguments();
3564
3614
  if (args.length === 0) return [];
3565
3615
  const optionsArg = args[0];
3566
3616
  if (!Node5.isObjectLiteralExpression(optionsArg)) return [];
3567
- const entityProp = optionsArg.getProperty("entity");
3568
- if (!entityProp || !Node5.isPropertyAssignment(entityProp)) return [];
3569
- const entityInit = entityProp.getInitializer();
3570
- if (!entityInit || !Node5.isIdentifier(entityInit)) return [];
3571
- const entityName = entityInit.getText();
3572
- const filterSourceFile = filterClass.getSourceFile();
3573
- const resolvedEntity = findType(entityName, filterSourceFile, project);
3574
- if (!resolvedEntity || resolvedEntity.kind !== "class") return [];
3575
- const entityDecl = resolvedEntity.decl;
3617
+ const entityDecl = entityOverride ?? resolveDeclaredEntity(optionsArg, filterClass, project);
3618
+ if (!entityDecl) return [];
3576
3619
  const fields = collectEntityFields(
3577
3620
  entityDecl,
3578
3621
  entityDecl.getSourceFile(),
@@ -3997,9 +4040,9 @@ function unwrapNamedContainer(node, names) {
3997
4040
  }
3998
4041
  return node;
3999
4042
  }
4000
- function extractDtoContract(method, sourceFile, project) {
4043
+ function extractDtoContract(method, sourceFile, project, mixin) {
4001
4044
  let body = extractBodyType(method, sourceFile, project);
4002
- const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
4045
+ const filterInfo = extractApplyFilterInfo(method, sourceFile, project, mixin);
4003
4046
  const query = extractQueryType(method, sourceFile, project);
4004
4047
  const uploads = extractUploadedFiles(method);
4005
4048
  const multipartBody = uploads.fields ? `{ ${uploads.fields} }` : null;
@@ -4109,12 +4152,94 @@ function resolveParamClass(method, decoratorName, sourceFile, project) {
4109
4152
  return null;
4110
4153
  }
4111
4154
 
4155
+ // src/discovery/heritage.ts
4156
+ import { Node as Node7, Project as Project3 } from "ts-morph";
4157
+ function unwrapExpression(node) {
4158
+ let current = node;
4159
+ while (Node7.isAsExpression(current) || Node7.isSatisfiesExpression(current) || Node7.isParenthesizedExpression(current) || Node7.isTypeAssertion(current)) {
4160
+ current = current.getExpression();
4161
+ }
4162
+ return current;
4163
+ }
4164
+ function resolveReturnedClass(factoryDecl) {
4165
+ const body = factoryDecl.getBody();
4166
+ if (!body) return void 0;
4167
+ const returns = body.getDescendants().filter((n) => Node7.isReturnStatement(n));
4168
+ for (const ret of returns) {
4169
+ const expr = ret.getExpression();
4170
+ if (!expr) continue;
4171
+ const inner = unwrapExpression(expr);
4172
+ if (Node7.isClassExpression(inner)) {
4173
+ return inner;
4174
+ }
4175
+ if (Node7.isIdentifier(inner)) {
4176
+ const name = inner.getText();
4177
+ const decl = body.getDescendants().find((n) => Node7.isClassDeclaration(n) && n.getName() === name);
4178
+ if (decl) return decl;
4179
+ }
4180
+ }
4181
+ return void 0;
4182
+ }
4183
+ function resolveInheritedMethods(cls) {
4184
+ const expr = cls.getExtends()?.getExpression();
4185
+ if (!expr || !Node7.isCallExpression(expr)) return void 0;
4186
+ const callee = expr.getExpression();
4187
+ if (!Node7.isIdentifier(callee)) return void 0;
4188
+ const factoryDecl = callee.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => Node7.isFunctionDeclaration(n));
4189
+ if (!factoryDecl) return void 0;
4190
+ const returnedClass = resolveReturnedClass(factoryDecl);
4191
+ if (!returnedClass) return void 0;
4192
+ const classArgs = [];
4193
+ for (const arg of expr.getArguments()) {
4194
+ if (!Node7.isIdentifier(arg)) continue;
4195
+ const decl = arg.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => Node7.isClassDeclaration(n));
4196
+ if (decl) {
4197
+ classArgs.push({
4198
+ name: decl.getName() ?? arg.getText(),
4199
+ filePath: decl.getSourceFile().getFilePath()
4200
+ });
4201
+ }
4202
+ }
4203
+ return {
4204
+ methods: returnedClass.getMethods(),
4205
+ factoryName: callee.getText(),
4206
+ factoryFilePath: factoryDecl.getSourceFile().getFilePath(),
4207
+ classArgs
4208
+ };
4209
+ }
4210
+ var mixinTypeProject;
4211
+ function getMixinTypeProject() {
4212
+ mixinTypeProject ??= new Project3({
4213
+ skipAddingFilesFromTsConfig: true,
4214
+ compilerOptions: { strict: true }
4215
+ });
4216
+ return mixinTypeProject;
4217
+ }
4218
+ function clearMixinTypeProject() {
4219
+ mixinTypeProject = void 0;
4220
+ }
4221
+ function resolveInstantiatedReturnType(cls, methodName) {
4222
+ const filePath = cls.getSourceFile().getFilePath();
4223
+ const className = cls.getName();
4224
+ if (!className) return void 0;
4225
+ const project = getMixinTypeProject();
4226
+ const sourceFile = project.getSourceFile(filePath) ?? project.addSourceFileAtPathIfExists(filePath);
4227
+ const typedCls = sourceFile?.getClass(className);
4228
+ if (!typedCls) return void 0;
4229
+ const prop = typedCls.getType().getProperty(methodName);
4230
+ if (!prop) return void 0;
4231
+ const returnType = prop.getTypeAtLocation(typedCls).getCallSignatures()[0]?.getReturnType();
4232
+ if (!returnType) return void 0;
4233
+ const unwrapped = returnType.getSymbol()?.getName() === "Promise" ? returnType.getTypeArguments()[0] ?? returnType : returnType;
4234
+ return unwrapped.getText(typedCls);
4235
+ }
4236
+
4112
4237
  // src/discovery/zod-ast-to-ts.ts
4113
- import { Node as Node7, SyntaxKind as SyntaxKind4 } from "ts-morph";
4238
+ import { Node as Node8, SyntaxKind as SyntaxKind4 } from "ts-morph";
4114
4239
  function zodAstToTs(node) {
4115
- if (!Node7.isCallExpression(node)) return "unknown";
4240
+ if (!Node8.isCallExpression(node)) return "unknown";
4116
4241
  const expr = node.getExpression();
4117
- if (Node7.isPropertyAccessExpression(expr)) {
4242
+ if (Node8.isPropertyAccessExpression(expr)) {
4118
4243
  const methodName = expr.getName();
4119
4244
  const receiver = expr.getExpression();
4120
4245
  if (methodName === "optional") {
@@ -4138,17 +4263,17 @@ function zodAstToTs(node) {
4138
4263
  case "literal": {
4139
4264
  const lit = args[0];
4140
4265
  if (!lit) return "unknown";
4141
- if (Node7.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
4142
- if (Node7.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
4266
+ if (Node8.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
4267
+ if (Node8.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
4143
4268
  if (lit.getKind() === SyntaxKind4.TrueKeyword) return "true";
4144
4269
  if (lit.getKind() === SyntaxKind4.FalseKeyword) return "false";
4145
4270
  return "unknown";
4146
4271
  }
4147
4272
  case "enum": {
4148
4273
  const arrArg = args[0];
4149
- if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
4274
+ if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
4150
4275
  const members = arrArg.getElements().map(
4151
- (el) => Node7.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
4276
+ (el) => Node8.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
4152
4277
  );
4153
4278
  return members.join(" | ");
4154
4279
  }
@@ -4159,10 +4284,10 @@ function zodAstToTs(node) {
4159
4284
  }
4160
4285
  case "object": {
4161
4286
  const objArg = args[0];
4162
- if (!objArg || !Node7.isObjectLiteralExpression(objArg)) return "unknown";
4287
+ if (!objArg || !Node8.isObjectLiteralExpression(objArg)) return "unknown";
4163
4288
  const lines = [];
4164
4289
  for (const prop of objArg.getProperties()) {
4165
- if (!Node7.isPropertyAssignment(prop)) continue;
4290
+ if (!Node8.isPropertyAssignment(prop)) continue;
4166
4291
  const key = prop.getName();
4167
4292
  const valNode = prop.getInitializer();
4168
4293
  if (!valNode) continue;
@@ -4174,7 +4299,7 @@ function zodAstToTs(node) {
4174
4299
  }
4175
4300
  case "union": {
4176
4301
  const arrArg = args[0];
4177
- if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
4302
+ if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
4178
4303
  return arrArg.getElements().map(zodAstToTs).join(" | ");
4179
4304
  }
4180
4305
  case "record": {
@@ -4184,7 +4309,7 @@ function zodAstToTs(node) {
4184
4309
  }
4185
4310
  case "tuple": {
4186
4311
  const arrArg = args[0];
4187
- if (!arrArg || !Node7.isArrayLiteralExpression(arrArg)) return "unknown";
4312
+ if (!arrArg || !Node8.isArrayLiteralExpression(arrArg)) return "unknown";
4188
4313
  return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
4189
4314
  }
4190
4315
  default:
@@ -4194,18 +4319,18 @@ function zodAstToTs(node) {
4194
4319
  return "unknown";
4195
4320
  }
4196
4321
  function isOptionalChain(node) {
4197
- if (!Node7.isCallExpression(node)) return false;
4322
+ if (!Node8.isCallExpression(node)) return false;
4198
4323
  const expr = node.getExpression();
4199
- return Node7.isPropertyAccessExpression(expr) && expr.getName() === "optional";
4324
+ return Node8.isPropertyAccessExpression(expr) && expr.getName() === "optional";
4200
4325
  }
4201
4326
  function parseDefineContractCall(callExpr) {
4202
- if (!Node7.isCallExpression(callExpr)) return null;
4327
+ if (!Node8.isCallExpression(callExpr)) return null;
4203
4328
  const callee = callExpr.getExpression();
4204
- const calleeName = Node7.isIdentifier(callee) ? callee.getText() : Node7.isPropertyAccessExpression(callee) ? callee.getName() : "";
4329
+ const calleeName = Node8.isIdentifier(callee) ? callee.getText() : Node8.isPropertyAccessExpression(callee) ? callee.getName() : "";
4205
4330
  if (calleeName !== "defineContract") return null;
4206
4331
  const args = callExpr.getArguments();
4207
4332
  const optsArg = args[0];
4208
- if (!optsArg || !Node7.isObjectLiteralExpression(optsArg)) return null;
4333
+ if (!optsArg || !Node8.isObjectLiteralExpression(optsArg)) return null;
4209
4334
  let query = null;
4210
4335
  let body = null;
4211
4336
  let response = "unknown";
@@ -4213,7 +4338,7 @@ function parseDefineContractCall(callExpr) {
4213
4338
  let bodyZodText = null;
4214
4339
  let queryZodText = null;
4215
4340
  for (const prop of optsArg.getProperties()) {
4216
- if (!Node7.isPropertyAssignment(prop)) continue;
4341
+ if (!Node8.isPropertyAssignment(prop)) continue;
4217
4342
  const propName = prop.getName();
4218
4343
  const val = prop.getInitializer();
4219
4344
  if (!val) continue;
@@ -4242,6 +4367,7 @@ async function discoverContractsFast(opts) {
4242
4367
  project.addSourceFileAtPath(f);
4243
4368
  }
4244
4369
  bindDiscoveryContext(project, cwd, tsconfigPath);
4370
+ clearMixinTypeProject();
4245
4371
  return extractAllRoutes(project);
4246
4372
  }
4247
4373
  function resolveTsconfigPath(cwd, tsconfig) {
@@ -4249,14 +4375,14 @@ function resolveTsconfigPath(cwd, tsconfig) {
4249
4375
  }
4250
4376
  function createDiscoveryProject(tsconfigPath) {
4251
4377
  try {
4252
- return new Project3({
4378
+ return new Project4({
4253
4379
  tsConfigFilePath: tsconfigPath,
4254
4380
  skipAddingFilesFromTsConfig: true,
4255
4381
  skipLoadingLibFiles: true,
4256
4382
  skipFileDependencyResolution: true
4257
4383
  });
4258
4384
  } catch {
4259
- return new Project3({
4385
+ return new Project4({
4260
4386
  skipAddingFilesFromTsConfig: true,
4261
4387
  skipLoadingLibFiles: true,
4262
4388
  skipFileDependencyResolution: true,
@@ -4363,15 +4489,16 @@ var PersistentDiscovery = class _PersistentDiscovery {
4363
4489
  runExtraction() {
4364
4490
  clearTypeResolutionCaches(this.project);
4365
4491
  clearEnumCache(this.project);
4492
+ clearMixinTypeProject();
4366
4493
  return extractRoutesFrom(this.project, this.controllerPaths);
4367
4494
  }
4368
4495
  };
4369
4496
  function decoratorStringArg(decoratorExpr) {
4370
4497
  if (!decoratorExpr) return void 0;
4371
- if (Node8.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
4372
- if (Node8.isArrayLiteralExpression(decoratorExpr)) {
4498
+ if (Node9.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
4499
+ if (Node9.isArrayLiteralExpression(decoratorExpr)) {
4373
4500
  const first = decoratorExpr.getElements()[0];
4374
- if (first && Node8.isStringLiteral(first)) return first.getLiteralValue();
4501
+ if (first && Node9.isStringLiteral(first)) return first.getLiteralValue();
4375
4502
  }
4376
4503
  return void 0;
4377
4504
  }
@@ -4393,7 +4520,8 @@ function joinPaths(prefix, suffix) {
4393
4520
  if (!prefix && !suffix) return "/";
4394
4521
  if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
4395
4522
  if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
4396
- const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
4523
+ const withLeadingSlash = prefix.startsWith("/") ? prefix : `/${prefix}`;
4524
+ const p = withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
4397
4525
  const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
4398
4526
  const combined = p + s;
4399
4527
  return combined === "" ? "/" : combined;
@@ -4447,7 +4575,8 @@ function buildRoute(args) {
4447
4575
  methodAs,
4448
4576
  sourceFile,
4449
4577
  seenNames,
4450
- contractSource
4578
+ contractSource,
4579
+ mixin
4451
4580
  } = args;
4452
4581
  const routeName = resolveRouteName(className, methodName, classAs, methodAs);
4453
4582
  const qualifiedRef = `${className}.${methodName}`;
@@ -4463,7 +4592,12 @@ function buildRoute(args) {
4463
4592
  path: combinedPath,
4464
4593
  name: routeName,
4465
4594
  params: extractParams(combinedPath),
4466
- controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
4595
+ controllerRef: {
4596
+ className,
4597
+ methodName,
4598
+ filePath: sourceFile.getFilePath(),
4599
+ ...mixin ? { mixin } : {}
4600
+ },
4467
4601
  contract: { contractSource }
4468
4602
  };
4469
4603
  }
@@ -4477,16 +4611,17 @@ function extractContractRoute(args) {
4477
4611
  className,
4478
4612
  sourceFile,
4479
4613
  project,
4480
- seenNames
4614
+ seenNames,
4615
+ mixin
4481
4616
  } = args;
4482
4617
  const firstDecoratorArg = applyContractDecorator.getArguments()[0];
4483
4618
  if (!firstDecoratorArg) return null;
4484
4619
  let contractDef = null;
4485
4620
  let bodyZodRef = null;
4486
4621
  let queryZodRef = null;
4487
- if (Node8.isCallExpression(firstDecoratorArg)) {
4622
+ if (Node9.isCallExpression(firstDecoratorArg)) {
4488
4623
  contractDef = parseDefineContractCall(firstDecoratorArg);
4489
- } else if (Node8.isIdentifier(firstDecoratorArg)) {
4624
+ } else if (Node9.isIdentifier(firstDecoratorArg)) {
4490
4625
  const identName = firstDecoratorArg.getText();
4491
4626
  const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
4492
4627
  if (!resolvedVar) {
@@ -4529,6 +4664,7 @@ function extractContractRoute(args) {
4529
4664
  methodAs,
4530
4665
  sourceFile,
4531
4666
  seenNames,
4667
+ mixin,
4532
4668
  contractSource: {
4533
4669
  query: contractDef.query,
4534
4670
  body: contractDef.body,
@@ -4545,13 +4681,14 @@ function extractContractRoute(args) {
4545
4681
  });
4546
4682
  }
4547
4683
  function extractDtoRoute(args) {
4548
- const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
4684
+ const { cls, method, verb, prefix, className, sourceFile, project, seenNames, mixin } = args;
4549
4685
  if (!verb) return null;
4550
4686
  const combined = joinPaths(prefix, verb.handlerPath);
4551
4687
  const methodName = method.getName();
4552
4688
  const classAs = readAsDecorator(cls, `class ${className}`);
4553
4689
  const methodAs = readAsDecorator(method, `${className}.${methodName}`);
4554
- const dtoContract = extractDtoContract(method, sourceFile, project);
4690
+ const dtoContract = extractDtoContract(method, sourceFile, project, mixin);
4691
+ const mixinResponse = mixin ? resolveInstantiatedReturnType(cls, methodName) : void 0;
4555
4692
  return buildRoute({
4556
4693
  className,
4557
4694
  methodName,
@@ -4561,10 +4698,11 @@ function extractDtoRoute(args) {
4561
4698
  methodAs,
4562
4699
  sourceFile,
4563
4700
  seenNames,
4701
+ mixin,
4564
4702
  contractSource: {
4565
4703
  query: dtoContract?.query ?? null,
4566
4704
  body: dtoContract?.body ?? null,
4567
- response: dtoContract?.response ?? "unknown",
4705
+ response: mixinResponse ?? dtoContract?.response ?? "unknown",
4568
4706
  error: dtoContract?.error ?? null,
4569
4707
  queryRef: dtoContract?.queryRef ?? null,
4570
4708
  bodyRef: dtoContract?.bodyRef ?? null,
@@ -4593,7 +4731,17 @@ function extractFromSourceFile(sourceFile, project) {
4593
4731
  const firstArg2 = controllerDecorator.getArguments()[0];
4594
4732
  const prefix = decoratorStringArg(firstArg2) ?? "";
4595
4733
  const className = cls.getName() ?? "Unknown";
4596
- for (const method of cls.getMethods()) {
4734
+ const inherited = resolveInheritedMethods(cls);
4735
+ const mixinBinding = inherited ? {
4736
+ factoryName: inherited.factoryName,
4737
+ factoryFilePath: inherited.factoryFilePath,
4738
+ classArgs: inherited.classArgs
4739
+ } : void 0;
4740
+ const methods = [
4741
+ ...cls.getMethods().map((method) => ({ method })),
4742
+ ...(inherited?.methods ?? []).map((method) => ({ method, mixin: mixinBinding }))
4743
+ ];
4744
+ for (const { method, mixin } of methods) {
4597
4745
  const verb = resolveVerb(method);
4598
4746
  const applyContractDecorator = method.getDecorator("ApplyContract");
4599
4747
  const route = applyContractDecorator ? extractContractRoute({
@@ -4605,7 +4753,8 @@ function extractFromSourceFile(sourceFile, project) {
4605
4753
  className,
4606
4754
  sourceFile,
4607
4755
  project,
4608
- seenNames
4756
+ seenNames,
4757
+ mixin
4609
4758
  }) : extractDtoRoute({
4610
4759
  cls,
4611
4760
  method,
@@ -4614,7 +4763,8 @@ function extractFromSourceFile(sourceFile, project) {
4614
4763
  className,
4615
4764
  sourceFile,
4616
4765
  project,
4617
- seenNames
4766
+ seenNames,
4767
+ mixin
4618
4768
  });
4619
4769
  if (route) routes.push(route);
4620
4770
  }
@@ -4815,7 +4965,7 @@ async function watch(config, onChange, options = {}) {
4815
4965
  }
4816
4966
 
4817
4967
  // src/index.ts
4818
- var VERSION = "0.14.2";
4968
+ var VERSION = "0.16.0";
4819
4969
 
4820
4970
  // src/cli/codegen.ts
4821
4971
  async function runCodegen(opts = {}) {