@techspokes/typescript-wsdl-client 0.10.2 → 0.11.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.
Files changed (48) hide show
  1. package/README.md +28 -2
  2. package/dist/app/generateApp.d.ts +11 -5
  3. package/dist/app/generateApp.d.ts.map +1 -1
  4. package/dist/app/generateApp.js +262 -157
  5. package/dist/cli.js +67 -9
  6. package/dist/client/generateOperations.d.ts +13 -0
  7. package/dist/client/generateOperations.d.ts.map +1 -0
  8. package/dist/client/generateOperations.js +71 -0
  9. package/dist/compiler/schemaCompiler.d.ts.map +1 -1
  10. package/dist/compiler/schemaCompiler.js +15 -1
  11. package/dist/gateway/generateGateway.d.ts +1 -0
  12. package/dist/gateway/generateGateway.d.ts.map +1 -1
  13. package/dist/gateway/generateGateway.js +4 -2
  14. package/dist/gateway/generators.d.ts +2 -15
  15. package/dist/gateway/generators.d.ts.map +1 -1
  16. package/dist/gateway/generators.js +111 -27
  17. package/dist/gateway/helpers.d.ts +4 -2
  18. package/dist/gateway/helpers.d.ts.map +1 -1
  19. package/dist/gateway/helpers.js +4 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +3 -0
  22. package/dist/loader/wsdlLoader.d.ts.map +1 -1
  23. package/dist/loader/wsdlLoader.js +30 -4
  24. package/dist/openapi/generateOpenAPI.d.ts +1 -0
  25. package/dist/openapi/generateOpenAPI.d.ts.map +1 -1
  26. package/dist/openapi/generateOpenAPI.js +1 -0
  27. package/dist/openapi/generateSchemas.d.ts +1 -0
  28. package/dist/openapi/generateSchemas.d.ts.map +1 -1
  29. package/dist/openapi/generateSchemas.js +4 -3
  30. package/dist/pipeline.d.ts +4 -0
  31. package/dist/pipeline.d.ts.map +1 -1
  32. package/dist/pipeline.js +10 -1
  33. package/dist/util/builder.d.ts.map +1 -1
  34. package/dist/util/builder.js +1 -0
  35. package/dist/util/cli.d.ts +3 -2
  36. package/dist/util/cli.d.ts.map +1 -1
  37. package/dist/util/cli.js +14 -4
  38. package/dist/util/errors.d.ts +37 -0
  39. package/dist/util/errors.d.ts.map +1 -0
  40. package/dist/util/errors.js +37 -0
  41. package/docs/README.md +1 -0
  42. package/docs/architecture.md +1 -1
  43. package/docs/cli-reference.md +46 -14
  44. package/docs/concepts.md +29 -2
  45. package/docs/gateway-guide.md +36 -2
  46. package/docs/generated-code.md +56 -0
  47. package/docs/testing.md +193 -0
  48. package/package.json +19 -13
package/dist/cli.js CHANGED
@@ -19,6 +19,7 @@ import { generateTypes } from "./client/generateTypes.js";
19
19
  import { generateUtils } from "./client/generateUtils.js";
20
20
  import { generateCatalog } from "./compiler/generateCatalog.js";
21
21
  import { generateClient } from "./client/generateClient.js";
22
+ import { generateOperations } from "./client/generateOperations.js";
22
23
  import { generateOpenAPI } from "./openapi/generateOpenAPI.js";
23
24
  import { runGenerationPipeline } from "./pipeline.js";
24
25
  import { resolveCompilerOptions } from "./config.js";
@@ -183,7 +184,7 @@ if (rawArgs[0] === "client") {
183
184
  success(`Compiled catalog written to ${catalogOutPath}`);
184
185
  }
185
186
  // Emit client artifacts (excluding catalog since we already emitted it above if needed)
186
- emitClientArtifacts(clientOutDir, compiled, generateClient, generateTypes, generateUtils);
187
+ emitClientArtifacts(clientOutDir, compiled, generateClient, generateTypes, generateUtils, generateOperations);
187
188
  process.exit(0);
188
189
  }
