@techspokes/typescript-wsdl-client 1.1.8 → 1.2.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 (73) hide show
  1. package/README.md +17 -3
  2. package/dist/checkedPlanning.js +27 -4
  3. package/dist/cli.js +17 -1
  4. package/dist/client/generateClient.js +72 -16
  5. package/dist/client/generateOperations.js +30 -11
  6. package/dist/client/generateUtils.js +5 -1
  7. package/dist/compiler/canonicalGraph.d.ts +4 -15
  8. package/dist/compiler/canonicalGraph.js +1 -8
  9. package/dist/compiler/componentRecordProjection.d.ts +3 -0
  10. package/dist/compiler/componentRecordProjection.js +10 -0
  11. package/dist/compiler/declarationIdentity.d.ts +22 -0
  12. package/dist/compiler/declarationIdentity.js +9 -0
  13. package/dist/compiler/directCodec.d.ts +22 -2
  14. package/dist/compiler/directCodec.js +40 -7
  15. package/dist/compiler/directOperation.d.ts +26 -4
  16. package/dist/compiler/directOperation.js +59 -5
  17. package/dist/compiler/directSoap.js +24 -7
  18. package/dist/compiler/directSourceCapsule.js +59 -3
  19. package/dist/compiler/directStructure.js +6 -2
  20. package/dist/compiler/directStructurePlan.d.ts +10 -1
  21. package/dist/compiler/directStructurePlan.js +27 -7
  22. package/dist/compiler/directValues.d.ts +15 -3
  23. package/dist/compiler/directValues.js +20 -4
  24. package/dist/compiler/legacyDeclarations.d.ts +26 -0
  25. package/dist/compiler/legacyDeclarations.js +144 -0
  26. package/dist/compiler/projectionScalar.d.ts +18 -1
  27. package/dist/compiler/projectionScalar.js +47 -13
  28. package/dist/compiler/rpcStringValues.d.ts +30 -0
  29. package/dist/compiler/rpcStringValues.js +196 -0
  30. package/dist/compiler/rpcTypedValues.d.ts +48 -0
  31. package/dist/compiler/rpcTypedValues.js +190 -0
  32. package/dist/compiler/schemaCompiler.js +44 -109
  33. package/dist/compiler/semanticCatalog.d.ts +2 -2
  34. package/dist/gateway/generateComposedGateway.d.ts +16 -0
  35. package/dist/gateway/generateComposedGateway.js +65 -0
  36. package/dist/gateway/generateGateway.js +1 -0
  37. package/dist/gateway/generators.js +19 -4
  38. package/dist/gateway/helpers.d.ts +1 -0
  39. package/dist/gateway/helpers.js +15 -0
  40. package/dist/generation/componentShape.js +25 -1
  41. package/dist/index.d.ts +4 -0
  42. package/dist/index.js +2 -0
  43. package/dist/loader/orderedSyntax.d.ts +4 -0
  44. package/dist/loader/orderedSyntax.js +18 -4
  45. package/dist/multiServicePipeline.d.ts +41 -0
  46. package/dist/multiServicePipeline.js +287 -0
  47. package/dist/openapi/composeOpenAPI.d.ts +29 -0
  48. package/dist/openapi/composeOpenAPI.js +322 -0
  49. package/dist/pipeline.d.ts +1 -1
  50. package/dist/pipeline.js +2 -0
  51. package/docs/agent-skill.md +10 -1
  52. package/docs/api-reference.md +127 -9
  53. package/docs/checked-gateway.md +2 -0
  54. package/docs/checked-generation.md +11 -5
  55. package/docs/cli-reference.md +25 -5
  56. package/docs/concepts.md +4 -5
  57. package/docs/configuration.md +10 -3
  58. package/docs/corpus.md +1 -1
  59. package/docs/gateway-guide.md +104 -27
  60. package/docs/generated-code.md +12 -6
  61. package/docs/migration-playbook.md +4 -0
  62. package/docs/migration.md +124 -6
  63. package/docs/output-anatomy.md +20 -17
  64. package/docs/production.md +7 -5
  65. package/docs/start-here.md +12 -2
  66. package/docs/supported-patterns.md +17 -0
  67. package/docs/testing.md +2 -0
  68. package/docs/troubleshooting.md +3 -1
  69. package/llms.txt +9 -1
  70. package/package.json +2 -2
  71. package/src/runtime/clientStreamMethods.tpl.txt +156 -148
  72. package/src/runtime/operationsStreamHelper.tpl.txt +6 -6
  73. package/src/runtime/streamXml.ts +48 -20
@@ -1,6 +1,7 @@
1
1
  import { getChildrenWithLocalName, getFirstWithLocalName, normalizeArray, pascal, resolveQName, } from "../util/tools.js";
2
2
  import { xsdToTsPrimitive } from "../xsd/primitives.js";
3
3
  import { WsdlCompilationError } from "../util/errors.js";
4
+ import { LegacyDeclarations } from "./legacyDeclarations.js";
4
5
  // XML Schema namespace constant
5
6
  const XS = "http://www.w3.org/2001/XMLSchema";
