@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.
- package/README.md +5 -0
- package/dist/app/generateApp.d.ts.map +1 -1
- package/dist/app/generateApp.js +4 -3
- package/dist/cli.js +46 -2
- package/dist/client/generateClient.d.ts.map +1 -1
- package/dist/client/generateClient.js +64 -8
- package/dist/client/generateOperations.d.ts.map +1 -1
- package/dist/client/generateOperations.js +29 -6
- package/dist/client/generateTypes.d.ts.map +1 -1
- package/dist/client/generateTypes.js +13 -0
- package/dist/compiler/schemaCompiler.d.ts +44 -11
- package/dist/compiler/schemaCompiler.d.ts.map +1 -1
- package/dist/compiler/schemaCompiler.js +102 -6
- package/dist/compiler/shapeResolver.d.ts +18 -0
- package/dist/compiler/shapeResolver.d.ts.map +1 -0
- package/dist/compiler/shapeResolver.js +280 -0
- package/dist/gateway/generateGateway.d.ts.map +1 -1
- package/dist/gateway/generateGateway.js +2 -1
- package/dist/gateway/generators.d.ts +13 -1
- package/dist/gateway/generators.d.ts.map +1 -1
- package/dist/gateway/generators.js +98 -13
- package/dist/gateway/helpers.d.ts +16 -0
- package/dist/gateway/helpers.d.ts.map +1 -1
- package/dist/gateway/helpers.js +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -4
- package/dist/openapi/generateOpenAPI.d.ts.map +1 -1
- package/dist/openapi/generateOpenAPI.js +30 -2
- package/dist/openapi/generatePaths.d.ts.map +1 -1
- package/dist/openapi/generatePaths.js +4 -2
- package/dist/openapi/generateSchemas.d.ts.map +1 -1
- package/dist/openapi/generateSchemas.js +20 -5
- package/dist/pipeline.d.ts +13 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +17 -1
- package/dist/runtime/ndjson.d.ts +24 -0
- package/dist/runtime/ndjson.d.ts.map +1 -0
- package/dist/runtime/ndjson.js +30 -0
- package/dist/runtime/streamXml.d.ts +45 -0
- package/dist/runtime/streamXml.d.ts.map +1 -0
- package/dist/runtime/streamXml.js +212 -0
- package/dist/test/generators.d.ts +2 -2
- package/dist/test/generators.d.ts.map +1 -1
- package/dist/test/generators.js +79 -26
- package/dist/test/mockData.d.ts +12 -2
- package/dist/test/mockData.d.ts.map +1 -1
- package/dist/test/mockData.js +17 -8
- package/dist/util/cli.d.ts +3 -0
- package/dist/util/cli.d.ts.map +1 -1
- package/dist/util/cli.js +6 -1
- package/dist/util/runtimeSource.d.ts +2 -0
- package/dist/util/runtimeSource.d.ts.map +1 -0
- package/dist/util/runtimeSource.js +38 -0
- package/dist/util/streamConfig.d.ts +59 -0
- package/dist/util/streamConfig.d.ts.map +1 -0
- package/dist/util/streamConfig.js +230 -0
- package/docs/README.md +1 -0
- package/docs/api-reference.md +146 -0
- package/docs/architecture.md +27 -5
- package/docs/cli-reference.md +30 -0
- package/docs/concepts.md +150 -11
- package/docs/configuration.md +40 -0
- package/docs/decisions/002-streamable-responses.md +308 -0
- package/docs/gateway-guide.md +37 -0
- package/docs/generated-code.md +21 -0
- package/docs/migration-playbook.md +33 -0
- package/docs/migration.md +31 -6
- package/docs/output-anatomy.md +49 -0
- package/docs/production.md +32 -0
- package/docs/start-here.md +33 -0
- package/docs/supported-patterns.md +29 -0
- package/docs/testing.md +14 -0
- package/docs/troubleshooting.md +18 -0
- package/package.json +9 -6
- package/src/runtime/clientStreamMethods.tpl.txt +183 -0
- package/src/runtime/ndjson.ts +32 -0
- package/src/runtime/operationsStreamHelper.tpl.txt +13 -0
- package/src/runtime/streamXml.ts +293 -0
|
@@ -58,6 +58,39 @@ Open `openapi.json` and review the paths, request/response schemas, and descript
|
|
|
58
58
|
|
|
59
59
|
Validate the spec with any OpenAPI tool. Built-in validation runs by default; disable with `--openapi-validate false` if you need to inspect an intermediate state.
|
|
60
60
|
|
|
61
|
+
## Step 2b (Optional): Opt Into Streaming for Large Responses
|
|
62
|
+
|
|
63
|
+
Skip this step unless a specific SOAP operation returns payloads too large or too slow to buffer in memory. Good candidates are batch-style operations with repeated record elements, responses that use `xs:any` wildcards to point at concrete records in a companion WSDL, and any operation where first-byte latency matters more than total duration.
|
|
64
|
+
|
|
65
|
+
Author a stream config naming those operations:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"operations": {
|
|
70
|
+
"MyBatchOp": {
|
|
71
|
+
"recordType": "MyRecordType",
|
|
72
|
+
"recordPath": ["MyBatchOpResponse", "Records", "Record"]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Regenerate with `--stream-config`:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npx wsdl-tsc pipeline \
|
|
82
|
+
--wsdl-source your-service.wsdl \
|
|
83
|
+
--client-dir ./generated/client \
|
|
84
|
+
--openapi-file ./generated/openapi.json \
|
|
85
|
+
--gateway-dir ./generated/gateway \
|
|
86
|
+
--gateway-service-name your-service \
|
|
87
|
+
--gateway-version-prefix v1 \
|
|
88
|
+
--stream-config ./stream.config.json \
|
|
89
|
+
--init-app
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The opted-in operations now return `StreamOperationResponse<RecordType>` on the client, serve `application/x-ndjson` on the gateway, and advertise the record schema in OpenAPI via `x-wsdl-tsc-stream`. Operations not listed in the config are unchanged. See [Stream Configuration](configuration.md#stream-configuration) for the full file reference and [ADR-002](decisions/002-streamable-responses.md) for the terminal-error policy.
|
|
93
|
+
|
|
61
94
|
## Step 3: Generate the REST Gateway
|
|
62
95
|
|
|
63
96
|
Generate Fastify route handlers that translate JSON HTTP requests into SOAP calls.
|
package/docs/migration.md
CHANGED
|
@@ -16,12 +16,37 @@ These steps apply to every version upgrade:
|
|
|
16
16
|
|
|
17
17
|
## Version Compatibility
|
|
18
18
|
|
|
19
|
-
| wsdl-tsc | Node.js | TypeScript | soap | Fastify |
|
|
20
|
-
|
|
21
|
-
| 0.
|
|
22
|
-
| 0.
|
|
23
|
-
| 0.
|
|
24
|
-
| 0.
|
|
19
|
+
| wsdl-tsc | Node.js | TypeScript | soap | Fastify | saxes |
|
|
20
|
+
|----------|---------|------------|------|---------|-------|
|
|
21
|
+
| 0.17.x | >= 20.0 | >= 6.0 | >= 1.9 | >= 5.8 | >= 6.0 |
|
|
22
|
+
| 0.16.x | >= 20.0 | >= 6.0 | >= 1.9 | >= 5.8 | N/A |
|
|
23
|
+
| 0.15.x | >= 20.0 | >= 6.0 | >= 1.8 | >= 5.4 | N/A |
|
|
24
|
+
| 0.11.x to 0.14.x | >= 20.0 | >= 5.6 | >= 1.3 | >= 5.2 | N/A |
|
|
25
|
+
| 0.10.x | >= 20.0 | >= 5.6 | >= 1.3 | >= 5.2 | N/A |
|
|
26
|
+
| 0.9.x | >= 20.0 | >= 5.6 | >= 1.3 | >= 5.2 | N/A |
|
|
27
|
+
| 0.8.x | >= 20.0 | >= 5.6 | >= 1.3 | >= 5.2 | N/A |
|
|
28
|
+
| 0.7.x | >= 20.0 | >= 5.6 | >= 1.3 | N/A | N/A |
|
|
29
|
+
|
|
30
|
+
Versions 0.11 through 0.16 are additive and non-breaking. See `CHANGELOG.md` for per-version detail rather than dedicated upgrade sections here.
|
|
31
|
+
|
|
32
|
+
## Upgrading to 0.17.x from 0.16.x
|
|
33
|
+
|
|
34
|
+
This upgrade adds opt-in streamable SOAP responses. No breaking changes; generated output is byte-for-byte unchanged when `--stream-config` is not provided.
|
|
35
|
+
|
|
36
|
+
### What Changed in 0.17.x
|
|
37
|
+
|
|
38
|
+
The CLI gains a `--stream-config <file>` flag on `compile`, `client`, and `pipeline`. Operations listed in that file emit a new client method signature returning `StreamOperationResponse<RecordType>` with `records: AsyncIterable<RecordType>`, an OpenAPI 200 response typed as `application/x-ndjson` with an `x-wsdl-tsc-stream` extension, and a Fastify route that streams NDJSON with backpressure. The compiler now retains `xs:any` wildcard particles on compiled types (previously dropped silently), enabling honest stream-candidate detection and companion-catalog shape resolution. `saxes ^6.0.0` is now a runtime dependency of the package and is pinned automatically into the generated app scaffold.
|
|
39
|
+
|
|
40
|
+
### Steps to Upgrade to 0.17.x
|
|
41
|
+
|
|
42
|
+
1. Update the package and regenerate; no flag or code changes are required for buffered operations
|
|
43
|
+
2. If consumers integrate the generated client directly (not via the app scaffold), install `saxes ^6.0.0` as a runtime dependency before using any stream operation
|
|
44
|
+
3. To opt into streaming for specific operations, author a stream-config file (see [Stream Configuration](configuration.md#stream-configuration)) and pass it via `--stream-config`
|
|
45
|
+
4. Review the new generated client method signatures for any opted-in operation; consumers must use `for await (const record of result.records)` instead of awaiting the full response
|
|
46
|
+
|
|
47
|
+
### Is 0.17.x Breaking?
|
|
48
|
+
|
|
49
|
+
No. Without `--stream-config`, generated output is byte-for-byte unchanged. Consumers only see new surfaces when they opt in.
|
|
25
50
|
|
|
26
51
|
## Upgrading to 0.10.x from 0.9.x
|
|
27
52
|
|
package/docs/output-anatomy.md
CHANGED
|
@@ -43,6 +43,24 @@ Exports a TypeScript interface with the same method signatures as the client cla
|
|
|
43
43
|
|
|
44
44
|
Contains runtime metadata and helper functions. Includes `unwrapArrayWrappers()` for bridging between SOAP array wrapper objects and flattened OpenAPI array schemas.
|
|
45
45
|
|
|
46
|
+
### Stream Operations
|
|
47
|
+
|
|
48
|
+
When `--stream-config` opts an operation into streaming (see [ADR-002](decisions/002-streamable-responses.md)), the client emits additional surfaces alongside the buffered ones. Buffered operations in the same client are unaffected.
|
|
49
|
+
|
|
50
|
+
`operations.ts` gains a `StreamOperationResponse<RecordType>` type that stream methods return instead of the buffered `{ response, headers, responseRaw, requestRaw }` shape:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
export type StreamOperationResponse<RecordType, HeadersType = Record<string, unknown>> = {
|
|
54
|
+
records: AsyncIterable<RecordType>;
|
|
55
|
+
headers: HeadersType;
|
|
56
|
+
requestRaw?: string;
|
|
57
|
+
};
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`client.ts` gains a protected `callStream()` transport. Stream methods bypass `node-soap` (which buffers the full response before invoking its callback, as confirmed in phase-0 research) and instead POST a hand-built SOAP envelope via global `fetch`, piping the response body through a SAX-driven record parser. `node-soap` remains the transport for buffered operations.
|
|
61
|
+
|
|
62
|
+
Generated imports required at runtime: `saxes` for SAX parsing. The generated app scaffold pins `saxes ^6.0.0` automatically; manual consumers must install it explicitly.
|
|
63
|
+
|
|
46
64
|
## OpenAPI Output
|
|
47
65
|
|
|
48
66
|
Generated at the path specified by `--openapi-file`.
|
|
@@ -55,6 +73,29 @@ The spec includes one POST path per WSDL operation, request and response schemas
|
|
|
55
73
|
|
|
56
74
|
OpenAPI validation runs by default using `@apidevtools/swagger-parser`. Disable with `--openapi-validate false`.
|
|
57
75
|
|
|
76
|
+
### Stream Schema Extension
|
|
77
|
+
|
|
78
|
+
Stream operations do not use the standard success envelope for `200` responses. The response content declares the configured stream media type (default `application/x-ndjson`) with `schema: { "type": "string" }` and an `x-wsdl-tsc-stream` extension that carries the record schema reference:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"200": {
|
|
83
|
+
"description": "Successful streamed SOAP operation response",
|
|
84
|
+
"content": {
|
|
85
|
+
"application/x-ndjson": {
|
|
86
|
+
"schema": { "type": "string" },
|
|
87
|
+
"x-wsdl-tsc-stream": {
|
|
88
|
+
"format": "ndjson",
|
|
89
|
+
"itemSchema": { "$ref": "#/components/schemas/UnitDescriptiveContentType" }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
OpenAPI 3.1 cannot fully describe an NDJSON sequence as a standard JSON Schema document, so the extension makes the item schema explicit for generated gateways, documentation tools, and future SDK generators. Error responses (400, 502, and the rest) still use the normal envelope.
|
|
98
|
+
|
|
58
99
|
## Gateway Output
|
|
59
100
|
|
|
60
101
|
Generated into the directory specified by `--gateway-dir`.
|
|
@@ -74,6 +115,14 @@ Generated into the directory specified by `--gateway-dir`.
|
|
|
74
115
|
|
|
75
116
|
Each route file in `routes/` follows the same pattern: validate the JSON request body against the operation schema, call the corresponding SOAP operation via the typed client, transform the SOAP response to JSON, and return it with the appropriate response schema.
|
|
76
117
|
|
|
118
|
+
### Stream Routes
|
|
119
|
+
|
|
120
|
+
Stream-configured operations generate a different route shape. The Fastify response serialization schema is omitted because Fastify cannot serialize an unbounded stream with a normal JSON response schema. The handler sets `reply.type("application/x-ndjson")` and returns `reply.send(toNdjson(result.records))`. `runtime.ts` gains a `toNdjson<T>(records: AsyncIterable<T>): Readable` helper that wraps the async iterable in a backpressure-aware Node `Readable`. See the [Gateway Guide](gateway-guide.md#streaming-handlers) for the full handler example and terminal-error policy.
|
|
121
|
+
|
|
122
|
+
### Generated Test Surface
|
|
123
|
+
|
|
124
|
+
When `--test-dir` is combined with `--stream-config`, the generated happy-path tests for stream operations assert on the `application/x-ndjson` content-type and parse each line as a separate JSON record. Mock clients use async-generator overrides that yield records to drive those tests; see the [Testing Guide](testing.md) for the pattern.
|
|
125
|
+
|
|
77
126
|
### Plugin registration
|
|
78
127
|
|
|
79
128
|
The generated plugin exports a Fastify plugin function. Register it with your Fastify app and provide a client instance (real or mock) and a route prefix.
|
package/docs/production.md
CHANGED
|
@@ -61,6 +61,30 @@ npx wsdl-tsc openapi --catalog-file ./build/catalog.json --openapi-file ./docs/a
|
|
|
61
61
|
}
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
## Streaming Operations
|
|
65
|
+
|
|
66
|
+
Operations opted into streaming with `--stream-config` change the production profile of the gateway. Plan for these characteristics before rolling out.
|
|
67
|
+
|
|
68
|
+
### Backpressure
|
|
69
|
+
|
|
70
|
+
Records flow through `Readable.from` in the generated `runtime.ts`. The iterator's `next()` is not called until the internal buffer has room, so a slow HTTP client propagates backpressure all the way to the upstream SOAP server. A client that opens a connection and stops reading will eventually stall the upstream SOAP socket; set idle timeouts on both the gateway and the SOAP endpoint accordingly.
|
|
71
|
+
|
|
72
|
+
### Connection Timeouts
|
|
73
|
+
|
|
74
|
+
Stream responses can stay open for the full duration of the upstream SOAP call. The Fastify `keepAliveTimeout`, `requestTimeout`, and any reverse proxy (nginx, ALB, CloudFront) must allow the maximum expected upstream duration. Default Fastify timeouts are shorter than typical long-running SOAP batch responses.
|
|
75
|
+
|
|
76
|
+
### Memory Profile
|
|
77
|
+
|
|
78
|
+
Memory stays bounded regardless of payload size because records are parsed and emitted one at a time. The SAX parser buffers only within the current record element. This is the primary reason to opt into streaming: buffered operations load the full response into memory before yielding.
|
|
79
|
+
|
|
80
|
+
### Observability
|
|
81
|
+
|
|
82
|
+
Log the time to first record, not just the time to response completion. First-record time is the most useful SLO signal because it measures the combined latency of upstream connect, first-byte, and parser spin-up. Track it separately from total stream duration.
|
|
83
|
+
|
|
84
|
+
### Terminal-Error Policy
|
|
85
|
+
|
|
86
|
+
Errors raised before the first record use the normal gateway error envelope (client sees a standard JSON error). Errors raised mid-stream truncate the chunked response without a terminating zero-chunk. Consumers detect this as an incomplete HTTP response. Document this behavior for downstream API consumers so they distinguish truncation from a legitimate empty stream.
|
|
87
|
+
|
|
64
88
|
## Known Limitations
|
|
65
89
|
|
|
66
90
|
### Choice Elements
|
|
@@ -78,3 +102,11 @@ Security hints extracted from policies. Custom policies may require manual secur
|
|
|
78
102
|
### Array Wrapper Flattening
|
|
79
103
|
|
|
80
104
|
Single-child sequences with maxOccurs>1 become array schemas. Sequences with multiple children preserve wrapper.
|
|
105
|
+
|
|
106
|
+
### Stream Format Coverage
|
|
107
|
+
|
|
108
|
+
Only `ndjson` is emitted today. `json-array` is reserved in the config schema but not yet implemented; opting in with `format: "json-array"` parses successfully but generates no routes for that operation.
|
|
109
|
+
|
|
110
|
+
### Stream Transport Bypasses node-soap
|
|
111
|
+
|
|
112
|
+
Stream operations bypass `node-soap` entirely and POST a hand-built SOAP envelope via `fetch`. Any `node-soap` middleware, interceptors, or custom security handlers that your buffered operations rely on will not apply to stream operations. Authentication headers, proxies, and TLS options must be configured on the stream transport path separately.
|
package/docs/start-here.md
CHANGED
|
@@ -64,6 +64,38 @@ The gateway transforms JSON HTTP requests into SOAP calls and returns JSON respo
|
|
|
64
64
|
|
|
65
65
|
Next: [Gateway Guide](gateway-guide.md) for integration details, then [Migration Playbook](migration-playbook.md) for the full modernization workflow
|
|
66
66
|
|
|
67
|
+
### I need to stream large SOAP responses
|
|
68
|
+
|
|
69
|
+
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.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx wsdl-tsc pipeline \
|
|
73
|
+
--wsdl-source your-service.wsdl \
|
|
74
|
+
--client-dir ./generated/client \
|
|
75
|
+
--openapi-file ./generated/openapi.json \
|
|
76
|
+
--gateway-dir ./generated/gateway \
|
|
77
|
+
--gateway-service-name my-service \
|
|
78
|
+
--gateway-version-prefix v1 \
|
|
79
|
+
--stream-config ./stream.config.json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Minimal `stream.config.json`:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"operations": {
|
|
87
|
+
"MyStreamOp": {
|
|
88
|
+
"recordType": "MyRecordType",
|
|
89
|
+
"recordPath": ["MyStreamOpResponse", "Records", "Record"]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Stream operations return `StreamOperationResponse<RecordType>` on the client (`records: AsyncIterable<RecordType>`), emit `application/x-ndjson` on the gateway, and advertise the record schema in OpenAPI via the `x-wsdl-tsc-stream` extension.
|
|
96
|
+
|
|
97
|
+
Next: [ADR-002: Streamable Responses](decisions/002-streamable-responses.md) for rationale and terminal-error policy, then [Stream Configuration](configuration.md#stream-configuration) for the full file reference.
|
|
98
|
+
|
|
67
99
|
## What NOT to Expect
|
|
68
100
|
|
|
69
101
|
This package does not replace a full API management platform. It does not provide rate limiting, policy enforcement, or multi-language SDK generation. It generates code; it does not run a proxy or manage deployments.
|
|
@@ -80,3 +112,4 @@ For more on scope boundaries, see the "When NOT to Use This" section of the [REA
|
|
|
80
112
|
| Plan a full SOAP-to-REST migration | [Migration Playbook](migration-playbook.md) |
|
|
81
113
|
| Set up testing for generated code | [Testing Guide](testing.md) |
|
|
82
114
|
| Review all CLI flags | [CLI Reference](cli-reference.md) |
|
|
115
|
+
| Opt specific operations into NDJSON streaming | [ADR-002](decisions/002-streamable-responses.md) and [Stream Configuration](configuration.md#stream-configuration) |
|
|
@@ -8,6 +8,7 @@ These patterns are handled end-to-end: WSDL parsing, TypeScript type generation,
|
|
|
8
8
|
|
|
9
9
|
- Complex types with `<xs:sequence>`, `<xs:all>`, and `<xs:choice>` compositors, including recursive nesting
|
|
10
10
|
- Simple content with attributes using the `$value` pattern to preserve text content alongside attribute properties
|
|
11
|
+
- Named simple type restrictions and enumerations emitted as TypeScript aliases and OpenAPI scalar schemas
|
|
11
12
|
- Type inheritance through `<xs:extension>` and `<xs:restriction>` on both simple and complex content
|
|
12
13
|
- Nested XSD imports across multiple schema files with relative and absolute URI resolution
|
|
13
14
|
- Multiple namespaces with deterministic collision resolution via PascalCase uniqueness
|
|
@@ -18,6 +19,34 @@ These patterns are handled end-to-end: WSDL parsing, TypeScript type generation,
|
|
|
18
19
|
- Circular type references detected and broken with minimal stub types
|
|
19
20
|
- Multiple WSDL ports and bindings; the first SOAP binding is selected, all ports are documented in service metadata
|
|
20
21
|
- SOAP 1.1 and SOAP 1.2 binding detection
|
|
22
|
+
- Streamable SOAP responses, opt-in per operation via `--stream-config` (ADR-002): client exposes `AsyncIterable<RecordType>`, gateway emits NDJSON with backpressure, OpenAPI advertises the record schema via `x-wsdl-tsc-stream`
|
|
23
|
+
- `xs:any` wildcard particles retained on compiled types (they used to be dropped silently) — enables honest stream-candidate detection and companion-catalog shape resolution
|
|
24
|
+
|
|
25
|
+
### Named simple types and same-name elements
|
|
26
|
+
|
|
27
|
+
Named `xs:simpleType` declarations compile to TypeScript type aliases. Restriction enumerations compile to string literal unions in TypeScript and `enum` scalar schemas in OpenAPI.
|
|
28
|
+
|
|
29
|
+
When a global `xs:element` has the same local name as a referenced named simple type, the generator reuses the simple type alias. It does not emit a second wrapper interface with the same TypeScript name.
|
|
30
|
+
|
|
31
|
+
```xml
|
|
32
|
+
<xs:simpleType name="MyEnum">
|
|
33
|
+
<xs:restriction base="xs:string">
|
|
34
|
+
<xs:enumeration value="Red"/>
|
|
35
|
+
<xs:enumeration value="Green"/>
|
|
36
|
+
</xs:restriction>
|
|
37
|
+
</xs:simpleType>
|
|
38
|
+
<xs:element name="MyEnum" nillable="true" type="tns:MyEnum"/>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The generated TypeScript type is a scalar alias:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
export type MyEnum = "Red" | "Green";
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If a message part uses `tns:MyEnum` as the operation root element, the generated client method uses `MyEnum` directly for the request or response payload type.
|
|
48
|
+
|
|
49
|
+
The compiler records this reuse as an informational catalog diagnostic. CLI commands print it as a `Note:` line, not as a warning.
|
|
21
50
|
|
|
22
51
|
## Partially Supported
|
|
23
52
|
|
package/docs/testing.md
CHANGED
|
@@ -246,6 +246,20 @@ const client = createMockClient({
|
|
|
246
246
|
|
|
247
247
|
Mock responses use the pre-unwrap SOAP wrapper shape. The generated `unwrapArrayWrappers()` function handles conversion at runtime.
|
|
248
248
|
|
|
249
|
+
For operations opted in via `--stream-config`, the mock returns `records: AsyncIterable<RecordType>` (via a small `asyncIterableOf` helper) and the generated happy-path test asserts on the NDJSON content-type and parseable record lines. Override a stream op with a multi-record iterable to exercise downstream backpressure:
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
const client = createMockClient({
|
|
253
|
+
UnitDescriptiveInfoStream: async () => ({
|
|
254
|
+
records: (async function* () {
|
|
255
|
+
yield { Id: "1", Name: "Villa A" };
|
|
256
|
+
yield { Id: "2", Name: "Villa B" };
|
|
257
|
+
})(),
|
|
258
|
+
headers: {},
|
|
259
|
+
}),
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
249
263
|
## For Consumer Projects
|
|
250
264
|
|
|
251
265
|
If you're using wsdl-tsc as a dependency and want to test your integration:
|
package/docs/troubleshooting.md
CHANGED
|
@@ -17,6 +17,12 @@ See [README](../README.md) for quick start and [CLI Reference](cli-reference.md)
|
|
|
17
17
|
| TypeScript compilation errors | Check --import-extensions matches your tsconfig moduleResolution |
|
|
18
18
|
| Gateway validation failures | Ensure OpenAPI has valid $ref paths and all schemas in components.schemas |
|
|
19
19
|
| Catalog file not found | Catalog defaults to output directory; use --catalog-file to specify |
|
|
20
|
+
| Stream config references unknown operation | Operation name must match the WSDL exactly; check spelling and casing |
|
|
21
|
+
| Stream record type not found | `recordType` must exist in the main catalog or a companion `shapeCatalog` must supply it; confirm the companion WSDL compiles cleanly in isolation |
|
|
22
|
+
| 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 |
|
|
23
|
+
| NDJSON response ends abruptly | Mid-stream upstream error per the terminal-error policy; check gateway logs for the classified error |
|
|
24
|
+
| Stream recordPath does not match | SAX matching is positional and case-sensitive; verify duplicate local-name segments are spelled exactly |
|
|
25
|
+
| 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 |
|
|
20
26
|
|
|
21
27
|
## SOAP Wire Logging
|
|
22
28
|
|
|
@@ -68,3 +74,15 @@ Or inspect catalog from client generation:
|
|
|
68
74
|
```bash
|
|
69
75
|
cat ./src/services/hotel/catalog.json | jq '.types'
|
|
70
76
|
```
|
|
77
|
+
|
|
78
|
+
## Streaming Debug
|
|
79
|
+
|
|
80
|
+
Inspect stream metadata on the compiled catalog to confirm the config was parsed and applied:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
cat ./src/services/hotel/catalog.json | jq '.operations[] | select(.stream) | {name, stream}'
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Each entry shows the normalized `OperationStreamMetadata` (mode, format, mediaType, recordPath, recordTypeName, and any `shapeCatalogName`). If an expected operation is missing, the config either did not match the WSDL operation name or was not passed to the generation command.
|
|
87
|
+
|
|
88
|
+
For record-path and chunk-boundary issues, the reference integration test pattern lives in `test/integration/stream-end-to-end.test.ts` and the SAX record matcher is exercised by `test/unit/stream-xml.test.ts`. Running them against a local fixture isolates whether the issue is in the config, the parser, or the upstream SOAP server.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@techspokes/typescript-wsdl-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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",
|
|
@@ -53,7 +53,9 @@
|
|
|
53
53
|
"docs",
|
|
54
54
|
"README.md",
|
|
55
55
|
"LICENSE",
|
|
56
|
-
"llms.txt"
|
|
56
|
+
"llms.txt",
|
|
57
|
+
"src/runtime/*.ts",
|
|
58
|
+
"src/runtime/*.tpl.txt"
|
|
57
59
|
],
|
|
58
60
|
"scripts": {
|
|
59
61
|
"dev": "tsx src/cli.ts",
|
|
@@ -82,18 +84,19 @@
|
|
|
82
84
|
"@types/js-yaml": "^4.0.9",
|
|
83
85
|
"@types/node": "^25.6.0",
|
|
84
86
|
"@types/yargs": "^17.0.35",
|
|
85
|
-
"fastify": "^5.8.
|
|
87
|
+
"fastify": "^5.8.5",
|
|
86
88
|
"fastify-plugin": "^5.1.0",
|
|
87
89
|
"rimraf": "^6.1.3",
|
|
88
90
|
"tsx": "^4.21.0",
|
|
89
|
-
"typescript": "^6.0.
|
|
91
|
+
"typescript": "^6.0.3",
|
|
90
92
|
"vitest": "^4.1.0"
|
|
91
93
|
},
|
|
92
94
|
"dependencies": {
|
|
93
95
|
"@apidevtools/swagger-parser": "^12.1.0",
|
|
94
|
-
"fast-xml-parser": "^5.
|
|
96
|
+
"fast-xml-parser": "^5.7.1",
|
|
95
97
|
"js-yaml": "^4.1.1",
|
|
96
|
-
"
|
|
98
|
+
"saxes": "^6.0.0",
|
|
99
|
+
"soap": "^1.9.1",
|
|
97
100
|
"yargs": "^18.0.0"
|
|
98
101
|
},
|
|
99
102
|
"funding": {
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
|
|
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.
|
|
9
|
+
*/
|
|
10
|
+
protected async callStream<RequestType, RecordType, HeadersType>(
|
|
11
|
+
args: RequestType,
|
|
12
|
+
operation: string,
|
|
13
|
+
requestType: string | undefined,
|
|
14
|
+
recordTypeName: string,
|
|
15
|
+
inputElementLocal: string,
|
|
16
|
+
inputElementNs: string,
|
|
17
|
+
soapAction: string,
|
|
18
|
+
recordPath: string[]
|
|
19
|
+
): Promise<StreamOperationResponse<RecordType, HeadersType>> {
|
|
20
|
+
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
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (!res.body) {
|
|
37
|
+
throw new Error(operation + " stream request returned an empty response body");
|
|
38
|
+
}
|
|
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,
|
|
46
|
+
};
|
|
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,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
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).
|
|
62
|
+
*/
|
|
63
|
+
protected resolveStreamEndpoint(client: soap.Client): string {
|
|
64
|
+
const services = (client as any)?.wsdl?.definitions?.services ?? {};
|
|
65
|
+
for (const svc of Object.values(services)) {
|
|
66
|
+
const ports = (svc as any)?.ports ?? {};
|
|
67
|
+
for (const port of Object.values(ports)) {
|
|
68
|
+
const location = (port as any)?.location;
|
|
69
|
+
if (typeof location === "string" && location) return location;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return this.source;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
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.
|
|
79
|
+
*/
|
|
80
|
+
protected buildSoapEnvelope(
|
|
81
|
+
args: unknown,
|
|
82
|
+
operation: string,
|
|
83
|
+
requestType: string | undefined,
|
|
84
|
+
inputElementLocal: string,
|
|
85
|
+
inputElementNs: string
|
|
86
|
+
): string {
|
|
87
|
+
const body = this.toXmlElement(args, requestType, inputElementLocal, inputElementNs);
|
|
88
|
+
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, "&")
|
|
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 20+
|
|
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
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NDJSON adapter for record iterables.
|
|
3
|
+
*
|
|
4
|
+
* Phase 3 of ADR-002. Given an `AsyncIterable<T>` (typically the output of
|
|
5
|
+
* `parseRecords`), produce a Node `Readable` that emits one JSON-encoded line
|
|
6
|
+
* per record and respects downstream backpressure.
|
|
7
|
+
*
|
|
8
|
+
* Terminal-error policy (Q3 resolved): the stream aborts on source errors.
|
|
9
|
+
* Before-first-byte errors surface as the stream's `error` event before any
|
|
10
|
+
* bytes are pushed, so Fastify can translate them into a normal JSON error
|
|
11
|
+
* envelope. Errors after the first byte propagate as `error` events too, but
|
|
12
|
+
* callers should treat them as a truncated response.
|
|
13
|
+
*/
|
|
14
|
+
import {Readable} from "node:stream";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Wrap an async iterable of records in a Node `Readable` stream that emits
|
|
18
|
+
* NDJSON (one JSON document per line, LF-terminated). Downstream backpressure
|
|
19
|
+
* is honored via `Readable.from`'s default behavior: the iterator's `next()`
|
|
20
|
+
* is not called until the internal buffer has room.
|
|
21
|
+
*
|
|
22
|
+
* Source errors are forwarded to the returned stream's `error` event.
|
|
23
|
+
*/
|
|
24
|
+
export function toNdjson<T>(records: AsyncIterable<T>): Readable {
|
|
25
|
+
return Readable.from(encode(records), {objectMode: false, encoding: "utf-8"});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function* encode<T>(records: AsyncIterable<T>): AsyncIterable<string> {
|
|
29
|
+
for await (const record of records) {
|
|
30
|
+
yield JSON.stringify(record) + "\n";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
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.
|
|
7
|
+
*/
|
|
8
|
+
export type StreamOperationResponse<RecordType, HeadersType = Record<string, unknown>> = {
|
|
9
|
+
records: AsyncIterable<RecordType>;
|
|
10
|
+
headers: HeadersType;
|
|
11
|
+
requestRaw?: string;
|
|
12
|
+
};
|
|
13
|
+
|