@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
@@ -2,6 +2,14 @@
2
2
 
3
3
  This page helps you understand what this package does, decide if it fits your needs, and find the fastest path to your first result.
4
4
 
5
+ See the root [README](../README.md) for installation and the documentation index.
6
+
7
+ ## Upgrade an existing project
8
+
9
+ Read the [1.1.x-to-1.2.0 migration guide](migration.md#from-11x-to-120) before regenerating an existing gateway. Installing the package does not itself rewrite generated code, but consumer build/install scripts can run generation. Legacy remains the default; affected generated names, stream record shapes and corrected scalar/simple-content results need review.
10
+
11
+ Use the existing generation command and preserve app/test customizations first. Adopt native composition or checked generation separately; successful representative release tests do not certify migration of every consumer project.
12
+
5
13
  ## What This Package Does
6
14
 
7
15
  `@techspokes/typescript-wsdl-client` reads a WSDL/XSD definition and generates typed TypeScript code. Depending on the command you run, you get a typed SOAP client, an OpenAPI 3.1 specification, a Fastify REST gateway, or all of the above.
@@ -17,6 +25,8 @@ The generated output is deterministic: re-running the same command produces iden
17
25
 
18
26
  ## Choose Your Path
19
27
 
28
+ Choose legacy single-source generation for existing DTO/envelope integrations. For multiple independent legacy sources, use [native composition](gateway-guide.md#multi-service-setup) with `pipeline --services-config`. For source-backed validation and the bounded SOAP 1.1 RPC/encoded profile, choose [checked generation](checked-generation.md); checked replies remain buffered values, without the legacy success envelope. Neither profile claims universal SOAP support.
29
+
20
30
  ### I need a typed SOAP client
21
31
 
22
32
  Run the `client` command. This generates TypeScript interfaces for all WSDL types and a typed client class with one method per SOAP operation.
@@ -66,13 +76,13 @@ Next: [Gateway Guide](gateway-guide.md) for integration details, then [Migration
66
76
 
67
77
  ### I want to try source-backed checked generation
68
78
 
69
- From version 1.1.6, `pipeline --model checked-components-v1` opts into the checked component preview. It derives unambiguous selections and supported structured mappings from a WSDL file or URL, acquires declared dependencies, and emits a client, OpenAPI, gateway, app and tests. The existing pipeline remains the default when `--model` is omitted.
79
+ The checked component preview shipped in npm version 1.1.8 after the unpublished 1.1.6/1.1.7 candidates. `pipeline --model checked-components-v1` derives unambiguous selections and supported structured mappings from a WSDL file or URL, acquires declared dependencies, and emits a client, OpenAPI, gateway, app and tests. The existing pipeline remains the default when `--model` is omitted.
70
80
 
71
81
  Start with [Try Opt-in Checked Generation](checked-generation.md) for a bundled installed trial, endpoint configuration, missing-schema recovery and feedback instructions. Supported contracts get native request/response/fault checks; unsupported capabilities refuse rather than bypassing validation. This preview is buffered and separate from the legacy streaming workflow below.
72
82
 
73
83
  ### I need to stream large SOAP responses
74
84
 
75
- Some SOAP services return payloads that are too large or too slow to buffer in memory. Opt selected operations into streaming with a small JSON config and the `--stream-config` flag. Operations not listed keep their buffered behavior, so existing output stays byte-for-byte unchanged.
85
+ Some SOAP services return large responses. Opt selected legacy operations into streaming with `--stream-config`. Unlisted operations retain their buffered API selection; generated mapper and metadata output can change in 1.2.0. Stream bodies bypass the legacy success envelope.
76
86
 
77
87
  ```bash
78
88
  npx wsdl-tsc pipeline \
@@ -6,6 +6,23 @@ The matrix describes the existing legacy generator. The unreleased opt-in [check
6
6
 
7
7
  The qualified checked domain includes [keyed mixed, namespace-collision and source-permitted open values](checked-gateway.md#mixed-namespaced-and-open-values), with production conformance and installed request/response/fault evidence. Native strict/lax/skip checks remain distinct from schema-source wildcard checks and from the legacy rows below.
8
8
 
9
+ ## Checked SOAP 1.1 RPC string profile
10
+
11
+ Checked component generation supports the bounded RPC/encoded profile below. The [NOAA qualification](https://github.com/TechSpokes/typescript-wsdl-client/blob/main/docs/noaa-ndfd-qualification.md) distinguishes all-12 owned-server execution, offline provider-response replay and the 2026-11-09 live retest: ten operations succeeded, while two GML operations returned non-SOAP provider errors. No geographic correctness claim follows.
12
+
13
+ | Capability | Status | Bounded contract |
14
+ |---|---|---|
15
+ | Typed RPC/encoded parts | Partial | Required strings, Booleans, decimal/integer/date/dateTime strings and native restrictions |
16
+ | RPC Boolean record | Partial | One flat required Boolean record with native particle and fixed-value validation |
17
+ | RPC result | Partial | One opaque string result under the original output part name |
18
+ | Component SOAP response encodings | Partial | UTF-8, BOM-marked UTF-16 and exact ISO-8859-1 with assertion agreement |
19
+ | SOAP string references | Partial | Same-Body string `id`/`href`, inline strings and unambiguous roots |
20
+ | Unused locationless imports | Supported | Native component qualification resolves actual required references |
21
+
22
+ Request accessors follow source declaration order and the WSDL body namespace, with resolved native type names. JSON record property order is immaterial; native all-group validation permits unordered children while canonical writing is deterministic. Response struct and return-accessor local names may vary; the generated JSON response retains the source-owned part name. RPC/literal, other primitives, encoded arrays/shared or cyclic graphs, multiple results, declared RPC faults, headers and `parameterOrder` remain unsupported by this profile.
23
+
24
+ The component response decoder uses an explicit supported HTTP charset or XML declaration without a BOM and requires all present header/declaration/BOM assertions to agree. Absent assertions default to UTF-8; invalid UTF-8 never triggers Latin1 guessing. ISO-8859-1 preserves byte-to-code-point identity, including C1 bytes; XML 1.0 legality and caller limits still apply. This compatible response extension belongs to the checked component runtime; source decoding and the shipped direct-runtime policy remain separate. Existing string-RPC artifacts retain their identity; typed inputs have a distinct interpretation and fresh source compatibility checks.
25
+
9
26
  ## Capability Evidence Matrix
10
27
 
11
28
  The rows below are backed by committed conformance fixtures under `test/conformance/fixtures/`. Status means the current product contract, not the full standards surface.
package/docs/testing.md CHANGED
@@ -362,6 +362,8 @@ const client = createMockClient({
362
362
 
363
363
  ## For Consumer Projects
364
364
 
365
+ When upgrading an existing project, use the [version migration guide](migration.md#from-11x-to-120) to identify changed names and record/value shapes before choosing checks. This package's release tests qualify representative generated consumers and compatibility fixtures; they do not certify every consumer's migration. Keep a known-working revision and verify the application's observable contracts with its own build, maintained tests and authorized backend acceptance.
366
+
365
367
  Choose tests for your integration's observable contracts and regressions. Start with the smallest meaningful check in your project's existing runner; using this generator does not require adopting the generator repository's CI or test configuration. The npm-shipped guide contains maintainer sections for reference, but repository scripts, shared locks and release gates do not apply to consumer projects.
366
368
 
367
369
  ### Choose legacy or checked generation
@@ -30,7 +30,7 @@ A missing, empty, or unreadable packaged preamble fails generation with the reso
30
30
  | Structural collision between main and companion catalog | Two types share a name but differ structurally; rename in the companion source or point `recordType` at a distinct subtree |
31
31
  | Stream response ends abruptly or JSON array parse fails | Mid-stream upstream error per the terminal-error policy; check gateway logs for the classified error |
32
32
  | Stream recordPath does not match | SAX matching is positional and case-sensitive; verify duplicate local-name segments are spelled exactly |
33
- | Stream client throws "stream request failed" | The upstream SOAP endpoint rejected the hand-built envelope; check `requestRaw` on the response and verify SOAP action and namespaces match the WSDL binding |
33
+ | Stream request fails | Check endpoint/action/binding, media type/encoding, shape config, DTO conversion and cancellation evidence |
34
34
 
35
35
  ## SOAP Wire Logging
36
36
 
@@ -85,6 +85,8 @@ cat ./src/services/hotel/catalog.json | jq '.types'
85
85
 
86
86
  ## Streaming Debug
87
87
 
88
+ Requests use node-soap's WSDL serializer and `requestStream`. Check the [supported transport settings](gateway-guide.md#streaming-transport-settings) before changing serialization. Pre-header failures can use the gateway error handler; once output is committed, failure truncates the stream and cannot become a recoverable JSON error envelope. Scoped diagnostics and explicit companion sources help configuration; a universal live-response mismatch doctor or wizard is not provided.
89
+
88
90
  Inspect stream metadata on the compiled catalog to confirm the config was parsed and applied:
89
91
 
90
92
  ```bash
package/llms.txt CHANGED
@@ -26,7 +26,15 @@ npx wsdl-tsc pipeline \
26
26
  --gateway-service-name <slug> \
27
27
  --gateway-version-prefix <version>
28
28
 
29
- ## Generated Files
29
+ ## Profile selection
30
+
31
+ Before upgrading a 1.1.x consumer, read [migration](docs/migration.md#from-11x-to-120). Legacy remains the default, but regeneration can change colliding DTO names, mapped stream records and corrected scalar/simple-content values. Preserve matched artifacts, lockfiles and app/test customizations; adopt a new profile separately. npm published 1.1.5 and 1.1.8, while 1.1.6/1.1.7 were unpublished candidates.
32
+
33
+ Use legacy pipeline for one source, or pipeline --services-config services.json for independent legacy sources. The aggregate typed createClients binds each runtime WSDL, endpoint and security separately; no checked/app/test aggregate mode is available. See [composition](docs/gateway-guide.md#multi-service-setup).
34
+
35
+ Use pipeline --model checked-components-v1 for the bounded document/literal or SOAP 1.1 RPC/encoded string profile. Checked replies remain buffered checked values. Legacy buffered success uses envelopes; opt-in legacy NDJSON/JSON-array streams deliver generated DTO records directly through node-soap requestStream. See [migration](docs/migration.md#from-11x-to-120).
36
+
37
+ ## Output inventory
30
38
 
31
39
  - client/client.ts: typed SOAP client class
32
40
  - client/types.ts: TypeScript interfaces from XSD
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techspokes/typescript-wsdl-client",
3
- "version": "1.1.8",
3
+ "version": "1.2.0",
4
4
  "description": "Turn legacy WSDL/SOAP services into typed TypeScript clients, OpenAPI 3.1 specs, and production-ready Fastify REST gateways. Built for enterprise SOAP modernization.",
5
5
  "keywords": [
6
6
  "wsdl",
@@ -164,7 +164,7 @@
164
164
  "rimraf": "^6.1.4",
165
165
  "tsx": "^4.23.15",
166
166
  "typescript": "^6.0.3",
167
- "typescript-eslint": "^8.72.1",
167
+ "typescript-eslint": "^8.72.2",
168
168
  "vitest": "^5.1.0"
169
169
  },
170
170
  "dependencies": {
@@ -1,11 +1,18 @@
1
1
 
2
2
  /**
3
- * Streaming transport for operations flagged with a stream configuration.
4
- *
5
- * node-soap buffers the full response before invoking the operation callback
6
- * (verified empirically in ADR-002 / phase-0 research), so this method
7
- * bypasses node-soap and POSTs a hand-built SOAP envelope directly, then
8
- * pipes the response body through the SAX-driven record parser.
3
+ * Stream complete SOAP records through the legacy DTO mapper.
4
+ * @param args - Flat request DTO, serialized using the generated metadata.
5
+ * @param operation - SOAP operation name for diagnostics.
6
+ * @param requestType - Request metadata key.
7
+ * @param recordTypeName - Record metadata key, including companion catalog types.
8
+ * @param inputElementLocal - SOAP body element local name.
9
+ * @param inputElementNs - SOAP body element namespace.
10
+ * @param soapAction - SOAP 1.1 action.
11
+ * @param recordPath - Local-name path to repeated records.
12
+ * @param callOptions - Cancellation and total response deadline.
13
+ * @returns Single-pass records; callers must exhaust or return their iterator to release the connection.
14
+ * @throws Error for unsupported transport settings, HTTP errors, XML faults, or invalid mapped values.
15
+ * @constraints Streaming supports SOAP 1.1 document/literal requests, static SOAP headers, and ISecurity hooks.
9
16
  */
10
17
  protected async callStream<RequestType, RecordType, HeadersType>(
11
18
  args: RequestType,
@@ -15,169 +22,170 @@
15
22
  inputElementLocal: string,
16
23
  inputElementNs: string,
17
24
  soapAction: string,
18
- recordPath: string[]
25
+ recordPath: string[],
26
+ callOptions?: StreamCallOptions
19
27
  ): Promise<StreamOperationResponse<RecordType, HeadersType>> {
28
+ callOptions?.signal?.throwIfAborted();
20
29
  const client = await this.soapClient();
21
- const endpoint = this.resolveStreamEndpoint(client);
22
- const envelope = this.buildSoapEnvelope(args, operation, requestType, inputElementLocal, inputElementNs);
23
- const res = await fetch(endpoint, {
24
- method: "POST",
25
- headers: {
26
- "content-type": "text/xml; charset=utf-8",
27
- "soapaction": '"' + soapAction + '"',
28
- },
29
- body: envelope,
30
- });
31
- if (!res.ok) {
32
- throw new Error(
33
- operation + " stream request failed: " + res.status + " " + res.statusText
34
- );
30
+ const soapOptions = client.wsdl.options;
31
+ if (soapOptions.forceSoap12Headers || soapOptions.envelopeSoapUrl && soapOptions.envelopeSoapUrl !== "http://schemas.xmlsoap.org/soap/envelope/") {
32
+ throw new Error("Streaming supports SOAP 1.1 only");
35
33
  }
36
- if (!res.body) {
37
- throw new Error(operation + " stream request returned an empty response body");
34
+ const envelopeKey = soapOptions.envelopeKey ?? "soap";
35
+ if (!/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(envelopeKey) || envelopeKey === "xml" || envelopeKey === "xmlns") {
36
+ throw new Error("Streaming requires a supported XML envelope prefix");
38
37
  }
39
- const headers: Record<string, string> = {};
40
- res.headers.forEach((value, key) => { headers[key] = value; });
41
- const spec: RecordParseSpec = {
42
- recordPath,
43
- recordTypeName,
44
- attributesKey: this.attributesKeyOut,
45
- childType: this.dataTypes?.ChildrenTypes,
38
+ const soapHeaders = client.getSoapHeaders() ?? [];
39
+ if (soapHeaders.some(header => typeof header !== "string")) {
40
+ throw new Error("Streaming requires static XML SOAP headers");
41
+ }
42
+ if (client.getBodyAttributes()?.length) {
43
+ throw new Error("Streaming does not support SOAP body attributes");
44
+ }
45
+ const transport = this.options?.httpClient ?? new __SOAP_HTTP_CLIENT__(this.options);
46
+ if (!transport.requestStream) throw new Error("Configured SOAP HTTP client does not support requestStream");
47
+ const security = (client as unknown as {security?: __SOAP_SECURITY__}).security ?? this.security;
48
+ const {endpoint, input} = this.resolveStreamBinding(client, operation, soapAction, inputElementLocal, inputElementNs);
49
+ const headers: Record<string, string> = {
50
+ "Content-Type": "text/xml; charset=utf-8",
51
+ SOAPAction: '"' + soapAction + '"',
46
52
  };
47
- const records = parseRecords<RecordType>(
48
- this.webStreamToAsyncIterable(res.body),
49
- spec
50
- );
51
- return {
52
- records,
53
- headers: headers as unknown as HeadersType,
54
- requestRaw: envelope,
53
+ const requestOptions: __SOAP_EX_OPTIONS__ = {...this.streamRequestOptions};
54
+ security?.addHeaders?.(headers);
55
+ security?.addOptions?.(requestOptions);
56
+ Object.assign(headers, client.getHttpHeaders());
57
+ if (requestOptions.forceMTOM || requestOptions.attachments?.length || requestOptions.ntlm) {
58
+ throw new Error("Streaming does not support MTOM attachments or NTLM transport");
59
+ }
60
+ // @why node-soap's schema serializer retains element qualification and foreign references that DTO type metadata omits.
61
+ const soapArgs = this.toSoapArgs(args, requestType);
62
+ const message = client.wsdl.objectToDocumentXML(input.$name, soapArgs, input.targetNSAlias, input.targetNamespace, input.$type || input.$lookupType, requestOptions);
63
+ let envelope = this.buildSoapEnvelope(message, envelopeKey, client);
64
+ const headerXml = soapHeaders.join(" ") + (security?.postProcess ? "" : security?.toXML?.() ?? "");
65
+ if (headerXml || security?.postProcess) {
66
+ const body = "<" + envelopeKey + ":Body>";
67
+ const headerNamespaces = client.wsdl.xmlnsInHeader ? " " + client.wsdl.xmlnsInHeader : "";
68
+ envelope = envelope.replace(body, "<" + envelopeKey + ":Header" + headerNamespaces + ">" + headerXml + "</" + envelopeKey + ":Header>" + body);
69
+ }
70
+ if (security?.postProcess) envelope = security.postProcess(envelope, envelopeKey);
71
+ if (typeof requestOptions.postProcess === "function") envelope = requestOptions.postProcess(envelope);
72
+ const controller = new AbortController();
73
+ const signals = [controller.signal, callOptions?.signal, requestOptions.signal].filter((signal): signal is AbortSignal => !!signal);
74
+ requestOptions.signal = AbortSignal.any(signals);
75
+ const timeoutMs = callOptions?.timeoutMs;
76
+ if (timeoutMs !== undefined && (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647)) {
77
+ throw new RangeError("Stream timeoutMs must be a positive safe timer duration");
78
+ }
79
+ const timer = timeoutMs === undefined ? undefined : setTimeout(() => controller.abort(new Error("Stream request timed out")), timeoutMs);
80
+ timer?.unref();
81
+ let body: (AsyncIterable<string | Uint8Array> & {destroy(): void}) | undefined;
82
+ const cleanup = () => {
83
+ if (timer) clearTimeout(timer);
84
+ if (typeof body?.destroy === "function") body.destroy();
85
+ controller.abort();
55
86
  };
87
+ try {
88
+ const res = await transport.requestStream(endpoint, envelope, headers, requestOptions, client);
89
+ body = res.data as unknown as typeof body;
90
+ if (!body || typeof body[Symbol.asyncIterator] !== "function" || typeof body.destroy !== "function") {
91
+ throw new Error(operation + " stream request returned an invalid response body");
92
+ }
93
+ if (res.status < 200 || res.status >= 300) {
94
+ throw new Error(operation + " stream request failed: " + res.status + " " + res.statusText);
95
+ }
96
+ const spec: RecordParseSpec = {
97
+ recordPath,
98
+ recordTypeName,
99
+ attributesKey: this.attributesKeyOut,
100
+ childType: this.dataTypes?.ChildrenTypes,
101
+ repeatedElements: this.dataTypes?.RepeatedElements,
102
+ };
103
+ const source = parseRecords(body, spec);
104
+ const self = this;
105
+ async function* mapped(): AsyncGenerator<RecordType> {
106
+ try {
107
+ for await (const record of source) yield self.fromSoapResult(record, recordTypeName) as RecordType;
108
+ } finally { cleanup(); }
109
+ }
110
+ const iterator = mapped();
111
+ // @constraints return() must close transport even before the generator's first next().
112
+ const records: AsyncIterable<RecordType> = {
113
+ [Symbol.asyncIterator]() {
114
+ return {
115
+ next: () => iterator.next(),
116
+ return: async () => { cleanup(); return iterator.return(undefined); },
117
+ throw: async error => { cleanup(); return iterator.throw(error); },
118
+ };
119
+ },
120
+ };
121
+ return {records, headers: res.headers as unknown as HeadersType, requestRaw: envelope};
122
+ } catch (error) {
123
+ cleanup();
124
+ throw error;
125
+ }
56
126
  }
57
127
 
58
128
  /**
59
- * Walk the node-soap WSDL descriptor to locate the first service port's
60
- * SOAP address. Falls back to the \`source\` URL when the descriptor is
61
- * unavailable (e.g., \`source\` was given as a direct endpoint URL).
129
+ * Select one compatible live document/literal operation binding and endpoint.
130
+ * @param client - Initialized node-soap client.
131
+ * @param operation - Compiled operation name.
132
+ * @param soapAction - Compiled SOAP action, used to disambiguate live methods.
133
+ * @param inputElementLocal - Expected input element local name.
134
+ * @param inputElementNs - Expected input namespace.
135
+ * @returns Matched input descriptor and current override or owning port address.
136
+ * @throws Error for missing, ambiguous, or incompatible live operation bindings.
137
+ * @constraints Distinct bindings cannot be assumed equivalent after node-soap has processed shared port-type metadata.
62
138
  */
63
- protected resolveStreamEndpoint(client: __SOAP_CLIENT_TYPE__): string {
139
+ protected resolveStreamBinding(
140
+ client: __SOAP_CLIENT_TYPE__,
141
+ operation: string,
142
+ soapAction: string,
143
+ inputElementLocal: string,
144
+ inputElementNs: string
145
+ ): {endpoint: string; input: {$name: string; targetNSAlias: string; targetNamespace: string; $type?: string; $lookupType?: string}} {
146
+ const override = (client as unknown as {endpoint?: string}).endpoint ?? this.options?.endpoint;
147
+ const candidates: Array<{endpoint: string; binding: unknown; method: any}> = [];
64
148
  const services = (client as any)?.wsdl?.definitions?.services ?? {};
65
149
  for (const svc of Object.values(services)) {
66
150
  const ports = (svc as any)?.ports ?? {};
67
151
  for (const port of Object.values(ports)) {
68
- const location = (port as any)?.location;
69
- if (typeof location === "string" && location) return location;
152
+ const binding = (port as any)?.binding;
153
+ const method = binding?.methods?.[operation];
154
+ if (!method || method.$name !== operation || (method.soapAction ?? "") !== soapAction) continue;
155
+ const endpoint = override || (port as any)?.location;
156
+ if (typeof endpoint !== "string" || !endpoint) throw new Error("No SOAP endpoint configured for stream request");
157
+ candidates.push({endpoint, binding, method});
70
158
  }
71
159
  }
72
- return this.source;
160
+ if (candidates.length === 0) throw new Error("No matching SOAP operation binding for stream request: " + operation);
161
+ const selected = candidates[0];
162
+ if (candidates.some(candidate => candidate.binding !== selected.binding || candidate.method !== selected.method || candidate.endpoint !== selected.endpoint)) {
163
+ throw new Error("Ambiguous or incompatible SOAP operation bindings for stream request: " + operation);
164
+ }
165
+ const method = selected.method;
166
+ const input = method.input;
167
+ if (method.style && method.style !== "document" || method.inputSoap?.use && method.inputSoap.use !== "literal" ||
168
+ !input || input.parts || input.$name !== inputElementLocal || input.targetNamespace !== inputElementNs) {
169
+ throw new Error("Incompatible SOAP document/literal input binding for stream request: " + operation);
170
+ }
171
+ return {endpoint: selected.endpoint, input};
73
172
  }
74
173
 
75
174
  /**
76
- * Build a SOAP 1.1 envelope around the operation's input element. Uses the
77
- * existing attributes / children metadata so attribute bags render as XML
78
- * attributes and child properties render as nested elements.
175
+ * Wrap the schema-serialized request using the initialized client's envelope namespace context.
176
+ * @param body - Document/literal XML from node-soap's public WSDL serializer.
177
+ * @param envelopeKey - Validated SOAP envelope prefix, also passed to security processing.
178
+ * @param client - Initialized client supplying the WSDL namespace declarations.
179
+ * @returns A SOAP 1.1 envelope ready for static headers and security hooks.
79
180
  */
80
181
  protected buildSoapEnvelope(
81
- args: unknown,
82
- operation: string,
83
- requestType: string | undefined,
84
- inputElementLocal: string,
85
- inputElementNs: string
182
+ body: string,
183
+ envelopeKey: string,
184
+ client: __SOAP_CLIENT_TYPE__
86
185
  ): string {
87
- const body = this.toXmlElement(args, requestType, inputElementLocal, inputElementNs);
88
186
  return '<?xml version="1.0" encoding="utf-8"?>' +
89
- '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">' +
90
- '<soap:Body>' + body + '</soap:Body>' +
91
- '</soap:Envelope>';
92
- }
93
-
94
- /**
95
- * Serialize a value as a single XML element. The element namespace is only
96
- * emitted when \`namespace\` is provided (i.e., the envelope's top-level body
97
- * element). Nested elements inherit the namespace via XML scoping.
98
- */
99
- protected toXmlElement(
100
- value: unknown,
101
- typeName: string | undefined,
102
- elementName: string,
103
- namespace?: string
104
- ): string {
105
- const nsAttr = namespace ? ' xmlns="' + this.escapeXml(namespace) + '"' : "";
106
- if (value === null || value === undefined) {
107
- return '<' + elementName + nsAttr +
108
- ' xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>';
109
- }
110
- if (typeof value !== "object") {
111
- return '<' + elementName + nsAttr + '>' + this.escapeXml(String(value)) + '</' + elementName + '>';
112
- }
113
- if (Array.isArray(value)) {
114
- return value.map((v) => this.toXmlElement(v, typeName, elementName, namespace)).join("");
115
- }
116
- const obj = value as Record<string, unknown>;
117
- const attributesList = (typeName && this.dataTypes?.Attributes?.[typeName]) || [];
118
- const childrenTypes = (typeName && this.dataTypes?.ChildrenTypes?.[typeName]) || {};
119
- const attrPairs: Array<[string, string]> = [];
120
- const bagIn = (obj as any)[this.attributesKeyIn] ?? (obj as any)["attributes"];
121
- if (bagIn && typeof bagIn === "object") {
122
- for (const [k, v] of Object.entries(bagIn)) attrPairs.push([k, this.normalizeAttr(v)]);
123
- }
124
- const childParts: string[] = [];
125
- let textContent: string | undefined;
126
- if ("$value" in obj) textContent = String(obj.$value ?? "");
127
- for (const [k, v] of Object.entries(obj)) {
128
- if (k === "$value" || k === this.attributesKeyIn || k === "attributes") continue;
129
- if ((attributesList as readonly string[]).includes(k)) {
130
- attrPairs.push([k, this.normalizeAttr(v)]);
131
- continue;
132
- }
133
- const childTypeRaw = (childrenTypes as Record<string, string>)[k];
134
- const childType = childTypeRaw?.endsWith("[]") ? childTypeRaw.slice(0, -2) : childTypeRaw;
135
- if (Array.isArray(v)) {
136
- for (const item of v) childParts.push(this.toXmlElement(item, childType, k));
137
- } else {
138
- childParts.push(this.toXmlElement(v, childType, k));
139
- }
140
- }
141
- const attrStr = attrPairs.map(([k, val]) => ' ' + k + '="' + this.escapeXml(val) + '"').join("");
142
- if (childParts.length === 0 && textContent === undefined) {
143
- return '<' + elementName + nsAttr + attrStr + '/>';
144
- }
145
- const inner = childParts.join("") + (textContent !== undefined ? this.escapeXml(textContent) : "");
146
- return '<' + elementName + nsAttr + attrStr + '>' + inner + '</' + elementName + '>';
147
- }
148
-
149
- protected normalizeAttr(v: unknown): string {
150
- if (v === null || v === undefined) return "";
151
- if (typeof v === "boolean") return v ? "true" : "false";
152
- if (typeof v === "number") return Number.isFinite(v) ? String(v) : "";
153
- return String(v);
154
- }
155
-
156
- protected escapeXml(s: string): string {
157
- return s
158
- .replace(/&/g, "&amp;")
159
- .replace(/</g, "&lt;")
160
- .replace(/>/g, "&gt;")
161
- .replace(/"/g, "&quot;")
162
- .replace(/'/g, "&apos;");
163
- }
164
-
165
- /**
166
- * Iterate an uploaded web ReadableStream as Uint8Array chunks. Node 24+
167
- * ReadableStream implements Symbol.asyncIterator natively; this helper
168
- * normalizes the types so TypeScript can drive it through \`for await\`.
169
- */
170
- protected async *webStreamToAsyncIterable(
171
- body: ReadableStream<Uint8Array>
172
- ): AsyncIterable<Uint8Array> {
173
- const reader = body.getReader();
174
- try {
175
- while (true) {
176
- const {value, done} = await reader.read();
177
- if (done) return;
178
- if (value) yield value;
179
- }
180
- } finally {
181
- try { reader.releaseLock(); } catch { /* noop */ }
182
- }
187
+ '<' + envelopeKey + ':Envelope xmlns:' + envelopeKey + '="http://schemas.xmlsoap.org/soap/envelope/" ' +
188
+ 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' + client.wsdl.xmlnsInEnvelope + '>' +
189
+ '<' + envelopeKey + ':Body>' + body + '</' + envelopeKey + ':Body>' +
190
+ '</' + envelopeKey + ':Envelope>';
183
191
  }
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Response shape for streaming __CLIENT_NAME__ operations. `records` is a
3
- * single-pass async iterable of parsed record objects; iteration pulls bytes
4
- * from the upstream SOAP response on demand. `headers` is the HTTP response
5
- * header map captured before the first record is parsed. `requestRaw`, when
6
- * populated, contains the serialized SOAP envelope that was sent upstream.
2
+ * Represents a streamed __CLIENT_NAME__ response.
3
+ * @typeParam RecordType Parsed record DTO yielded by the single-pass upstream iterator.
4
+ * @typeParam HeadersType HTTP headers captured before the first record is parsed.
5
+ * @constraints The structural default permits WSDL DTOs named Record without shadowing a utility type.
6
+ * @remarks requestRaw contains the serialized SOAP request when populated.
7
7
  */
8
- export type StreamOperationResponse<RecordType, HeadersType = Record<string, unknown>> = {
8
+ export type StreamOperationResponse<RecordType, HeadersType = {[key: string]: unknown}> = {
9
9
  records: AsyncIterable<RecordType>;
10
10
  headers: HeadersType;
11
11
  requestRaw?: string;