@techspokes/typescript-wsdl-client 1.1.8 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -3
- package/dist/checkedPlanning.js +27 -4
- package/dist/cli.js +17 -1
- package/dist/client/generateClient.js +72 -16
- package/dist/client/generateOperations.js +30 -11
- package/dist/client/generateUtils.js +5 -1
- package/dist/compiler/canonicalGraph.d.ts +4 -15
- package/dist/compiler/canonicalGraph.js +1 -8
- package/dist/compiler/componentRecordProjection.d.ts +3 -0
- package/dist/compiler/componentRecordProjection.js +10 -0
- package/dist/compiler/declarationIdentity.d.ts +22 -0
- package/dist/compiler/declarationIdentity.js +9 -0
- package/dist/compiler/directCodec.d.ts +22 -2
- package/dist/compiler/directCodec.js +40 -7
- package/dist/compiler/directOperation.d.ts +26 -4
- package/dist/compiler/directOperation.js +59 -5
- package/dist/compiler/directSoap.js +24 -7
- package/dist/compiler/directSourceCapsule.js +59 -3
- package/dist/compiler/directStructure.js +6 -2
- package/dist/compiler/directStructurePlan.d.ts +10 -1
- package/dist/compiler/directStructurePlan.js +27 -7
- package/dist/compiler/directValues.d.ts +15 -3
- package/dist/compiler/directValues.js +20 -4
- package/dist/compiler/legacyDeclarations.d.ts +26 -0
- package/dist/compiler/legacyDeclarations.js +144 -0
- package/dist/compiler/projectionScalar.d.ts +18 -1
- package/dist/compiler/projectionScalar.js +47 -13
- package/dist/compiler/rpcStringValues.d.ts +30 -0
- package/dist/compiler/rpcStringValues.js +196 -0
- package/dist/compiler/rpcTypedValues.d.ts +48 -0
- package/dist/compiler/rpcTypedValues.js +190 -0
- package/dist/compiler/schemaCompiler.js +44 -109
- package/dist/compiler/semanticCatalog.d.ts +2 -2
- package/dist/gateway/generateComposedGateway.d.ts +16 -0
- package/dist/gateway/generateComposedGateway.js +65 -0
- package/dist/gateway/generateGateway.js +1 -0
- package/dist/gateway/generators.js +19 -4
- package/dist/gateway/helpers.d.ts +1 -0
- package/dist/gateway/helpers.js +15 -0
- package/dist/generation/componentShape.js +25 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/loader/orderedSyntax.d.ts +4 -0
- package/dist/loader/orderedSyntax.js +18 -4
- package/dist/multiServicePipeline.d.ts +41 -0
- package/dist/multiServicePipeline.js +287 -0
- package/dist/openapi/composeOpenAPI.d.ts +29 -0
- package/dist/openapi/composeOpenAPI.js +322 -0
- package/dist/pipeline.d.ts +1 -1
- package/dist/pipeline.js +2 -0
- package/docs/agent-skill.md +10 -1
- package/docs/api-reference.md +127 -9
- package/docs/checked-gateway.md +2 -0
- package/docs/checked-generation.md +11 -5
- package/docs/cli-reference.md +25 -5
- package/docs/concepts.md +4 -5
- package/docs/configuration.md +10 -3
- package/docs/corpus.md +1 -1
- package/docs/gateway-guide.md +104 -27
- package/docs/generated-code.md +12 -6
- package/docs/migration-playbook.md +4 -0
- package/docs/migration.md +124 -6
- package/docs/output-anatomy.md +20 -17
- package/docs/production.md +7 -5
- package/docs/start-here.md +12 -2
- package/docs/supported-patterns.md +17 -0
- package/docs/testing.md +2 -0
- package/docs/troubleshooting.md +3 -1
- package/llms.txt +9 -1
- package/package.json +2 -2
- package/src/runtime/clientStreamMethods.tpl.txt +156 -148
- package/src/runtime/operationsStreamHelper.tpl.txt +6 -6
- package/src/runtime/streamXml.ts +48 -20
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getChildrenWithLocalName, getFirstWithLocalName, normalizeArray, pascal, resolveQName, } from "../util/tools.js";
|
|
2
2
|
import { xsdToTsPrimitive } from "../xsd/primitives.js";
|
|
3
3
|
import { WsdlCompilationError } from "../util/errors.js";
|
|
4
|
+
import { LegacyDeclarations } from "./legacyDeclarations.js";
|
|
4
5
|
// XML Schema namespace constant
|
|
5
6
|
const XS = "http://www.w3.org/2001/XMLSchema";
|
|
6
7
|
/**
|
|
@@ -13,25 +14,6 @@ const XS = "http://www.w3.org/2001/XMLSchema";
|
|
|
13
14
|
function qkey(q) {
|
|
14
15
|
return `{${q.ns}}${q.local}`;
|
|
15
16
|
}
|
|
16
|
-
/**
|
|
17
|
-
* Generates a name for an inline complex type
|
|
18
|
-
*
|
|
19
|
-
* This function creates appropriate TypeScript names for anonymous types
|
|
20
|
-
* that are defined inline within element declarations.
|
|
21
|
-
*
|
|
22
|
-
* @param {string} parentTypeName - Name of the parent type
|
|
23
|
-
* @param {string} [propName] - Name of the property containing the inline type
|
|
24
|
-
* @param {number|"unbounded"} [_max] - Maximum occurrence (unused but kept for API compatibility)
|
|
25
|
-
* @returns {string} - Generated TypeScript name for the inline type
|
|
26
|
-
*/
|
|
27
|
-
function makeInlineTypeName(parentTypeName, propName, _max) {
|
|
28
|
-
const base = pascal(parentTypeName || "AnonParent");
|
|
29
|
-
const prop = pascal(propName || "");
|
|
30
|
-
if (prop) {
|
|
31
|
-
return prop;
|
|
32
|
-
}
|
|
33
|
-
return `${base}Anon`;
|
|
34
|
-
}
|
|
35
17
|
// --- Minimal WS-Policy scanning (inline policies only; PolicyReference not resolved) ---
|
|
36
18
|
/**
|
|
37
19
|
* Extracts security requirements from WS-Policy nodes
|
|
@@ -255,9 +237,13 @@ export function compileCatalog(cat, options, streamConfig) {
|
|
|
255
237
|
}
|
|
256
238
|
}
|
|
257
239
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
240
|
+
const declarations = new LegacyDeclarations([
|
|
241
|
+
...Array.from(complexTypes.values(), rec => ({ kind: "complex", name: String(rec.node["@_name"]), ns: rec.tns, node: rec.node, prefixes: rec.prefixes })),
|
|
242
|
+
...Array.from(simpleTypes.values(), rec => ({ kind: "simple", name: String(rec.node["@_name"]), ns: rec.tns, node: rec.node, prefixes: rec.prefixes })),
|
|
243
|
+
...Array.from(elements.values(), rec => ({ kind: "element", name: String(rec.node["@_name"]), ns: rec.tns, node: rec.node, prefixes: rec.prefixes })),
|
|
244
|
+
]);
|
|
245
|
+
const compiledMap = new Map();
|
|
246
|
+
const aliasMap = new Map();
|
|
261
247
|
const inProgress = new Set();
|
|
262
248
|
// meta accumulators
|
|
263
249
|
const attrSpec = {};
|
|
@@ -314,22 +300,23 @@ export function compileCatalog(cat, options, streamConfig) {
|
|
|
314
300
|
const list = getFirstWithLocalName(simpleNode, "list");
|
|
315
301
|
if (list?.["@_itemType"]) {
|
|
316
302
|
const q = resolveQName(list["@_itemType"], schemaNS, prefixes);
|
|
317
|
-
const
|
|
318
|
-
|
|
303
|
+
const item = resolveTypeRef(q, schemaNS, prefixes);
|
|
304
|
+
const itemType = item.tsType.includes("|") ? `(${item.tsType})` : item.tsType;
|
|
305
|
+
return { tsType: `${itemType}[]`, declared: item.declared };
|
|
319
306
|
}
|
|
320
307
|
// fallback
|
|
321
308
|
return { tsType: "string", declared: "xs:string" };
|
|
322
309
|
}
|
|
323
310
|
/** Compile and cache a named simpleType */
|
|
324
311
|
function getOrCompileAlias(name, sNode, schemaNS, prefixes) {
|
|
325
|
-
const key =
|
|
312
|
+
const key = declarations.type(schemaNS, name);
|
|
326
313
|
const present = aliasMap.get(key);
|
|
327
314
|
if (present) {
|
|
328
315
|
return present;
|
|
329
316
|
}
|
|
330
317
|
const { tsType, declared, jsdoc } = compileSimpleTypeNode(sNode, schemaNS, prefixes);
|
|
331
318
|
const alias = {
|
|
332
|
-
name:
|
|
319
|
+
name: declarations.name(key),
|
|
333
320
|
ns: schemaNS,
|
|
334
321
|
tsType,
|
|
335
322
|
declared,
|
|
@@ -373,19 +360,26 @@ export function compileCatalog(cat, options, streamConfig) {
|
|
|
373
360
|
const k = qkey(q);
|
|
374
361
|
return complexTypes.get(k);
|
|
375
362
|
}
|
|
376
|
-
function getOrCompileComplex(name, cnode, schemaNS, prefixes) {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
if (
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
return { name:
|
|
363
|
+
function getOrCompileComplex(name, cnode, schemaNS, prefixes, declarationId = declarations.type(schemaNS, name)) {
|
|
364
|
+
const key = declarationId;
|
|
365
|
+
const outName = declarations.name(key);
|
|
366
|
+
const present = compiledMap.get(key);
|
|
367
|
+
if (present)
|
|
368
|
+
return present;
|
|
369
|
+
if (inProgress.has(key))
|
|
370
|
+
return { name: outName, ns: schemaNS, attrs: [], elems: [] };
|
|
371
|
+
inProgress.add(key);
|
|
372
|
+
try {
|
|
373
|
+
return compileComplexBody(cnode, schemaNS, prefixes, declarationId);
|
|
374
|
+
}
|
|
375
|
+
finally {
|
|
376
|
+
inProgress.delete(key);
|
|
384
377
|
}
|
|
385
|
-
|
|
386
|
-
|
|
378
|
+
}
|
|
379
|
+
function compileComplexBody(cnode, schemaNS, prefixes, declarationId) {
|
|
380
|
+
const key = declarationId;
|
|
381
|
+
const outName = declarations.name(key);
|
|
387
382
|
const typeDoc = extractAnnotationDocumentation(cnode);
|
|
388
|
-
const key = `${schemaNS}|${outName}`;
|
|
389
383
|
// hoisted helpers for merging and collecting
|
|
390
384
|
const mergeAttrs = (into, list) => {
|
|
391
385
|
const idx = new Map();
|
|
@@ -415,40 +409,6 @@ export function compileCatalog(cat, options, streamConfig) {
|
|
|
415
409
|
}
|
|
416
410
|
}
|
|
417
411
|
};
|
|
418
|
-
const mergeChoiceGroups = (into, list) => {
|
|
419
|
-
if (list.length === 0)
|
|
420
|
-
return into;
|
|
421
|
-
const merged = into ? [...into] : [];
|
|
422
|
-
const idx = new Map();
|
|
423
|
-
merged.forEach((g, i) => idx.set(g.name, i));
|
|
424
|
-
for (const g of list) {
|
|
425
|
-
const pos = idx.get(g.name);
|
|
426
|
-
if (pos == null) {
|
|
427
|
-
idx.set(g.name, merged.length);
|
|
428
|
-
merged.push(g);
|
|
429
|
-
}
|
|
430
|
-
else {
|
|
431
|
-
merged[pos] = g;
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
return merged;
|
|
435
|
-
};
|
|
436
|
-
const mergeJsonUnique = (into, list) => {
|
|
437
|
-
if (list.length === 0)
|
|
438
|
-
return into;
|
|
439
|
-
const merged = into ? [...into] : [];
|
|
440
|
-
const seen = new Set(merged.map((w) => JSON.stringify(w)));
|
|
441
|
-
for (const w of list) {
|
|
442
|
-
const key = JSON.stringify(w);
|
|
443
|
-
if (!seen.has(key)) {
|
|
444
|
-
seen.add(key);
|
|
445
|
-
merged.push(w);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
return merged;
|
|
449
|
-
};
|
|
450
|
-
const mergeWildcards = (into, list) => mergeJsonUnique(into, list);
|
|
451
|
-
const mergeAttributeWildcards = (into, list) => mergeJsonUnique(into, list);
|
|
452
412
|
const collectAttributes = (node) => {
|
|
453
413
|
const out = [];
|
|
454
414
|
const attrs = getChildrenWithLocalName(node, "attribute");
|
|
@@ -547,7 +507,7 @@ export function compileCatalog(cat, options, streamConfig) {
|
|
|
547
507
|
}
|
|
548
508
|
return out;
|
|
549
509
|
};
|
|
550
|
-
const compileElementParticle = (
|
|
510
|
+
const compileElementParticle = (e) => {
|
|
551
511
|
const nameOrRef = e["@_name"] || e["@_ref"];
|
|
552
512
|
if (!nameOrRef)
|
|
553
513
|
return undefined;
|
|
@@ -557,8 +517,8 @@ export function compileCatalog(cat, options, streamConfig) {
|
|
|
557
517
|
const inlineComplex = getFirstWithLocalName(e, "complexType");
|
|
558
518
|
const inlineSimple = getFirstWithLocalName(e, "simpleType");
|
|
559
519
|
if (inlineComplex) {
|
|
560
|
-
const
|
|
561
|
-
const rec = getOrCompileComplex(
|
|
520
|
+
const inlineId = declarations.inline(declarationId, e);
|
|
521
|
+
const rec = getOrCompileComplex(String(propName || nameOrRef), inlineComplex, schemaNS, prefixes, inlineId);
|
|
562
522
|
return {
|
|
563
523
|
name: propName || nameOrRef,
|
|
564
524
|
tsType: rec.name,
|
|
@@ -603,7 +563,7 @@ export function compileCatalog(cat, options, streamConfig) {
|
|
|
603
563
|
});
|
|
604
564
|
const recurse = (groupNode, inherited = one, inheritSequenceOccurs = true) => {
|
|
605
565
|
for (const e of getChildrenWithLocalName(groupNode, "element")) {
|
|
606
|
-
const particle = compileElementParticle(
|
|
566
|
+
const particle = compileElementParticle(e);
|
|
607
567
|
if (!particle)
|
|
608
568
|
continue;
|
|
609
569
|
// @constraints Preserve disabled particles until the content model can represent their absence (#145).
|
|
@@ -636,7 +596,7 @@ export function compileCatalog(cat, options, streamConfig) {
|
|
|
636
596
|
const branches = [];
|
|
637
597
|
let branchSourceOrder = 0;
|
|
638
598
|
for (const e of getChildrenWithLocalName(child.node, "element")) {
|
|
639
|
-
const particle = compileElementParticle(
|
|
599
|
+
const particle = compileElementParticle(e);
|
|
640
600
|
if (particle) {
|
|
641
601
|
branches.push({
|
|
642
602
|
name: particle.name,
|
|
@@ -672,29 +632,6 @@ export function compileCatalog(cat, options, streamConfig) {
|
|
|
672
632
|
recurse(node);
|
|
673
633
|
return out;
|
|
674
634
|
};
|
|
675
|
-
const present = compiledMap.get(key);
|
|
676
|
-
if (present) {
|
|
677
|
-
// On duplicate definitions, merge new attributes and elements
|
|
678
|
-
const newAttrs = collectAttributes(cnode);
|
|
679
|
-
const newElems = collectParticles(outName, cnode);
|
|
680
|
-
const newWildcards = collectWildcards(cnode);
|
|
681
|
-
const newAttributeWildcards = collectAttributeWildcards(cnode);
|
|
682
|
-
const newChoiceGroups = collectChoiceGroups(outName, cnode);
|
|
683
|
-
mergeAttrs(present.attrs, newAttrs);
|
|
684
|
-
mergeElems(present.elems, newElems);
|
|
685
|
-
present.wildcards = mergeWildcards(present.wildcards, newWildcards);
|
|
686
|
-
present.attributeWildcards = mergeAttributeWildcards(present.attributeWildcards, newAttributeWildcards);
|
|
687
|
-
const mergedChoiceGroups = mergeChoiceGroups(present.choiceGroups, newChoiceGroups);
|
|
688
|
-
if (mergedChoiceGroups && mergedChoiceGroups.length > 0) {
|
|
689
|
-
present.choiceGroups = mergedChoiceGroups;
|
|
690
|
-
}
|
|
691
|
-
if (!present.doc && typeDoc) {
|
|
692
|
-
present.doc = typeDoc;
|
|
693
|
-
}
|
|
694
|
-
// Remove from inProgress since we're done with this cycle
|
|
695
|
-
inProgress.delete(rawKey);
|
|
696
|
-
return present;
|
|
697
|
-
}
|
|
698
635
|
// result accumulators
|
|
699
636
|
const attrs = [];
|
|
700
637
|
const elems = [];
|
|
@@ -749,7 +686,6 @@ export function compileCatalog(cat, options, streamConfig) {
|
|
|
749
686
|
...(localChoiceGroups.length > 0 ? { localChoiceGroups } : {}),
|
|
750
687
|
};
|
|
751
688
|
compiledMap.set(key, result);
|
|
752
|
-
inProgress.delete(key);
|
|
753
689
|
return result;
|
|
754
690
|
}
|
|
755
691
|
}
|
|
@@ -784,7 +720,6 @@ export function compileCatalog(cat, options, streamConfig) {
|
|
|
784
720
|
...(attributeWildcards.length > 0 ? { attributeWildcards } : {}),
|
|
785
721
|
};
|
|
786
722
|
compiledMap.set(key, result);
|
|
787
|
-
inProgress.delete(rawKey);
|
|
788
723
|
return result;
|
|
789
724
|
};
|
|
790
725
|
if (ext) {
|
|
@@ -811,20 +746,19 @@ export function compileCatalog(cat, options, streamConfig) {
|
|
|
811
746
|
...(choiceGroups.length > 0 ? { choiceGroups } : {}),
|
|
812
747
|
};
|
|
813
748
|
compiledMap.set(key, result);
|
|
814
|
-
inProgress.delete(rawKey);
|
|
815
749
|
return result;
|
|
816
750
|
}
|
|
817
751
|
// Helper: compile a global element into a surface type (wrapper)
|
|
818
752
|
function compileElementAsType(name, enode, schemaNS, prefixes) {
|
|
819
|
-
const
|
|
753
|
+
const key = declarations.surface(schemaNS, name);
|
|
754
|
+
const outName = declarations.name(key);
|
|
820
755
|
const elementDoc = extractAnnotationDocumentation(enode);
|
|
821
|
-
const key = `${schemaNS}|${outName}`;
|
|
822
756
|
const present = compiledMap.get(key);
|
|
823
757
|
if (present)
|
|
824
758
|
return present;
|
|
825
759
|
const inlineComplex = getFirstWithLocalName(enode, "complexType");
|
|
826
760
|
if (inlineComplex) {
|
|
827
|
-
return getOrCompileComplex(name, inlineComplex, schemaNS, prefixes);
|
|
761
|
+
return getOrCompileComplex(name, inlineComplex, schemaNS, prefixes, key);
|
|
828
762
|
}
|
|
829
763
|
const inlineSimple = getFirstWithLocalName(enode, "simpleType");
|
|
830
764
|
if (inlineSimple) {
|
|
@@ -871,7 +805,7 @@ export function compileCatalog(cat, options, streamConfig) {
|
|
|
871
805
|
// record alias: ElementName -> BaseTypeName
|
|
872
806
|
const aliasKey = key;
|
|
873
807
|
const existingAlias = aliasMap.get(aliasKey);
|
|
874
|
-
const declared = `{${base.ns}}${
|
|
808
|
+
const declared = `{${base.ns}}${q.local}`;
|
|
875
809
|
if (!existingAlias) {
|
|
876
810
|
aliasMap.set(aliasKey, { name: outName, ns: schemaNS, tsType: base.name, declared, doc: elementDoc });
|
|
877
811
|
}
|
|
@@ -1138,9 +1072,10 @@ export function compileCatalog(cat, options, streamConfig) {
|
|
|
1138
1072
|
const inputElement = elementOfMessage(inMsg);
|
|
1139
1073
|
const outputElement = elementOfMessage(outMsg);
|
|
1140
1074
|
const doc = extractDirectDocumentation(po);
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
const
|
|
1075
|
+
const surfaceName = (element) => elements.has(qkey(element))
|
|
1076
|
+
? declarations.name(declarations.surface(element.ns, element.local)) : pascal(element.local);
|
|
1077
|
+
const inputTypeName = inputElement ? surfaceName(inputElement) : undefined;
|
|
1078
|
+
const outputTypeName = outputElement ? surfaceName(outputElement) : undefined;
|
|
1144
1079
|
const op = {
|
|
1145
1080
|
name,
|
|
1146
1081
|
soapAction: bOps.get(name) || "",
|
|
@@ -58,8 +58,8 @@ export declare function deriveCompatibilityView(graph: CanonicalGraph): {
|
|
|
58
58
|
model: "xsd10-faithful-v1";
|
|
59
59
|
symbols: {
|
|
60
60
|
id: string;
|
|
61
|
-
kind: "element" | "attribute" | "wsdl" | "
|
|
62
|
-
identity: import("./
|
|
61
|
+
kind: "element" | "attribute" | "wsdl" | "group" | "attributeGroup" | "complexType" | "simpleType" | "attributeWildcard" | "particle" | "attributeUse" | "attributeGroupUse";
|
|
62
|
+
identity: import("./declarationIdentity.js").Identity;
|
|
63
63
|
}[];
|
|
64
64
|
};
|
|
65
65
|
/** Internal faithful companion dispatch. Regeneration never fabricates missing structure. */
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface ComposedGatewayService {
|
|
2
|
+
key: string;
|
|
3
|
+
clientDir: string;
|
|
4
|
+
gatewayDir: string;
|
|
5
|
+
clientClassName: string;
|
|
6
|
+
}
|
|
7
|
+
/** Build a typed aggregate over independently generated service routers.
|
|
8
|
+
* @returns Complete generated source; the caller publishes it only after composition succeeds.
|
|
9
|
+
* @constraints Routes already carry generation-time prefixes. Runtime prefixing is refused to keep the served definition accurate.
|
|
10
|
+
*/
|
|
11
|
+
export declare function buildComposedGatewaySource(services: readonly ComposedGatewayService[], options: {
|
|
12
|
+
outDir: string;
|
|
13
|
+
openapiFile: string;
|
|
14
|
+
imports?: "js" | "ts" | "bare";
|
|
15
|
+
openapiPath?: string | false;
|
|
16
|
+
}): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { composeGeneratedSource, resolvePreamble } from "../generation/preamble.js";
|
|
3
|
+
/** Build a typed aggregate over independently generated service routers.
|
|
4
|
+
* @returns Complete generated source; the caller publishes it only after composition succeeds.
|
|
5
|
+
* @constraints Routes already carry generation-time prefixes. Runtime prefixing is refused to keep the served definition accurate.
|
|
6
|
+
*/
|
|
7
|
+
export function buildComposedGatewaySource(services, options) {
|
|
8
|
+
const suffix = options.imports === "bare" ? "" : `.${options.imports ?? "js"}`;
|
|
9
|
+
const literal = (value) => JSON.stringify(value).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
10
|
+
const relative = (target) => {
|
|
11
|
+
const resolved = path.relative(options.outDir, target).replaceAll("\\", "/");
|
|
12
|
+
if (path.isAbsolute(resolved) || /^[a-z]:/i.test(resolved))
|
|
13
|
+
throw new Error("Composed outputs must share a filesystem root");
|
|
14
|
+
return resolved.startsWith(".") ? resolved : "./" + resolved;
|
|
15
|
+
};
|
|
16
|
+
const ordered = [...services].sort((a, b) => a.key.localeCompare(b.key));
|
|
17
|
+
const lines = [
|
|
18
|
+
'import type {FastifyPluginAsync} from "fastify";',
|
|
19
|
+
'import {types} from "node:util";',
|
|
20
|
+
`import definition from ${literal(relative(options.openapiFile))} with {type: "json"};`,
|
|
21
|
+
...ordered.flatMap((service, index) => [
|
|
22
|
+
`import {${service.clientClassName} as Client${index}} from ${literal(relative(path.join(service.clientDir, "client" + suffix)))};`,
|
|
23
|
+
`import type {${service.clientClassName}Operations as Operations${index}} from ${literal(relative(path.join(service.clientDir, "operations" + suffix)))};`,
|
|
24
|
+
`import service${index} from ${literal(relative(path.join(service.gatewayDir, "plugin" + suffix)))};`,
|
|
25
|
+
]),
|
|
26
|
+
"",
|
|
27
|
+
"export const openapi = definition;",
|
|
28
|
+
"export interface MultiServiceClients {",
|
|
29
|
+
...ordered.map((service, index) => ` ${literal(service.key)}: Operations${index};`),
|
|
30
|
+
"}",
|
|
31
|
+
"export interface MultiServiceClientOptions {",
|
|
32
|
+
...ordered.map((service, index) => ` ${literal(service.key)}: ConstructorParameters<typeof Client${index}>[0];`),
|
|
33
|
+
"}",
|
|
34
|
+
"export interface MultiServiceGatewayOptions {clients: MultiServiceClients}",
|
|
35
|
+
"function registry(value: unknown): void {",
|
|
36
|
+
' if (!value || typeof value !== "object" || types.isProxy(value) || ![null, Object.prototype].includes(Object.getPrototypeOf(value) as object | null)) throw new TypeError("Service registry must be a data object");',
|
|
37
|
+
` const expected = ${literal(ordered.map(service => service.key))};`,
|
|
38
|
+
' if (Reflect.ownKeys(value).length !== expected.length) throw new TypeError("Service registry must contain exactly the configured services");',
|
|
39
|
+
" for (const key of expected) {",
|
|
40
|
+
" const property = Object.getOwnPropertyDescriptor(value, key);",
|
|
41
|
+
' if (!property || !Object.hasOwn(property, "value") || !property.value) throw new TypeError("Missing service registry value: " + key);',
|
|
42
|
+
" }",
|
|
43
|
+
"}",
|
|
44
|
+
"",
|
|
45
|
+
"/** Bind each generated SOAP client to its own explicit runtime WSDL and options. */",
|
|
46
|
+
"export function createClients(options: MultiServiceClientOptions): {",
|
|
47
|
+
...ordered.map((service, index) => ` ${literal(service.key)}: Client${index};`),
|
|
48
|
+
"} {",
|
|
49
|
+
" registry(options);",
|
|
50
|
+
" return {",
|
|
51
|
+
...ordered.map((service, index) => ` ${literal(service.key)}: new Client${index}(options[${literal(service.key)}]),`),
|
|
52
|
+
" };",
|
|
53
|
+
"}",
|
|
54
|
+
"",
|
|
55
|
+
"const plugin: FastifyPluginAsync<MultiServiceGatewayOptions> = async (fastify, options) => {",
|
|
56
|
+
' if (fastify.prefix) throw new TypeError("Configure service OpenAPI basePath instead of a runtime aggregate prefix");',
|
|
57
|
+
" registry(options.clients);",
|
|
58
|
+
...ordered.map((service, index) => ` await fastify.register(async scope => {await scope.register(service${index}, {client: options.clients[${literal(service.key)}]});});`),
|
|
59
|
+
...(options.openapiPath === false ? [] : [` fastify.get(${literal(options.openapiPath ?? "/openapi.json")}, async () => openapi);`]),
|
|
60
|
+
"};",
|
|
61
|
+
"export default plugin;",
|
|
62
|
+
"",
|
|
63
|
+
];
|
|
64
|
+
return composeGeneratedSource(lines.join("\n"), resolvePreamble("gateway"));
|
|
65
|
+
}
|
|
@@ -131,6 +131,7 @@ export async function generateGateway(opts) {
|
|
|
131
131
|
}
|
|
132
132
|
// Resolve client metadata for full handler generation
|
|
133
133
|
const clientMeta = resolveClientMeta({
|
|
134
|
+
gatewayDir: opts.outDir,
|
|
134
135
|
clientDir: opts.clientDir,
|
|
135
136
|
catalogFile: opts.catalogFile,
|
|
136
137
|
clientClassName: opts.clientClassName,
|
|
@@ -922,7 +922,7 @@ export function emitRouteFilesWithHandlers(outDir, routesDir, versionSlug, servi
|
|
|
922
922
|
? "toJsonArray"
|
|
923
923
|
: "toNdjson";
|
|
924
924
|
const runtimeImport = op.stream
|
|
925
|
-
? `import { ${streamHelperName} } from "../runtime${suffix}";`
|
|
925
|
+
? `import { ${streamHelperName}${hasUnwrap ? ", unwrapArrayWrappers" : ""} } from "../runtime${suffix}";`
|
|
926
926
|
: hasUnwrap
|
|
927
927
|
? `import { buildSuccessEnvelope, unwrapArrayWrappers } from "../runtime${suffix}";`
|
|
928
928
|
: `import { buildSuccessEnvelope } from "../runtime${suffix}";`;
|
|
@@ -942,9 +942,24 @@ export function emitRouteFilesWithHandlers(outDir, routesDir, versionSlug, servi
|
|
|
942
942
|
const handlerBody = op.stream
|
|
943
943
|
? ` handler: async (request, reply) => {
|
|
944
944
|
const client = fastify.${clientMeta.decoratorName};
|
|
945
|
-
const
|
|
946
|
-
|
|
947
|
-
|
|
945
|
+
const controller = new AbortController();
|
|
946
|
+
const disconnected = () => { if (!reply.raw.writableEnded) controller.abort(); };
|
|
947
|
+
request.raw.once("aborted", disconnected);
|
|
948
|
+
reply.raw.once("close", disconnected);
|
|
949
|
+
if (request.raw.aborted || reply.raw.destroyed) controller.abort();
|
|
950
|
+
try {
|
|
951
|
+
const result = await client.${clientMethod}(${bodyArg}, { signal: controller.signal });
|
|
952
|
+
reply.type(${JSON.stringify(op.stream.mediaType)});
|
|
953
|
+
${hasUnwrap ? `async function* mappedRecords() {
|
|
954
|
+
for await (const record of result.records) yield unwrapArrayWrappers(record, ${JSON.stringify(op.stream.recordTypeName ?? "unknown")});
|
|
955
|
+
}
|
|
956
|
+
// @constraints Keep disconnect cancellation active until the response stream finishes.
|
|
957
|
+
return await reply.send(${streamHelperName}(mappedRecords()));` : `// @constraints Keep disconnect cancellation active until the response stream finishes.
|
|
958
|
+
return await reply.send(${streamHelperName}(result.records));`}
|
|
959
|
+
} finally {
|
|
960
|
+
request.raw.removeListener("aborted", disconnected);
|
|
961
|
+
reply.raw.removeListener("close", disconnected);
|
|
962
|
+
}
|
|
948
963
|
},`
|
|
949
964
|
: ` handler: async (request) => {
|
|
950
965
|
const client = fastify.${clientMeta.decoratorName};
|
|
@@ -199,6 +199,7 @@ export interface ResolvedOperationMeta {
|
|
|
199
199
|
* @property {"js"|"ts"|"bare"} importsMode - Import extension mode
|
|
200
200
|
*/
|
|
201
201
|
export interface ResolveClientMetaOptions {
|
|
202
|
+
gatewayDir?: string;
|
|
202
203
|
clientDir?: string;
|
|
203
204
|
catalogFile?: string;
|
|
204
205
|
clientClassName?: string;
|
package/dist/gateway/helpers.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* - Automatic version/service detection from OpenAPI paths
|
|
12
12
|
*/
|
|
13
13
|
import { pascal, trimRepeatedEdgeChar } from "../util/tools.js";
|
|
14
|
+
import path from "node:path";
|
|
14
15
|
/**
|
|
15
16
|
* Converts a name to a deterministic slug for use in file names and URN IDs
|
|
16
17
|
*
|
|
@@ -393,6 +394,20 @@ export function resolveClientMeta(opts, catalog) {
|
|
|
393
394
|
const operationsImportPath = opts.clientDir
|
|
394
395
|
? `../client/operations${suffix}`
|
|
395
396
|
: `./client/operations${suffix}`;
|
|
397
|
+
const modulePath = (from, module) => {
|
|
398
|
+
const relative = path.relative(from, path.join(opts.clientDir, module + suffix)).replaceAll("\\", "/");
|
|
399
|
+
if (path.isAbsolute(relative) || /^[a-z]:/i.test(relative))
|
|
400
|
+
throw new Error("Generated client and gateway modules must share a filesystem root");
|
|
401
|
+
return relative.startsWith(".") ? relative : "./" + relative;
|
|
402
|
+
};
|
|
403
|
+
if (opts.clientDir && opts.gatewayDir)
|
|
404
|
+
return {
|
|
405
|
+
className, decoratorName,
|
|
406
|
+
importPath: modulePath(path.join(opts.gatewayDir, "routes"), "client"),
|
|
407
|
+
typesImportPath: modulePath(path.join(opts.gatewayDir, "routes"), "types"),
|
|
408
|
+
pluginImportPath: modulePath(opts.gatewayDir, "client"),
|
|
409
|
+
operationsImportPath: modulePath(opts.gatewayDir, "operations"),
|
|
410
|
+
};
|
|
396
411
|
return {
|
|
397
412
|
className,
|
|
398
413
|
decoratorName,
|
|
@@ -19,7 +19,31 @@ export function lowerComponentShape(contract, supplied, role, check) {
|
|
|
19
19
|
const owned = inspectComponentRecordProjection(supplied, contract);
|
|
20
20
|
return lowerComponentShape(owned.companion, owned.mapping, role, check);
|
|
21
21
|
}
|
|
22
|
-
const { projection, applicationGroups } = inspectComponentValueMapping(supplied, contract), { context } = inspectDirectOperation(contract);
|
|
22
|
+
const { mapping, projection, applicationGroups, rpcParts } = inspectComponentValueMapping(supplied, contract), { context } = inspectDirectOperation(contract);
|
|
23
|
+
if ('parts' in mapping.root) {
|
|
24
|
+
const properties = Object.create(null);
|
|
25
|
+
const required = [], members = [];
|
|
26
|
+
for (const part of mapping.root.parts) {
|
|
27
|
+
check();
|
|
28
|
+
const plan = rpcParts?.find(plan => plan.name === part.name)?.projection;
|
|
29
|
+
let type = 'string';
|
|
30
|
+
if (plan?.root.kind === 'record') {
|
|
31
|
+
const record = plan.records[0];
|
|
32
|
+
const fields = Object.fromEntries(record.slots.map(slot => [slot.element.name.local, { type: 'boolean' }]));
|
|
33
|
+
properties[part.name] = { type: 'object', properties: fields, required: Object.keys(fields), additionalProperties: false };
|
|
34
|
+
type = `{${record.slots.map(slot => `readonly ${literal(slot.element.name.local)}: boolean`).join('; ')}}`;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
type = plan?.root.kind === 'scalar' && plan.root.builtin.local === 'boolean' ? 'boolean' : 'string';
|
|
38
|
+
properties[part.name] = { type };
|
|
39
|
+
}
|
|
40
|
+
required.push(part.name);
|
|
41
|
+
members.push(`readonly ${literal(part.name)}: ${type}`);
|
|
42
|
+
}
|
|
43
|
+
return { type: `{${members.join('; ')}}`, declarations: [], schema: { type: 'object', additionalProperties: false, properties, required } };
|
|
44
|
+
}
|
|
45
|
+
if (!projection)
|
|
46
|
+
throw new TypeError('Native document projection required');
|
|
23
47
|
const names = new Map(projection.records.map((record, index) => [record.type.id, applicationGroups
|
|
24
48
|
? applicationRecordName(record.type.id) : `${role}_Record_${index}`]));
|
|
25
49
|
const definitions = Object.create(null);
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,10 @@ export { buildSecurity, loadSecurityConfigFile, parseSecurityConfig, SecurityCon
|
|
|
5
5
|
export { generateTests } from "./test/generateTests.js";
|
|
6
6
|
export { runGenerationPipeline } from "./pipeline.js";
|
|
7
7
|
export type { PipelineOptions } from './pipeline.js';
|
|
8
|
+
export { runMultiServicePipeline, loadMultiServiceConfig } from './multiServicePipeline.js';
|
|
9
|
+
export type { MultiServicePipelineOptions, MultiServicePipelineResult, MultiServiceEntry } from './multiServicePipeline.js';
|
|
10
|
+
export { composeOpenApiDocuments } from './openapi/composeOpenAPI.js';
|
|
11
|
+
export type { ComposedOpenApiDocument, OpenApiCompositionEntry, OpenApiCompositionOptions } from './openapi/composeOpenAPI.js';
|
|
8
12
|
export type { CheckedComponentOptions, CheckedInventory } from './checkedPipeline.js';
|
|
9
13
|
export { planCheckedComponents, formatCheckedComponentPlan, writeCheckedPlanningFile, CheckedPlanningError } from './checkedPlanning.js';
|
|
10
14
|
export type { CheckedComponentPlan, ComponentPlanBlocker } from './checkedPlanning.js';
|
package/dist/index.js
CHANGED
|
@@ -27,6 +27,8 @@ export { generateGateway } from "./gateway/generateGateway.js";
|
|
|
27
27
|
export { buildSecurity, loadSecurityConfigFile, parseSecurityConfig, SecurityConfigError, } from "./util/securityConfig.js";
|
|
28
28
|
export { generateTests } from "./test/generateTests.js";
|
|
29
29
|
export { runGenerationPipeline } from "./pipeline.js";
|
|
30
|
+
export { runMultiServicePipeline, loadMultiServiceConfig } from './multiServicePipeline.js';
|
|
31
|
+
export { composeOpenApiDocuments } from './openapi/composeOpenAPI.js';
|
|
30
32
|
export { planCheckedComponents, formatCheckedComponentPlan, writeCheckedPlanningFile, CheckedPlanningError } from './checkedPlanning.js';
|
|
31
33
|
export { loadStreamConfigFile, parseStreamConfig, StreamConfigError, } from "./util/streamConfig.js";
|
|
32
34
|
export { applyShapeCatalogs } from "./compiler/shapeResolver.js";
|
|
@@ -70,6 +70,10 @@ export declare function resolveLexicalQName(lexical: string, node: SyntaxElement
|
|
|
70
70
|
export declare function decodeMetadataXml(bytes: Uint8Array): string;
|
|
71
71
|
/** Positions are one-based lines/columns and zero-based UTF-16 offsets in decoded XML. */
|
|
72
72
|
export interface OrderedSyntaxOptions {
|
|
73
|
+
/** Explicit component SOAP response policy; source and shipped direct decoding remain separate. */
|
|
74
|
+
componentResponseEncoding?: {
|
|
75
|
+
charset?: string;
|
|
76
|
+
};
|
|
73
77
|
/** Internal metadata boundary; payload/capsule callers retain the existing encoding policy. */
|
|
74
78
|
metadataEncoding?: boolean;
|
|
75
79
|
maxDepth?: number;
|
|
@@ -55,19 +55,33 @@ const xmlEncodingDeclaration = /^<\?xml\s[^?]*encoding\s*=\s*["']([^"']+)["']/i;
|
|
|
55
55
|
export function decodeMetadataXml(bytes) {
|
|
56
56
|
return decodeXml(bytes, true, true);
|
|
57
57
|
}
|
|
58
|
-
function decodeXml(bytes, strictBom = false, metadata = false) {
|
|
58
|
+
function decodeXml(bytes, strictBom = false, metadata = false, response) {
|
|
59
59
|
let encoding = bytes[0] === 0xff && bytes[1] === 0xfe ? "utf-16le"
|
|
60
60
|
: bytes[0] === 0xfe && bytes[1] === 0xff ? "utf-16be" : "utf-8";
|
|
61
61
|
try {
|
|
62
62
|
const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
63
63
|
// @why Sniff only an ASCII declaration, never guess Latin1 after UTF-8 failure.
|
|
64
64
|
// A BOM fixes the encoding and must agree with the decoded declaration.
|
|
65
|
-
|
|
65
|
+
const bom = encoding !== 'utf-8' || bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf;
|
|
66
|
+
const canonical = (label) => label.toLowerCase() === 'utf8' ? 'utf-8' : label.toLowerCase();
|
|
67
|
+
const agrees = (label) => canonical(label) === encoding || canonical(label) === 'utf-16' && encoding.startsWith('utf-16');
|
|
68
|
+
if ((metadata || response) && !bom) {
|
|
66
69
|
const end = buffer.indexOf('>');
|
|
67
70
|
const declaration = xmlEncodingDeclaration.exec(buffer.toString('latin1', 0, end < 0 ? 0 : end + 1))?.[1].toLowerCase();
|
|
68
|
-
if (
|
|
71
|
+
if (response) {
|
|
72
|
+
const asserted = response.charset ?? declaration;
|
|
73
|
+
if (asserted) {
|
|
74
|
+
const selected = canonical(asserted);
|
|
75
|
+
if (!['utf-8', 'iso-8859-1'].includes(selected))
|
|
76
|
+
throw new SchemaLoadingError('unsupported-capability', 'Unsupported XML byte encoding');
|
|
77
|
+
encoding = selected;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else if (declaration === 'iso-8859-1')
|
|
69
81
|
encoding = 'iso-8859-1';
|
|
70
82
|
}
|
|
83
|
+
if (response?.charset && !agrees(response.charset))
|
|
84
|
+
throw new SchemaLoadingError('unsupported-capability', 'HTTP charset disagrees with XML bytes');
|
|
71
85
|
// TextDecoder's ISO label aliases Windows-1252; Buffer supplies actual ISO byte identity.
|
|
72
86
|
const text = encoding === 'iso-8859-1' ? buffer.toString('latin1') : new TextDecoder(encoding, { fatal: true }).decode(bytes);
|
|
73
87
|
if (strictBom && text.startsWith('\ufeff'))
|
|
@@ -107,7 +121,7 @@ export function parseOrderedSyntax(bytes, uri, options = {}) {
|
|
|
107
121
|
const charge = (amount) => options.chargeWork?.(amount);
|
|
108
122
|
charge(4096 + bytes.byteLength * 64);
|
|
109
123
|
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
110
|
-
const text = options.metadataEncoding ? decodeMetadataXml(bytes) : decodeXml(bytes, options.xml10Only);
|
|
124
|
+
const text = options.metadataEncoding ? decodeMetadataXml(bytes) : decodeXml(bytes, options.xml10Only, false, options.componentResponseEncoding);
|
|
111
125
|
const lines = [0];
|
|
112
126
|
for (let i = 0; i < text.length; i++) {
|
|
113
127
|
if (text[i] === "\r") {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type PipelineOptions } from "./pipeline.js";
|
|
2
|
+
import { type ComposedOpenApiDocument, type OpenApiCompositionOptions } from "./openapi/composeOpenAPI.js";
|
|
3
|
+
import type { CompiledCatalog } from "./compiler/schemaCompiler.js";
|
|
4
|
+
export interface MultiServiceEntry {
|
|
5
|
+
key: string;
|
|
6
|
+
generation: PipelineOptions;
|
|
7
|
+
}
|
|
8
|
+
export interface MultiServicePipelineOptions {
|
|
9
|
+
services: readonly MultiServiceEntry[];
|
|
10
|
+
openapi: OpenApiCompositionOptions & {
|
|
11
|
+
outFile: string;
|
|
12
|
+
};
|
|
13
|
+
gateway: {
|
|
14
|
+
outDir: string;
|
|
15
|
+
imports?: "js" | "ts" | "bare";
|
|
16
|
+
openapiPath?: string | false;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export interface MultiServicePipelineResult {
|
|
20
|
+
openapiDoc: ComposedOpenApiDocument;
|
|
21
|
+
services: Record<string, {
|
|
22
|
+
compiled: CompiledCatalog;
|
|
23
|
+
clientDir: string;
|
|
24
|
+
gatewayDir: string;
|
|
25
|
+
}>;
|
|
26
|
+
pluginFile: string;
|
|
27
|
+
openapiFile: string;
|
|
28
|
+
}
|
|
29
|
+
/** Generate independent WSDL clients behind one typed gateway and discoverable API definition.
|
|
30
|
+
* @param options Explicit per-service pipelines and disjoint aggregate output paths.
|
|
31
|
+
* @returns Matched aggregate artifacts and each service's unmerged compiled catalog.
|
|
32
|
+
* @throws For configuration/output conflicts, source generation failures or invalid API composition.
|
|
33
|
+
* @sideEffects Generates each selected legacy service in caller order, then publishes the aggregate spec/plugin pair.
|
|
34
|
+
* @constraints Failure preserves previous aggregate spec/plugin files; earlier generated service dependencies are not rolled back.
|
|
35
|
+
*/
|
|
36
|
+
export declare function runMultiServicePipeline(options: MultiServicePipelineOptions): Promise<MultiServicePipelineResult>;
|
|
37
|
+
/** Load a composition config with paths resolved against its own directory.
|
|
38
|
+
* @throws For invalid JSON or missing required output/source paths when the pipeline validates it.
|
|
39
|
+
* @constraints Runtime client options and credentials are deliberately not generation configuration.
|
|
40
|
+
*/
|
|
41
|
+
export declare function loadMultiServiceConfig(file: string): Promise<MultiServicePipelineOptions>;
|