@techspokes/typescript-wsdl-client 0.15.2 → 0.17.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 (79) hide show
  1. package/README.md +5 -0
  2. package/dist/app/generateApp.d.ts.map +1 -1
  3. package/dist/app/generateApp.js +4 -3
  4. package/dist/cli.js +46 -2
  5. package/dist/client/generateClient.d.ts.map +1 -1
  6. package/dist/client/generateClient.js +64 -8
  7. package/dist/client/generateOperations.d.ts.map +1 -1
  8. package/dist/client/generateOperations.js +29 -6
  9. package/dist/client/generateTypes.d.ts.map +1 -1
  10. package/dist/client/generateTypes.js +13 -0
  11. package/dist/compiler/schemaCompiler.d.ts +44 -11
  12. package/dist/compiler/schemaCompiler.d.ts.map +1 -1
  13. package/dist/compiler/schemaCompiler.js +102 -6
  14. package/dist/compiler/shapeResolver.d.ts +18 -0
  15. package/dist/compiler/shapeResolver.d.ts.map +1 -0
  16. package/dist/compiler/shapeResolver.js +280 -0
  17. package/dist/gateway/generateGateway.d.ts.map +1 -1
  18. package/dist/gateway/generateGateway.js +2 -1
  19. package/dist/gateway/generators.d.ts +13 -1
  20. package/dist/gateway/generators.d.ts.map +1 -1
  21. package/dist/gateway/generators.js +98 -13
  22. package/dist/gateway/helpers.d.ts +16 -0
  23. package/dist/gateway/helpers.d.ts.map +1 -1
  24. package/dist/gateway/helpers.js +1 -0
  25. package/dist/index.d.ts +6 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +23 -4
  28. package/dist/openapi/generateOpenAPI.d.ts.map +1 -1
  29. package/dist/openapi/generateOpenAPI.js +30 -2
  30. package/dist/openapi/generatePaths.d.ts.map +1 -1
  31. package/dist/openapi/generatePaths.js +4 -2
  32. package/dist/openapi/generateSchemas.d.ts.map +1 -1
  33. package/dist/openapi/generateSchemas.js +20 -5
  34. package/dist/pipeline.d.ts +13 -0
  35. package/dist/pipeline.d.ts.map +1 -1
  36. package/dist/pipeline.js +17 -1
  37. package/dist/runtime/ndjson.d.ts +24 -0
  38. package/dist/runtime/ndjson.d.ts.map +1 -0
  39. package/dist/runtime/ndjson.js +30 -0
  40. package/dist/runtime/streamXml.d.ts +45 -0
  41. package/dist/runtime/streamXml.d.ts.map +1 -0
  42. package/dist/runtime/streamXml.js +212 -0
  43. package/dist/test/generators.d.ts +2 -2
  44. package/dist/test/generators.d.ts.map +1 -1
  45. package/dist/test/generators.js +79 -26
  46. package/dist/test/mockData.d.ts +12 -2
  47. package/dist/test/mockData.d.ts.map +1 -1
  48. package/dist/test/mockData.js +17 -8
  49. package/dist/util/cli.d.ts +3 -0
  50. package/dist/util/cli.d.ts.map +1 -1
  51. package/dist/util/cli.js +6 -1
  52. package/dist/util/runtimeSource.d.ts +2 -0
  53. package/dist/util/runtimeSource.d.ts.map +1 -0
  54. package/dist/util/runtimeSource.js +38 -0
  55. package/dist/util/streamConfig.d.ts +59 -0
  56. package/dist/util/streamConfig.d.ts.map +1 -0
  57. package/dist/util/streamConfig.js +230 -0
  58. package/docs/README.md +1 -0
  59. package/docs/api-reference.md +146 -0
  60. package/docs/architecture.md +27 -5
  61. package/docs/cli-reference.md +30 -0
  62. package/docs/concepts.md +150 -11
  63. package/docs/configuration.md +40 -0
  64. package/docs/decisions/002-streamable-responses.md +308 -0
  65. package/docs/gateway-guide.md +37 -0
  66. package/docs/generated-code.md +21 -0
  67. package/docs/migration-playbook.md +33 -0
  68. package/docs/migration.md +31 -6
  69. package/docs/output-anatomy.md +49 -0
  70. package/docs/production.md +32 -0
  71. package/docs/start-here.md +33 -0
  72. package/docs/supported-patterns.md +29 -0
  73. package/docs/testing.md +14 -0
  74. package/docs/troubleshooting.md +18 -0
  75. package/package.json +9 -6
  76. package/src/runtime/clientStreamMethods.tpl.txt +183 -0
  77. package/src/runtime/ndjson.ts +32 -0
  78. package/src/runtime/operationsStreamHelper.tpl.txt +13 -0
  79. package/src/runtime/streamXml.ts +293 -0
