@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
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Streaming SOAP-payload to record iterator.
3
+ *
4
+ * Phase 3 of ADR-002. Driven by saxes (chunk-boundary safe — proven in
5
+ * test/research/sax-record-path.test.ts) and the compiled-catalog metadata
6
+ * that the buffered client already relies on. The parser accepts an async
7
+ * iterable of bytes/strings (typically an upstream SOAP HTTP response) and
8
+ * yields fully-materialized record objects as the corresponding end tags
9
+ * close. Consumers never see partial records.
10
+ *
11
+ * Open questions resolved here:
12
+ * Q3 (terminal error policy): stream aborts. Errors that happen before the
13
+ * first record bubble out as a rejected promise from the first
14
+ * iterator.next(). Errors after a record was emitted throw from a
15
+ * later iterator.next() — callers are expected to treat that as a
16
+ * truncated stream.
17
+ * Q4 (saxes placement): runtime dependency of the wsdl-tsc package; the
18
+ * generated client imports the emitted copy of this module and
19
+ * inherits saxes via its own dependency tree.
20
+ */
21
+ import {SaxesParser, type SaxesTagPlain} from "saxes";
22
+
23
+ /**
24
+ * Catalog-driven parse specification. `recordPath` is an ordered XML element
25
+ * path from the SOAP body payload down to the repeated record element. The
26
+ * path is matched as a suffix of the open-tag stack, so callers may either
27
+ * pre-strip the SOAP envelope or feed the entire response body.
28
+ *
29
+ * Duplicate local names in `recordPath` are supported and expected (Escapia's
30
+ * EVRN content service nests two elements named `EVRN_UnitDescriptiveInfoRS`).
31
+ */
32
+ export interface RecordParseSpec {
33
+ recordPath: string[];
34
+ /** TypeScript type name of the record, used to look up propMeta. */
35
+ recordTypeName: string;
36
+ /** Attribute bag key to stash XML attributes under. Defaults to "$attributes". */
37
+ attributesKey?: string;
38
+ /**
39
+ * Compiled-catalog child-type map: `childType[typeName][propName] = tsType`.
40
+ * Used to descend into nested complex types and to detect array-valued
41
+ * props (trailing `[]`). Optional — absent means occurrence-based array
42
+ * detection only.
43
+ */
44
+ childType?: Record<string, Record<string, string>>;
45
+ /**
46
+ * Compiled-catalog prop-meta map: carries min/max/nillable/declaredType.
47
+ * When present, `max > 1` or `max === "unbounded"` drives array emission
48
+ * even for props that happen to occur just once in a given record.
49
+ */
50
+ propMeta?: Record<string, Record<string, PropMeta>>;
51
+ }
52
+
53
+ export interface PropMeta {
54
+ min?: number;
55
+ max?: number | "unbounded";
56
+ nillable?: boolean;
57
+ declaredType?: string;
58
+ }
59
+
60
+ /**
61
+ * Consume an async iterable of XML bytes/strings and yield parsed records.
62
+ *
63
+ * The returned iterator is single-pass; iteration must complete (or be
64
+ * interrupted by the consumer via `break`/`return`) before the upstream
65
+ * source is released. Errors from the source or from the SAX parser abort
66
+ * the iteration via a rejected `next()`.
67
+ */
68
+ export async function* parseRecords<T = unknown>(
69
+ source: AsyncIterable<string | Uint8Array>,
70
+ spec: RecordParseSpec,
71
+ ): AsyncIterable<T> {
72
+ const parser = new SaxesParser({xmlns: false, position: false});
73
+ const recordPath = spec.recordPath;
74
+ if (recordPath.length === 0) {
75
+ throw new Error("parseRecords: recordPath must not be empty");
76
+ }
77
+ const attrsKey = spec.attributesKey ?? "$attributes";
78
+
79
+ // Global tag stack, maintained across the entire document.
80
+ const stack: string[] = [];
81
+ // Records materialized during the current chunk write, awaiting yield.
82
+ const pending: T[] = [];
83
+ // When a parser.on('error') fires we buffer the error for re-throw on the
84
+ // next yield cycle; saxes emits 'error' and continues unless we stop it.
85
+ let parseError: Error | null = null;
86
+
87
+ // While we're inside the record element (stack tail matches recordPath),
88
+ // we maintain a stack of "open" element nodes capturing their XML shape.
89
+ interface OpenNode {
90
+ obj: Record<string, unknown>;
91
+ /** Type name for `childType`/`propMeta` lookup on direct children. */
92
+ typeName: string | null;
93
+ /** Name this node was opened as, in the parent object. null for the record root. */
94
+ propName: string | null;
95
+ /** Accumulated text content (including CDATA). */
96
+ textBuf: string[];
97
+ /** True once we've seen at least one child element — distinguishes leaves from containers. */
98
+ hadChildren: boolean;
99
+ /** xsi:nil="true" marker → materialize as null regardless of content. */
100
+ isNil: boolean;
101
+ }
102
+ const openNodes: OpenNode[] = [];
103
+
104
+ parser.on("opentag", (tag: SaxesTagPlain) => {
105
+ stack.push(tag.name);
106
+ if (openNodes.length === 0) {
107
+ // Not yet inside a record. Check whether entering the path tail.
108
+ if (tailMatches(stack, recordPath)) {
109
+ openNodes.push(newNode(tag, spec.recordTypeName, null, attrsKey));
110
+ }
111
+ return;
112
+ }
113
+ // Inside a record: descend.
114
+ const parent = openNodes[openNodes.length - 1];
115
+ parent.hadChildren = true;
116
+ const parentType = parent.typeName;
117
+ const childTypeRaw = parentType ? spec.childType?.[parentType]?.[tag.name] : undefined;
118
+ const childTypeName = childTypeRaw ? stripArraySuffix(childTypeRaw) : null;
119
+ openNodes.push(newNode(tag, childTypeName, tag.name, attrsKey));
120
+ });
121
+
122
+ const appendText = (t: string) => {
123
+ if (openNodes.length > 0) openNodes[openNodes.length - 1].textBuf.push(t);
124
+ };
125
+ parser.on("text", appendText);
126
+ parser.on("cdata", appendText);
127
+
128
+ parser.on("closetag", (_tag: SaxesTagPlain) => {
129
+ if (openNodes.length > 0) {
130
+ const closing = openNodes.pop()!;
131
+ const value = materialize(closing, attrsKey);
132
+ if (openNodes.length === 0) {
133
+ // We just closed the record root. Stack tail must match once more.
134
+ if (tailMatches(stack, recordPath)) {
135
+ pending.push(value as T);
136
+ }
137
+ } else {
138
+ assignChild(openNodes[openNodes.length - 1], closing.propName!, value, spec);
139
+ }
140
+ }
141
+ stack.pop();
142
+ });
143
+
144
+ parser.on("error", (err: Error) => {
145
+ parseError = err;
146
+ });
147
+
148
+ try {
149
+ for await (const chunk of source) {
150
+ if (parseError) throw parseError;
151
+ const text = typeof chunk === "string" ? chunk : decodeUtf8(chunk);
152
+ parser.write(text);
153
+ if (parseError) throw parseError;
154
+ while (pending.length > 0) {
155
+ yield pending.shift()!;
156
+ }
157
+ }
158
+ parser.close();
159
+ if (parseError) throw parseError;
160
+ while (pending.length > 0) {
161
+ yield pending.shift()!;
162
+ }
163
+ } finally {
164
+ // Best-effort cleanup: detach handlers so the parser can be GC'd even
165
+ // when the consumer aborts iteration early.
166
+ parser.off("opentag");
167
+ parser.off("closetag");
168
+ parser.off("text");
169
+ parser.off("cdata");
170
+ parser.off("error");
171
+ }
172
+ }
173
+
174
+ function newNode(
175
+ tag: SaxesTagPlain,
176
+ typeName: string | null,
177
+ propName: string | null,
178
+ attrsKey: string,
179
+ ): {
180
+ obj: Record<string, unknown>;
181
+ typeName: string | null;
182
+ propName: string | null;
183
+ textBuf: string[];
184
+ hadChildren: boolean;
185
+ isNil: boolean;
186
+ } {
187
+ const attrs = tag.attributes;
188
+ const obj: Record<string, unknown> = {};
189
+ let isNil = false;
190
+ const attrKeys = Object.keys(attrs);
191
+ if (attrKeys.length > 0) {
192
+ // Detect xsi:nil and drop it from the attribute bag — it's a wire-level
193
+ // concern that should not pollute the user-visible record.
194
+ const cleaned: Record<string, string> = {};
195
+ for (const k of attrKeys) {
196
+ if ((k === "xsi:nil" || k === "nil") && attrs[k] === "true") {
197
+ isNil = true;
198
+ continue;
199
+ }
200
+ cleaned[k] = attrs[k];
201
+ }
202
+ if (Object.keys(cleaned).length > 0) {
203
+ obj[attrsKey] = cleaned;
204
+ }
205
+ }
206
+ return {
207
+ obj,
208
+ typeName,
209
+ propName,
210
+ textBuf: [],
211
+ hadChildren: false,
212
+ isNil,
213
+ };
214
+ }
215
+
216
+ function materialize(
217
+ node: {obj: Record<string, unknown>; textBuf: string[]; hadChildren: boolean; isNil: boolean},
218
+ attrsKey: string,
219
+ ): unknown {
220
+ if (node.isNil) return null;
221
+ if (!node.hadChildren) {
222
+ // Leaf with no child elements: it's simple text. Preserve attributes via
223
+ // a `$value` pairing when present, mirroring how the buffered mapper
224
+ // surfaces simpleContent-with-attributes types.
225
+ const text = node.textBuf.join("");
226
+ if (attrsKey in node.obj) {
227
+ return {...node.obj, $value: text};
228
+ }
229
+ return text;
230
+ }
231
+ return node.obj;
232
+ }
233
+
234
+ function assignChild(
235
+ parent: {obj: Record<string, unknown>; typeName: string | null},
236
+ propName: string,
237
+ value: unknown,
238
+ spec: RecordParseSpec,
239
+ ): void {
240
+ const parentType = parent.typeName;
241
+ const propMetaEntry = parentType ? spec.propMeta?.[parentType]?.[propName] : undefined;
242
+ const childTypeHint = parentType ? spec.childType?.[parentType]?.[propName] : undefined;
243
+
244
+ // Array if: (a) propMeta says max > 1 or "unbounded", or (b) childType hint
245
+ // ends in `[]`, or (c) the slot is already occupied (implicit repetition).
246
+ const metaSaysArray =
247
+ propMetaEntry?.max === "unbounded" ||
248
+ (typeof propMetaEntry?.max === "number" && propMetaEntry.max > 1);
249
+ const hintSaysArray = !!childTypeHint && childTypeHint.endsWith("[]");
250
+ const existing = parent.obj[propName];
251
+ const slotTaken = existing !== undefined;
252
+
253
+ if (metaSaysArray || hintSaysArray) {
254
+ if (Array.isArray(existing)) {
255
+ existing.push(value);
256
+ } else if (slotTaken) {
257
+ parent.obj[propName] = [existing, value];
258
+ } else {
259
+ parent.obj[propName] = [value];
260
+ }
261
+ return;
262
+ }
263
+ if (slotTaken) {
264
+ // Schema said scalar but the wire repeated it. Promote to array rather
265
+ // than drop data.
266
+ if (Array.isArray(existing)) {
267
+ existing.push(value);
268
+ } else {
269
+ parent.obj[propName] = [existing, value];
270
+ }
271
+ return;
272
+ }
273
+ parent.obj[propName] = value;
274
+ }
275
+
276
+ function stripArraySuffix(tsType: string): string {
277
+ return tsType.endsWith("[]") ? tsType.slice(0, -2) : tsType;
278
+ }
279
+
280
+ function tailMatches(stack: string[], path: string[]): boolean {
281
+ if (stack.length < path.length) return false;
282
+ const offset = stack.length - path.length;
283
+ for (let i = 0; i < path.length; i++) {
284
+ if (stack[offset + i] !== path[i]) return false;
285
+ }
286
+ return true;
287
+ }
288
+
289
+ const UTF8_DECODER = new TextDecoder("utf-8");
290
+
291
+ function decodeUtf8(buf: Uint8Array): string {
292
+ return UTF8_DECODER.decode(buf);
293
+ }