@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/index.cjs CHANGED
@@ -807,6 +807,9 @@ function buildErrorType(c) {
807
807
  }
808
808
  return c.contractSource.error ?? "unknown";
809
809
  }
810
+ function filterFieldLiterals(fields) {
811
+ return fields?.length ? fields.map((f) => JSON.stringify(f)) : [];
812
+ }
810
813
  function emitRouterTypeBlock(tree, indent, outDir, serialization) {
811
814
  const pad = " ".repeat(indent);
812
815
  const lines = [];
@@ -833,7 +836,8 @@ function emitRouterTypeBlock(tree, indent, outDir, serialization) {
833
836
  const params = buildParamsType(c.params);
834
837
  const safeMethod = JSON.stringify(method);
835
838
  const safeUrl = JSON.stringify(c.path);
836
- const filterFields = c.contractSource.filterFields?.length ? c.contractSource.filterFields.map((f) => JSON.stringify(f)).join(" | ") : "never";
839
+ const filterLiterals = filterFieldLiterals(c.contractSource.filterFields);
840
+ const filterFields = filterLiterals.length ? filterLiterals.join(" | ") : "never";
837
841
  const stream = c.contractSource.stream ? "true" : "false";
838
842
  const binary = c.contractSource.binaryResponse ? "true" : "false";
839
843
  lines.push(
@@ -873,6 +877,7 @@ function buildRequestModel(c) {
873
877
  const TA = buildRouterTypeAccess(c.name);
874
878
  const withParams = hasPathParams(c.params);
875
879
  const { isGet, isQuery, hasBody, hasQuery } = requestShape(c.route);
880
+ const filterLiterals = filterFieldLiterals(c.contractSource.filterFields);
876
881
  const fields = [];
877
882
  if (withParams) fields.push(`params: ${TA}['params']`);
878
883
  if (hasQuery) fields.push(`query?: ${TA}['query']`);
@@ -902,7 +907,12 @@ function buildRequestModel(c) {
902
907
  // (`[name]` rather than `[name, undefined]`) so the bare `.queryKey()` is a
903
908
  // clean prefix that partial-matches every parametrized variant — making it
904
909
  // directly usable for `invalidateQueries`.
905
- queryKeyExpr: `(input === undefined ? [${flat}] as const : [${flat}, input] as const)`
910
+ queryKeyExpr: `(input === undefined ? [${flat}] as const : [${flat}, input] as const)`,
911
+ // Runtime counterpart to the type-level `filterFields` union: the same
912
+ // discovered field list, emitted as a literal `[...] as const` so apps can
913
+ // validate a dynamic/user-supplied field string with `isFilterField(...)`
914
+ // instead of casting. Omitted for routes with no filter.
915
+ ...filterLiterals.length ? { filterFieldsExpr: `[${filterLiterals.join(", ")}] as const` } : {}
906
916
  };
907
917
  }
908
918
  function renderFetcherRequest(req, binaryResponse) {
@@ -937,9 +947,24 @@ function emitReqHelper() {
937
947
  ""
938
948
  ];
939
949
  }
950
+ function emitFilterFieldGuard() {
951
+ return [
952
+ "/** 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. */",
953
+ "export function isFilterField<const K extends string>(",
954
+ " fields: readonly K[],",
955
+ " value: string,",
956
+ "): value is K {",
957
+ " return (fields as readonly string[]).includes(value);",
958
+ "}",
959
+ ""
960
+ ];
961
+ }
940
962
  function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
941
963
  const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
942
964
  lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
965
+ if (req.filterFieldsExpr) {
966
+ lines.push(`${pad} filterFields: ${req.filterFieldsExpr},`);
967
+ }
943
968
  if (streamExpr) {
944
969
  lines.push(`${pad} stream: () => ${streamExpr},`);
945
970
  }
@@ -1211,6 +1236,9 @@ function buildApiFile(routes, outDir, opts = {}) {
1211
1236
  lines.push("};");
1212
1237
  lines.push("");
1213
1238
  lines.push(...emitReqHelper());
1239
+ if (contracted.some((r) => r.contract?.contractSource.filterFields?.length)) {
1240
+ lines.push(...emitFilterFieldGuard());
1241
+ }
1214
1242
  lines.push("export function createApi(fetcher: Fetcher) {");
1215
1243
  lines.push(" return {");
1216
1244
  lines.push(
@@ -2449,7 +2477,7 @@ var import_chokidar = __toESM(require("chokidar"), 1);
2449
2477
  // src/discovery/contracts-fast.ts
2450
2478
  var import_node_path15 = require("path");
2451
2479
  var import_fast_glob3 = __toESM(require("fast-glob"), 1);
2452
- var import_ts_morph9 = require("ts-morph");
2480
+ var import_ts_morph10 = require("ts-morph");
2453
2481
 
2454
2482
  // src/discovery/dto-type-resolver.ts
2455
2483
  var import_ts_morph7 = require("ts-morph");
@@ -3428,6 +3456,12 @@ function toFilterFieldType(name, r) {
3428
3456
  }
3429
3457
 
3430
3458
  // src/discovery/filter-for.ts
3459
+ function resolveMixinEntityClass(mixin, project) {
3460
+ const entityArg = mixin?.classArgs[0];
3461
+ if (!entityArg) return void 0;
3462
+ const file = project.getSourceFile(entityArg.filePath) ?? project.addSourceFileAtPathIfExists(entityArg.filePath);
3463
+ return file?.getClass(entityArg.name);
3464
+ }
3431
3465
  function classifyFilterForHint(typeInit) {
3432
3466
  if (import_ts_morph6.Node.isStringLiteral(typeInit)) {
3433
3467
  switch (typeInit.getLiteralValue()) {
@@ -3501,7 +3535,9 @@ function extractFilterForHints(classDecl, project) {
3501
3535
  }
3502
3536
  return hints;
3503
3537
  }
3504
- function extractApplyFilterInfo(method, sourceFile, project) {
3538
+ function extractApplyFilterInfo(method, sourceFile, project, mixin) {
3539
+ const declFile = method.getSourceFile();
3540
+ const mixinEntity = resolveMixinEntityClass(mixin, project);
3505
3541
  for (const param of method.getParameters()) {
3506
3542
  const filterDecorator = param.getDecorators().find((d) => d.getName() === "ApplyFilter");
3507
3543
  if (!filterDecorator) continue;
@@ -3521,12 +3557,12 @@ function extractApplyFilterInfo(method, sourceFile, project) {
3521
3557
  }
3522
3558
  }
3523
3559
  const filterClassName = filterClassArg.getText();
3524
- const resolved = findType(filterClassName, sourceFile, project);
3525
- if (resolved && resolved.kind === "class") {
3526
- const classDecl = resolved.decl;
3560
+ const resolved = findType(filterClassName, declFile, project);
3561
+ const classDecl = resolved?.kind === "class" ? resolved.decl : resolveLocalClassDeclaration(filterClassArg);
3562
+ if (classDecl) {
3527
3563
  let fieldTypes = extractClassPropertyTypes(classDecl, project);
3528
3564
  if (fieldTypes.length === 0) {
3529
- fieldTypes = extractFilterableEntityFields(classDecl, project);
3565
+ fieldTypes = extractFilterableEntityFields(classDecl, project, mixinEntity);
3530
3566
  }
3531
3567
  const filterForHints = extractFilterForHints(classDecl, project);
3532
3568
  if (filterForHints.size > 0) {
@@ -3611,22 +3647,29 @@ function extractClassPropertyTypes(classDecl, project) {
3611
3647
  }
3612
3648
  return fields;
3613
3649
  }
3614
- function extractFilterableEntityFields(filterClass, project) {
3650
+ function resolveLocalClassDeclaration(identifier) {
3651
+ if (!import_ts_morph6.Node.isIdentifier(identifier)) return void 0;
3652
+ return identifier.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => n !== void 0 && import_ts_morph6.Node.isClassDeclaration(n));
3653
+ }
3654
+ function resolveDeclaredEntity(optionsArg, filterClass, project) {
3655
+ if (!import_ts_morph6.Node.isObjectLiteralExpression(optionsArg)) return void 0;
3656
+ const entityProp = optionsArg.getProperty("entity");
3657
+ if (!entityProp || !import_ts_morph6.Node.isPropertyAssignment(entityProp)) return void 0;
3658
+ const entityInit = entityProp.getInitializer();
3659
+ if (!entityInit || !import_ts_morph6.Node.isIdentifier(entityInit)) return void 0;
3660
+ const resolved = findType(entityInit.getText(), filterClass.getSourceFile(), project);
3661
+ if (!resolved || resolved.kind !== "class") return void 0;
3662
+ return resolved.decl;
3663
+ }
3664
+ function extractFilterableEntityFields(filterClass, project, entityOverride) {
3615
3665
  const filterableDecorator = filterClass.getDecorators().find((d) => d.getName() === "Filterable");
3616
3666
  if (!filterableDecorator) return [];
3617
3667
  const args = filterableDecorator.getArguments();
3618
3668
  if (args.length === 0) return [];
3619
3669
  const optionsArg = args[0];
3620
3670
  if (!import_ts_morph6.Node.isObjectLiteralExpression(optionsArg)) return [];
3621
- const entityProp = optionsArg.getProperty("entity");
3622
- if (!entityProp || !import_ts_morph6.Node.isPropertyAssignment(entityProp)) return [];
3623
- const entityInit = entityProp.getInitializer();
3624
- if (!entityInit || !import_ts_morph6.Node.isIdentifier(entityInit)) return [];
3625
- const entityName = entityInit.getText();
3626
- const filterSourceFile = filterClass.getSourceFile();
3627
- const resolvedEntity = findType(entityName, filterSourceFile, project);
3628
- if (!resolvedEntity || resolvedEntity.kind !== "class") return [];
3629
- const entityDecl = resolvedEntity.decl;
3671
+ const entityDecl = entityOverride ?? resolveDeclaredEntity(optionsArg, filterClass, project);
3672
+ if (!entityDecl) return [];
3630
3673
  const fields = collectEntityFields(
3631
3674
  entityDecl,
3632
3675
  entityDecl.getSourceFile(),
@@ -4051,9 +4094,9 @@ function unwrapNamedContainer(node, names) {
4051
4094
  }
4052
4095
  return node;
4053
4096
  }
4054
- function extractDtoContract(method, sourceFile, project) {
4097
+ function extractDtoContract(method, sourceFile, project, mixin) {
4055
4098
  let body = extractBodyType(method, sourceFile, project);
4056
- const filterInfo = extractApplyFilterInfo(method, sourceFile, project);
4099
+ const filterInfo = extractApplyFilterInfo(method, sourceFile, project, mixin);
4057
4100
  const query = extractQueryType(method, sourceFile, project);
4058
4101
  const uploads = extractUploadedFiles(method);
4059
4102
  const multipartBody = uploads.fields ? `{ ${uploads.fields} }` : null;
@@ -4163,12 +4206,94 @@ function resolveParamClass(method, decoratorName, sourceFile, project) {
4163
4206
  return null;
4164
4207
  }
4165
4208
 
4166
- // src/discovery/zod-ast-to-ts.ts
4209
+ // src/discovery/heritage.ts
4167
4210
  var import_ts_morph8 = require("ts-morph");
4211
+ function unwrapExpression(node) {
4212
+ let current = node;
4213
+ 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)) {
4214
+ current = current.getExpression();
4215
+ }
4216
+ return current;
4217
+ }
4218
+ function resolveReturnedClass(factoryDecl) {
4219
+ const body = factoryDecl.getBody();
4220
+ if (!body) return void 0;
4221
+ const returns = body.getDescendants().filter((n) => import_ts_morph8.Node.isReturnStatement(n));
4222
+ for (const ret of returns) {
4223
+ const expr = ret.getExpression();
4224
+ if (!expr) continue;
4225
+ const inner = unwrapExpression(expr);
4226
+ if (import_ts_morph8.Node.isClassExpression(inner)) {
4227
+ return inner;
4228
+ }
4229
+ if (import_ts_morph8.Node.isIdentifier(inner)) {
4230
+ const name = inner.getText();
4231
+ const decl = body.getDescendants().find((n) => import_ts_morph8.Node.isClassDeclaration(n) && n.getName() === name);
4232
+ if (decl) return decl;
4233
+ }
4234
+ }
4235
+ return void 0;
4236
+ }
4237
+ function resolveInheritedMethods(cls) {
4238
+ const expr = cls.getExtends()?.getExpression();
4239
+ if (!expr || !import_ts_morph8.Node.isCallExpression(expr)) return void 0;
4240
+ const callee = expr.getExpression();
4241
+ if (!import_ts_morph8.Node.isIdentifier(callee)) return void 0;
4242
+ const factoryDecl = callee.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => import_ts_morph8.Node.isFunctionDeclaration(n));
4243
+ if (!factoryDecl) return void 0;
4244
+ const returnedClass = resolveReturnedClass(factoryDecl);
4245
+ if (!returnedClass) return void 0;
4246
+ const classArgs = [];
4247
+ for (const arg of expr.getArguments()) {
4248
+ if (!import_ts_morph8.Node.isIdentifier(arg)) continue;
4249
+ const decl = arg.getDefinitions().map((d) => d.getDeclarationNode()).find((n) => import_ts_morph8.Node.isClassDeclaration(n));
4250
+ if (decl) {
4251
+ classArgs.push({
4252
+ name: decl.getName() ?? arg.getText(),
4253
+ filePath: decl.getSourceFile().getFilePath()
4254
+ });
4255
+ }
4256
+ }
4257
+ return {
4258
+ methods: returnedClass.getMethods(),
4259
+ factoryName: callee.getText(),
4260
+ factoryFilePath: factoryDecl.getSourceFile().getFilePath(),
4261
+ classArgs
4262
+ };
4263
+ }
4264
+ var mixinTypeProject;
4265
+ function getMixinTypeProject() {
4266
+ mixinTypeProject ??= new import_ts_morph8.Project({
4267
+ skipAddingFilesFromTsConfig: true,
4268
+ compilerOptions: { strict: true }
4269
+ });
4270
+ return mixinTypeProject;
4271
+ }
4272
+ function clearMixinTypeProject() {
4273
+ mixinTypeProject = void 0;
4274
+ }
4275
+ function resolveInstantiatedReturnType(cls, methodName) {
4276
+ const filePath = cls.getSourceFile().getFilePath();
4277
+ const className = cls.getName();
4278
+ if (!className) return void 0;
4279
+ const project = getMixinTypeProject();
4280
+ const sourceFile = project.getSourceFile(filePath) ?? project.addSourceFileAtPathIfExists(filePath);
4281
+ const typedCls = sourceFile?.getClass(className);
4282
+ if (!typedCls) return void 0;
4283
+ const prop = typedCls.getType().getProperty(methodName);
4284
+ if (!prop) return void 0;
4285
+ const returnType = prop.getTypeAtLocation(typedCls).getCallSignatures()[0]?.getReturnType();
4286
+ if (!returnType) return void 0;
4287
+ const unwrapped = returnType.getSymbol()?.getName() === "Promise" ? returnType.getTypeArguments()[0] ?? returnType : returnType;
4288
+ return unwrapped.getText(typedCls);
4289
+ }
4290
+
4291
+ // src/discovery/zod-ast-to-ts.ts
4292
+ var import_ts_morph9 = require("ts-morph");
4168
4293
  function zodAstToTs(node) {
4169
- if (!import_ts_morph8.Node.isCallExpression(node)) return "unknown";
4294
+ if (!import_ts_morph9.Node.isCallExpression(node)) return "unknown";
4170
4295
  const expr = node.getExpression();
4171
- if (import_ts_morph8.Node.isPropertyAccessExpression(expr)) {
4296
+ if (import_ts_morph9.Node.isPropertyAccessExpression(expr)) {
4172
4297
  const methodName = expr.getName();
4173
4298
  const receiver = expr.getExpression();
4174
4299
  if (methodName === "optional") {
@@ -4192,17 +4317,17 @@ function zodAstToTs(node) {
4192
4317
  case "literal": {
4193
4318
  const lit = args[0];
4194
4319
  if (!lit) return "unknown";
4195
- if (import_ts_morph8.Node.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
4196
- if (import_ts_morph8.Node.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
4197
- if (lit.getKind() === import_ts_morph8.SyntaxKind.TrueKeyword) return "true";
4198
- if (lit.getKind() === import_ts_morph8.SyntaxKind.FalseKeyword) return "false";
4320
+ if (import_ts_morph9.Node.isStringLiteral(lit)) return JSON.stringify(lit.getLiteralValue());
4321
+ if (import_ts_morph9.Node.isNumericLiteral(lit)) return lit.getLiteralValue().toString();
4322
+ if (lit.getKind() === import_ts_morph9.SyntaxKind.TrueKeyword) return "true";
4323
+ if (lit.getKind() === import_ts_morph9.SyntaxKind.FalseKeyword) return "false";
4199
4324
  return "unknown";
4200
4325
  }
4201
4326
  case "enum": {
4202
4327
  const arrArg = args[0];
4203
- if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4328
+ if (!arrArg || !import_ts_morph9.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4204
4329
  const members = arrArg.getElements().map(
4205
- (el) => import_ts_morph8.Node.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
4330
+ (el) => import_ts_morph9.Node.isStringLiteral(el) ? JSON.stringify(el.getLiteralValue()) : "unknown"
4206
4331
  );
4207
4332
  return members.join(" | ");
4208
4333
  }
@@ -4213,10 +4338,10 @@ function zodAstToTs(node) {
4213
4338
  }
4214
4339
  case "object": {
4215
4340
  const objArg = args[0];
4216
- if (!objArg || !import_ts_morph8.Node.isObjectLiteralExpression(objArg)) return "unknown";
4341
+ if (!objArg || !import_ts_morph9.Node.isObjectLiteralExpression(objArg)) return "unknown";
4217
4342
  const lines = [];
4218
4343
  for (const prop of objArg.getProperties()) {
4219
- if (!import_ts_morph8.Node.isPropertyAssignment(prop)) continue;
4344
+ if (!import_ts_morph9.Node.isPropertyAssignment(prop)) continue;
4220
4345
  const key = prop.getName();
4221
4346
  const valNode = prop.getInitializer();
4222
4347
  if (!valNode) continue;
@@ -4228,7 +4353,7 @@ function zodAstToTs(node) {
4228
4353
  }
4229
4354
  case "union": {
4230
4355
  const arrArg = args[0];
4231
- if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4356
+ if (!arrArg || !import_ts_morph9.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4232
4357
  return arrArg.getElements().map(zodAstToTs).join(" | ");
4233
4358
  }
4234
4359
  case "record": {
@@ -4238,7 +4363,7 @@ function zodAstToTs(node) {
4238
4363
  }
4239
4364
  case "tuple": {
4240
4365
  const arrArg = args[0];
4241
- if (!arrArg || !import_ts_morph8.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4366
+ if (!arrArg || !import_ts_morph9.Node.isArrayLiteralExpression(arrArg)) return "unknown";
4242
4367
  return `[${arrArg.getElements().map(zodAstToTs).join(", ")}]`;
4243
4368
  }
4244
4369
  default:
@@ -4248,18 +4373,18 @@ function zodAstToTs(node) {
4248
4373
  return "unknown";
4249
4374
  }
4250
4375
  function isOptionalChain(node) {
4251
- if (!import_ts_morph8.Node.isCallExpression(node)) return false;
4376
+ if (!import_ts_morph9.Node.isCallExpression(node)) return false;
4252
4377
  const expr = node.getExpression();
4253
- return import_ts_morph8.Node.isPropertyAccessExpression(expr) && expr.getName() === "optional";
4378
+ return import_ts_morph9.Node.isPropertyAccessExpression(expr) && expr.getName() === "optional";
4254
4379
  }
4255
4380
  function parseDefineContractCall(callExpr) {
4256
- if (!import_ts_morph8.Node.isCallExpression(callExpr)) return null;
4381
+ if (!import_ts_morph9.Node.isCallExpression(callExpr)) return null;
4257
4382
  const callee = callExpr.getExpression();
4258
- const calleeName = import_ts_morph8.Node.isIdentifier(callee) ? callee.getText() : import_ts_morph8.Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
4383
+ const calleeName = import_ts_morph9.Node.isIdentifier(callee) ? callee.getText() : import_ts_morph9.Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
4259
4384
  if (calleeName !== "defineContract") return null;
4260
4385
  const args = callExpr.getArguments();
4261
4386
  const optsArg = args[0];
4262
- if (!optsArg || !import_ts_morph8.Node.isObjectLiteralExpression(optsArg)) return null;
4387
+ if (!optsArg || !import_ts_morph9.Node.isObjectLiteralExpression(optsArg)) return null;
4263
4388
  let query = null;
4264
4389
  let body = null;
4265
4390
  let response = "unknown";
@@ -4267,7 +4392,7 @@ function parseDefineContractCall(callExpr) {
4267
4392
  let bodyZodText = null;
4268
4393
  let queryZodText = null;
4269
4394
  for (const prop of optsArg.getProperties()) {
4270
- if (!import_ts_morph8.Node.isPropertyAssignment(prop)) continue;
4395
+ if (!import_ts_morph9.Node.isPropertyAssignment(prop)) continue;
4271
4396
  const propName = prop.getName();
4272
4397
  const val = prop.getInitializer();
4273
4398
  if (!val) continue;
@@ -4296,6 +4421,7 @@ async function discoverContractsFast(opts) {
4296
4421
  project.addSourceFileAtPath(f);
4297
4422
  }
4298
4423
  bindDiscoveryContext(project, cwd, tsconfigPath);
4424
+ clearMixinTypeProject();
4299
4425
  return extractAllRoutes(project);
4300
4426
  }
4301
4427
  function resolveTsconfigPath(cwd, tsconfig) {
@@ -4303,14 +4429,14 @@ function resolveTsconfigPath(cwd, tsconfig) {
4303
4429
  }
4304
4430
  function createDiscoveryProject(tsconfigPath) {
4305
4431
  try {
4306
- return new import_ts_morph9.Project({
4432
+ return new import_ts_morph10.Project({
4307
4433
  tsConfigFilePath: tsconfigPath,
4308
4434
  skipAddingFilesFromTsConfig: true,
4309
4435
  skipLoadingLibFiles: true,
4310
4436
  skipFileDependencyResolution: true
4311
4437
  });
4312
4438
  } catch {
4313
- return new import_ts_morph9.Project({
4439
+ return new import_ts_morph10.Project({
4314
4440
  skipAddingFilesFromTsConfig: true,
4315
4441
  skipLoadingLibFiles: true,
4316
4442
  skipFileDependencyResolution: true,
@@ -4417,15 +4543,16 @@ var PersistentDiscovery = class _PersistentDiscovery {
4417
4543
  runExtraction() {
4418
4544
  clearTypeResolutionCaches(this.project);
4419
4545
  clearEnumCache(this.project);
4546
+ clearMixinTypeProject();
4420
4547
  return extractRoutesFrom(this.project, this.controllerPaths);
4421
4548
  }
4422
4549
  };
4423
4550
  function decoratorStringArg(decoratorExpr) {
4424
4551
  if (!decoratorExpr) return void 0;
4425
- if (import_ts_morph9.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
4426
- if (import_ts_morph9.Node.isArrayLiteralExpression(decoratorExpr)) {
4552
+ if (import_ts_morph10.Node.isStringLiteral(decoratorExpr)) return decoratorExpr.getLiteralValue();
4553
+ if (import_ts_morph10.Node.isArrayLiteralExpression(decoratorExpr)) {
4427
4554
  const first = decoratorExpr.getElements()[0];
4428
- if (first && import_ts_morph9.Node.isStringLiteral(first)) return first.getLiteralValue();
4555
+ if (first && import_ts_morph10.Node.isStringLiteral(first)) return first.getLiteralValue();
4429
4556
  }
4430
4557
  return void 0;
4431
4558
  }
@@ -4447,7 +4574,8 @@ function joinPaths(prefix, suffix) {
4447
4574
  if (!prefix && !suffix) return "/";
4448
4575
  if (!prefix) return suffix.startsWith("/") ? suffix : `/${suffix}`;
4449
4576
  if (!suffix) return prefix.startsWith("/") ? prefix : `/${prefix}`;
4450
- const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
4577
+ const withLeadingSlash = prefix.startsWith("/") ? prefix : `/${prefix}`;
4578
+ const p = withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
4451
4579
  const s = suffix.startsWith("/") ? suffix : `/${suffix}`;
4452
4580
  const combined = p + s;
4453
4581
  return combined === "" ? "/" : combined;
@@ -4501,7 +4629,8 @@ function buildRoute(args) {
4501
4629
  methodAs,
4502
4630
  sourceFile,
4503
4631
  seenNames,
4504
- contractSource
4632
+ contractSource,
4633
+ mixin
4505
4634
  } = args;
4506
4635
  const routeName = resolveRouteName(className, methodName, classAs, methodAs);
4507
4636
  const qualifiedRef = `${className}.${methodName}`;
@@ -4517,7 +4646,12 @@ function buildRoute(args) {
4517
4646
  path: combinedPath,
4518
4647
  name: routeName,
4519
4648
  params: extractParams(combinedPath),
4520
- controllerRef: { className, methodName, filePath: sourceFile.getFilePath() },
4649
+ controllerRef: {
4650
+ className,
4651
+ methodName,
4652
+ filePath: sourceFile.getFilePath(),
4653
+ ...mixin ? { mixin } : {}
4654
+ },
4521
4655
  contract: { contractSource }
4522
4656
  };
4523
4657
  }
@@ -4531,16 +4665,17 @@ function extractContractRoute(args) {
4531
4665
  className,
4532
4666
  sourceFile,
4533
4667
  project,
4534
- seenNames
4668
+ seenNames,
4669
+ mixin
4535
4670
  } = args;
4536
4671
  const firstDecoratorArg = applyContractDecorator.getArguments()[0];
4537
4672
  if (!firstDecoratorArg) return null;
4538
4673
  let contractDef = null;
4539
4674
  let bodyZodRef = null;
4540
4675
  let queryZodRef = null;
4541
- if (import_ts_morph9.Node.isCallExpression(firstDecoratorArg)) {
4676
+ if (import_ts_morph10.Node.isCallExpression(firstDecoratorArg)) {
4542
4677
  contractDef = parseDefineContractCall(firstDecoratorArg);
4543
- } else if (import_ts_morph9.Node.isIdentifier(firstDecoratorArg)) {
4678
+ } else if (import_ts_morph10.Node.isIdentifier(firstDecoratorArg)) {
4544
4679
  const identName = firstDecoratorArg.getText();
4545
4680
  const resolvedVar = resolveImportedVariable(identName, sourceFile, project);
4546
4681
  if (!resolvedVar) {
@@ -4583,6 +4718,7 @@ function extractContractRoute(args) {
4583
4718
  methodAs,
4584
4719
  sourceFile,
4585
4720
  seenNames,
4721
+ mixin,
4586
4722
  contractSource: {
4587
4723
  query: contractDef.query,
4588
4724
  body: contractDef.body,
@@ -4599,13 +4735,14 @@ function extractContractRoute(args) {
4599
4735
  });
4600
4736
  }
4601
4737
  function extractDtoRoute(args) {
4602
- const { cls, method, verb, prefix, className, sourceFile, project, seenNames } = args;
4738
+ const { cls, method, verb, prefix, className, sourceFile, project, seenNames, mixin } = args;
4603
4739
  if (!verb) return null;
4604
4740
  const combined = joinPaths(prefix, verb.handlerPath);
4605
4741
  const methodName = method.getName();
4606
4742
  const classAs = readAsDecorator(cls, `class ${className}`);
4607
4743
  const methodAs = readAsDecorator(method, `${className}.${methodName}`);
4608
- const dtoContract = extractDtoContract(method, sourceFile, project);
4744
+ const dtoContract = extractDtoContract(method, sourceFile, project, mixin);
4745
+ const mixinResponse = mixin ? resolveInstantiatedReturnType(cls, methodName) : void 0;
4609
4746
  return buildRoute({
4610
4747
  className,
4611
4748
  methodName,
@@ -4615,10 +4752,11 @@ function extractDtoRoute(args) {
4615
4752
  methodAs,
4616
4753
  sourceFile,
4617
4754
  seenNames,
4755
+ mixin,
4618
4756
  contractSource: {
4619
4757
  query: dtoContract?.query ?? null,
4620
4758
  body: dtoContract?.body ?? null,
4621
- response: dtoContract?.response ?? "unknown",
4759
+ response: mixinResponse ?? dtoContract?.response ?? "unknown",
4622
4760
  error: dtoContract?.error ?? null,
4623
4761
  queryRef: dtoContract?.queryRef ?? null,
4624
4762
  bodyRef: dtoContract?.bodyRef ?? null,
@@ -4647,7 +4785,17 @@ function extractFromSourceFile(sourceFile, project) {
4647
4785
  const firstArg2 = controllerDecorator.getArguments()[0];
4648
4786
  const prefix = decoratorStringArg(firstArg2) ?? "";
4649
4787
  const className = cls.getName() ?? "Unknown";
4650
- for (const method of cls.getMethods()) {
4788
+ const inherited = resolveInheritedMethods(cls);
4789
+ const mixinBinding = inherited ? {
4790
+ factoryName: inherited.factoryName,
4791
+ factoryFilePath: inherited.factoryFilePath,
4792
+ classArgs: inherited.classArgs
4793
+ } : void 0;
4794
+ const methods = [
4795
+ ...cls.getMethods().map((method) => ({ method })),
4796
+ ...(inherited?.methods ?? []).map((method) => ({ method, mixin: mixinBinding }))
4797
+ ];
4798
+ for (const { method, mixin } of methods) {
4651
4799
  const verb = resolveVerb(method);
4652
4800
  const applyContractDecorator = method.getDecorator("ApplyContract");
4653
4801
  const route = applyContractDecorator ? extractContractRoute({
@@ -4659,7 +4807,8 @@ function extractFromSourceFile(sourceFile, project) {
4659
4807
  className,
4660
4808
  sourceFile,
4661
4809
  project,
4662
- seenNames
4810
+ seenNames,
4811
+ mixin
4663
4812
  }) : extractDtoRoute({
4664
4813
  cls,
4665
4814
  method,
@@ -4668,7 +4817,8 @@ function extractFromSourceFile(sourceFile, project) {
4668
4817
  className,
4669
4818
  sourceFile,
4670
4819
  project,
4671
- seenNames
4820
+ seenNames,
4821
+ mixin
4672
4822
  });
4673
4823
  if (route) routes.push(route);
4674
4824
  }
@@ -4952,7 +5102,7 @@ function createChainModuleRenderer(opts) {
4952
5102
  }
4953
5103
 
4954
5104
  // src/index.ts
4955
- var VERSION = "0.14.2";
5105
+ var VERSION = "0.16.0";
4956
5106
  // Annotate the CommonJS export names for ESM import in node:
4957
5107
  0 && (module.exports = {
4958
5108
  CodegenError,