@@ -153,7 +153,7 @@ function extractAnnotationDocumentation(node) {
153
153
  * 4. Extract WSDL operations: pick the appropriate SOAP binding (v1.1 or v1.2), resolve its
154
154
  * portType reference, then enumerate operations and their soapAction URIs.
155
155
  */
156
- export function compileCatalog(cat, options) {
156
+ export function compileCatalog(cat, options, streamConfig) {
157
157
  // symbol tables discovered across all schemas
158
158
  const complexTypes = new Map();
159
159
  const simpleTypes = new Map();
@@ -190,6 +190,13 @@ export function compileCatalog(cat, options) {
190
190
  const attrType = {};
191
191
  const childType = {};
192
192
  const propMeta = {};
193
+ const diagnostics = {};
194
+ const addDiagnosticNote = (note) => {
195
+ diagnostics.notes ??= [];
196
+ if (!diagnostics.notes.includes(note)) {
197
+ diagnostics.notes.push(note);
198
+ }
199
+ };
193
200
  /** Compile a simpleType node to TS */
194
201
  function compileSimpleTypeNode(simpleNode, schemaNS, prefixes) {
195
202
  const rest = getFirstWithLocalName(simpleNode, "restriction");
@@ -346,6 +353,33 @@ export function compileCatalog(cat, options) {
346
353
  }
347
354
  return out;
348
355
  };
356
+ // Walks the same compositor structure as collectParticles and returns any
357
+ // xs:any wildcard particles found. Kept as a sibling to avoid reshaping
358
+ // collectParticles' return type at the three call sites below.
359
+ const collectWildcards = (node) => {
360
+ const out = [];
361
+ const recurse = (groupNode) => {
362
+ for (const a of getChildrenWithLocalName(groupNode, "any")) {
363
+ const min = a["@_minOccurs"] ? Number(a["@_minOccurs"]) : 1;
364
+ const maxAttr = a["@_maxOccurs"];
365
+ const max = maxAttr === "unbounded" ? "unbounded" : maxAttr ? Number(maxAttr) : 1;
366
+ const pc = a["@_processContents"];
367
+ out.push({
368
+ min,
369
+ max,
370
+ ...(a["@_namespace"] ? { namespace: a["@_namespace"] } : {}),
371
+ ...(pc === "lax" || pc === "strict" || pc === "skip" ? { processContents: pc } : {}),
372
+ });
373
+ }
374
+ for (const comp of ["sequence", "all", "choice"]) {
375
+ for (const sub of getChildrenWithLocalName(groupNode, comp)) {
376
+ recurse(sub);
377
+ }
378
+ }
379
+ };
380
+ recurse(node);
381
+ return out;
382
+ };
349
383
  const collectParticles = (ownerTypeName, node) => {
350
384
  const out = [];
351
385
  // process a compositor or element container recursively
@@ -417,8 +451,12 @@ export function compileCatalog(cat, options) {
417
451
  // On duplicate definitions, merge new attributes and elements
418
452
  const newAttrs = collectAttributes(cnode);
419
453
  const newElems = collectParticles(outName, cnode);
454
+ const newWildcards = collectWildcards(cnode);
420
455
  mergeAttrs(present.attrs, newAttrs);
421
456
  mergeElems(present.elems, newElems);
457
+ if (newWildcards.length > 0) {
458
+ present.wildcards = [...(present.wildcards ?? []), ...newWildcards];
459
+ }
422
460
  if (!present.doc && typeDoc) {
423
461
  present.doc = typeDoc;
424
462
  }
@@ -453,6 +491,7 @@ export function compileCatalog(cat, options) {
453
491
  // collect local additions/overrides
454
492
  const locals = collectAttributes(node);
455
493
  const localElems = collectParticles(outName, node);
494
+ const localWildcards = collectWildcards(node);
456
495
  attrs.push(...locals);
457
496
  elems.push(...localElems);
458
497
  const result = {
@@ -463,7 +502,8 @@ export function compileCatalog(cat, options) {
463
502
  doc: typeDoc,
464
503
  base: baseName,
465
504
  localAttrs: locals,
466
- localElems
505
+ localElems,
506
+ ...(localWildcards.length > 0 ? { wildcards: localWildcards } : {}),
467
507
  };
468
508
  compiledMap.set(key, result);
469
509
  inProgress.delete(key);
@@ -506,7 +546,15 @@ export function compileCatalog(cat, options) {
506
546
  // Attributes + particles
507
547
  mergeAttrs(attrs, collectAttributes(cnode));
508
548
  mergeElems(elems, collectParticles(outName, cnode));
509
- const result = { name: outName, ns: schemaNS, attrs, elems, doc: typeDoc };
549
+ const wildcards = collectWildcards(cnode);
550
+ const result = {
551
+ name: outName,
552
+ ns: schemaNS,
553
+ attrs,
554
+ elems,
555
+ doc: typeDoc,
556
+ ...(wildcards.length > 0 ? { wildcards } : {}),
557
+ };
510
558
  compiledMap.set(key, result);
511
559
  inProgress.delete(rawKey);
512
560
  return result;
@@ -585,6 +633,19 @@ export function compileCatalog(cat, options) {
585
633
  const srec = simpleTypes.get(qkey(q));
586
634
  if (srec) {
587
635
  const a = getOrCompileAlias(q.local, srec.node, srec.tns, srec.prefixes);
636
+ if (outName === a.name) {
637
+ addDiagnosticNote(`Element {${schemaNS}}${name} reuses same-name simple type alias ${a.name} instead of emitting a wrapper interface.`);
638
+ if (!a.doc && elementDoc) {
639
+ a.doc = elementDoc;
640
+ }
641
+ return {
642
+ name: a.name,
643
+ ns: schemaNS,
644
+ attrs: [],
645
+ elems: [],
646
+ doc: elementDoc,
647
+ };
648
+ }
588
649
  const t = {
589
650
  name: outName,
590
651
  ns: schemaNS,
@@ -812,7 +873,7 @@ export function compileCatalog(cat, options) {
812
873
  .filter((x) => x != null)
813
874
  .sort((a, b) => a.name.localeCompare(b.name));
814
875
  // build operations list
815
- const ops = (pOps
876
+ const ops = pOps
816
877
  .map(po => {
817
878
  const name = po?.["@_name"];
818
879
  if (!name)
@@ -825,9 +886,18 @@ export function compileCatalog(cat, options) {
825
886
  // Derive TypeScript type names from element local names
826
887
  const inputTypeName = inputElement ? pascal(inputElement.local) : undefined;
827
888
  const outputTypeName = outputElement ? pascal(outputElement.local) : undefined;
828
- return { name, soapAction: bOps.get(name) || "", inputElement, outputElement, inputTypeName, outputTypeName, doc };
889
+ const op = {
890
+ name,
891
+ soapAction: bOps.get(name) || "",
892
+ ...(inputElement ? { inputElement } : {}),
893
+ ...(outputElement ? { outputElement } : {}),
894
+ ...(inputTypeName ? { inputTypeName } : {}),
895
+ ...(outputTypeName ? { outputTypeName } : {}),
896
+ ...(doc ? { doc } : {}),
897
+ };
898
+ return op;
829
899
  })
830
- .filter((x) => x != null));
900
+ .filter((x) => x != null);
831
901
  // --- WS-Policy: scan for security requirements (inline policies only) ---
832
902
  const bindingPolicies = getChildrenWithLocalName(soapBinding || {}, "Policy");
833
903
  const bindingSec = collectSecurityFromPolicyNodes(bindingPolicies);
@@ -838,6 +908,31 @@ export function compileCatalog(cat, options) {
838
908
  const secSet = new Set([...bindingSec, ...opSec]);
839
909
  op.security = Array.from(secSet);
840
910
  }
911
+ // --- Stream config application ---
912
+ // A stream-configured operation is matched by name against the WSDL's
913
+ // portType operations. Unknown operation names are fatal so that consumers
914
+ // find out about stale configs at generation time, not at runtime.
915
+ if (streamConfig) {
916
+ const opByName = new Map(ops.map((op) => [op.name, op]));
917
+ for (const [opName, meta] of Object.entries(streamConfig.operations)) {
918
+ const op = opByName.get(opName);
919
+ if (!op) {
920
+ throw new WsdlCompilationError(`Stream config references operation "${opName}" but the WSDL portType does not declare it.`, {
921
+ element: opName,
922
+ suggestion: `Remove the entry under "operations.${opName}" in the stream config, or correct the operation name to match one defined in the WSDL.`,
923
+ });
924
+ }
925
+ op.stream = {
926
+ mode: meta.mode,
927
+ format: meta.format,
928
+ mediaType: meta.mediaType,
929
+ recordPath: [...meta.recordPath],
930
+ recordTypeName: meta.recordTypeName,
931
+ ...(meta.shapeCatalogName ? { shapeCatalogName: meta.shapeCatalogName } : {}),
932
+ ...(op.outputTypeName ? { sourceOutputTypeName: op.outputTypeName } : {}),
933
+ };
934
+ }
935
+ }
841
936
  // --- Service discovery (for client naming) ---
842
937
  let serviceName;
843
938
  const soapBindingName = soapBinding?.["@_name"];
@@ -869,5 +964,6 @@ export function compileCatalog(cat, options) {
869
964
  serviceName,
870
965
  wsdlUri: cat.wsdlUri,
871
966
  ...(wsdlDocs ? { wsdlDocs } : {}),
967
+ ...(diagnostics.notes?.length ? { diagnostics } : {}),
872
968
  };
873
969
  }
@@ -0,0 +1,18 @@
1
+ import { type CompiledCatalog } from "./schemaCompiler.js";
2
+ import type { StreamConfig } from "../util/streamConfig.js";
3
+ export interface ApplyShapeCatalogsOptions {
4
+ /** Directory against which relative `catalogFile`/`wsdlSource` paths resolve. Defaults to process.cwd(). */
5
+ baseDir?: string;
6
+ }
7
+ /**
8
+ * Apply a parsed StreamConfig to a compiled catalog:
9
+ * 1. Verify every opted-in operation's record type is resolvable.
10
+ * 2. For each operation that names a shapeCatalog, load the companion
11
+ * catalog (once, cached) and copy the reachable record-type graph.
12
+ *
13
+ * Mutates `compiled` in place. Safe to call with a StreamConfig that has
14
+ * zero shapeCatalogs — it will still validate record-type presence against
15
+ * the current catalog.
16
+ */
17
+ export declare function applyShapeCatalogs(compiled: CompiledCatalog, streamConfig: StreamConfig, options?: ApplyShapeCatalogsOptions): Promise<void>;
18
+ //# sourceMappingURL=shapeResolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shapeResolver.d.ts","sourceRoot":"","sources":["../../src/compiler/shapeResolver.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAqC,KAAK,eAAe,EAAoB,MAAM,qBAAqB,CAAC;AAGhH,OAAO,KAAK,EAAkB,YAAY,EAAC,MAAM,yBAAyB,CAAC;AAG3E,MAAM,WAAW,yBAAyB;IACxC,4GAA4G;IAC5G,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,eAAe,EACzB,YAAY,EAAE,YAAY,EAC1B,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,IAAI,CAAC,CAoDf"}
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Companion-catalog shape resolver (phase-2 of ADR-002).
3
+ *
4
+ * For stream operations whose record types live in a different WSDL (e.g.
5
+ * Escapia's main EVRN service provides the concrete UnitDescriptiveContentType
6
+ * while the content-service WSDL only exposes an xs:any-wrapped envelope),
7
+ * load the companion catalog and copy the reachable record-type graph into
8
+ * the current catalog.
9
+ *
10
+ * Invariants:
11
+ * - A name collision against a type already in the current catalog is only
12
+ * allowed when the two types are *structurally identical* — otherwise we
13
+ * fail the build rather than silently rename public API types.
14
+ * - Existing buffered generation stays byte-for-byte identical when no
15
+ * streamConfig.shapeCatalogs entries are declared.
16
+ */
17
+ import fs from "node:fs";
18
+ import path from "node:path";
19
+ import { compileCatalog } from "./schemaCompiler.js";
20
+ import { loadWsdl } from "../loader/wsdlLoader.js";
21
+ import { resolveCompilerOptions } from "../config.js";
22
+ import { WsdlCompilationError } from "../util/errors.js";
23
+ /**
24
+ * Apply a parsed StreamConfig to a compiled catalog:
25
+ * 1. Verify every opted-in operation's record type is resolvable.
26
+ * 2. For each operation that names a shapeCatalog, load the companion
27
+ * catalog (once, cached) and copy the reachable record-type graph.
28
+ *
29
+ * Mutates `compiled` in place. Safe to call with a StreamConfig that has
30
+ * zero shapeCatalogs — it will still validate record-type presence against
31
+ * the current catalog.
32
+ */
33
+ export async function applyShapeCatalogs(compiled, streamConfig, options = {}) {
34
+ const baseDir = options.baseDir ?? process.cwd();
35
+ // Load each declared shape catalog once, on-demand. Shape catalogs declared
36
+ // but never referenced are harmless — we only load the ones an operation
37
+ // actually uses.
38
+ const referencedCatalogs = new Set();
39
+ for (const meta of Object.values(streamConfig.operations)) {
40
+ if (meta.shapeCatalogName)
41
+ referencedCatalogs.add(meta.shapeCatalogName);
42
+ }
43
+ const companionByName = new Map();
44
+ for (const name of referencedCatalogs) {
45
+ const ref = streamConfig.shapeCatalogs[name];
46
+ if (!ref) {
47
+ throw new WsdlCompilationError(`Stream config references shape catalog "${name}" that is not declared under "shapeCatalogs".`, {
48
+ suggestion: `Add a "shapeCatalogs.${name}" entry pointing to a wsdlSource or catalogFile, or remove the reference.`,
49
+ });
50
+ }
51
+ companionByName.set(name, await loadCompanionCatalog(name, ref, baseDir, compiled));
52
+ }
53
+ for (const [opName, meta] of Object.entries(streamConfig.operations)) {
54
+ if (meta.shapeCatalogName) {
55
+ const companion = companionByName.get(meta.shapeCatalogName);
56
+ if (!companion) {
57
+ // Guarded by the loop above; this path is defensive.
58
+ throw new WsdlCompilationError(`Stream config for "${opName}" references shape catalog "${meta.shapeCatalogName}" which failed to load.`);
59
+ }
60
+ copyReachableGraph(compiled, companion, meta.recordTypeName, {
61
+ opName,
62
+ shapeCatalog: meta.shapeCatalogName,
63
+ });
64
+ }
65
+ else {
66
+ // No companion: the record type must already live in the main catalog.
67
+ if (!hasNamedType(compiled, meta.recordTypeName)) {
68
+ throw new WsdlCompilationError(`Stream config for operation "${opName}" references record type "${meta.recordTypeName}" which is not present in the compiled catalog.`, {
69
+ element: meta.recordTypeName,
70
+ suggestion: `Either declare a "shapeCatalogs.<name>" entry and set shapeCatalog on the operation, or correct the recordType to a type defined in the WSDL.`,
71
+ });
72
+ }
73
+ }
74
+ }
75
+ }
76
+ async function loadCompanionCatalog(name, ref, baseDir, compiled) {
77
+ if (ref.catalogFile) {
78
+ const abs = path.resolve(baseDir, ref.catalogFile);
79
+ let text;
80
+ try {
81
+ text = fs.readFileSync(abs, "utf-8");
82
+ }
83
+ catch (err) {
84
+ throw new WsdlCompilationError(`Failed to read companion catalog "${name}" at ${abs}: ${err.message}`, { suggestion: `Check that shapeCatalogs.${name}.catalogFile points to an existing catalog.json.` });
85
+ }
86
+ try {
87
+ return JSON.parse(text);
88
+ }
89
+ catch (err) {
90
+ throw new WsdlCompilationError(`Companion catalog "${name}" at ${abs} is not valid JSON: ${err.message}`);
91
+ }
92
+ }
93
+ if (ref.wsdlSource) {
94
+ // Relative paths resolve against baseDir; URLs pass through untouched.
95
+ const src = isLikelyUrl(ref.wsdlSource) ? ref.wsdlSource : path.resolve(baseDir, ref.wsdlSource);
96
+ const wsdlCatalog = await loadWsdl(src);
97
+ // Reuse the current catalog's compiler options so primitives / attrs keys
98
+ // / choice handling stay consistent with the main generation.
99
+ const companionOptions = resolveCompilerOptions({ ...compiled.options, catalog: false }, { wsdl: src, out: baseDir });
100
+ return compileCatalog(wsdlCatalog, companionOptions);
101
+ }
102
+ throw new WsdlCompilationError(`Shape catalog "${name}" declares neither wsdlSource nor catalogFile.`);
103
+ }
104
+ function isLikelyUrl(s) {
105
+ return /^[a-z][a-z0-9+.-]*:\/\//i.test(s);
106
+ }
107
+ function hasNamedType(cat, name) {
108
+ return cat.types.some((t) => t.name === name) || cat.aliases.some((a) => a.name === name);
109
+ }
110
+ /**
111
+ * Walk the reachable type graph from `rootTypeName` within the companion
112
+ * catalog and copy each reachable entry into `dst`. Types with the same name
113
+ * already present in `dst` are checked for structural equality; any divergence
114
+ * is fatal.
115
+ */
116
+ function copyReachableGraph(dst, src, rootTypeName, context) {
117
+ const srcTypes = new Map(src.types.map((t) => [t.name, t]));
118
+ const srcAliases = new Map(src.aliases.map((a) => [a.name, a]));
119
+ if (!srcTypes.has(rootTypeName) && !srcAliases.has(rootTypeName)) {
120
+ throw new WsdlCompilationError(`Stream config for operation "${context.opName}" references record type "${rootTypeName}" but it is not present in shape catalog "${context.shapeCatalog}".`, {
121
+ element: rootTypeName,
122
+ suggestion: `Check the companion catalog or correct the recordType.`,
123
+ });
124
+ }
125
+ const known = new Set([...srcTypes.keys(), ...srcAliases.keys()]);
126
+ const queue = [rootTypeName];
127
+ const visited = new Set();
128
+ while (queue.length > 0) {
129
+ const typeName = queue.shift();
130
+ if (visited.has(typeName))
131
+ continue;
132
+ visited.add(typeName);
133
+ const srcType = srcTypes.get(typeName);
134
+ if (srcType) {
135
+ mergeType(dst, src, srcType, context);
136
+ // Enqueue referenced names from elems, attrs, base.
137
+ for (const e of srcType.elems ?? []) {
138
+ for (const ref of extractReferencedNames(e.tsType, known))
139
+ queue.push(ref);
140
+ }
141
+ for (const a of srcType.attrs ?? []) {
142
+ for (const ref of extractReferencedNames(a.tsType, known))
143
+ queue.push(ref);
144
+ }
145
+ if (srcType.base && known.has(srcType.base))
146
+ queue.push(srcType.base);
147
+ continue;
148
+ }
149
+ const srcAlias = srcAliases.get(typeName);
150
+ if (srcAlias) {
151
+ mergeAlias(dst, src, srcAlias, context);
152
+ for (const ref of extractReferencedNames(srcAlias.tsType, known))
153
+ queue.push(ref);
154
+ }
155
+ // Not in companion; silently skip — either a primitive or a type that
156
+ // the main catalog is expected to own.
157
+ }
158
+ }
159
+ function mergeType(dst, src, srcType, context) {
160
+ const existingIdx = dst.types.findIndex((t) => t.name === srcType.name);
161
+ if (existingIdx < 0) {
162
+ dst.types.push({ ...srcType });
163
+ copyTypeMeta(dst, src, srcType.name);
164
+ return;
165
+ }
166
+ const existing = dst.types[existingIdx];
167
+ if (!structurallyEqualType(existing, srcType)) {
168
+ throw new WsdlCompilationError(`Companion catalog "${context.shapeCatalog}" declares type "${srcType.name}" that conflicts structurally with the current catalog.`, {
169
+ element: srcType.name,
170
+ suggestion: `Rename one of the conflicting types in the source WSDL, or resolve the conflict before streaming. wsdl-tsc refuses to silently rename public API types.`,
171
+ });
172
+ }
173
+ // Structurally identical — keep the existing entry (and its meta) as-is.
174
+ }
175
+ function mergeAlias(dst, src, srcAlias, context) {
176
+ const existingIdx = dst.aliases.findIndex((a) => a.name === srcAlias.name);
177
+ if (existingIdx < 0) {
178
+ dst.aliases.push({ ...srcAlias });
179
+ // Aliases that point at a complex type get meta synonyms in compileCatalog;
180
+ // the main catalog already owns that path for its own types, so we only
181
+ // need to add meta if the companion aliases a complex type whose type we
182
+ // also just copied.
183
+ copyAliasSynonymMeta(dst, src, srcAlias.name);
184
+ return;
185
+ }
186
+ const existing = dst.aliases[existingIdx];
187
+ if (!structurallyEqualAlias(existing, srcAlias)) {
188
+ throw new WsdlCompilationError(`Companion catalog "${context.shapeCatalog}" declares alias "${srcAlias.name}" that conflicts structurally with the current catalog.`, {
189
+ element: srcAlias.name,
190
+ suggestion: `Resolve the conflicting type name before streaming. wsdl-tsc refuses to silently rename public API types.`,
191
+ });
192
+ }
193
+ }
194
+ function copyTypeMeta(dst, src, typeName) {
195
+ if (typeName in src.meta.attrSpec)
196
+ dst.meta.attrSpec[typeName] = [...src.meta.attrSpec[typeName]];
197
+ if (typeName in src.meta.attrType)
198
+ dst.meta.attrType[typeName] = { ...src.meta.attrType[typeName] };
199
+ if (typeName in src.meta.childType)
200
+ dst.meta.childType[typeName] = { ...src.meta.childType[typeName] };
201
+ if (typeName in src.meta.propMeta)
202
+ dst.meta.propMeta[typeName] = { ...src.meta.propMeta[typeName] };
203
+ }
204
+ function copyAliasSynonymMeta(dst, src, aliasName) {
205
+ // Phase-2 MVP: only copy meta for aliases whose name already keys into the
206
+ // source's meta maps. Aliases that are just simple-type renames don't need
207
+ // child/attr meta at all.
208
+ copyTypeMeta(dst, src, aliasName);
209
+ }
210
+ const TS_BUILTINS = new Set([
211
+ "string", "number", "boolean", "bigint", "null", "undefined", "void", "any", "unknown", "never",
212
+ "Date", "Array", "Record", "Map", "Set", "Promise", "Buffer", "Object", "Function",
213
+ ]);
214
+ /**
215
+ * Extract PascalCase identifiers from a TypeScript type expression that are
216
+ * known to the companion catalog. Deliberately cheap — matches identifiers
217
+ * starting with an uppercase letter, skips TS built-ins and anything not
218
+ * present in the companion's known-names set.
219
+ */
220
+ function extractReferencedNames(tsType, known) {
221
+ if (!tsType)
222
+ return [];
223
+ const out = [];
224
+ // Identifiers starting with an uppercase letter; bare word boundaries.
225
+ for (const m of tsType.matchAll(/\b[A-Z][A-Za-z0-9_]*\b/g)) {
226
+ const name = m[0];
227
+ if (TS_BUILTINS.has(name))
228
+ continue;
229
+ if (known.has(name))
230
+ out.push(name);
231
+ }
232
+ return out;
233
+ }
234
+ function structurallyEqualType(a, b) {
235
+ return canonicalizeType(a) === canonicalizeType(b);
236
+ }
237
+ function structurallyEqualAlias(a, b) {
238
+ return canonicalizeAlias(a) === canonicalizeAlias(b);
239
+ }
240
+ function canonicalizeType(t) {
241
+ // Drop fields that legitimately differ between catalogs (ns, docs) and
242
+ // stabilize ordering. Stream metadata is on operations, not on types.
243
+ return JSON.stringify({
244
+ name: t.name,
245
+ attrs: (t.attrs ?? []).map(canonicalizeAttr),
246
+ elems: (t.elems ?? []).map(canonicalizeElem),
247
+ base: t.base ?? null,
248
+ wildcards: (t.wildcards ?? []).map((w) => ({
249
+ min: w.min,
250
+ max: w.max,
251
+ namespace: w.namespace ?? null,
252
+ processContents: w.processContents ?? null,
253
+ })),
254
+ });
255
+ }
256
+ function canonicalizeAlias(a) {
257
+ return JSON.stringify({
258
+ name: a.name,
259
+ tsType: a.tsType,
260
+ declared: a.declared,
261
+ });
262
+ }
263
+ function canonicalizeAttr(a) {
264
+ return {
265
+ name: a.name,
266
+ tsType: a.tsType,
267
+ use: a.use ?? "optional",
268
+ declaredType: a.declaredType,
269
+ };
270
+ }
271
+ function canonicalizeElem(e) {
272
+ return {
273
+ name: e.name,
274
+ tsType: e.tsType,
275
+ min: e.min,
276
+ max: e.max,
277
+ nillable: !!e.nillable,
278
+ declaredType: e.declaredType,
279
+ };
280
+ }
@@ -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;IACvB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6JjF"}
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,CA8JjF"}
@@ -183,7 +183,8 @@ export async function generateGateway(opts) {
183
183
  }
184
184
  // Step 6: Emit runtime.ts (if enabled)
185
185
  if (emitRuntime) {
186
- emitRuntimeModule(outDir, versionSlug, serviceSlug, shouldUnwrap ? catalog : undefined);
186
+ const hasStreams = operations.some((o) => !!o.stream);
187
+ emitRuntimeModule(outDir, versionSlug, serviceSlug, shouldUnwrap ? catalog : undefined, { hasStreams });
187
188
  }
188
189
  // Step 7: Emit plugin.ts and type-check fixture (if enabled)
189
190
  if (emitPlugin) {
@@ -40,6 +40,16 @@ export interface OperationMetadata {
40
40
  description?: string;
41
41
  /** When true, response schema is omitted from route registration to avoid fast-json-stringify stack overflow on deeply nested $ref graphs */
42
42
  skipResponseSchema?: boolean;
43
+ /**
44
+ * Stream metadata populated from the OpenAPI `x-wsdl-tsc-stream` extension.
45
+ * When present, the route handler pipes `result.records` through the NDJSON
46
+ * helper instead of envelope-wrapping a single response object.
47
+ */
48
+ stream?: {
49
+ mediaType: string;
50
+ format: "ndjson" | "json-array";
51
+ recordTypeName?: string;
52
+ };
43
53
  }
44
54
  /**
45
55
  * Emits Fastify-compatible operation schema files
@@ -107,7 +117,9 @@ export declare function emitSchemasModule(outDir: string, modelsDir: string, ver
107
117
  * @param {"js"|"ts"|"bare"} importsMode - Import-extension mode for generated TypeScript modules
108
118
  */
109
119
  export declare function emitRouteFiles(outDir: string, routesDir: string, versionSlug: string, serviceSlug: string, operations: OperationMetadata[], importsMode: "js" | "ts" | "bare"): void;
110
- export declare function emitRuntimeModule(outDir: string, versionSlug: string, serviceSlug: string, catalog?: any): void;
120
+ export declare function emitRuntimeModule(outDir: string, versionSlug: string, serviceSlug: string, catalog?: any, opts?: {
121
+ hasStreams?: boolean;
122
+ }): void;
111
123
  /**
112
124
  * Emits plugin.ts module as the primary Fastify plugin wrapper
113
125
  *
@@ -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":"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"}