189
190
  /**
@@ -237,6 +238,11 @@ if (rawArgs[0] === "openapi") {
237
238
  type: "boolean",
238
239
  default: false,
239
240
  desc: "Emit additionalProperties:false for object schemas"
241
+ })
242
+ .option("openapi-flatten-array-wrappers", {
243
+ type: "boolean",
244
+ default: true,
245
+ desc: "Flatten ArrayOf* wrapper types to plain arrays in schemas (default: true)"
240
246
  })
241
247
  .option("openapi-prune-unused-schemas", {
242
248
  type: "boolean",
@@ -357,6 +363,11 @@ if (rawArgs[0] === "gateway") {
357
363
  type: "boolean",
358
364
  default: false,
359
365
  desc: "Skip generating runtime.ts utilities"
366
+ })
367
+ .option("openapi-flatten-array-wrappers", {
368
+ type: "boolean",
369
+ default: true,
370
+ desc: "Generate runtime unwrap for ArrayOf* wrapper types (default: true)"
360
371
  })
361
372
  .strict()
362
373
  .help()
@@ -388,6 +399,7 @@ if (rawArgs[0] === "gateway") {
388
399
  // Only override defaults if explicitly skipping (otherwise let generateGateway decide based on stubHandlers)
389
400
  emitPlugin: gatewayArgv["gateway-skip-plugin"] ? false : undefined,
390
401
  emitRuntime: gatewayArgv["gateway-skip-runtime"] ? false : undefined,
402
+ flattenArrayWrappers: gatewayArgv["openapi-flatten-array-wrappers"],
391
403
  });
392
404
  success(`Gateway code generated in ${outDir}`);
393
405
  process.exit(0);
@@ -457,6 +469,11 @@ if (rawArgs[0] === "app") {
457
469
  choices: ["copy", "reference"],
458
470
  default: "copy",
459
471
  desc: "How to handle OpenAPI file: copy into app dir or reference original"
472
+ })
473
+ .option("force", {
474
+ type: "boolean",
475
+ default: false,
476
+ desc: "Overwrite existing scaffold files"
460
477
  })
461
478
  .strict()
462
479
  .help()
@@ -500,6 +517,7 @@ if (rawArgs[0] === "app") {
500
517
  prefix: String(appArgv.prefix),
501
518
  logger: Boolean(appArgv.logger),
502
519
  openapiMode: appArgv["openapi-mode"],
520
+ force: Boolean(appArgv.force),
503
521
  });
504
522
  process.exit(0);
505
523
  }
@@ -563,6 +581,11 @@ if (rawArgs[0] === "pipeline") {
563
581
  .option("openapi-tags-file", { type: "string" })
564
582
  .option("openapi-ops-file", { type: "string" })
565
583
  .option("openapi-closed-schemas", { type: "boolean", default: false })
584
+ .option("openapi-flatten-array-wrappers", {
585
+ type: "boolean",
586
+ default: true,
587
+ desc: "Flatten ArrayOf* wrapper types to plain arrays in schemas (default: true)"
588
+ })
566
589
  .option("openapi-prune-unused-schemas", { type: "boolean", default: false })
567
590
  .option("openapi-envelope-namespace", { type: "string" })
568
591
  .option("openapi-error-namespace", { type: "string" })
@@ -602,11 +625,21 @@ if (rawArgs[0] === "pipeline") {
602
625
  default: false,
603
626
  desc: "Skip generating runtime.ts utilities"
604
627
  })
605
- // App generation flags
628
+ // App scaffold flags
629
+ .option("init-app", {
630
+ type: "boolean",
631
+ default: false,
632
+ desc: "Scaffold a runnable Fastify application (one-time setup, requires --client-dir, --gateway-dir, --openapi-file)"
633
+ })
634
+ .option("force-init", {
635
+ type: "boolean",
636
+ default: false,
637
+ desc: "Overwrite existing scaffold files when using --init-app"
638
+ })
606
639
  .option("generate-app", {
607
640
  type: "boolean",
608
641
  default: false,
609
- desc: "Generate runnable Fastify application (requires --client-dir, --gateway-dir, --openapi-file)"
642
+ hidden: true, // deprecated, use --init-app
610
643
  })
611
644
  .option("app-dir", {
612
645
  type: "string",
@@ -617,6 +650,21 @@ if (rawArgs[0] === "pipeline") {
617
650
  choices: ["copy", "reference"],
618
651
  default: "copy",
619
652
  desc: "How to handle OpenAPI file in app: copy or reference"
653
+ })
654
+ .option("app-host", {
655
+ type: "string",
656
+ default: "127.0.0.1",
657
+ desc: "Default server host for app scaffold"
658
+ })
659
+ .option("app-port", {
660
+ type: "number",
661
+ default: 3000,
662
+ desc: "Default server port for app scaffold"
663
+ })
664
+ .option("app-prefix", {
665
+ type: "string",
666
+ default: "",
667
+ desc: "Route prefix for app scaffold"
620
668
  })
621
669
  .strict()
622
670
  .help()
@@ -662,7 +710,14 @@ if (rawArgs[0] === "pipeline") {
662
710
  fs.rmSync(clientOutDir, { recursive: true, force: true });
663
711
  }
664
712
  }
665
- const servers = parseServers(pipelineArgv["openapi-servers"]);
713
+ // Resolve --init-app (new name) or --generate-app (deprecated alias)
714
+ const initApp = pipelineArgv["init-app"] || pipelineArgv["generate-app"];
715
+ const forceInit = pipelineArgv["force-init"];
716
+ let servers = parseServers(pipelineArgv["openapi-servers"]);
717
+ // Default OpenAPI servers to localhost when scaffolding an app and no explicit servers provided
718
+ if (initApp && servers.length === 0) {
719
+ servers = ["http://localhost:3000"];
720
+ }
666
721
  // Validate gateway requirements
667
722
  validateGatewayRequirements(gatewayOut, openapiOut, pipelineArgv["gateway-service-name"], pipelineArgv["gateway-version-prefix"]);
668
723
  // Parse gateway default response status codes if provided
@@ -680,11 +735,10 @@ if (rawArgs[0] === "pipeline") {
680
735
  const openApiOptions = openapiOut
681
736
  ? buildOpenApiOptionsFromArgv(pipelineArgv, format, servers)
682
737
  : undefined;
683
- // Validate app generation requirements
684
- const generateApp = pipelineArgv["generate-app"];
685
- if (generateApp) {
738
+ // Validate app scaffold requirements
739
+ if (initApp) {
686
740
  if (!clientOut || !gatewayOut || !openapiOut) {
687
- handleCLIError("--generate-app requires --client-dir, --gateway-dir, and --openapi-file to be set");
741
+ handleCLIError("--init-app requires --client-dir, --gateway-dir, and --openapi-file to be set");
688
742
  }
689
743
  }
690
744
  await runGenerationPipeline({
@@ -707,11 +761,15 @@ if (rawArgs[0] === "pipeline") {
707
761
  emitPlugin: pipelineArgv["gateway-skip-plugin"] ? false : undefined,
708
762
  emitRuntime: pipelineArgv["gateway-skip-runtime"] ? false : undefined,
709
763
  } : undefined,
710
- app: generateApp ? {
764
+ app: initApp ? {
711
765
  appDir: pipelineArgv["app-dir"]
712
766
  ? path.resolve(pipelineArgv["app-dir"])
713
767
  : path.join(path.dirname(path.resolve(gatewayOut)), "app"),
714
768
  openapiMode: pipelineArgv["app-openapi-mode"],
769
+ force: forceInit,
770
+ host: pipelineArgv["app-host"],
771
+ port: pipelineArgv["app-port"],
772
+ prefix: pipelineArgv["app-prefix"],
715
773
  } : undefined,
716
774
  });
717
775
  process.exit(0);
@@ -0,0 +1,13 @@
1
+ import type { CompiledCatalog } from "../compiler/schemaCompiler.js";
2
+ /**
3
+ * Generates an operations.ts file with a fully-typed interface for all SOAP operations
4
+ *
5
+ * The interface mirrors the async method signatures of the generated client class
6
+ * but uses concrete input/output types from types.ts instead of generic parameters.
7
+ * This enables type-safe mocking without SOAP runtime dependencies.
8
+ *
9
+ * @param outFile - Path to the output TypeScript file
10
+ * @param compiled - The compiled WSDL catalog
11
+ */
12
+ export declare function generateOperations(outFile: string, compiled: CompiledCatalog): void;
13
+ //# sourceMappingURL=generateOperations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generateOperations.d.ts","sourceRoot":"","sources":["../../src/client/generateOperations.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAIrE;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,GAAG,IAAI,CA0DnF"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Operations Interface Generator
3
+ *
4
+ * Generates a fully-typed operations interface for the SOAP client.
5
+ * This standalone interface enables mocking and testing without importing
6
+ * the concrete SOAP client class or its runtime dependencies.
7
+ */
8
+ import fs from "node:fs";
9
+ import { deriveClientName, pascal } from "../util/tools.js";
10
+ import { error } from "../util/cli.js";
11
+ /**
12
+ * Generates an operations.ts file with a fully-typed interface for all SOAP operations
13
+ *
14
+ * The interface mirrors the async method signatures of the generated client class
15
+ * but uses concrete input/output types from types.ts instead of generic parameters.
16
+ * This enables type-safe mocking without SOAP runtime dependencies.
17
+ *
18
+ * @param outFile - Path to the output TypeScript file
19
+ * @param compiled - The compiled WSDL catalog
20
+ */
21
+ export function generateOperations(outFile, compiled) {
22
+ const ext = compiled.options.imports ?? "bare";
23
+ const suffix = ext === "bare" ? "" : `.${ext}`;
24
+ const clientName = deriveClientName(compiled);
25
+ // Collect type names used in method signatures for the import statement
26
+ const importedTypes = new Set();
27
+ const methods = [];
28
+ for (const op of compiled.operations) {
29
+ const inTypeName = op.inputElement ? pascal(op.inputElement.local) : undefined;
30
+ const outTypeName = op.outputElement ? pascal(op.outputElement.local) : undefined;
31
+ if (!inTypeName && !outTypeName) {
32
+ continue;
33
+ }
34
+ const inTs = inTypeName ?? "Record<string, unknown>";
35
+ const outTs = outTypeName ?? "unknown";
36
+ if (inTypeName)
37
+ importedTypes.add(inTypeName);
38
+ if (outTypeName)
39
+ importedTypes.add(outTypeName);
40
+ methods.push(` ${op.name}(\n` +
41
+ ` args: ${inTs}\n` +
42
+ ` ): Promise<{ response: ${outTs}; headers: unknown }>;\n`);
43
+ }
44
+ // Build sorted import list for deterministic output
45
+ const sortedImports = Array.from(importedTypes).sort();
46
+ const typeImport = sortedImports.length > 0
47
+ ? `import type {\n${sortedImports.map((t) => ` ${t},`).join("\n")}\n} from "./types${suffix}";\n\n`
48
+ : "";
49
+ const content = `/**
50
+ * Typed operations interface for the ${clientName} service.
51
+ *
52
+ * Implement this interface to create mock clients or alternative
53
+ * transport layers without depending on the SOAP runtime.
54
+ *
55
+ * Auto-generated - do not edit manually.
56
+ */
57
+ ${typeImport}/**
58
+ * All operations exposed by the ${clientName} SOAP service.
59
+ *
60
+ * The concrete ${clientName} class satisfies this interface.
61
+ * Use this type for dependency injection, mocking, or testing.
62
+ */
63
+ export interface ${clientName}Operations {
64
+ ${methods.join("\n")}}\n`;
65
+ try {
66
+ fs.writeFileSync(outFile, content, "utf8");
67
+ }
68
+ catch (e) {
69
+ error(`Failed to write operations interface to ${outFile}`);
70
+ }
71
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"schemaCompiler.d.ts","sourceRoot":"","sources":["../../src/compiler/schemaCompiler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,yBAAyB,CAAC;AAIzD;;;;;;GAMG;AACH,MAAM,MAAM,KAAK,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAElD;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,KAAK,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;QAC9B,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;IACH,KAAK,EAAE,KAAK,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,GAAG,WAAW,CAAC;QAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,UAAU,CAAC,EAAE,KAAK,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;QAC9B,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;IAEH,UAAU,CAAC,EAAE,KAAK,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,GAAG,WAAW,CAAC;QAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;CACJ,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,eAAe,CAAC;IACzB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,IAAI,EAAE;QACJ,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;QACnC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;QAClD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;KAC/C,CAAC;IACF,UAAU,EAAE,KAAK,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,CAAC,EAAE,KAAK,CAAC;QACrB,aAAa,CAAC,EAAE,KAAK,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC,CAAC;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAiGF;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,eAAe,GAAG,eAAe,CAylB1F"}
1
+ {"version":3,"file":"schemaCompiler.d.ts","sourceRoot":"","sources":["../../src/compiler/schemaCompiler.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { getChildrenWithLocalName, getFirstWithLocalName, normalizeArray, pascal, resolveQName, } from "../util/tools.js";
2
2
  import { xsdToTsPrimitive } from "../xsd/primitives.js";
3
+ import { WsdlCompilationError } from "../util/errors.js";
3
4
  // XML Schema namespace constant
4
5
  const XS = "http://www.w3.org/2001/XMLSchema";
5
6
  /**
@@ -194,7 +195,14 @@ export function compileCatalog(cat, options) {
194
195
  const t = getOrCompileComplex(q.local, crec.node, crec.tns, crec.prefixes);
195
196
  return { tsType: t.name, declared: `{${t.ns}}${q.local}` };
196
197
  }
197
- // fallback
198
+ // Unresolved type reference
199
+ if (options.failOnUnresolved) {
200
+ throw new WsdlCompilationError(`Unresolved type reference: "${q.local}" in namespace "${q.ns}".`, {
201
+ element: q.local,
202
+ namespace: q.ns,
203
+ suggestion: "Check that the XSD import for this namespace is included in the WSDL, or use --no-fail-on-unresolved to emit 'any'.",
204
+ });
205
+ }
198
206
  return { tsType: "any", declared: `{${q.ns}}${q.local}` };
199
207
  }
200
208
  function findComplexRec(q) {
@@ -565,6 +573,12 @@ export function compileCatalog(cat, options) {
565
573
  const tns = defs?.["@_targetNamespace"] || "";
566
574
  const bindingDefs = normalizeArray(defs?.["wsdl:binding"] || defs?.["binding"]);
567
575
  const soapBinding = bindingDefs.find(b => Object.keys(b).some(k => k === "soap:binding" || k === "soap12:binding")) || bindingDefs[0];
576
+ if (!soapBinding) {
577
+ throw new WsdlCompilationError("No SOAP binding found in the WSDL document.", {
578
+ file: cat.wsdlUri,
579
+ suggestion: "Ensure the WSDL defines a <wsdl:binding> element with a <soap:binding> or <soap12:binding> child.",
580
+ });
581
+ }
568
582
  // binding @type typically looks like "tns:MyPortType", so resolve via prefixes map
569
583
  const portTypeAttr = soapBinding?.["@_type"];
570
584
  const portTypeQName = portTypeAttr ? resolveQName(portTypeAttr, tns, cat.prefixMap) : undefined;
@@ -32,6 +32,7 @@ export interface GenerateGatewayOptions {
32
32
  emitPlugin?: boolean;
33
33
  emitRuntime?: boolean;
34
34
  stubHandlers?: boolean;
35
+ flattenArrayWrappers?: boolean;
35
36
  }
36
37
  /**
37
38
  * Generates Fastify gateway code from an OpenAPI 3.1 specification
@@ -1 +1 @@
1
- {"version":3,"file":"generateGateway.d.ts","sourceRoot":"","sources":["../../src/gateway/generateGateway.ts"],"names":[],"mappings":"AA6CA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,WAAW,sBAAsB;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,GAAG,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,0BAA0B,CAAC,EAAE,MAAM,EAAE,CAAC;IACtC,OAAO,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,CAAC;IAE/B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsJjF"}
1
+ {"version":3,"file":"generateGateway.d.ts","sourceRoot":"","sources":["../../src/gateway/generateGateway.ts"],"names":[],"mappings":"AA6CA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,WAAW,sBAAsB;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,GAAG,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,0BAA0B,CAAC,EAAE,MAAM,EAAE,CAAC;IACtC,OAAO,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,CAAC;IAE/B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyJjF"}
@@ -165,6 +165,8 @@ export async function generateGateway(opts) {
165
165
  });
166
166
  // Step 4: Emit schemas.ts module
167
167
  emitSchemasModule(outDir, modelsDir, versionSlug, serviceSlug);
168
+ // Determine whether to generate unwrap code for ArrayOf* wrappers
169
+ const shouldUnwrap = opts.flattenArrayWrappers !== false && catalog;
168
170
  // Step 5: Emit route files (with handlers or stubs)
169
171
  // Note: Route URLs come from OpenAPI paths which already include any base path
170
172
  if (stubHandlers) {
@@ -173,11 +175,11 @@ export async function generateGateway(opts) {
173
175
  }
174
176
  else {
175
177
  // Full handler mode: emit working implementations
176
- emitRouteFilesWithHandlers(outDir, routesDir, versionSlug, serviceSlug, operations, importsMode, clientMeta);
178
+ emitRouteFilesWithHandlers(outDir, routesDir, versionSlug, serviceSlug, operations, importsMode, clientMeta, shouldUnwrap ? catalog : undefined);
177
179
  }
178
180
  // Step 6: Emit runtime.ts (if enabled)
179
181
  if (emitRuntime) {
180
- emitRuntimeModule(outDir, versionSlug, serviceSlug);
182
+ emitRuntimeModule(outDir, versionSlug, serviceSlug, shouldUnwrap ? catalog : undefined);
181
183
  }
182
184
  // Step 7: Emit plugin.ts and type-check fixture (if enabled)
183
185
  if (emitPlugin) {
@@ -103,20 +103,7 @@ export declare function emitSchemasModule(outDir: string, modelsDir: string, ver
103
103
  * @param {"js"|"ts"|"bare"} importsMode - Import-extension mode for generated TypeScript modules
104
104
  */
105
105
  export declare function emitRouteFiles(outDir: string, routesDir: string, versionSlug: string, serviceSlug: string, operations: OperationMetadata[], importsMode: "js" | "ts" | "bare"): void;
106
- /**
107
- * Emits runtime.ts module with envelope builders and error handling utilities
108
- *
109
- * Generated code includes:
110
- * - Response envelope types (SuccessEnvelope, ErrorEnvelope)
111
- * - buildSuccessEnvelope() and buildErrorEnvelope() functions
112
- * - classifyError() for mapping errors to HTTP status codes
113
- * - createGatewayErrorHandler_{version}_{service}() factory function
114
- *
115
- * @param {string} outDir - Root output directory
116
- * @param {string} versionSlug - Version slug for function naming
117
- * @param {string} serviceSlug - Service slug for function naming
118
- */
119
- export declare function emitRuntimeModule(outDir: string, versionSlug: string, serviceSlug: string): void;
106
+ export declare function emitRuntimeModule(outDir: string, versionSlug: string, serviceSlug: string, catalog?: any): void;
120
107
  /**
121
108
  * Emits plugin.ts module as the primary Fastify plugin wrapper
122
109
  *
@@ -165,5 +152,5 @@ export declare function emitTypeCheckFixture(outDir: string, clientMeta: ClientM
165
152
  * @param {"js"|"ts"|"bare"} importsMode - Import-extension mode
166
153
  * @param {ClientMeta} clientMeta - Client class metadata
167
154
  */
168
- export declare function emitRouteFilesWithHandlers(outDir: string, routesDir: string, versionSlug: string, serviceSlug: string, operations: OperationMetadata[], importsMode: "js" | "ts" | "bare", clientMeta: ClientMeta): void;
155
+ export declare function emitRouteFilesWithHandlers(outDir: string, routesDir: string, versionSlug: string, serviceSlug: string, operations: OperationMetadata[], importsMode: "js" | "ts" | "bare", clientMeta: ClientMeta, catalog?: any): void;
169
156
  //# sourceMappingURL=generators.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"generators.d.ts","sourceRoot":"","sources":["../../src/gateway/generators.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"generators.d.ts","sourceRoot":"","sources":["../../src/gateway/generators.ts"],"names":[],"mappings":"AAcA,OAAO,EAAC,KAAK,UAAU,EAAE,KAAK,eAAe,EAA6C,MAAM,cAAc,CAAC;AAE/G;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC5B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA6BxB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,eAAe,EACpB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACtC,0BAA0B,EAAE,MAAM,EAAE,EACpC,iBAAiB,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,GAAG,EACvH,UAAU,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,MAAM,EACnC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GACnC,iBAAiB,EAAE,CA2HrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,IAAI,CAsBN;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,iBAAiB,EAAE,EAC/B,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAChC,IAAI,CA2CN;AAqCD,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,GAAG,GACZ,IAAI,CA4ON;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,UAAU,EACtB,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,iBAAiB,EAAE,GAC9B,IAAI,CAwFN;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,EACtB,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAChC,IAAI,CA6BN;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,iBAAiB,EAAE,EAC/B,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,UAAU,EACtB,OAAO,CAAC,EAAE,GAAG,GACZ,IAAI,CAkGN"}
@@ -288,9 +288,88 @@ export function emitRouteFiles(outDir, routesDir, versionSlug, serviceSlug, oper
288
288
  * @param {string} versionSlug - Version slug for function naming
289
289
  * @param {string} serviceSlug - Service slug for function naming
290
290
  */
291
- export function emitRuntimeModule(outDir, versionSlug, serviceSlug) {
291
+ /**
292
+ * Detects ArrayOf* wrapper types from catalog type metadata.
293
+ *
294
+ * An ArrayOf* wrapper has exactly one element with max "unbounded" (or > 1)
295
+ * and no attributes — mirroring the logic in generateSchemas.ts isArrayWrapper().
296
+ *
297
+ * @param catalog - Compiled catalog object
298
+ * @returns Record mapping wrapper type name to inner element property name
299
+ */
300
+ function detectArrayWrappers(catalog) {
301
+ const wrappers = {};
302
+ for (const t of catalog.types || []) {
303
+ if (t.attrs && t.attrs.length !== 0)
304
+ continue;
305
+ if (!t.elems || t.elems.length !== 1)
306
+ continue;
307
+ const e = t.elems[0];
308
+ if (e.max !== "unbounded" && !(e.max > 1))
309
+ continue;
310
+ wrappers[t.name] = e.name;
311
+ }
312
+ return wrappers;
313
+ }
314
+ export function emitRuntimeModule(outDir, versionSlug, serviceSlug, catalog) {
292
315
  const vSlug = slugName(versionSlug);
293
316
  const sSlug = slugName(serviceSlug);
317
+ // Build unwrap maps from catalog if provided
318
+ let unwrapSection = "";
319
+ if (catalog) {
320
+ const arrayWrappers = detectArrayWrappers(catalog);
321
+ const childTypes = catalog.meta?.childType || {};
322
+ // Only emit if there are actual wrapper types to unwrap
323
+ if (Object.keys(arrayWrappers).length > 0) {
324
+ unwrapSection = `
325
+ /**
326
+ * ArrayOf* wrapper type → inner element property name.
327
+ * These types are flattened to plain arrays in the OpenAPI schema,
328
+ * so the runtime must unwrap { InnerElement: [...] } → [...].
329
+ */
330
+ const ARRAY_WRAPPERS: Record<string, string> = ${JSON.stringify(arrayWrappers, null, 2)};
331
+
332
+ /**
333
+ * Type name → { propertyName: propertyTypeName } for recursive unwrapping.
334
+ */
335
+ const CHILDREN_TYPES: Record<string, Record<string, string>> = ${JSON.stringify(childTypes, null, 2)};
336
+
337
+ /**
338
+ * Recursively unwraps ArrayOf* wrapper objects in a SOAP response so the
339
+ * data matches the flattened OpenAPI array schemas.
340
+ *
341
+ * Safe to call on any response — returns data unchanged when the type
342
+ * has no wrapper fields.
343
+ *
344
+ * @param data - SOAP response data (potentially with wrapper objects)
345
+ * @param typeName - The type name for the current data level
346
+ * @returns Unwrapped data matching the OpenAPI schema shape
347
+ */
348
+ export function unwrapArrayWrappers(data: unknown, typeName: string): unknown {
349
+ if (data == null || typeof data !== "object") return data;
350
+
351
+ // If this type is itself a wrapper, unwrap it
352
+ if (typeName in ARRAY_WRAPPERS) {
353
+ const innerKey = ARRAY_WRAPPERS[typeName];
354
+ return (data as Record<string, unknown>)[innerKey] ?? [];
355
+ }
356
+
357
+ // Recurse into children whose types may contain wrappers
358
+ if (typeName in CHILDREN_TYPES) {
359
+ const children = CHILDREN_TYPES[typeName];
360
+ for (const [propName, propType] of Object.entries(children)) {
361
+ const val = (data as Record<string, unknown>)[propName];
362
+ if (val !== undefined) {
363
+ (data as Record<string, unknown>)[propName] = unwrapArrayWrappers(val, propType);
364
+ }
365
+ }
366
+ }
367
+
368
+ return data;
369
+ }
370
+ `;
371
+ }
372
+ }
294
373
  const runtimeTs = `/**
295
374
  * Gateway Runtime Utilities
296
375
  *
@@ -463,7 +542,7 @@ export function createGatewayErrorHandler_${vSlug}_${sSlug}() {
463
542
  };
464
543
  }
465
544
  `;
466
- fs.writeFileSync(path.join(outDir, "runtime.ts"), runtimeTs, "utf8");
545
+ fs.writeFileSync(path.join(outDir, "runtime.ts"), runtimeTs + unwrapSection, "utf8");
467
546
  }
468
547
  /**
469
548
  * Emits plugin.ts module as the primary Fastify plugin wrapper
@@ -485,11 +564,6 @@ export function emitPluginModule(outDir, versionSlug, serviceSlug, clientMeta, i
485
564
  const vSlug = slugName(versionSlug);
486
565
  const sSlug = slugName(serviceSlug);
487
566
  const suffix = importsMode === "bare" ? "" : `.${importsMode}`;
488
- // Build named operations interface methods from operation metadata
489
- const operationMethods = operations
490
- .filter(op => op.clientMethodName)
491
- .map(op => ` ${op.clientMethodName}(args: unknown): Promise<{ response: unknown; headers: unknown }>;`)
492
- .join("\n");
493
567
  const pluginTs = `/**
494
568
  * ${clientMeta.className} Gateway Plugin
495
569
  *
@@ -499,10 +573,14 @@ export function emitPluginModule(outDir, versionSlug, serviceSlug, clientMeta, i
499
573
  import fp from "fastify-plugin";
500
574
  import type { FastifyInstance, FastifyPluginOptions } from "fastify";
501
575
  import type { ${clientMeta.className} } from "${clientMeta.pluginImportPath}";
576
+ import type { ${clientMeta.className}Operations } from "${clientMeta.operationsImportPath}";
502
577
  import { registerSchemas_${vSlug}_${sSlug} } from "./schemas${suffix}";
503
578
  import { registerRoutes_${vSlug}_${sSlug} } from "./routes${suffix}";
504
579
  import { createGatewayErrorHandler_${vSlug}_${sSlug} } from "./runtime${suffix}";
505
580
 
581
+ // Re-export the operations interface for convenience
582
+ export type { ${clientMeta.className}Operations };
583
+
506
584
  /**
507
585
  * Options for the ${clientMeta.className} gateway plugin
508
586
  */
@@ -510,8 +588,9 @@ export interface ${clientMeta.className}GatewayOptions extends FastifyPluginOpti
510
588
  /**
511
589
  * SOAP client instance (pre-configured).
512
590
  * The client should be instantiated with appropriate source and security settings.
591
+ * Accepts the concrete client class or any implementation of ${clientMeta.className}Operations.
513
592
  */
514
- client: ${clientMeta.className};
593
+ client: ${clientMeta.className}Operations;
515
594
  /**
516
595
  * Optional additional route prefix applied at runtime.
517
596
  * Note: If you used --openapi-base-path during generation, routes already have that prefix baked in.
@@ -520,21 +599,9 @@ export interface ${clientMeta.className}GatewayOptions extends FastifyPluginOpti
520
599
  prefix?: string;
521
600
  }
522
601
 
523
- /**
524
- * Convenience type listing all SOAP operations with \`args: unknown\` signatures.
525
- *
526
- * This interface is NOT used for the decorator type (which uses the concrete
527
- * client class for full type safety). It is exported as a lightweight alternative
528
- * for mocking or testing scenarios where importing the full client class is
529
- * undesirable.
530
- */
531
- export interface ${clientMeta.className}Operations {
532
- ${operationMethods}
533
- }
534
-
535
602
  declare module "fastify" {
536
603
  interface FastifyInstance {
537
- ${clientMeta.decoratorName}: ${clientMeta.className};
604
+ ${clientMeta.decoratorName}: ${clientMeta.className}Operations;
538
605
  }
539
606
  }
540
607
 
@@ -598,11 +665,19 @@ export function emitTypeCheckFixture(outDir, clientMeta, importsMode) {
598
665
  * Auto-generated. Not intended for runtime use.
599
666
  */
600
667
  import type { ${clientMeta.className} } from "${clientMeta.pluginImportPath}";
668
+ import type { ${clientMeta.className}Operations } from "${clientMeta.operationsImportPath}";
601
669
  import type { ${clientMeta.className}GatewayOptions } from "./plugin${suffix}";
602
670
 
603
- // This function verifies structural compatibility at the type level.
604
- // If the plugin options interface diverges from the client class, this
605
- // will produce a compile error with a clear message.
671
+ // Verify the concrete client class satisfies the operations interface.
672
+ // If the client class diverges from the operations interface, this
673
+ // will produce a compile error.
674
+ function _assertClientSatisfiesOps(client: ${clientMeta.className}): ${clientMeta.className}Operations {
675
+ return client;
676
+ }
677
+ void _assertClientSatisfiesOps;
678
+
679
+ // Verify the concrete client class is accepted by plugin options.
680
+ // This ensures the gateway plugin can be used with the generated client.
606
681
  function _assertClientCompatible(client: ${clientMeta.className}): void {
607
682
  const _opts: ${clientMeta.className}GatewayOptions = { client };
608
683
  void _opts;
@@ -630,13 +705,15 @@ void _assertClientCompatible;
630
705
  * @param {"js"|"ts"|"bare"} importsMode - Import-extension mode
631
706
  * @param {ClientMeta} clientMeta - Client class metadata
632
707
  */
633
- export function emitRouteFilesWithHandlers(outDir, routesDir, versionSlug, serviceSlug, operations, importsMode, clientMeta) {
708
+ export function emitRouteFilesWithHandlers(outDir, routesDir, versionSlug, serviceSlug, operations, importsMode, clientMeta, catalog) {
634
709
  fs.mkdirSync(routesDir, { recursive: true });
635
710
  // Sort operations for deterministic output
636
711
  operations.sort((a, b) => a.operationSlug.localeCompare(b.operationSlug));
637
712
  const suffix = importsMode === "bare" ? "" : `.${importsMode}`;
638
713
  const vSlug = slugName(versionSlug);
639
714
  const sSlug = slugName(serviceSlug);
715
+ // Detect if unwrap code should be emitted
716
+ const hasUnwrap = catalog ? Object.keys(detectArrayWrappers(catalog)).length > 0 : false;
640
717
  let routesTs = `import type { FastifyInstance } from "fastify";\n`;
641
718
  operations.forEach((op) => {
642
719
  const fnName = `registerRoute_${vSlug}_${sSlug}_${op.operationSlug.replace(/[^a-zA-Z0-9_]/g, "_")}`;
@@ -661,6 +738,13 @@ export function emitRouteFilesWithHandlers(outDir, routesDir, versionSlug, servi
661
738
  const bodyArg = hasRequestType
662
739
  ? `request.body as ${reqTypeName}`
663
740
  : "request.body";
741
+ // Build the runtime import and return expression based on unwrap availability
742
+ const runtimeImport = hasUnwrap
743
+ ? `import { buildSuccessEnvelope, unwrapArrayWrappers } from "../runtime${suffix}";`
744
+ : `import { buildSuccessEnvelope } from "../runtime${suffix}";`;
745
+ const returnExpr = hasUnwrap
746
+ ? `return buildSuccessEnvelope(unwrapArrayWrappers(result.response, "${resTypeName}"));`
747
+ : `return buildSuccessEnvelope(result.response);`;
664
748
  // Note: op.path comes from OpenAPI and already includes any base path
665
749
  let routeTs = `/**
666
750
  * Route: ${op.method.toUpperCase()} ${op.path}
@@ -671,7 +755,7 @@ export function emitRouteFilesWithHandlers(outDir, routesDir, versionSlug, servi
671
755
  */
672
756
  import type { FastifyInstance } from "fastify";
673
757
  ${typeImport}import schema from "../schemas/operations/${op.operationSlug}.json" with { type: "json" };
674
- import { buildSuccessEnvelope } from "../runtime${suffix}";
758
+ ${runtimeImport}
675
759
 
676
760
  export async function ${fnName}(fastify: FastifyInstance) {
677
761
  fastify.route${routeGeneric}({
@@ -681,7 +765,7 @@ export async function ${fnName}(fastify: FastifyInstance) {
681
765
  handler: async (request) => {
682
766
  const client = fastify.${clientMeta.decoratorName};
683
767
  const result = await client.${clientMethod}(${bodyArg});
684
- return buildSuccessEnvelope(result.response);
768
+ ${returnExpr}
685
769
  },
686
770
  });
687
771
  }
@@ -141,9 +141,10 @@ export declare function buildParamSchemasForOperation(pathItem: any, operation:
141
141
  * @interface ClientMeta
142
142
  * @property {string} className - Client class name (e.g., "EVRNService")
143
143
  * @property {string} decoratorName - Fastify decorator name (e.g., "evrnserviceClient")
144
- * @property {string} importPath - Import path relative to routes/ directory — for future typed route handlers
145
- * @property {string} typesImportPath - Types import path relative to routes/ directory — for future typed route handlers
144
+ * @property {string} importPath - Import path relative to routes/ directory — for typed route handlers
145
+ * @property {string} typesImportPath - Types import path relative to routes/ directory — for typed route handlers
146
146
  * @property {string} pluginImportPath - Import path relative to gateway output directory — used by emitPluginModule()
147
+ * @property {string} operationsImportPath - Operations interface import path relative to gateway output directory
147
148
  */
148
149
  export interface ClientMeta {
149
150
  className: string;
@@ -151,6 +152,7 @@ export interface ClientMeta {
151
152
  importPath: string;
152
153
  typesImportPath: string;
153
154
  pluginImportPath: string;
155
+ operationsImportPath: string;
154
156
  }
155
157
  /**
156
158
  * Extended operation metadata for full handler generation