@techspokes/typescript-wsdl-client 0.26.0 → 0.28.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 +3 -3
- package/dist/gateway/generators.d.ts +3 -2
- package/dist/gateway/generators.d.ts.map +1 -1
- package/dist/gateway/generators.js +51 -9
- package/dist/openapi/generateOpenAPI.d.ts.map +1 -1
- package/dist/openapi/generateOpenAPI.js +4 -1
- package/dist/runtime/jsonArray.d.ts +24 -0
- package/dist/runtime/jsonArray.d.ts.map +1 -0
- package/dist/runtime/jsonArray.js +52 -0
- package/dist/test/generators.d.ts.map +1 -1
- package/dist/test/generators.js +25 -3
- package/docs/README.md +1 -1
- package/docs/api-reference.md +3 -1
- package/docs/architecture.md +1 -1
- package/docs/cli-reference.md +1 -1
- package/docs/concepts.md +2 -2
- package/docs/configuration.md +1 -0
- package/docs/decisions/002-streamable-responses.md +32 -10
- package/docs/gateway-guide.md +5 -10
- package/docs/generated-code.md +1 -1
- package/docs/migration-playbook.md +1 -1
- package/docs/migration.md +1 -1
- package/docs/output-anatomy.md +26 -4
- package/docs/production.md +2 -2
- package/docs/releases/v0.27.0.md +31 -0
- package/docs/releases/v0.28.0.md +32 -0
- package/docs/roadmap/README.md +2 -2
- package/docs/roadmap/v1.0-contract-audit.md +1 -1
- package/docs/roadmap/v1.0-json-array-streaming.md +6 -6
- package/docs/start-here.md +4 -2
- package/docs/supported-patterns.md +1 -1
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +1 -1
- package/package.json +1 -1
- package/src/runtime/jsonArray.ts +55 -0
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ Most tools in this space stop at one layer: a SOAP runtime, type generation, or
|
|
|
28
28
|
- Handles complex inheritance, `xs:attribute`, namespace collisions, nested XSD imports, and configurable `xs:choice` modeling
|
|
29
29
|
- Generated `operations.ts` interface enables testing without importing `soap` or calling a live service
|
|
30
30
|
- OpenAPI is a first-class output, not an afterthought; types, schemas, and descriptions stay aligned
|
|
31
|
-
- Opt-in
|
|
31
|
+
- Opt-in streaming for large SOAP responses: client emits `AsyncIterable<RecordType>`, gateway flushes NDJSON or JSON array records incrementally, OpenAPI advertises the record schema via `x-wsdl-tsc-stream`
|
|
32
32
|
- Optional agent skill ZIP for consumer-project AI agents that need package-specific generation guidance
|
|
33
33
|
- MIT licensed; generated code is yours with no attribution required
|
|
34
34
|
|
|
@@ -140,7 +140,7 @@ Platform API gateways solve governance, policy, and multi-language SDK generatio
|
|
|
140
140
|
| REST gateway generation | no | no | no | yes |
|
|
141
141
|
| Runnable app scaffolding | no | no | no | yes |
|
|
142
142
|
| Mockable operations interface | no | no | no | yes |
|
|
143
|
-
| Streaming large responses (NDJSON) | no | no | no | yes |
|
|
143
|
+
| Streaming large responses (NDJSON or JSON array) | no | no | no | yes |
|
|
144
144
|
|
|
145
145
|
Data as of April 2026.
|
|
146
146
|
|
|
@@ -234,7 +234,7 @@ See [CLI Reference](docs/cli-reference.md) for all flags and examples.
|
|
|
234
234
|
| [Architecture](docs/architecture.md) | Internal pipeline for contributors |
|
|
235
235
|
| [Agent Skill Artifact](docs/agent-skill.md) | Release ZIP for consumer-project AI agents |
|
|
236
236
|
| [Version 1.0 Roadmap Plan](docs/roadmap/README.md) | Implementation slices and release gates for 1.0 |
|
|
237
|
-
| [Streamable Responses (ADR-002)](docs/decisions/002-streamable-responses.md) | Opt-in streaming: client `AsyncIterable`, gateway NDJSON, `x-wsdl-tsc-stream` |
|
|
237
|
+
| [Streamable Responses (ADR-002)](docs/decisions/002-streamable-responses.md) | Opt-in streaming: client `AsyncIterable`, gateway NDJSON or JSON array, `x-wsdl-tsc-stream` |
|
|
238
238
|
| [Version Migration](docs/migration.md) | Upgrading between package versions |
|
|
239
239
|
|
|
240
240
|
## Why This Exists
|
|
@@ -42,8 +42,9 @@ export interface OperationMetadata {
|
|
|
42
42
|
skipResponseSchema?: boolean;
|
|
43
43
|
/**
|
|
44
44
|
* Stream metadata populated from the OpenAPI `x-wsdl-tsc-stream` extension.
|
|
45
|
-
* When present, the route handler pipes `result.records` through the
|
|
46
|
-
*
|
|
45
|
+
* When present, the route handler pipes `result.records` through the helper
|
|
46
|
+
* matching the configured stream format instead of envelope-wrapping a single
|
|
47
|
+
* response object.
|
|
47
48
|
*/
|
|
48
49
|
stream?: {
|
|
49
50
|
mediaType: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generators.d.ts","sourceRoot":"","sources":["../../src/gateway/generators.ts"],"names":[],"mappings":"AAcA,OAAO,EAAC,KAAK,UAAU,EAAE,KAAK,eAAe,EAAyE,MAAM,cAAc,CAAC;AAG3I;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC5B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA6BxB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6IAA6I;IAC7I,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B
|
|
1
|
+
{"version":3,"file":"generators.d.ts","sourceRoot":"","sources":["../../src/gateway/generators.ts"],"names":[],"mappings":"AAcA,OAAO,EAAC,KAAK,UAAU,EAAE,KAAK,eAAe,EAAyE,MAAM,cAAc,CAAC;AAG3I;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC5B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA6BxB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6IAA6I;IAC7I,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;;OAKG;IACH,MAAM,CAAC,EAAE;QACP,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,QAAQ,GAAG,YAAY,CAAC;QAChC,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAwBD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,eAAe,EACpB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACtC,0BAA0B,EAAE,MAAM,EAAE,EACpC,iBAAiB,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,GAAG,EACvH,UAAU,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,MAAM,EACnC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GACnC,iBAAiB,EAAE,CAyJrB;AAyBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,IAAI,CAsBN;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,iBAAiB,EAAE,EAC/B,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAChC,IAAI,CA6DN;AA4BD,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,GAAG,EACb,IAAI,CAAC,EAAE;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAC,GAC5B,IAAI,CA2PN;AA6ED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,UAAU,EACtB,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAChC,IAAI,CAuFN;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,EACtB,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAChC,IAAI,CA6BN;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,iBAAiB,EAAE,EAC/B,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,UAAU,EACtB,OAAO,CAAC,EAAE,GAAG,GACZ,IAAI,CA6HN"}
|
|
@@ -230,8 +230,8 @@ export function emitOperationSchemas(doc, opsDir, versionSlug, serviceSlug, sche
|
|
|
230
230
|
path: p,
|
|
231
231
|
summary: normalizeDocCommentText(typeof operation.summary === "string" ? operation.summary : undefined),
|
|
232
232
|
description: normalizeDocCommentText(typeof operation.description === "string" ? operation.description : undefined),
|
|
233
|
-
// Stream ops always skip response schema because
|
|
234
|
-
//
|
|
233
|
+
// Stream ops always skip response schema because the handler sends a
|
|
234
|
+
// Readable directly instead of a value for fast-json-stringify.
|
|
235
235
|
skipResponseSchema: skipResponseSchema || !!streamEntry,
|
|
236
236
|
...(streamEntry ? { stream: streamEntry } : {}),
|
|
237
237
|
});
|
|
@@ -646,10 +646,10 @@ export function createGatewayErrorHandler_${vSlug}_${sSlug}() {
|
|
|
646
646
|
/**
|
|
647
647
|
* Returns the streaming helpers block for runtime.ts.
|
|
648
648
|
*
|
|
649
|
-
* The emitted
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
649
|
+
* The emitted helpers mirror the reference implementations in src/runtime but
|
|
650
|
+
* are inlined here to avoid a cross-package import from the generated gateway
|
|
651
|
+
* (which would require wsdl-tsc to be a runtime dependency of the consumer's
|
|
652
|
+
* project).
|
|
653
653
|
*/
|
|
654
654
|
function buildStreamRuntimeSection() {
|
|
655
655
|
return `
|
|
@@ -676,6 +676,45 @@ async function* encodeNdjson<T>(records: AsyncIterable<T>): AsyncIterable<string
|
|
|
676
676
|
yield JSON.stringify(record) + "\\n";
|
|
677
677
|
}
|
|
678
678
|
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Wrap an async iterable of records in a Node Readable stream that emits a JSON
|
|
682
|
+
* array without buffering the full record set. The first record is prefetched
|
|
683
|
+
* before the opening bracket is pushed, so before-first-record source errors
|
|
684
|
+
* can still be translated into the standard JSON error envelope.
|
|
685
|
+
*/
|
|
686
|
+
export function toJsonArray<T>(records: AsyncIterable<T>): Readable {
|
|
687
|
+
return Readable.from(encodeJsonArray(records), { objectMode: false, encoding: "utf-8" });
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async function* encodeJsonArray<T>(records: AsyncIterable<T>): AsyncIterable<string> {
|
|
691
|
+
const iterator = records[Symbol.asyncIterator]();
|
|
692
|
+
let complete = false;
|
|
693
|
+
try {
|
|
694
|
+
const first = await iterator.next();
|
|
695
|
+
if (first.done) {
|
|
696
|
+
complete = true;
|
|
697
|
+
yield "[]";
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
yield "[" + JSON.stringify(first.value);
|
|
702
|
+
|
|
703
|
+
while (true) {
|
|
704
|
+
const next = await iterator.next();
|
|
705
|
+
if (next.done) {
|
|
706
|
+
complete = true;
|
|
707
|
+
yield "]";
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
yield "," + JSON.stringify(next.value);
|
|
711
|
+
}
|
|
712
|
+
} finally {
|
|
713
|
+
if (!complete && typeof iterator.return === "function") {
|
|
714
|
+
await iterator.return();
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
679
718
|
`;
|
|
680
719
|
}
|
|
681
720
|
/**
|
|
@@ -872,8 +911,11 @@ export function emitRouteFilesWithHandlers(outDir, routesDir, versionSlug, servi
|
|
|
872
911
|
? `request.body as ${reqTypeName}`
|
|
873
912
|
: "request.body";
|
|
874
913
|
// Build the runtime import and return expression based on unwrap availability
|
|
914
|
+
const streamHelperName = op.stream?.format === "json-array"
|
|
915
|
+
? "toJsonArray"
|
|
916
|
+
: "toNdjson";
|
|
875
917
|
const runtimeImport = op.stream
|
|
876
|
-
? `import {
|
|
918
|
+
? `import { ${streamHelperName} } from "../runtime${suffix}";`
|
|
877
919
|
: hasUnwrap
|
|
878
920
|
? `import { buildSuccessEnvelope, unwrapArrayWrappers } from "../runtime${suffix}";`
|
|
879
921
|
: `import { buildSuccessEnvelope } from "../runtime${suffix}";`;
|
|
@@ -883,7 +925,7 @@ export function emitRouteFilesWithHandlers(outDir, routesDir, versionSlug, servi
|
|
|
883
925
|
// Note: op.path comes from OpenAPI and already includes any base path
|
|
884
926
|
const schemaBinding = op.skipResponseSchema
|
|
885
927
|
? `\n// Response schema omitted: ${op.stream
|
|
886
|
-
? "stream operations
|
|
928
|
+
? "stream operations send a Readable directly"
|
|
887
929
|
: "$ref graph exceeds fast-json-stringify depth limit"}\nconst { response: _response, ...routeSchema } = schema as Record<string, unknown>;\n`
|
|
888
930
|
: "";
|
|
889
931
|
const schemaLine = op.skipResponseSchema
|
|
@@ -895,7 +937,7 @@ export function emitRouteFilesWithHandlers(outDir, routesDir, versionSlug, servi
|
|
|
895
937
|
const client = fastify.${clientMeta.decoratorName};
|
|
896
938
|
const result = await client.${clientMethod}(${bodyArg});
|
|
897
939
|
reply.type(${JSON.stringify(op.stream.mediaType)});
|
|
898
|
-
return reply.send(
|
|
940
|
+
return reply.send(${streamHelperName}(result.records));
|
|
899
941
|
},`
|
|
900
942
|
: ` handler: async (request) => {
|
|
901
943
|
const client = fastify.${clientMeta.decoratorName};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generateOpenAPI.d.ts","sourceRoot":"","sources":["../../src/openapi/generateOpenAPI.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAMH,OAAO,EAAiB,KAAK,eAAe,EAAC,MAAM,+BAA+B,CAAC;AAKnF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,aAAa,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;IAC3C,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IAClC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC;IAC3E,GAAG,EAAE,GAAG,CAAC;IACT,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC,
|
|
1
|
+
{"version":3,"file":"generateOpenAPI.d.ts","sourceRoot":"","sources":["../../src/openapi/generateOpenAPI.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAMH,OAAO,EAAiB,KAAK,eAAe,EAAC,MAAM,+BAA+B,CAAC;AAKnF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,aAAa,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;IAC3C,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IAClC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC;IAC3E,GAAG,EAAE,GAAG,CAAC;IACT,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC,CA4TD"}
|
|
@@ -191,12 +191,15 @@ export async function generateOpenAPI(opts) {
|
|
|
191
191
|
// Errors raised before the first byte still use the standard envelope.
|
|
192
192
|
const recordType = op.stream.recordTypeName;
|
|
193
193
|
const itemRef = { $ref: `#/components/schemas/${recordType}` };
|
|
194
|
+
const streamSchema = op.stream.format === "json-array"
|
|
195
|
+
? { type: "array", items: itemRef }
|
|
196
|
+
: { type: "string" };
|
|
194
197
|
if (methodObj.responses?.["200"]) {
|
|
195
198
|
methodObj.responses["200"] = {
|
|
196
199
|
description: "Successful streamed SOAP operation response",
|
|
197
200
|
content: {
|
|
198
201
|
[op.stream.mediaType]: {
|
|
199
|
-
schema:
|
|
202
|
+
schema: streamSchema,
|
|
200
203
|
"x-wsdl-tsc-stream": {
|
|
201
204
|
format: op.stream.format,
|
|
202
205
|
itemSchema: itemRef,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON array adapter for record iterables.
|
|
3
|
+
*
|
|
4
|
+
* Given an `AsyncIterable<T>` (typically the output of `parseRecords`), produce
|
|
5
|
+
* a Node `Readable` that emits a single JSON array document without buffering
|
|
6
|
+
* the full record set.
|
|
7
|
+
*
|
|
8
|
+
* Terminal-error policy: the first record is prefetched before any bytes are
|
|
9
|
+
* pushed. A source error before the first record reaches Fastify before the
|
|
10
|
+
* response starts, so the gateway can return the standard JSON error envelope.
|
|
11
|
+
* Errors after the first record abort the stream and leave a truncated JSON
|
|
12
|
+
* document for clients to treat as a failed stream.
|
|
13
|
+
*/
|
|
14
|
+
import { Readable } from "node:stream";
|
|
15
|
+
/**
|
|
16
|
+
* Wrap an async iterable of records in a Node `Readable` stream that emits a
|
|
17
|
+
* JSON array. Downstream backpressure is honored via `Readable.from`'s default
|
|
18
|
+
* behavior: the iterator's `next()` is not called until the internal buffer has
|
|
19
|
+
* room.
|
|
20
|
+
*
|
|
21
|
+
* Source errors are forwarded to the returned stream's `error` event.
|
|
22
|
+
*/
|
|
23
|
+
export declare function toJsonArray<T>(records: AsyncIterable<T>): Readable;
|
|
24
|
+
//# sourceMappingURL=jsonArray.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonArray.d.ts","sourceRoot":"","sources":["../../src/runtime/jsonArray.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAC,QAAQ,EAAC,MAAM,aAAa,CAAC;AAErC;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,QAAQ,CAElE"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON array adapter for record iterables.
|
|
3
|
+
*
|
|
4
|
+
* Given an `AsyncIterable<T>` (typically the output of `parseRecords`), produce
|
|
5
|
+
* a Node `Readable` that emits a single JSON array document without buffering
|
|
6
|
+
* the full record set.
|
|
7
|
+
*
|
|
8
|
+
* Terminal-error policy: the first record is prefetched before any bytes are
|
|
9
|
+
* pushed. A source error before the first record reaches Fastify before the
|
|
10
|
+
* response starts, so the gateway can return the standard JSON error envelope.
|
|
11
|
+
* Errors after the first record abort the stream and leave a truncated JSON
|
|
12
|
+
* document for clients to treat as a failed stream.
|
|
13
|
+
*/
|
|
14
|
+
import { Readable } from "node:stream";
|
|
15
|
+
/**
|
|
16
|
+
* Wrap an async iterable of records in a Node `Readable` stream that emits a
|
|
17
|
+
* JSON array. Downstream backpressure is honored via `Readable.from`'s default
|
|
18
|
+
* behavior: the iterator's `next()` is not called until the internal buffer has
|
|
19
|
+
* room.
|
|
20
|
+
*
|
|
21
|
+
* Source errors are forwarded to the returned stream's `error` event.
|
|
22
|
+
*/
|
|
23
|
+
export function toJsonArray(records) {
|
|
24
|
+
return Readable.from(encode(records), { objectMode: false, encoding: "utf-8" });
|
|
25
|
+
}
|
|
26
|
+
async function* encode(records) {
|
|
27
|
+
const iterator = records[Symbol.asyncIterator]();
|
|
28
|
+
let complete = false;
|
|
29
|
+
try {
|
|
30
|
+
const first = await iterator.next();
|
|
31
|
+
if (first.done) {
|
|
32
|
+
complete = true;
|
|
33
|
+
yield "[]";
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
yield "[" + JSON.stringify(first.value);
|
|
37
|
+
while (true) {
|
|
38
|
+
const next = await iterator.next();
|
|
39
|
+
if (next.done) {
|
|
40
|
+
complete = true;
|
|
41
|
+
yield "]";
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
yield "," + JSON.stringify(next.value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
if (!complete && typeof iterator.return === "function") {
|
|
49
|
+
await iterator.return();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generators.d.ts","sourceRoot":"","sources":["../../src/test/generators.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAC,UAAU,EAAE,qBAAqB,EAAC,MAAM,uBAAuB,CAAC;AAC7E,OAAO,EAA0C,KAAK,eAAe,EAAC,MAAM,eAAe,CAAC;AAG5F,+EAA+E;AAC/E,MAAM,MAAM,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC,CAAC;AA6FrF;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAkBzC;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,qBAAqB,EAAE,EACnC,KAAK,EAAE,iBAAiB,GACvB,MAAM,CAgFR;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,UAAU,GACrB,MAAM,CAsCR;AAED;;;;;;;GAOG;AAEH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,qBAAqB,EAAE,EACnC,KAAK,EAAE,iBAAiB,GACvB,MAAM,
|
|
1
|
+
{"version":3,"file":"generators.d.ts","sourceRoot":"","sources":["../../src/test/generators.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAC,UAAU,EAAE,qBAAqB,EAAC,MAAM,uBAAuB,CAAC;AAC7E,OAAO,EAA0C,KAAK,eAAe,EAAC,MAAM,eAAe,CAAC;AAG5F,+EAA+E;AAC/E,MAAM,MAAM,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC,CAAC;AA6FrF;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAkBzC;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,qBAAqB,EAAE,EACnC,KAAK,EAAE,iBAAiB,GACvB,MAAM,CAgFR;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,UAAU,GACrB,MAAM,CAsCR;AAED;;;;;;;GAOG;AAEH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,qBAAqB,EAAE,EACnC,KAAK,EAAE,iBAAiB,GACvB,MAAM,CA8FR;AAED;;;;;;;GAOG;AAEH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,qBAAqB,EAAE,EACnC,KAAK,EAAE,iBAAiB,GACvB,MAAM,CAiIR;AAED;;GAEG;AAEH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,qBAAqB,EAAE,EACnC,KAAK,EAAE,iBAAiB,GACvB,MAAM,CAuER;AAED;;GAEG;AAEH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,UAAU,EAAE,qBAAqB,EAAE,EACnC,KAAK,CAAC,EAAE,iBAAiB,EACzB,OAAO,CAAC,EAAE,eAAe,GACxB,MAAM,CAwDR;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAChC,MAAM,CAgFR;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAChC,MAAM,CAgDR;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,EACjC,OAAO,EAAE,eAAe,GACvB,MAAM,GAAG,IAAI,CAkEf"}
|
package/dist/test/generators.js
CHANGED
|
@@ -256,10 +256,32 @@ export function emitRoutesTest(testDir, importsMode, operations, mocks) {
|
|
|
256
256
|
const requestPayload = JSON.stringify(mockData?.request ?? {}, null, 4).replace(/\n/g, "\n ");
|
|
257
257
|
const hint = formatOperationHint(op.summary, op.description);
|
|
258
258
|
const hintComment = hint ? ` // ${hint}\n` : "";
|
|
259
|
+
if (op.stream?.format === "json-array") {
|
|
260
|
+
// JSON array streams return one JSON document containing streamed records.
|
|
261
|
+
return `${hintComment} it("${op.method.toUpperCase()} ${op.path} streams json-array records", async () => {
|
|
262
|
+
const app = await createTestApp();
|
|
263
|
+
try {
|
|
264
|
+
const res = await app.inject({
|
|
265
|
+
method: "${op.method.toUpperCase()}",
|
|
266
|
+
url: "${op.path}",
|
|
267
|
+
payload: ${requestPayload},
|
|
268
|
+
});
|
|
269
|
+
expect(res.statusCode).toBe(200);
|
|
270
|
+
expect(String(res.headers["content-type"] ?? "")).toContain(${JSON.stringify(op.stream.mediaType)});
|
|
271
|
+
const records = JSON.parse(res.body);
|
|
272
|
+
expect(Array.isArray(records)).toBe(true);
|
|
273
|
+
expect(records.length).toBeGreaterThan(0);
|
|
274
|
+
for (const record of records) {
|
|
275
|
+
expect(record).toBeDefined();
|
|
276
|
+
}
|
|
277
|
+
} finally {
|
|
278
|
+
await app.close();
|
|
279
|
+
}
|
|
280
|
+
});`;
|
|
281
|
+
}
|
|
259
282
|
if (op.stream) {
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
return `${hintComment} it("${op.method.toUpperCase()} ${op.path} streams ${op.stream.format} records", async () => {
|
|
283
|
+
// NDJSON streams return one parseable JSON document per line.
|
|
284
|
+
return `${hintComment} it("${op.method.toUpperCase()} ${op.path} streams ndjson records", async () => {
|
|
263
285
|
const app = await createTestApp();
|
|
264
286
|
try {
|
|
265
287
|
const res = await app.inject({
|
package/docs/README.md
CHANGED
|
@@ -30,7 +30,7 @@ Human-maintained reference documents for `@techspokes/typescript-wsdl-client`. T
|
|
|
30
30
|
- [architecture.md](architecture.md): internal pipeline for contributors
|
|
31
31
|
- [agent-skill.md](agent-skill.md): release ZIP for consumer-project AI agents
|
|
32
32
|
- [roadmap/README.md](roadmap/README.md): implementation slices and release gates for 1.0
|
|
33
|
-
- [decisions/002-streamable-responses.md](decisions/002-streamable-responses.md): opt-in streaming design (client `AsyncIterable`, gateway NDJSON, `x-wsdl-tsc-stream` OpenAPI extension); shipped in 0.17.0
|
|
33
|
+
- [decisions/002-streamable-responses.md](decisions/002-streamable-responses.md): opt-in streaming design (client `AsyncIterable`, gateway NDJSON or JSON array, `x-wsdl-tsc-stream` OpenAPI extension); shipped in 0.17.0 and extended in 0.28.0
|
|
34
34
|
- [migration.md](migration.md): upgrading between package versions
|
|
35
35
|
|
|
36
36
|
## Conventions
|
package/docs/api-reference.md
CHANGED
|
@@ -357,6 +357,8 @@ interface OperationStreamMetadata {
|
|
|
357
357
|
|
|
358
358
|
Exactly one of `wsdlSource` or `catalogFile` must be set on each `ShapeCatalogRef`. `OperationStreamMetadata` is produced by the parser; `sourceOutputTypeName` is populated by the compiler when it binds the operation to the main WSDL.
|
|
359
359
|
|
|
360
|
+
`format` selects the gateway wire format. `ndjson` emits one JSON document per line with the default `application/x-ndjson` media type, and `json-array` emits one JSON array document with the default `application/json` media type.
|
|
361
|
+
|
|
360
362
|
## Security Configuration Helpers
|
|
361
363
|
|
|
362
364
|
Parse and build the shared security configuration used by OpenAPI generation and
|
|
@@ -408,7 +410,7 @@ const { compiled, openapiDoc } = await runGenerationPipeline({
|
|
|
408
410
|
});
|
|
409
411
|
|
|
410
412
|
const streamOp = compiled.operations.find((op) => op.stream);
|
|
411
|
-
console.log(streamOp?.stream?.mediaType); // "application/x-ndjson"
|
|
413
|
+
console.log(streamOp?.stream?.mediaType); // "application/x-ndjson" by default
|
|
412
414
|
```
|
|
413
415
|
|
|
414
416
|
The generated client exports a `StreamOperationResponse<RecordType>` type; each stream-configured operation returns `Promise<StreamOperationResponse<RecordType>>` with `records: AsyncIterable<RecordType>`.
|
package/docs/architecture.md
CHANGED
|
@@ -73,7 +73,7 @@ tools.ts provides string helpers (pascal, kebab, QName resolution). cli.ts provi
|
|
|
73
73
|
|
|
74
74
|
### runtime/
|
|
75
75
|
|
|
76
|
-
Template sources embedded into generated clients and gateways. streamXml.ts implements a SAX-driven `parseRecords(stream, spec)` that tracks the configured `recordPath` positionally (duplicate local names allowed) and yields records as their closing tags arrive. ndjson.ts wraps an async iterable of records in a `Readable` that emits NDJSON lines with honored backpressure. clientStreamMethods.tpl.txt and operationsStreamHelper.tpl.txt are text templates emitted into the generated `client.ts` and `operations.ts` when stream operations are present; they encode the `callStream()` transport and the `StreamOperationResponse<T>` type.
|
|
76
|
+
Template sources embedded into generated clients and gateways. streamXml.ts implements a SAX-driven `parseRecords(stream, spec)` that tracks the configured `recordPath` positionally (duplicate local names allowed) and yields records as their closing tags arrive. ndjson.ts wraps an async iterable of records in a `Readable` that emits NDJSON lines with honored backpressure. jsonArray.ts wraps the same record iterable as one streamed JSON array document. clientStreamMethods.tpl.txt and operationsStreamHelper.tpl.txt are text templates emitted into the generated `client.ts` and `operations.ts` when stream operations are present; they encode the `callStream()` transport and the `StreamOperationResponse<T>` type.
|
|
77
77
|
|
|
78
78
|
### xsd/
|
|
79
79
|
|
package/docs/cli-reference.md
CHANGED
|
@@ -135,7 +135,7 @@ When `--init-app` is used and `--openapi-servers` is not provided, servers defau
|
|
|
135
135
|
|
|
136
136
|
| Flag | Default | Description |
|
|
137
137
|
|------|---------|-------------|
|
|
138
|
-
| `--stream-config` | (none) | Path to a JSON stream-configuration file (ADR-002). Marks selected operations as streaming: client emits `AsyncIterable<RecordType>`, gateway serves NDJSON, OpenAPI advertises the record schema via `x-wsdl-tsc-stream`. Buffered output is unchanged when the flag is omitted. |
|
|
138
|
+
| `--stream-config` | (none) | Path to a JSON stream-configuration file (ADR-002). Marks selected operations as streaming: client emits `AsyncIterable<RecordType>`, gateway serves NDJSON or JSON array output, OpenAPI advertises the record schema via `x-wsdl-tsc-stream`. Buffered output is unchanged when the flag is omitted. |
|
|
139
139
|
|
|
140
140
|
`--stream-config` is accepted on the `compile`, `client`, and `pipeline` commands. It is not accepted on `openapi`, `gateway`, or `app` because those consume a pre-compiled `catalog.json` that already carries the normalized stream metadata.
|
|
141
141
|
|
package/docs/concepts.md
CHANGED
|
@@ -232,7 +232,7 @@ details:
|
|
|
232
232
|
|
|
233
233
|
### Streaming Bypass
|
|
234
234
|
|
|
235
|
-
Operations opted into streaming with `--stream-config` bypass the success envelope on the `200` response path. The OpenAPI response content is declared as the configured stream media type
|
|
235
|
+
Operations opted into streaming with `--stream-config` bypass the success envelope on the `200` response path. The OpenAPI response content is declared as the configured stream media type, and the gateway writes raw NDJSON lines or one streamed JSON array straight to the response body. Error responses (400, 502, and the rest) still use the normal envelope so clients always see structured failures before the first record. See [ADR-002](decisions/002-streamable-responses.md) for the full rationale.
|
|
236
236
|
|
|
237
237
|
### Envelope Naming
|
|
238
238
|
|
|
@@ -368,7 +368,7 @@ The catalog is the source of truth for stream metadata. Each opted-in operation
|
|
|
368
368
|
|
|
369
369
|
### Terminal-Error Policy
|
|
370
370
|
|
|
371
|
-
Errors before the first record use the normal gateway error envelope because the response headers and status have not been committed yet. Errors mid-stream truncate the
|
|
371
|
+
Errors before the first record use the normal gateway error envelope because the response headers and status have not been committed yet. Errors mid-stream truncate the response; NDJSON consumers detect this as an incomplete HTTP response, and JSON array consumers see an incomplete or invalid JSON document. Both cases must be treated as failed streams. This behavior is documented for operators in the [Production Guide](production.md#terminal-error-policy).
|
|
372
372
|
|
|
373
373
|
## Companion Catalogs and Shape Resolution
|
|
374
374
|
|
package/docs/configuration.md
CHANGED
|
@@ -125,6 +125,7 @@ operations not listed in the file.
|
|
|
125
125
|
|
|
126
126
|
- `recordType` and `recordPath` are required; `format` defaults to `ndjson` and
|
|
127
127
|
`mediaType` to `application/x-ndjson`.
|
|
128
|
+
- `format` accepts `ndjson` and `json-array`; JSON array streams default `mediaType` to `application/json` and emit one valid JSON array document.
|
|
128
129
|
- `shapeCatalog` references a `shapeCatalogs` entry and is only needed when
|
|
129
130
|
the record type lives in a different WSDL than the one driving generation.
|
|
130
131
|
- Shape catalogs accept either `wsdlSource` (fetched and compiled on the fly)
|
|
@@ -101,7 +101,7 @@ Add a stream configuration file passed with `--stream-config`. The same option s
|
|
|
101
101
|
|
|
102
102
|
`recordType` is the TypeScript and schema model to emit for each streamed record.
|
|
103
103
|
|
|
104
|
-
`format`
|
|
104
|
+
`format` supports `ndjson` and `json-array`. `ndjson` remains the default; `json-array` is available for clients that require a single JSON array response.
|
|
105
105
|
|
|
106
106
|
## CLI and Programmatic API
|
|
107
107
|
|
|
@@ -197,7 +197,7 @@ The converter must reuse catalog metadata for:
|
|
|
197
197
|
- Text content and `$value`
|
|
198
198
|
- Primitive mapping
|
|
199
199
|
|
|
200
|
-
The converter should collect SOAP faults, response errors, and warnings when they appear before the first record. After streaming starts, terminal errors cannot be represented as a normal JSON error envelope
|
|
200
|
+
The converter should collect SOAP faults, response errors, and warnings when they appear before the first record. After streaming starts, terminal errors cannot be represented as a normal JSON error envelope. The shipped behavior aborts the stream; NDJSON clients see an incomplete HTTP response, and JSON array clients see an incomplete or invalid JSON document.
|
|
201
201
|
|
|
202
202
|
## OpenAPI Output
|
|
203
203
|
|
|
@@ -222,25 +222,47 @@ Stream operations should not use the standard success envelope for `200` respons
|
|
|
222
222
|
|
|
223
223
|
OpenAPI 3.1 cannot fully describe an NDJSON sequence as a standard JSON Schema document. The `x-wsdl-tsc-stream` extension makes the item schema explicit for generated gateways, documentation tools, and future SDK generators.
|
|
224
224
|
|
|
225
|
+
For `format: "json-array"`, OpenAPI uses an array schema under the configured media type while keeping the same extension:
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
{
|
|
229
|
+
"description": "Successful streamed SOAP operation response",
|
|
230
|
+
"content": {
|
|
231
|
+
"application/json": {
|
|
232
|
+
"schema": {
|
|
233
|
+
"type": "array",
|
|
234
|
+
"items": {
|
|
235
|
+
"$ref": "#/components/schemas/UnitDescriptiveContentType"
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
"x-wsdl-tsc-stream": {
|
|
239
|
+
"format": "json-array",
|
|
240
|
+
"itemSchema": {
|
|
241
|
+
"$ref": "#/components/schemas/UnitDescriptiveContentType"
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
225
249
|
## Gateway Output
|
|
226
250
|
|
|
227
251
|
Generated stream routes should call the stream operation and send a Node stream through Fastify.
|
|
228
252
|
|
|
229
253
|
```typescript
|
|
230
|
-
import { Readable } from "node:stream";
|
|
231
|
-
|
|
232
254
|
handler: async (request, reply) => {
|
|
233
255
|
const client = fastify.escapiaContentClient;
|
|
234
256
|
const result = await client.UnitDescriptiveInfoStream(request.body);
|
|
235
257
|
|
|
236
258
|
reply.type("application/x-ndjson");
|
|
237
|
-
return reply.send(
|
|
259
|
+
return reply.send(toNdjson(result.records));
|
|
238
260
|
}
|
|
239
261
|
```
|
|
240
262
|
|
|
241
263
|
The generated operation schema should keep request validation but omit the Fastify response serialization schema for streamed `200` responses. Fastify cannot serialize an unbounded stream with a normal JSON response schema.
|
|
242
264
|
|
|
243
|
-
The gateway runtime
|
|
265
|
+
The gateway runtime adds `toNdjson()` and `toJsonArray()` helpers. These helpers are deterministic, backpressure-aware, and safe for large payloads.
|
|
244
266
|
|
|
245
267
|
## Test Strategy
|
|
246
268
|
|
|
@@ -250,7 +272,7 @@ Add converter tests that split XML chunks across element boundaries to prove the
|
|
|
250
272
|
|
|
251
273
|
Add a local Escapia-like WSDL fixture with `xs:any` output wrappers and a companion WSDL fixture with the concrete record types.
|
|
252
274
|
|
|
253
|
-
Add an integration test with a fake SOAP HTTP server that writes one record, waits, writes another record, and then closes the envelope. The test should assert that the gateway emits the first NDJSON line before the upstream response completes.
|
|
275
|
+
Add an integration test with a fake SOAP HTTP server that writes one record, waits, writes another record, and then closes the envelope. The test should assert that the gateway emits the first NDJSON line or JSON array record before the upstream response completes.
|
|
254
276
|
|
|
255
277
|
Add snapshot tests for generated `client.ts`, `operations.ts`, OpenAPI output, gateway route files, and gateway runtime helpers.
|
|
256
278
|
|
|
@@ -273,7 +295,7 @@ Run `npm run smoke:pipeline` after implementation to verify ordinary buffered ge
|
|
|
273
295
|
|
|
274
296
|
- Existing generated output is unchanged when no stream config is provided.
|
|
275
297
|
- A stream-configured Escapia content WSDL generates typed stream client methods.
|
|
276
|
-
- The generated gateway emits
|
|
298
|
+
- The generated gateway emits configured stream records incrementally.
|
|
277
299
|
- The generated OpenAPI document identifies stream operations and record schemas.
|
|
278
300
|
- The converter maps XML attributes, arrays, text values, and nillable values consistently with buffered responses.
|
|
279
301
|
- The chunked integration test proves the first record is sent before the full SOAP response is available.
|
|
@@ -286,7 +308,7 @@ The generator gains a second response execution model. This increases complexity
|
|
|
286
308
|
|
|
287
309
|
The catalog becomes more important as the shared source of truth because OpenAPI alone cannot carry enough stream conversion metadata.
|
|
288
310
|
|
|
289
|
-
NDJSON
|
|
311
|
+
NDJSON remains the default stream format because it is simple and broadly consumable. JSON array streaming is available for clients that require one JSON document and still streams without buffering the full response.
|
|
290
312
|
|
|
291
313
|
Companion catalogs are required for vendors that split stream wrappers and concrete record shapes across separate WSDLs.
|
|
292
314
|
|
|
@@ -298,7 +320,7 @@ Captured after the 0.17.0 ship for future maintainers:
|
|
|
298
320
|
- `GenerateOpenAPIOptions` and `GenerateGatewayOptions` in the programmatic API do not carry stream-config fields for the same reason. `compileWsdlToProject` and `runGenerationPipeline` (PipelineOptions) do.
|
|
299
321
|
- The client stream transport is emitted from two templates (`clientStreamMethods.tpl.txt`, `operationsStreamHelper.tpl.txt`). They embed the `StreamOperationResponse<T>` type and a `callStream()` method that POSTs a hand-built SOAP envelope via global `fetch`, bypassing `node-soap` as required by the phase-0 finding.
|
|
300
322
|
- `saxes ^6.0.0` was promoted from devDependency to runtime dependency on the package, and added as a pinned dependency in the generated app scaffold so stream-enabled consumers install it automatically.
|
|
301
|
-
- The `json-array` format is
|
|
323
|
+
- The `json-array` format is implemented in `0.28.0`. The helper prefetches the first record before emitting `[`, which preserves normal error envelopes for failures before the first record and documents truncation or invalid JSON for failures after streaming starts.
|
|
302
324
|
|
|
303
325
|
## References
|
|
304
326
|
|
package/docs/gateway-guide.md
CHANGED
|
@@ -183,10 +183,7 @@ export async function registerRoute_v1_weather_getcityforecastbyzip(fastify: Fas
|
|
|
183
183
|
|
|
184
184
|
### Streaming Handlers
|
|
185
185
|
|
|
186
|
-
Operations opted in via `--stream-config` emit
|
|
187
|
-
instead of the standard envelope. The generated handler streams records as
|
|
188
|
-
they arrive, and the Fastify response is flushed line-by-line with
|
|
189
|
-
backpressure (ADR-002).
|
|
186
|
+
Operations opted in via `--stream-config` emit a stream response instead of the standard envelope. The generated handler streams records as they arrive, and the Fastify response is flushed with backpressure. The default format is NDJSON; `format: "json-array"` emits one JSON array document.
|
|
190
187
|
|
|
191
188
|
```typescript
|
|
192
189
|
import type { FastifyInstance } from "fastify";
|
|
@@ -194,7 +191,7 @@ import type { GetWeatherInformation } from "../../client/types.js";
|
|
|
194
191
|
import schema from "../schemas/operations/getweatherinformation.json" with { type: "json" };
|
|
195
192
|
import { toNdjson } from "../runtime.js";
|
|
196
193
|
|
|
197
|
-
// Response schema omitted: stream operations
|
|
194
|
+
// Response schema omitted: stream operations send a Readable directly
|
|
198
195
|
const { response: _response, ...routeSchema } = schema as Record<string, unknown>;
|
|
199
196
|
|
|
200
197
|
export async function registerRoute_v1_weather_getweatherinformation(fastify: FastifyInstance) {
|
|
@@ -212,11 +209,9 @@ export async function registerRoute_v1_weather_getweatherinformation(fastify: Fa
|
|
|
212
209
|
}
|
|
213
210
|
```
|
|
214
211
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
use the normal error envelope
|
|
218
|
-
response's truncation. Consumers detect these via the absence of a clean
|
|
219
|
-
terminating zero-chunk.
|
|
212
|
+
For `format: "json-array"`, the same handler imports `toJsonArray`, sets `reply.type("application/json")`, and sends `reply.send(toJsonArray(result.records))`.
|
|
213
|
+
|
|
214
|
+
The client method returns `StreamOperationResponse<RecordType>` with a `records: AsyncIterable<RecordType>`. Errors raised before the first record use the normal error envelope. Errors raised mid-stream truncate the response; NDJSON clients see an incomplete HTTP response, and JSON array clients see an incomplete or invalid JSON document.
|
|
220
215
|
|
|
221
216
|
## Error Handling
|
|
222
217
|
|
package/docs/generated-code.md
CHANGED
|
@@ -176,7 +176,7 @@ Key features of the generated handlers:
|
|
|
176
176
|
- Envelope wrapping: `buildSuccessEnvelope()` wraps the raw SOAP response in the standard `{ status, message, data, error }` envelope
|
|
177
177
|
- Route file header comments include propagated operation summary and description when present
|
|
178
178
|
|
|
179
|
-
Stream-configured operations generate a different handler shape: the response serialization schema is omitted
|
|
179
|
+
Stream-configured operations generate a different handler shape: the response serialization schema is omitted and the handler streams `result.records` through the helper for the configured format. NDJSON handlers use `reply.type("application/x-ndjson")` with `toNdjson(result.records)`, and JSON array handlers use `reply.type("application/json")` with `toJsonArray(result.records)`. See the [Gateway Guide Streaming Handlers section](gateway-guide.md#streaming-handlers) for the full example and terminal-error policy.
|
|
180
180
|
|
|
181
181
|
See [Gateway Guide](gateway-guide.md) for the full architecture and [CLI Reference](cli-reference.md) for generation flags.
|
|
182
182
|
|
|
@@ -89,7 +89,7 @@ npx wsdl-tsc pipeline \
|
|
|
89
89
|
--init-app
|
|
90
90
|
```
|
|
91
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.
|
|
92
|
+
The opted-in operations now return `StreamOperationResponse<RecordType>` on the client, serve `application/x-ndjson` by default on the gateway, and advertise the record schema in OpenAPI via `x-wsdl-tsc-stream`. Set `format: "json-array"` for `application/json` array streaming. 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
93
|
|
|
94
94
|
## Step 3: Generate the REST Gateway
|
|
95
95
|
|
package/docs/migration.md
CHANGED
|
@@ -35,7 +35,7 @@ This upgrade adds opt-in streamable SOAP responses. No breaking changes; generat
|
|
|
35
35
|
|
|
36
36
|
### What Changed in 0.17.x
|
|
37
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
|
|
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` by default with an `x-wsdl-tsc-stream` extension, and a Fastify route that streams records with backpressure. Set `format: "json-array"` for `application/json` array streaming. 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
39
|
|
|
40
40
|
### Steps to Upgrade to 0.17.x
|
|
41
41
|
|
package/docs/output-anatomy.md
CHANGED
|
@@ -75,7 +75,7 @@ CLI OpenAPI generation always validates the generated spec using `@apidevtools/s
|
|
|
75
75
|
|
|
76
76
|
### Stream Schema Extension
|
|
77
77
|
|
|
78
|
-
Stream operations do not use the standard success envelope for `200` responses.
|
|
78
|
+
Stream operations do not use the standard success envelope for `200` responses. NDJSON streams declare 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
79
|
|
|
80
80
|
```json
|
|
81
81
|
{
|
|
@@ -94,7 +94,29 @@ Stream operations do not use the standard success envelope for `200` responses.
|
|
|
94
94
|
}
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
JSON array streams default to `application/json` and declare the record schema as the array item schema while keeping the same extension:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"200": {
|
|
102
|
+
"description": "Successful streamed SOAP operation response",
|
|
103
|
+
"content": {
|
|
104
|
+
"application/json": {
|
|
105
|
+
"schema": {
|
|
106
|
+
"type": "array",
|
|
107
|
+
"items": { "$ref": "#/components/schemas/UnitDescriptiveContentType" }
|
|
108
|
+
},
|
|
109
|
+
"x-wsdl-tsc-stream": {
|
|
110
|
+
"format": "json-array",
|
|
111
|
+
"itemSchema": { "$ref": "#/components/schemas/UnitDescriptiveContentType" }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
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. JSON array streams use a normal array schema and keep the extension for downstream tooling. Error responses (400, 502, and the rest) still use the normal envelope.
|
|
98
120
|
|
|
99
121
|
## Gateway Output
|
|
100
122
|
|
|
@@ -117,11 +139,11 @@ Each route file in `routes/` follows the same pattern: validate the JSON request
|
|
|
117
139
|
|
|
118
140
|
### Stream Routes
|
|
119
141
|
|
|
120
|
-
Stream-configured operations generate a different route shape. The Fastify response serialization schema is omitted because
|
|
142
|
+
Stream-configured operations generate a different route shape. The Fastify response serialization schema is omitted because the handler sends a `Readable` directly. NDJSON handlers set `reply.type("application/x-ndjson")` and return `reply.send(toNdjson(result.records))`; JSON array handlers set `reply.type("application/json")` and return `reply.send(toJsonArray(result.records))`. `runtime.ts` gains `toNdjson<T>(records: AsyncIterable<T>): Readable` and `toJsonArray<T>(records: AsyncIterable<T>): Readable` helpers. See the [Gateway Guide](gateway-guide.md#streaming-handlers) for the full handler example and terminal-error policy.
|
|
121
143
|
|
|
122
144
|
### Generated Test Surface
|
|
123
145
|
|
|
124
|
-
When `--test-dir` is combined with `--stream-config`, the generated happy-path tests for stream operations assert on the
|
|
146
|
+
When `--test-dir` is combined with `--stream-config`, the generated happy-path tests for stream operations assert on the configured content type. NDJSON tests parse each line as a separate JSON record, and JSON array tests parse the response body once as an array. Mock clients use async-generator overrides that yield records to drive those tests; see the [Testing Guide](testing.md) for the pattern.
|
|
125
147
|
|
|
126
148
|
### Plugin registration
|
|
127
149
|
|
package/docs/production.md
CHANGED
|
@@ -83,7 +83,7 @@ Log the time to first record, not just the time to response completion. First-re
|
|
|
83
83
|
|
|
84
84
|
### Terminal-Error Policy
|
|
85
85
|
|
|
86
|
-
Errors raised before the first record use the normal gateway error envelope
|
|
86
|
+
Errors raised before the first record use the normal gateway error envelope because the JSON array helper prefetches the first record before emitting `[`. Errors raised mid-stream truncate the response. NDJSON consumers detect this as an incomplete HTTP response, and JSON array consumers must treat an incomplete or invalid JSON document as a failed stream. Document this behavior for downstream API consumers so they distinguish truncation from a legitimate empty stream.
|
|
87
87
|
|
|
88
88
|
## Known Limitations
|
|
89
89
|
|
|
@@ -105,7 +105,7 @@ Single-child sequences with maxOccurs>1 become array schemas. Sequences with mul
|
|
|
105
105
|
|
|
106
106
|
### Stream Format Coverage
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
`ndjson` is the default stream format. `json-array` is supported for operations that need a single JSON document response; it streams records incrementally as a JSON array and does not buffer the full SOAP response.
|
|
109
109
|
|
|
110
110
|
### Stream Transport Bypasses node-soap
|
|
111
111
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# TypeScript WSDL Client v0.27.0
|
|
2
|
+
|
|
3
|
+
## Code Scanning Remediation
|
|
4
|
+
|
|
5
|
+
This release resolves GitHub CodeQL reflected-XSS alerts reported against Fastify schema compatibility tests.
|
|
6
|
+
|
|
7
|
+
## What This Improves
|
|
8
|
+
|
|
9
|
+
The Fastify schema compatibility probes no longer echo request-controlled payloads through HTTP responses. The tests still verify Fastify request parsing and schema behavior, but CodeQL can now validate the repository without reflected response-body findings in test routes.
|
|
10
|
+
|
|
11
|
+
## Highlights
|
|
12
|
+
|
|
13
|
+
- Removes reflected request-body response sinks from Fastify schema compatibility tests.
|
|
14
|
+
- Preserves coverage for choice exclusivity, peer `not` constraints, and `anyOf` compatibility behavior.
|
|
15
|
+
- Confirms no Dependabot or secret scanning remediation was required for this release.
|
|
16
|
+
|
|
17
|
+
## Upgrade Notes
|
|
18
|
+
|
|
19
|
+
No runtime upgrade steps.
|
|
20
|
+
|
|
21
|
+
## Validation
|
|
22
|
+
|
|
23
|
+
- CI passed.
|
|
24
|
+
- NPM package contents were validated.
|
|
25
|
+
- Documentation links and TypeScript fenced snippets were validated.
|
|
26
|
+
- Agent skill artifact was validated and packaged.
|
|
27
|
+
- GitHub code scanning alerts for the reflected test response sinks were resolved.
|
|
28
|
+
|
|
29
|
+
## Notes
|
|
30
|
+
|
|
31
|
+
Release tag: `v0.27.0`.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# TypeScript WSDL Client v0.28.0
|
|
2
|
+
|
|
3
|
+
## JSON Array Streaming
|
|
4
|
+
|
|
5
|
+
This release implements `format: "json-array"` for operations opted into streaming with `--stream-config`.
|
|
6
|
+
|
|
7
|
+
## What This Improves
|
|
8
|
+
|
|
9
|
+
Teams can now serve large SOAP response records as one streamed `application/json` array without buffering the full upstream response. NDJSON remains the default stream format, and JSON array operations keep the same generated client return shape: `StreamOperationResponse<RecordType>` with `records: AsyncIterable<RecordType>`.
|
|
10
|
+
|
|
11
|
+
## Highlights
|
|
12
|
+
|
|
13
|
+
- Adds `toJsonArray()` runtime streaming alongside the existing NDJSON helper.
|
|
14
|
+
- Emits OpenAPI `application/json` array schemas for JSON array stream operations.
|
|
15
|
+
- Generates Fastify routes that call `toJsonArray(result.records)` for `format: "json-array"`.
|
|
16
|
+
- Generates route tests that parse JSON array stream responses as one array document.
|
|
17
|
+
- Documents the terminal-error behavior for JSON array streams.
|
|
18
|
+
|
|
19
|
+
## Upgrade Notes
|
|
20
|
+
|
|
21
|
+
No special upgrade steps. Existing stream operations keep `ndjson` unless the stream config opts an operation into `format: "json-array"`.
|
|
22
|
+
|
|
23
|
+
## Validation
|
|
24
|
+
|
|
25
|
+
- CI passed.
|
|
26
|
+
- NPM package contents were validated.
|
|
27
|
+
- Documentation links and TypeScript fenced snippets were validated.
|
|
28
|
+
- Agent skill artifact was validated and packaged.
|
|
29
|
+
|
|
30
|
+
## Notes
|
|
31
|
+
|
|
32
|
+
Release tag: `v0.28.0`.
|
package/docs/roadmap/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Version 1.0 Roadmap Plan
|
|
2
2
|
|
|
3
|
-
Detailed plan for moving `@techspokes/typescript-wsdl-client` from `0.
|
|
3
|
+
Detailed plan for moving `@techspokes/typescript-wsdl-client` from `0.28.x` to a stable `1.0.0` release.
|
|
4
4
|
|
|
5
5
|
See the root [README.md](../../README.md) for project overview and the root [ROADMAP.md](../../ROADMAP.md) for the public roadmap summary.
|
|
6
6
|
|
|
@@ -37,7 +37,7 @@ Choice union mode is complete in `0.26.0`. The default `all-optional` mode remai
|
|
|
37
37
|
|
|
38
38
|
### Slice 4: JSON Array Streaming
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
JSON array streaming is complete in `0.28.0`. The default `ndjson` format remains unchanged, and `format: "json-array"` streams records incrementally without buffering the full SOAP response.
|
|
41
41
|
|
|
42
42
|
### Slice 5: WSDL Coverage Matrix
|
|
43
43
|
|
|
@@ -36,7 +36,7 @@ This slice prevents later work from building on ambiguous behavior. It also give
|
|
|
36
36
|
|
|
37
37
|
### JSON Array Streaming
|
|
38
38
|
|
|
39
|
-
`format: "json-array"`
|
|
39
|
+
`format: "json-array"` is implemented in `0.28.0`. The audit should keep every parser, client, OpenAPI, gateway, runtime, and docs surface aligned across both stream formats.
|
|
40
40
|
|
|
41
41
|
### Roadmap State
|
|
42
42
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Version 1.0 JSON Array Streaming
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Status: implemented for `0.28.0`.
|
|
4
4
|
|
|
5
|
-
This
|
|
5
|
+
This document records the implementation slice for `format: "json-array"` streaming for SOAP operations that already use the stream configuration model.
|
|
6
6
|
|
|
7
7
|
See the root [README.md](../../README.md) for project overview and [Version 1.0 Roadmap Plan](README.md) for the complete 1.0 route.
|
|
8
8
|
|
|
@@ -12,9 +12,9 @@ See the root [README.md](../../README.md) for project overview and [Version 1.0
|
|
|
12
12
|
|
|
13
13
|
## Design Direction
|
|
14
14
|
|
|
15
|
-
JSON array streaming
|
|
15
|
+
JSON array streaming writes an opening bracket with the first record, streams comma-separated JSON records, and writes a closing bracket after the async iterable completes. The implementation preserves backpressure by converting the async iterable into a Node `Readable` stream.
|
|
16
16
|
|
|
17
|
-
The key
|
|
17
|
+
The key researched constraint is error timing. The helper prefetches the first record before sending the first byte, so an upstream error before the first record still returns the normal error envelope. Once the JSON array response has started, a later error truncates the response and clients must treat the JSON parse failure as a failed stream.
|
|
18
18
|
|
|
19
19
|
## Scope
|
|
20
20
|
|
|
@@ -39,7 +39,7 @@ Research whether generated routes should prefetch the first record before callin
|
|
|
39
39
|
|
|
40
40
|
### Empty Stream Handling
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
An empty stream returns `[]` with `application/json`. The helper emits a syntactically valid empty array without special route code.
|
|
43
43
|
|
|
44
44
|
### Backpressure Handling
|
|
45
45
|
|
|
@@ -47,7 +47,7 @@ Confirm that the helper does not pull from the async iterable faster than the HT
|
|
|
47
47
|
|
|
48
48
|
### Terminal Error Handling
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
Fastify surfaces stream errors after the JSON array has begun as a destroyed or reset response under inject and as a truncated response to real clients. Production docs and troubleshooting docs describe this as a failed stream.
|
|
51
51
|
|
|
52
52
|
## Implementation Units
|
|
53
53
|
|
package/docs/start-here.md
CHANGED
|
@@ -92,7 +92,9 @@ Minimal `stream.config.json`:
|
|
|
92
92
|
}
|
|
93
93
|
```
|
|
94
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.
|
|
95
|
+
Stream operations return `StreamOperationResponse<RecordType>` on the client (`records: AsyncIterable<RecordType>`), emit `application/x-ndjson` by default on the gateway, and advertise the record schema in OpenAPI via the `x-wsdl-tsc-stream` extension.
|
|
96
|
+
|
|
97
|
+
Set `"format": "json-array"` in the stream config when downstream clients need one `application/json` array document instead of NDJSON lines.
|
|
96
98
|
|
|
97
99
|
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
100
|
|
|
@@ -118,5 +120,5 @@ For more on scope boundaries, see the "When NOT to Use This" section of the [REA
|
|
|
118
120
|
| Plan a full SOAP-to-REST migration | [Migration Playbook](migration-playbook.md) |
|
|
119
121
|
| Set up testing for generated code | [Testing Guide](testing.md) |
|
|
120
122
|
| Review all CLI flags | [CLI Reference](cli-reference.md) |
|
|
121
|
-
| Opt specific operations into
|
|
123
|
+
| Opt specific operations into response streaming | [ADR-002](decisions/002-streamable-responses.md) and [Stream Configuration](configuration.md#stream-configuration) |
|
|
122
124
|
| Install package-specific agent guidance | [Agent Skill Artifact](agent-skill.md) |
|
|
@@ -20,7 +20,7 @@ These patterns are handled end-to-end: WSDL parsing, TypeScript type generation,
|
|
|
20
20
|
- Circular type references detected and broken with minimal stub types
|
|
21
21
|
- Multiple WSDL ports and bindings; the first SOAP binding is selected, all ports are documented in service metadata
|
|
22
22
|
- SOAP 1.1 and SOAP 1.2 binding detection
|
|
23
|
-
- 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
|
+
- Streamable SOAP responses, opt-in per operation via `--stream-config` (ADR-002): client exposes `AsyncIterable<RecordType>`, gateway emits NDJSON or JSON array streams with backpressure, OpenAPI advertises the record schema via `x-wsdl-tsc-stream`
|
|
24
24
|
- `xs:any` wildcard particles retained on compiled types (they used to be dropped silently) — enables honest stream-candidate detection and companion-catalog shape resolution
|
|
25
25
|
|
|
26
26
|
### Named simple types and same-name elements
|
package/docs/testing.md
CHANGED
|
@@ -246,7 +246,7 @@ 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>`
|
|
249
|
+
For operations opted in via `--stream-config`, the mock returns `records: AsyncIterable<RecordType>` via a small `asyncIterableOf` helper. Generated happy-path tests assert on the configured content type; NDJSON tests parse record lines, and JSON array tests parse the response body once as an array. Override a stream op with a multi-record iterable to exercise downstream backpressure:
|
|
250
250
|
|
|
251
251
|
```typescript
|
|
252
252
|
const client = createMockClient({
|
package/docs/troubleshooting.md
CHANGED
|
@@ -20,7 +20,7 @@ See [README](../README.md) for quick start and [CLI Reference](cli-reference.md)
|
|
|
20
20
|
| Stream config references unknown operation | Operation name must match the WSDL exactly; check spelling and casing |
|
|
21
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
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
|
-
|
|
|
23
|
+
| 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 |
|
|
24
24
|
| Stream recordPath does not match | SAX matching is positional and case-sensitive; verify duplicate local-name segments are spelled exactly |
|
|
25
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 |
|
|
26
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@techspokes/typescript-wsdl-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.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",
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON array adapter for record iterables.
|
|
3
|
+
*
|
|
4
|
+
* Given an `AsyncIterable<T>` (typically the output of `parseRecords`), produce
|
|
5
|
+
* a Node `Readable` that emits a single JSON array document without buffering
|
|
6
|
+
* the full record set.
|
|
7
|
+
*
|
|
8
|
+
* Terminal-error policy: the first record is prefetched before any bytes are
|
|
9
|
+
* pushed. A source error before the first record reaches Fastify before the
|
|
10
|
+
* response starts, so the gateway can return the standard JSON error envelope.
|
|
11
|
+
* Errors after the first record abort the stream and leave a truncated JSON
|
|
12
|
+
* document for clients to treat as a failed stream.
|
|
13
|
+
*/
|
|
14
|
+
import {Readable} from "node:stream";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Wrap an async iterable of records in a Node `Readable` stream that emits a
|
|
18
|
+
* JSON array. Downstream backpressure is honored via `Readable.from`'s default
|
|
19
|
+
* behavior: the iterator's `next()` is not called until the internal buffer has
|
|
20
|
+
* room.
|
|
21
|
+
*
|
|
22
|
+
* Source errors are forwarded to the returned stream's `error` event.
|
|
23
|
+
*/
|
|
24
|
+
export function toJsonArray<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
|
+
const iterator = records[Symbol.asyncIterator]();
|
|
30
|
+
let complete = false;
|
|
31
|
+
try {
|
|
32
|
+
const first = await iterator.next();
|
|
33
|
+
if (first.done) {
|
|
34
|
+
complete = true;
|
|
35
|
+
yield "[]";
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
yield "[" + JSON.stringify(first.value);
|
|
40
|
+
|
|
41
|
+
while (true) {
|
|
42
|
+
const next = await iterator.next();
|
|
43
|
+
if (next.done) {
|
|
44
|
+
complete = true;
|
|
45
|
+
yield "]";
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
yield "," + JSON.stringify(next.value);
|
|
49
|
+
}
|
|
50
|
+
} finally {
|
|
51
|
+
if (!complete && typeof iterator.return === "function") {
|
|
52
|
+
await iterator.return();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|