6
7
  /**
@@ -13,25 +14,6 @@ const XS = "http://www.w3.org/2001/XMLSchema";
13
14
  function qkey(q) {
14
15
  return `{${q.ns}}${q.local}`;
15
16
  }
16
- /**
17
- * Generates a name for an inline complex type
18
- *
19
- * This function creates appropriate TypeScript names for anonymous types
20
- * that are defined inline within element declarations.
21
- *
22
- * @param {string} parentTypeName - Name of the parent type
23
- * @param {string} [propName] - Name of the property containing the inline type
24
- * @param {number|"unbounded"} [_max] - Maximum occurrence (unused but kept for API compatibility)
25
- * @returns {string} - Generated TypeScript name for the inline type
26
- */
27
- function makeInlineTypeName(parentTypeName, propName, _max) {
28
- const base = pascal(parentTypeName || "AnonParent");
29
- const prop = pascal(propName || "");
30
- if (prop) {
31
- return prop;
32
- }
33
- return `${base}Anon`;
34
- }
35
17
  // --- Minimal WS-Policy scanning (inline policies only; PolicyReference not resolved) ---
36
18
  /**
37
19
  * Extracts security requirements from WS-Policy nodes
@@ -255,9 +237,13 @@ export function compileCatalog(cat, options, streamConfig) {
255
237
  }
256
238
  }
257
239
  }
258
- // outputs & state
259
- const compiledMap = new Map(); // key: ns|name
260
- const aliasMap = new Map(); // key: ns|name
240
+ const declarations = new LegacyDeclarations([
241
+ ...Array.from(complexTypes.values(), rec => ({ kind: "complex", name: String(rec.node["@_name"]), ns: rec.tns, node: rec.node, prefixes: rec.prefixes })),
242
+ ...Array.from(simpleTypes.values(), rec => ({ kind: "simple", name: String(rec.node["@_name"]), ns: rec.tns, node: rec.node, prefixes: rec.prefixes })),
243
+ ...Array.from(elements.values(), rec => ({ kind: "element", name: String(rec.node["@_name"]), ns: rec.tns, node: rec.node, prefixes: rec.prefixes })),
244
+ ]);
245
+ const compiledMap = new Map();
246
+ const aliasMap = new Map();
261
247
  const inProgress = new Set();
262
248
  // meta accumulators
263
249
  const attrSpec = {};
@@ -314,22 +300,23 @@ export function compileCatalog(cat, options, streamConfig) {
314
300
  const list = getFirstWithLocalName(simpleNode, "list");
315
301
  if (list?.["@_itemType"]) {
316
302
  const q = resolveQName(list["@_itemType"], schemaNS, prefixes);
317
- const declared = q.ns === XS ? `xs:${q.local}` : `{${q.ns}}${q.local}`;
318
- return { tsType: `${xsdToTsPrimitive(declared, options?.primitive)}[]`, declared };
303
+ const item = resolveTypeRef(q, schemaNS, prefixes);
304
+ const itemType = item.tsType.includes("|") ? `(${item.tsType})` : item.tsType;
305
+ return { tsType: `${itemType}[]`, declared: item.declared };
319
306
  }
320
307
  // fallback
321
308
  return { tsType: "string", declared: "xs:string" };
322
309
  }
323
310
  /** Compile and cache a named simpleType */
