@techspokes/typescript-wsdl-client 1.1.8 → 1.3.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.
- package/README.md +22 -3
- package/dist/checkedPlanning.js +27 -4
- package/dist/cli.js +17 -1
- package/dist/client/checkedDirectRuntime.d.ts +11 -1
- package/dist/client/checkedDirectRuntime.js +19 -2
- package/dist/client/directClientRuntime.d.ts +18 -1
- package/dist/client/directClientRuntime.js +15 -3
- package/dist/client/directClientVersion.d.ts +2 -0
- package/dist/client/directClientVersion.js +2 -0
- package/dist/client/generateClient.js +72 -16
- package/dist/client/generateDirectClient.js +14 -9
- package/dist/client/generateOperations.js +30 -11
- package/dist/client/generateUtils.js +5 -1
- package/dist/compiler/canonicalGraph.d.ts +4 -15
- package/dist/compiler/canonicalGraph.js +1 -8
- package/dist/compiler/componentRecordProjection.d.ts +3 -0
- package/dist/compiler/componentRecordProjection.js +10 -0
- package/dist/compiler/declarationIdentity.d.ts +22 -0
- package/dist/compiler/declarationIdentity.js +9 -0
- package/dist/compiler/directCodec.d.ts +22 -2
- package/dist/compiler/directCodec.js +40 -7
- package/dist/compiler/directOperation.d.ts +28 -5
- package/dist/compiler/directOperation.js +84 -14
- package/dist/compiler/directSoap.d.ts +40 -10
- package/dist/compiler/directSoap.js +151 -18
- package/dist/compiler/directSoapSource.js +3 -2
- package/dist/compiler/directSourceCapsule.js +69 -4
- package/dist/compiler/directStructure.js +6 -2
- package/dist/compiler/directStructurePlan.d.ts +10 -1
- package/dist/compiler/directStructurePlan.js +27 -7
- package/dist/compiler/directValues.d.ts +15 -3
- package/dist/compiler/directValues.js +20 -4
- package/dist/compiler/legacyDeclarations.d.ts +26 -0
- package/dist/compiler/legacyDeclarations.js +144 -0
- package/dist/compiler/projectionScalar.d.ts +18 -1
- package/dist/compiler/projectionScalar.js +47 -13
- package/dist/compiler/rpcStringValues.d.ts +30 -0
- package/dist/compiler/rpcStringValues.js +196 -0
- package/dist/compiler/rpcTypedValues.d.ts +48 -0
- package/dist/compiler/rpcTypedValues.js +190 -0
- package/dist/compiler/schemaCompiler.js +44 -109
- package/dist/compiler/semanticCatalog.d.ts +2 -2
- package/dist/compiler/soapHeaders.d.ts +10 -4
- package/dist/compiler/soapHeaders.js +28 -9
- package/dist/gateway/generateCheckedGateway.js +26 -6
- package/dist/gateway/generateComposedGateway.d.ts +16 -0
- package/dist/gateway/generateComposedGateway.js +65 -0
- package/dist/gateway/generateGateway.js +1 -0
- package/dist/gateway/generators.js +19 -4
- package/dist/gateway/helpers.d.ts +1 -0
- package/dist/gateway/helpers.js +15 -0
- package/dist/generation/componentShape.js +25 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/loader/orderedSyntax.d.ts +4 -0
- package/dist/loader/orderedSyntax.js +18 -4
- package/dist/multiServicePipeline.d.ts +41 -0
- package/dist/multiServicePipeline.js +287 -0
- package/dist/openapi/composeOpenAPI.d.ts +29 -0
- package/dist/openapi/composeOpenAPI.js +322 -0
- package/dist/pipeline.d.ts +1 -1
- package/dist/pipeline.js +2 -0
- package/docs/agent-skill.md +10 -1
- package/docs/api-reference.md +127 -9
- package/docs/checked-gateway.md +10 -0
- package/docs/checked-generation.md +11 -5
- package/docs/cli-reference.md +25 -5
- package/docs/concepts.md +4 -5
- package/docs/configuration.md +10 -3
- package/docs/corpus.md +1 -1
- package/docs/gateway-guide.md +104 -27
- package/docs/generated-code.md +12 -6
- package/docs/migration-playbook.md +4 -0
- package/docs/migration.md +134 -6
- package/docs/output-anatomy.md +20 -17
- package/docs/production.md +7 -5
- package/docs/start-here.md +12 -2
- package/docs/supported-patterns.md +23 -0
- package/docs/testing.md +2 -0
- package/docs/troubleshooting.md +3 -1
- package/llms.txt +9 -1
- package/package.json +2 -2
- package/src/runtime/clientStreamMethods.tpl.txt +156 -148
- package/src/runtime/operationsStreamHelper.tpl.txt +6 -6
- package/src/runtime/streamXml.ts +48 -20
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
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
|
|
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
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
'<
|
|
90
|
-
'
|
|
91
|
-
'</
|
|
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, "&")
|
|
159
|
-
.replace(/</g, "<")
|
|
160
|
-
.replace(/>/g, ">")
|
|
161
|
-
.replace(/"/g, """)
|
|
162
|
-
.replace(/'/g, "'");
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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 =
|
|
8
|
+
export type StreamOperationResponse<RecordType, HeadersType = {[key: string]: unknown}> = {
|
|
9
9
|
records: AsyncIterable<RecordType>;
|
|
10
10
|
headers: HeadersType;
|
|
11
11
|
requestRaw?: string;
|
package/src/runtime/streamXml.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* Q4 (saxes placement): runtime dependency of the wsdl-tsc package; the
|
|
18
18
|
* generated client imports the emitted copy of this module and
|
|
19
19
|
* inherits saxes via its own dependency tree.
|
|
20
|
+
* @since Unreleased
|
|
20
21
|
*/
|
|
21
22
|
import {SaxesParser, type SaxesTagPlain} from "saxes";
|
|
22
23
|
|
|
@@ -42,6 +43,8 @@ export interface RecordParseSpec {
|
|
|
42
43
|
* detection only.
|
|
43
44
|
*/
|
|
44
45
|
childType?: Record<string, Record<string, string>>;
|
|
46
|
+
/** Explicit repeated XML properties; when supplied, child-type [] hints describe list values instead. */
|
|
47
|
+
repeatedElements?: Record<string, readonly string[]>;
|
|
45
48
|
/**
|
|
46
49
|
* Compiled-catalog prop-meta map: carries min/max/nillable/declaredType.
|
|
47
50
|
* When present, `max > 1` or `max === "unbounded"` drives array emission
|
|
@@ -58,12 +61,12 @@ export interface PropMeta {
|
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
/**
|
|
61
|
-
* Consume
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* the
|
|
64
|
+
* Consume XML bytes or strings and yield complete legacy record nodes.
|
|
65
|
+
* @param source - Single-pass XML bytes or strings; UTF-8 byte boundaries may split characters.
|
|
66
|
+
* @param spec - Local-name record path and optional catalog metadata; attribute bags remain lexical strings.
|
|
67
|
+
* @returns Complete records without buffering the complete XML document.
|
|
68
|
+
* @throws Error for malformed XML, SOAP Body faults, or an empty record path.
|
|
69
|
+
* @constraints Exhaust or return the single-pass iterator to release its source; undeclared xsi:nil remains accepted for legacy compatibility.
|
|
67
70
|
*/
|
|
68
71
|
export async function* parseRecords<T = unknown>(
|
|
69
72
|
source: AsyncIterable<string | Uint8Array>,
|
|
@@ -75,9 +78,12 @@ export async function* parseRecords<T = unknown>(
|
|
|
75
78
|
throw new Error("parseRecords: recordPath must not be empty");
|
|
76
79
|
}
|
|
77
80
|
const attrsKey = spec.attributesKey ?? "$attributes";
|
|
81
|
+
const decoder = new TextDecoder("utf-8");
|
|
78
82
|
|
|
79
83
|
// Global tag stack, maintained across the entire document.
|
|
80
84
|
const stack: string[] = [];
|
|
85
|
+
const namespaces: Array<Record<string, string>> = [];
|
|
86
|
+
const elementNamespaces: string[] = [];
|
|
81
87
|
// Records materialized during the current chunk write, awaiting yield.
|
|
82
88
|
const pending: T[] = [];
|
|
83
89
|
// When a parser.on('error') fires we buffer the error for re-throw on the
|
|
@@ -102,11 +108,27 @@ export async function* parseRecords<T = unknown>(
|
|
|
102
108
|
const openNodes: OpenNode[] = [];
|
|
103
109
|
|
|
104
110
|
parser.on("opentag", (tag: SaxesTagPlain) => {
|
|
105
|
-
|
|
111
|
+
const inherited = namespaces.at(-1);
|
|
112
|
+
const scope: Record<string, string> = Object.assign(Object.create(null) as Record<string, string>, inherited);
|
|
113
|
+
for (const [name, value] of Object.entries(tag.attributes)) {
|
|
114
|
+
if (name === "xmlns") scope[""] = value;
|
|
115
|
+
else if (name.startsWith("xmlns:")) scope[name.slice(6)] = value;
|
|
116
|
+
}
|
|
117
|
+
const localName = local(tag.name);
|
|
118
|
+
const prefix = tag.name.includes(":") ? tag.name.slice(0, tag.name.indexOf(":")) : "";
|
|
119
|
+
const namespace = scope[prefix] ?? "";
|
|
120
|
+
if (localName === "Fault" && namespace === elementNamespaces.at(-1) &&
|
|
121
|
+
(namespace === "http://schemas.xmlsoap.org/soap/envelope/" || namespace === "http://www.w3.org/2003/05/soap-envelope") &&
|
|
122
|
+
stack.at(-1) === "Body") {
|
|
123
|
+
parseError = new Error("SOAP Fault received in stream response");
|
|
124
|
+
}
|
|
125
|
+
namespaces.push(scope);
|
|
126
|
+
elementNamespaces.push(namespace);
|
|
127
|
+
stack.push(localName);
|
|
106
128
|
if (openNodes.length === 0) {
|
|
107
129
|
// Not yet inside a record. Check whether entering the path tail.
|
|
108
130
|
if (tailMatches(stack, recordPath)) {
|
|
109
|
-
openNodes.push(newNode(tag, spec.recordTypeName, null, attrsKey));
|
|
131
|
+
openNodes.push(newNode(tag, spec.recordTypeName, null, attrsKey, scope));
|
|
110
132
|
}
|
|
111
133
|
return;
|
|
112
134
|
}
|
|
@@ -114,9 +136,9 @@ export async function* parseRecords<T = unknown>(
|
|
|
114
136
|
const parent = openNodes[openNodes.length - 1];
|
|
115
137
|
parent.hadChildren = true;
|
|
116
138
|
const parentType = parent.typeName;
|
|
117
|
-
const childTypeRaw = parentType ? spec.childType?.[parentType]?.[
|
|
139
|
+
const childTypeRaw = parentType ? spec.childType?.[parentType]?.[localName] : undefined;
|
|
118
140
|
const childTypeName = childTypeRaw ? stripArraySuffix(childTypeRaw) : null;
|
|
119
|
-
openNodes.push(newNode(tag, childTypeName,
|
|
141
|
+
openNodes.push(newNode(tag, childTypeName, localName, attrsKey, scope));
|
|
120
142
|
});
|
|
121
143
|
|
|
122
144
|
const appendText = (t: string) => {
|
|
@@ -139,6 +161,8 @@ export async function* parseRecords<T = unknown>(
|
|
|
139
161
|
}
|
|
140
162
|
}
|
|
141
163
|
stack.pop();
|
|
164
|
+
namespaces.pop();
|
|
165
|
+
elementNamespaces.pop();
|
|
142
166
|
});
|
|
143
167
|
|
|
144
168
|
parser.on("error", (err: Error) => {
|
|
@@ -148,13 +172,14 @@ export async function* parseRecords<T = unknown>(
|
|
|
148
172
|
try {
|
|
149
173
|
for await (const chunk of source) {
|
|
150
174
|
if (parseError) throw parseError;
|
|
151
|
-
const text = typeof chunk === "string" ? chunk :
|
|
175
|
+
const text = typeof chunk === "string" ? decoder.decode() + chunk : decoder.decode(chunk, {stream: true});
|
|
152
176
|
parser.write(text);
|
|
153
177
|
if (parseError) throw parseError;
|
|
154
178
|
while (pending.length > 0) {
|
|
155
179
|
yield pending.shift()!;
|
|
156
180
|
}
|
|
157
181
|
}
|
|
182
|
+
parser.write(decoder.decode());
|
|
158
183
|
parser.close();
|
|
159
184
|
if (parseError) throw parseError;
|
|
160
185
|
while (pending.length > 0) {
|
|
@@ -176,6 +201,7 @@ function newNode(
|
|
|
176
201
|
typeName: string | null,
|
|
177
202
|
propName: string | null,
|
|
178
203
|
attrsKey: string,
|
|
204
|
+
namespaces: Record<string, string>,
|
|
179
205
|
): {
|
|
180
206
|
obj: Record<string, unknown>;
|
|
181
207
|
typeName: string | null;
|
|
@@ -193,7 +219,11 @@ function newNode(
|
|
|
193
219
|
// concern that should not pollute the user-visible record.
|
|
194
220
|
const cleaned: Record<string, string> = {};
|
|
195
221
|
for (const k of attrKeys) {
|
|
196
|
-
if (
|
|
222
|
+
if (k === "xmlns" || k.startsWith("xmlns:")) continue;
|
|
223
|
+
const prefix = k.includes(":") ? k.slice(0, k.indexOf(":")) : "";
|
|
224
|
+
const isNilAttribute = k === "nil" || k === "xsi:nil" && !namespaces.xsi ||
|
|
225
|
+
local(k) === "nil" && namespaces[prefix] === "http://www.w3.org/2001/XMLSchema-instance";
|
|
226
|
+
if (isNilAttribute && (attrs[k] === "true" || attrs[k] === "1")) {
|
|
197
227
|
isNil = true;
|
|
198
228
|
continue;
|
|
199
229
|
}
|
|
@@ -241,16 +271,16 @@ function assignChild(
|
|
|
241
271
|
const propMetaEntry = parentType ? spec.propMeta?.[parentType]?.[propName] : undefined;
|
|
242
272
|
const childTypeHint = parentType ? spec.childType?.[parentType]?.[propName] : undefined;
|
|
243
273
|
|
|
244
|
-
//
|
|
245
|
-
// ends in `[]`, or (c) the slot is already occupied (implicit repetition).
|
|
274
|
+
// @constraints Explicit repetition metadata distinguishes inline xs:list values from repeated XML elements.
|
|
246
275
|
const metaSaysArray =
|
|
247
276
|
propMetaEntry?.max === "unbounded" ||
|
|
248
277
|
(typeof propMetaEntry?.max === "number" && propMetaEntry.max > 1);
|
|
249
|
-
const hintSaysArray = !!childTypeHint && childTypeHint.endsWith("[]");
|
|
278
|
+
const hintSaysArray = spec.repeatedElements === undefined && !!childTypeHint && childTypeHint.endsWith("[]");
|
|
279
|
+
const declaredRepeated = parentType ? spec.repeatedElements?.[parentType]?.includes(propName) : false;
|
|
250
280
|
const existing = parent.obj[propName];
|
|
251
281
|
const slotTaken = existing !== undefined;
|
|
252
282
|
|
|
253
|
-
if (metaSaysArray || hintSaysArray) {
|
|
283
|
+
if (metaSaysArray || hintSaysArray || declaredRepeated) {
|
|
254
284
|
if (Array.isArray(existing)) {
|
|
255
285
|
existing.push(value);
|
|
256
286
|
} else if (slotTaken) {
|
|
@@ -286,8 +316,6 @@ function tailMatches(stack: string[], path: string[]): boolean {
|
|
|
286
316
|
return true;
|
|
287
317
|
}
|
|
288
318
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
function decodeUtf8(buf: Uint8Array): string {
|
|
292
|
-
return UTF8_DECODER.decode(buf);
|
|
319
|
+
function local(name: string): string {
|
|
320
|
+
return name.slice(name.lastIndexOf(":") + 1);
|
|
293
321
|
}
|