324
311
  function getOrCompileAlias(name, sNode, schemaNS, prefixes) {
325
- const key = `${schemaNS}|${name}`;
312
+ const key = declarations.type(schemaNS, name);
326
313
  const present = aliasMap.get(key);
327
314
  if (present) {
328
315
  return present;
329
316
  }
330
317
  const { tsType, declared, jsdoc } = compileSimpleTypeNode(sNode, schemaNS, prefixes);
331
318
  const alias = {
332
- name: pascal(name),
319
+ name: declarations.name(key),
333
320
  ns: schemaNS,
334
321
  tsType,
335
322
  declared,
@@ -373,19 +360,26 @@ export function compileCatalog(cat, options, streamConfig) {
373
360
  const k = qkey(q);
374
361
  return complexTypes.get(k);
375
362
  }
376
- function getOrCompileComplex(name, cnode, schemaNS, prefixes) {
377
- // Early cycle detection: check if we're already processing this raw name + namespace combo
378
- // BEFORE calling pascal() to avoid stack overflow on circular references
379
- const rawKey = `${schemaNS}|${name}`;
380
- if (inProgress.has(rawKey)) {
381
- // Return a minimal stub to break the cycle
382
- const stubName = pascal(name);
383
- return { name: stubName, ns: schemaNS, attrs: [], elems: [] };
363
+ function getOrCompileComplex(name, cnode, schemaNS, prefixes, declarationId = declarations.type(schemaNS, name)) {
364
+ const key = declarationId;
365
+ const outName = declarations.name(key);
366
+ const present = compiledMap.get(key);
367
+ if (present)
368
+ return present;
369
+ if (inProgress.has(key))
370
+ return { name: outName, ns: schemaNS, attrs: [], elems: [] };
371
+ inProgress.add(key);
372
+ try {
373
+ return compileComplexBody(cnode, schemaNS, prefixes, declarationId);
374
+ }
375
+ finally {
376
+ inProgress.delete(key);
384
377
  }
385
- inProgress.add(rawKey);
386
- const outName = pascal(name);
378
+ }
379
+ function compileComplexBody(cnode, schemaNS, prefixes, declarationId) {
380
+ const key = declarationId;
381
+ const outName = declarations.name(key);
387
382
  const typeDoc = extractAnnotationDocumentation(cnode);
388
- const key = `${schemaNS}|${outName}`;
389
383
  // hoisted helpers for merging and collecting
390
384
  const mergeAttrs = (into, list) => {
391
385
  const idx = new Map();
@@ -415,40 +409,6 @@ export function compileCatalog(cat, options, streamConfig) {
415
409
  }
416
410
  }
417
411
  };
418
- const mergeChoiceGroups = (into, list) => {
419
- if (list.length === 0)
420
- return into;
421
- const merged = into ? [...into] : [];
422
- const idx = new Map();
423
- merged.forEach((g, i) => idx.set(g.name, i));
424
- for (const g of list) {
425
- const pos = idx.get(g.name);
426
- if (pos == null) {
427
- idx.set(g.name, merged.length);
428
- merged.push(g);
429
- }
430
- else {
431
- merged[pos] = g;
432
- }
433
- }
434
- return merged;
435
- };
436
- const mergeJsonUnique = (into, list) => {
437
- if (list.length === 0)
438
- return into;
439
- const merged = into ? [...into] : [];
440
- const seen = new Set(merged.map((w) => JSON.stringify(w)));
441
- for (const w of list) {
442
- const key = JSON.stringify(w);
443
- if (!seen.has(key)) {
444
- seen.add(key);
445
- merged.push(w);
446
- }
447
- }
448
- return merged;
449
- };
450
- const mergeWildcards = (into, list) => mergeJsonUnique(into, list);
451
- const mergeAttributeWildcards = (into, list) => mergeJsonUnique(into, list);
452
412
  const collectAttributes = (node) => {
453
413
  const out = [];
454
414
  const attrs = getChildrenWithLocalName(node, "attribute");
@@ -547,7 +507,7 @@ export function compileCatalog(cat, options, streamConfig) {
547
507
  }
548
508
  return out;
549
509
  };
550
- const compileElementParticle = (ownerTypeName, e) => {
510
+ const compileElementParticle = (e) => {
551
511
  const nameOrRef = e["@_name"] || e["@_ref"];
552
512
  if (!nameOrRef)
553
513
  return undefined;
@@ -557,8 +517,8 @@ export function compileCatalog(cat, options, streamConfig) {
557
517
  const inlineComplex = getFirstWithLocalName(e, "complexType");
558
518
  const inlineSimple = getFirstWithLocalName(e, "simpleType");
559
519
  if (inlineComplex) {
560
- const inlineName = makeInlineTypeName(ownerTypeName, propName || nameOrRef, max);
561
- const rec = getOrCompileComplex(inlineName, inlineComplex, schemaNS, prefixes);
520
+ const inlineId = declarations.inline(declarationId, e);
521
+ const rec = getOrCompileComplex(String(propName || nameOrRef), inlineComplex, schemaNS, prefixes, inlineId);
562
522
  return {
563
523
  name: propName || nameOrRef,
564
524
  tsType: rec.name,
@@ -603,7 +563,7 @@ export function compileCatalog(cat, options, streamConfig) {
603
563
  });
604
564
  const recurse = (groupNode, inherited = one, inheritSequenceOccurs = true) => {
605
565
  for (const e of getChildrenWithLocalName(groupNode, "element")) {
606
- const particle = compileElementParticle(ownerTypeName, e);
566
+ const particle = compileElementParticle(e);
607
567
  if (!particle)
608
568
  continue;
609
569
  // @constraints Preserve disabled particles until the content model can represent their absence (#145).
@@ -636,7 +596,7 @@ export function compileCatalog(cat, options, streamConfig) {
636
596
  const branches = [];
637
597
  let branchSourceOrder = 0;
638
598
  for (const e of getChildrenWithLocalName(child.node, "element")) {
639
- const particle = compileElementParticle(ownerTypeName, e);
599
+ const particle = compileElementParticle(e);
640
600
  if (particle) {
641
601
  branches.push({
642
602
  name: particle.name,
@@ -672,29 +632,6 @@ export function compileCatalog(cat, options, streamConfig) {
672
632
  recurse(node);
673
633
  return out;
674
634
  };
675
- const present = compiledMap.get(key);
676
- if (present) {
677
- // On duplicate definitions, merge new attributes and elements
678
- const newAttrs = collectAttributes(cnode);
679
- const newElems = collectParticles(outName, cnode);
680
- const newWildcards = collectWildcards(cnode);
681
- const newAttributeWildcards = collectAttributeWildcards(cnode);
682
- const newChoiceGroups = collectChoiceGroups(outName, cnode);
683
- mergeAttrs(present.attrs, newAttrs);
684
- mergeElems(present.elems, newElems);
685
- present.wildcards = mergeWildcards(present.wildcards, newWildcards);
686
- present.attributeWildcards = mergeAttributeWildcards(present.attributeWildcards, newAttributeWildcards);
687
- const mergedChoiceGroups = mergeChoiceGroups(present.choiceGroups, newChoiceGroups);
688
- if (mergedChoiceGroups && mergedChoiceGroups.length > 0) {
689
- present.choiceGroups = mergedChoiceGroups;
690
- }
691
- if (!present.doc && typeDoc) {
692
- present.doc = typeDoc;
693
- }
694
- // Remove from inProgress since we're done with this cycle
695
- inProgress.delete(rawKey);
696
- return present;
697
- }
698
635
  // result accumulators
699
636
  const attrs = [];
700
637
  const elems = [];
@@ -749,7 +686,6 @@ export function compileCatalog(cat, options, streamConfig) {
749
686
  ...(localChoiceGroups.length > 0 ? { localChoiceGroups } : {}),
750
687
  };
751
688
  compiledMap.set(key, result);
752
- inProgress.delete(key);
753
689
  return result;
754
690
  }
755
691
  }
@@ -784,7 +720,6 @@ export function compileCatalog(cat, options, streamConfig) {
784
720
  ...(attributeWildcards.length > 0 ? { attributeWildcards } : {}),
785
721
  };
786
722
  compiledMap.set(key, result);
787
- inProgress.delete(rawKey);
788
723
  return result;
789
724
  };
790
725
  if (ext) {
@@ -811,20 +746,19 @@ export function compileCatalog(cat, options, streamConfig) {
811
746
  ...(choiceGroups.length > 0 ? { choiceGroups } : {}),
812
747
  };
813
748
  compiledMap.set(key, result);
814
- inProgress.delete(rawKey);
815
749
  return result;
816
750
  }
817
751
  // Helper: compile a global element into a surface type (wrapper)
818
752
  function compileElementAsType(name, enode, schemaNS, prefixes) {
819
- const outName = pascal(name);
753
+ const key = declarations.surface(schemaNS, name);
754
+ const outName = declarations.name(key);
820
755
  const elementDoc = extractAnnotationDocumentation(enode);
821
- const key = `${schemaNS}|${outName}`;
822
756
  const present = compiledMap.get(key);
823
757
  if (present)
824
758
  return present;
825
759
  const inlineComplex = getFirstWithLocalName(enode, "complexType");
826
760
  if (inlineComplex) {
827
- return getOrCompileComplex(name, inlineComplex, schemaNS, prefixes);
761
+ return getOrCompileComplex(name, inlineComplex, schemaNS, prefixes, key);
828
762
  }
829
763
  const inlineSimple = getFirstWithLocalName(enode, "simpleType");
830
764
  if (inlineSimple) {
@@ -871,7 +805,7 @@ export function compileCatalog(cat, options, streamConfig) {
871
805
  // record alias: ElementName -> BaseTypeName
872
806
  const aliasKey = key;
873
807
  const existingAlias = aliasMap.get(aliasKey);
874
- const declared = `{${base.ns}}${base.name}`;
808
+ const declared = `{${base.ns}}${q.local}`;
875
809
  if (!existingAlias) {
876
810
  aliasMap.set(aliasKey, { name: outName, ns: schemaNS, tsType: base.name, declared, doc: elementDoc });
877
811
  }
@@ -1138,9 +1072,10 @@ export function compileCatalog(cat, options, streamConfig) {
1138
1072
  const inputElement = elementOfMessage(inMsg);
1139
1073
  const outputElement = elementOfMessage(outMsg);
1140
1074
  const doc = extractDirectDocumentation(po);
1141
- // Derive TypeScript type names from element local names
1142
- const inputTypeName = inputElement ? pascal(inputElement.local) : undefined;
1143
- const outputTypeName = outputElement ? pascal(outputElement.local) : undefined;
1075
+ const surfaceName = (element) => elements.has(qkey(element))
1076
+ ? declarations.name(declarations.surface(element.ns, element.local)) : pascal(element.local);
1077
+ const inputTypeName = inputElement ? surfaceName(inputElement) : undefined;
1078
+ const outputTypeName = outputElement ? surfaceName(outputElement) : undefined;
1144
1079
  const op = {
1145
1080
  name,
1146
1081
  soapAction: bOps.get(name) || "",
@@ -58,8 +58,8 @@ export declare function deriveCompatibilityView(graph: CanonicalGraph): {
58
58
  model: "xsd10-faithful-v1";
59
59
  symbols: {
60
60
  id: string;
61
- kind: "element" | "attribute" | "wsdl" | "complexType" | "simpleType" | "attributeWildcard" | "group" | "attributeGroup" | "particle" | "attributeUse" | "attributeGroupUse";
62
- identity: import("./canonicalGraph.js").Identity;
61
+ kind: "element" | "attribute" | "wsdl" | "group" | "attributeGroup" | "complexType" | "simpleType" | "attributeWildcard" | "particle" | "attributeUse" | "attributeGroupUse";
62
+ identity: import("./declarationIdentity.js").Identity;
63
63
  }[];
64
64
  };
65
65
  /** Internal faithful companion dispatch. Regeneration never fabricates missing structure. */
@@ -0,0 +1,16 @@
1
+ export interface ComposedGatewayService {
2
+ key: string;
3
+ clientDir: string;
4
+ gatewayDir: string;
5
+ clientClassName: string;
6
+ }
7
+ /** Build a typed aggregate over independently generated service routers.
8
+ * @returns Complete generated source; the caller publishes it only after composition succeeds.
9
+ * @constraints Routes already carry generation-time prefixes. Runtime prefixing is refused to keep the served definition accurate.
10
+ */
11
+ export declare function buildComposedGatewaySource(services: readonly ComposedGatewayService[], options: {
12
+ outDir: string;
13
+ openapiFile: string;
14
+ imports?: "js" | "ts" | "bare";
15
+ openapiPath?: string | false;
16
+ }): string;
@@ -0,0 +1,65 @@
1
+ import path from "node:path";
2
+ import { composeGeneratedSource, resolvePreamble } from "../generation/preamble.js";
3
+ /** Build a typed aggregate over independently generated service routers.
4
+ * @returns Complete generated source; the caller publishes it only after composition succeeds.
5
+ * @constraints Routes already carry generation-time prefixes. Runtime prefixing is refused to keep the served definition accurate.
6
+ */
7
+ export function buildComposedGatewaySource(services, options) {
8
+ const suffix = options.imports === "bare" ? "" : `.${options.imports ?? "js"}`;
9
+ const literal = (value) => JSON.stringify(value).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
10
+ const relative = (target) => {
11
+ const resolved = path.relative(options.outDir, target).replaceAll("\\", "/");
12
+ if (path.isAbsolute(resolved) || /^[a-z]:/i.test(resolved))
13
+ throw new Error("Composed outputs must share a filesystem root");
14
+ return resolved.startsWith(".") ? resolved : "./" + resolved;
15
+ };
16
+ const ordered = [...services].sort((a, b) => a.key.localeCompare(b.key));
17
+ const lines = [
18
+ 'import type {FastifyPluginAsync} from "fastify";',
19
+ 'import {types} from "node:util";',
20
+ `import definition from ${literal(relative(options.openapiFile))} with {type: "json"};`,
21
+ ...ordered.flatMap((service, index) => [
22
+ `import {${service.clientClassName} as Client${index}} from ${literal(relative(path.join(service.clientDir, "client" + suffix)))};`,
23
+ `import type {${service.clientClassName}Operations as Operations${index}} from ${literal(relative(path.join(service.clientDir, "operations" + suffix)))};`,
24
+ `import service${index} from ${literal(relative(path.join(service.gatewayDir, "plugin" + suffix)))};`,
25
+ ]),
26
+ "",
27
+ "export const openapi = definition;",
28
+ "export interface MultiServiceClients {",
29
+ ...ordered.map((service, index) => ` ${literal(service.key)}: Operations${index};`),
30
+ "}",
31
+ "export interface MultiServiceClientOptions {",
32
+ ...ordered.map((service, index) => ` ${literal(service.key)}: ConstructorParameters<typeof Client${index}>[0];`),
33
+ "}",
34
+ "export interface MultiServiceGatewayOptions {clients: MultiServiceClients}",
35
+ "function registry(value: unknown): void {",
36
+ ' if (!value || typeof value !== "object" || types.isProxy(value) || ![null, Object.prototype].includes(Object.getPrototypeOf(value) as object | null)) throw new TypeError("Service registry must be a data object");',
37
+ ` const expected = ${literal(ordered.map(service => service.key))};`,
38
+ ' if (Reflect.ownKeys(value).length !== expected.length) throw new TypeError("Service registry must contain exactly the configured services");',
39
+ " for (const key of expected) {",
40
+ " const property = Object.getOwnPropertyDescriptor(value, key);",
41
+ ' if (!property || !Object.hasOwn(property, "value") || !property.value) throw new TypeError("Missing service registry value: " + key);',
42
+ " }",
43
+ "}",
44
+ "",
45
+ "/** Bind each generated SOAP client to its own explicit runtime WSDL and options. */",
46
+ "export function createClients(options: MultiServiceClientOptions): {",
47
+ ...ordered.map((service, index) => ` ${literal(service.key)}: Client${index};`),
48
+ "} {",
49
+ " registry(options);",
50
+ " return {",
51
+ ...ordered.map((service, index) => ` ${literal(service.key)}: new Client${index}(options[${literal(service.key)}]),`),
52
+ " };",
53
+ "}",
54
+ "",
55
+ "const plugin: FastifyPluginAsync<MultiServiceGatewayOptions> = async (fastify, options) => {",
56
+ ' if (fastify.prefix) throw new TypeError("Configure service OpenAPI basePath instead of a runtime aggregate prefix");',
57
+ " registry(options.clients);",
58
+ ...ordered.map((service, index) => ` await fastify.register(async scope => {await scope.register(service${index}, {client: options.clients[${literal(service.key)}]});});`),
59
+ ...(options.openapiPath === false ? [] : [` fastify.get(${literal(options.openapiPath ?? "/openapi.json")}, async () => openapi);`]),
60
+ "};",
61
+ "export default plugin;",
62
+ "",
63
+ ];
64
+ return composeGeneratedSource(lines.join("\n"), resolvePreamble("gateway"));
65
+ }
@@ -131,6 +131,7 @@ export async function generateGateway(opts) {
131
131
  }
132
132
  // Resolve client metadata for full handler generation
133
133
  const clientMeta = resolveClientMeta({
134
+ gatewayDir: opts.outDir,
134
135
  clientDir: opts.clientDir,
135
136
  catalogFile: opts.catalogFile,
136
137
  clientClassName: opts.clientClassName,
@@ -922,7 +922,7 @@ export function emitRouteFilesWithHandlers(outDir, routesDir, versionSlug, servi
922
922
  ? "toJsonArray"
923
923
  : "toNdjson";
924
924
  const runtimeImport = op.stream
925
- ? `import { ${streamHelperName} } from "../runtime${suffix}";`
925
+ ? `import { ${streamHelperName}${hasUnwrap ? ", unwrapArrayWrappers" : ""} } from "../runtime${suffix}";`
926
926
  : hasUnwrap
927
927
  ? `import { buildSuccessEnvelope, unwrapArrayWrappers } from "../runtime${suffix}";`
928
928
  : `import { buildSuccessEnvelope } from "../runtime${suffix}";`;
@@ -942,9 +942,24 @@ export function emitRouteFilesWithHandlers(outDir, routesDir, versionSlug, servi
942
942
  const handlerBody = op.stream
943
943
  ? ` handler: async (request, reply) => {
944
944
  const client = fastify.${clientMeta.decoratorName};
945
- const result = await client.${clientMethod}(${bodyArg});
946
- reply.type(${JSON.stringify(op.stream.mediaType)});
947
- return reply.send(${streamHelperName}(result.records));
945
+ const controller = new AbortController();
946
+ const disconnected = () => { if (!reply.raw.writableEnded) controller.abort(); };
947
+ request.raw.once("aborted", disconnected);
948
+ reply.raw.once("close", disconnected);
949
+ if (request.raw.aborted || reply.raw.destroyed) controller.abort();
950
+ try {
951
+ const result = await client.${clientMethod}(${bodyArg}, { signal: controller.signal });
952
+ reply.type(${JSON.stringify(op.stream.mediaType)});
953
+ ${hasUnwrap ? `async function* mappedRecords() {
954
+ for await (const record of result.records) yield unwrapArrayWrappers(record, ${JSON.stringify(op.stream.recordTypeName ?? "unknown")});
955
+ }
956
+ // @constraints Keep disconnect cancellation active until the response stream finishes.
957
+ return await reply.send(${streamHelperName}(mappedRecords()));` : `// @constraints Keep disconnect cancellation active until the response stream finishes.
958
+ return await reply.send(${streamHelperName}(result.records));`}
959
+ } finally {
960
+ request.raw.removeListener("aborted", disconnected);
961
+ reply.raw.removeListener("close", disconnected);
962
+ }
948
963
  },`
949
964
  : ` handler: async (request) => {
950
965
  const client = fastify.${clientMeta.decoratorName};
@@ -199,6 +199,7 @@ export interface ResolvedOperationMeta {
199
199
  * @property {"js"|"ts"|"bare"} importsMode - Import extension mode
200
200
  */
201
201
  export interface ResolveClientMetaOptions {
202
+ gatewayDir?: string;
202
203
  clientDir?: string;
203
204
  catalogFile?: string;
204
205
  clientClassName?: string;
@@ -11,6 +11,7 @@
11
11
  * - Automatic version/service detection from OpenAPI paths
12
12
  */
13
13
  import { pascal, trimRepeatedEdgeChar } from "../util/tools.js";
14
+ import path from "node:path";
14
15
  /**
15
16
  * Converts a name to a deterministic slug for use in file names and URN IDs
16
17
  *
@@ -393,6 +394,20 @@ export function resolveClientMeta(opts, catalog) {
393
394
  const operationsImportPath = opts.clientDir
394
395
  ? `../client/operations${suffix}`
395
396
  : `./client/operations${suffix}`;
397
+ const modulePath = (from, module) => {
398
+ const relative = path.relative(from, path.join(opts.clientDir, module + suffix)).replaceAll("\\", "/");
399
+ if (path.isAbsolute(relative) || /^[a-z]:/i.test(relative))
400
+ throw new Error("Generated client and gateway modules must share a filesystem root");
401
+ return relative.startsWith(".") ? relative : "./" + relative;
402
+ };
403
+ if (opts.clientDir && opts.gatewayDir)
404
+ return {
405
+ className, decoratorName,
406
+ importPath: modulePath(path.join(opts.gatewayDir, "routes"), "client"),
407
+ typesImportPath: modulePath(path.join(opts.gatewayDir, "routes"), "types"),
408
+ pluginImportPath: modulePath(opts.gatewayDir, "client"),
409
+ operationsImportPath: modulePath(opts.gatewayDir, "operations"),
410
+ };
396
411
  return {
397
412
  className,
398
413
  decoratorName,
@@ -19,7 +19,31 @@ export function lowerComponentShape(contract, supplied, role, check) {
19
19
  const owned = inspectComponentRecordProjection(supplied, contract);
20
20
  return lowerComponentShape(owned.companion, owned.mapping, role, check);
21
21
  }
22
- const { projection, applicationGroups } = inspectComponentValueMapping(supplied, contract), { context } = inspectDirectOperation(contract);
22
+ const { mapping, projection, applicationGroups, rpcParts } = inspectComponentValueMapping(supplied, contract), { context } = inspectDirectOperation(contract);
23
+ if ('parts' in mapping.root) {
24
+ const properties = Object.create(null);
25
+ const required = [], members = [];
26
+ for (const part of mapping.root.parts) {
27
+ check();
28
+ const plan = rpcParts?.find(plan => plan.name === part.name)?.projection;
29
+ let type = 'string';
30
+ if (plan?.root.kind === 'record') {
31
+ const record = plan.records[0];
32
+ const fields = Object.fromEntries(record.slots.map(slot => [slot.element.name.local, { type: 'boolean' }]));
33
+ properties[part.name] = { type: 'object', properties: fields, required: Object.keys(fields), additionalProperties: false };
34
+ type = `{${record.slots.map(slot => `readonly ${literal(slot.element.name.local)}: boolean`).join('; ')}}`;
35
+ }
36
+ else {
37
+ type = plan?.root.kind === 'scalar' && plan.root.builtin.local === 'boolean' ? 'boolean' : 'string';
38
+ properties[part.name] = { type };
39
+ }
40
+ required.push(part.name);
41
+ members.push(`readonly ${literal(part.name)}: ${type}`);
42
+ }
43
+ return { type: `{${members.join('; ')}}`, declarations: [], schema: { type: 'object', additionalProperties: false, properties, required } };
44
+ }
45
+ if (!projection)
46
+ throw new TypeError('Native document projection required');
23
47
  const names = new Map(projection.records.map((record, index) => [record.type.id, applicationGroups
24
48
  ? applicationRecordName(record.type.id) : `${role}_Record_${index}`]));
25
49
  const definitions = Object.create(null);
package/dist/index.d.ts CHANGED
@@ -5,6 +5,10 @@ export { buildSecurity, loadSecurityConfigFile, parseSecurityConfig, SecurityCon
5
5
  export { generateTests } from "./test/generateTests.js";
6
6
  export { runGenerationPipeline } from "./pipeline.js";
7
7
  export type { PipelineOptions } from './pipeline.js';
8
+ export { runMultiServicePipeline, loadMultiServiceConfig } from './multiServicePipeline.js';
9
+ export type { MultiServicePipelineOptions, MultiServicePipelineResult, MultiServiceEntry } from './multiServicePipeline.js';
10
+ export { composeOpenApiDocuments } from './openapi/composeOpenAPI.js';
11
+ export type { ComposedOpenApiDocument, OpenApiCompositionEntry, OpenApiCompositionOptions } from './openapi/composeOpenAPI.js';
8
12
  export type { CheckedComponentOptions, CheckedInventory } from './checkedPipeline.js';
9
13
  export { planCheckedComponents, formatCheckedComponentPlan, writeCheckedPlanningFile, CheckedPlanningError } from './checkedPlanning.js';
10
14
  export type { CheckedComponentPlan, ComponentPlanBlocker } from './checkedPlanning.js';
package/dist/index.js CHANGED
@@ -27,6 +27,8 @@ export { generateGateway } from "./gateway/generateGateway.js";
27
27
  export { buildSecurity, loadSecurityConfigFile, parseSecurityConfig, SecurityConfigError, } from "./util/securityConfig.js";
28
28
  export { generateTests } from "./test/generateTests.js";
29
29
  export { runGenerationPipeline } from "./pipeline.js";
30
+ export { runMultiServicePipeline, loadMultiServiceConfig } from './multiServicePipeline.js';
31
+ export { composeOpenApiDocuments } from './openapi/composeOpenAPI.js';
30
32
  export { planCheckedComponents, formatCheckedComponentPlan, writeCheckedPlanningFile, CheckedPlanningError } from './checkedPlanning.js';
31
33
  export { loadStreamConfigFile, parseStreamConfig, StreamConfigError, } from "./util/streamConfig.js";
32
34
  export { applyShapeCatalogs } from "./compiler/shapeResolver.js";
@@ -70,6 +70,10 @@ export declare function resolveLexicalQName(lexical: string, node: SyntaxElement
70
70
  export declare function decodeMetadataXml(bytes: Uint8Array): string;
71
71
  /** Positions are one-based lines/columns and zero-based UTF-16 offsets in decoded XML. */
72
72
  export interface OrderedSyntaxOptions {
73
+ /** Explicit component SOAP response policy; source and shipped direct decoding remain separate. */
74
+ componentResponseEncoding?: {
75
+ charset?: string;
76
+ };
73
77
  /** Internal metadata boundary; payload/capsule callers retain the existing encoding policy. */
74
78
  metadataEncoding?: boolean;
75
79
  maxDepth?: number;
@@ -55,19 +55,33 @@ const xmlEncodingDeclaration = /^<\?xml\s[^?]*encoding\s*=\s*["']([^"']+)["']/i;
55
55
  export function decodeMetadataXml(bytes) {
56
56
  return decodeXml(bytes, true, true);
57
57
  }
58
- function decodeXml(bytes, strictBom = false, metadata = false) {
58
+ function decodeXml(bytes, strictBom = false, metadata = false, response) {
59
59
  let encoding = bytes[0] === 0xff && bytes[1] === 0xfe ? "utf-16le"
60
60
  : bytes[0] === 0xfe && bytes[1] === 0xff ? "utf-16be" : "utf-8";
61
61
  try {
62
62
  const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
63
63
  // @why Sniff only an ASCII declaration, never guess Latin1 after UTF-8 failure.
64
64
  // A BOM fixes the encoding and must agree with the decoded declaration.
65
- if (metadata && encoding === 'utf-8' && !(bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf)) {
65
+ const bom = encoding !== 'utf-8' || bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf;
66
+ const canonical = (label) => label.toLowerCase() === 'utf8' ? 'utf-8' : label.toLowerCase();
67
+ const agrees = (label) => canonical(label) === encoding || canonical(label) === 'utf-16' && encoding.startsWith('utf-16');
68
+ if ((metadata || response) && !bom) {
66
69
  const end = buffer.indexOf('>');
67
70
  const declaration = xmlEncodingDeclaration.exec(buffer.toString('latin1', 0, end < 0 ? 0 : end + 1))?.[1].toLowerCase();
68
- if (declaration === 'iso-8859-1')
71
+ if (response) {
72
+ const asserted = response.charset ?? declaration;
73
+ if (asserted) {
74
+ const selected = canonical(asserted);
75
+ if (!['utf-8', 'iso-8859-1'].includes(selected))
76
+ throw new SchemaLoadingError('unsupported-capability', 'Unsupported XML byte encoding');
77
+ encoding = selected;
78
+ }
79
+ }
80
+ else if (declaration === 'iso-8859-1')
69
81
  encoding = 'iso-8859-1';
70
82
  }
83
+ if (response?.charset && !agrees(response.charset))
84
+ throw new SchemaLoadingError('unsupported-capability', 'HTTP charset disagrees with XML bytes');
71
85
  // TextDecoder's ISO label aliases Windows-1252; Buffer supplies actual ISO byte identity.
72
86
  const text = encoding === 'iso-8859-1' ? buffer.toString('latin1') : new TextDecoder(encoding, { fatal: true }).decode(bytes);
73
87
  if (strictBom && text.startsWith('\ufeff'))
@@ -107,7 +121,7 @@ export function parseOrderedSyntax(bytes, uri, options = {}) {
107
121
  const charge = (amount) => options.chargeWork?.(amount);
108
122
  charge(4096 + bytes.byteLength * 64);
109
123
  const digest = createHash("sha256").update(bytes).digest("hex");
110
- const text = options.metadataEncoding ? decodeMetadataXml(bytes) : decodeXml(bytes, options.xml10Only);
124
+ const text = options.metadataEncoding ? decodeMetadataXml(bytes) : decodeXml(bytes, options.xml10Only, false, options.componentResponseEncoding);
111
125
  const lines = [0];
112
126
  for (let i = 0; i < text.length; i++) {
113
127
  if (text[i] === "\r") {
@@ -0,0 +1,41 @@
1
+ import { type PipelineOptions } from "./pipeline.js";
2
+ import { type ComposedOpenApiDocument, type OpenApiCompositionOptions } from "./openapi/composeOpenAPI.js";
3
+ import type { CompiledCatalog } from "./compiler/schemaCompiler.js";
4
+ export interface MultiServiceEntry {
5
+ key: string;
6
+ generation: PipelineOptions;
7
+ }
8
+ export interface MultiServicePipelineOptions {
9
+ services: readonly MultiServiceEntry[];
10
+ openapi: OpenApiCompositionOptions & {
11
+ outFile: string;
12
+ };
13
+ gateway: {
14
+ outDir: string;
15
+ imports?: "js" | "ts" | "bare";
16
+ openapiPath?: string | false;
17
+ };
18
+ }
19
+ export interface MultiServicePipelineResult {
20
+ openapiDoc: ComposedOpenApiDocument;
21
+ services: Record<string, {
22
+ compiled: CompiledCatalog;
23
+ clientDir: string;
24
+ gatewayDir: string;
25
+ }>;
26
+ pluginFile: string;
27
+ openapiFile: string;
28
+ }
29
+ /** Generate independent WSDL clients behind one typed gateway and discoverable API definition.
30
+ * @param options Explicit per-service pipelines and disjoint aggregate output paths.
31
+ * @returns Matched aggregate artifacts and each service's unmerged compiled catalog.
32
+ * @throws For configuration/output conflicts, source generation failures or invalid API composition.
33
+ * @sideEffects Generates each selected legacy service in caller order, then publishes the aggregate spec/plugin pair.
34
+ * @constraints Failure preserves previous aggregate spec/plugin files; earlier generated service dependencies are not rolled back.
35
+ */
36
+ export declare function runMultiServicePipeline(options: MultiServicePipelineOptions): Promise<MultiServicePipelineResult>;
37
+ /** Load a composition config with paths resolved against its own directory.
38
+ * @throws For invalid JSON or missing required output/source paths when the pipeline validates it.
39
+ * @constraints Runtime client options and credentials are deliberately not generation configuration.
40
+ */
41
+ export declare function loadMultiServiceConfig(file: string): Promise<MultiServicePipelineOptions>;