@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
package/README.md
CHANGED
|
@@ -9,13 +9,21 @@ Turn legacy WSDL/SOAP services into typed TypeScript clients and optional REST A
|
|
|
9
9
|
|
|
10
10
|
Created from production integration work at [TechSpokes](https://www.techspokes.com); built for teams modernizing real enterprise SOAP integrations.
|
|
11
11
|
|
|
12
|
+
## Upgrading to 1.2.0
|
|
13
|
+
|
|
14
|
+
Existing projects keep legacy generation unless they explicitly choose another profile. Regeneration can change affected DTO names, stream records and corrected scalar/simple-content results; 1.2.0 is not an unconditional drop-in regeneration upgrade. Read the [1.1.x migration guide](docs/migration.md#from-11x-to-120) before updating, including matched artifact regeneration, scaffold preservation, consumer checks and rollback.
|
|
15
|
+
|
|
16
|
+
Published npm starting points include 1.1.5 and 1.1.8; 1.1.6 and 1.1.7 were unpublished candidates. Native multi-service composition and checked generation are optional capabilities with separate adoption instructions, not automatic migrations of an existing gateway.
|
|
17
|
+
|
|
12
18
|
## Pick Your Path
|
|
13
19
|
|
|
14
20
|
| Goal | Command | What you get |
|
|
15
21
|
|------|---------|-------------|
|
|
16
22
|
| Typed SOAP client | `npx wsdl-tsc client` | TypeScript types, typed operations, mockable interface |
|
|
17
23
|
| OpenAPI 3.1 spec | `npx wsdl-tsc openapi` | OpenAPI JSON/YAML generated from WSDL |
|
|
18
|
-
| Full SOAP-to-REST bridge | `npx wsdl-tsc pipeline` | Client + OpenAPI + Fastify gateway + runnable app |
|
|
24
|
+
| Full SOAP-to-REST bridge | `npx wsdl-tsc pipeline --init-app` | Client + OpenAPI + Fastify gateway + runnable app |
|
|
25
|
+
| Several SOAP sources behind one API | `npx wsdl-tsc pipeline --services-config services.json` | Separate legacy clients, one gateway plugin and combined OpenAPI |
|
|
26
|
+
| Checked document/literal or bounded RPC | `npx wsdl-tsc pipeline --model checked-components-v1` | Source-backed validation and buffered checked values |
|
|
19
27
|
|
|
20
28
|
See [Start Here](docs/start-here.md) for detailed guidance on choosing a path.
|
|
21
29
|
|
|
@@ -31,7 +39,7 @@ Checked generation can opt into [application groups and configured names](docs/c
|
|
|
31
39
|
|
|
32
40
|
## Why Choose This
|
|
33
41
|
|
|
34
|
-
Most tools in this space stop at one layer: a SOAP runtime, type generation, or spec conversion. This package generates the full stack from a
|
|
42
|
+
Most tools in this space stop at one layer: a SOAP runtime, type generation, or spec conversion. This package generates the full stack from a WSDL source and can compose independent legacy sources into one REST gateway.
|
|
35
43
|
|
|
36
44
|
- Typed client, OpenAPI spec, REST gateway, and runnable app from one command
|
|
37
45
|
- Deterministic output safe for CI regeneration and version control
|
|
@@ -101,6 +109,10 @@ curl -X POST http://localhost:3000/get-weather-information \
|
|
|
101
109
|
-H "Content-Type: application/json" -d '{}'
|
|
102
110
|
```
|
|
103
111
|
|
|
112
|
+
### Compose several SOAP services
|
|
113
|
+
|
|
114
|
+
Use `pipeline --services-config ./services.json` to generate each source into separate client, catalog, OpenAPI and gateway paths, then emit one aggregate plugin and API document. The [multi-service guide](docs/gateway-guide.md#multi-service-setup) shows the configuration and generated `createClients` helper; each runtime client keeps its own WSDL source, endpoint and security settings. Native composition currently supports legacy generation; checked composition remains deferred.
|
|
115
|
+
|
|
104
116
|
## What Gets Generated
|
|
105
117
|
|
|
106
118
|
| Output | Files | Purpose |
|
|
@@ -229,6 +241,7 @@ See [CLI Reference](docs/cli-reference.md) for all flags and examples.
|
|
|
229
241
|
| [Configuration](docs/configuration.md) | Gateway security, upstream SOAP security, tags, operation overrides |
|
|
230
242
|
| [Migration Playbook](docs/migration-playbook.md) | End-to-end SOAP modernization guide |
|
|
231
243
|
| [Gateway Guide](docs/gateway-guide.md) | Fastify integration, inbound enforcement, and error handling |
|
|
244
|
+
| [Multi-Service Gateway Status](https://github.com/TechSpokes/typescript-wsdl-client/blob/main/docs/roadmap/multi-service-gateway.md) | Implemented legacy composition, mapping boundaries, and deferred checked composition |
|
|
232
245
|
| [Testing Guide](docs/testing.md) | Consumer tests and separate maintainer verification guidance |
|
|
233
246
|
| [Production Guide](docs/production.md) | CI/CD, validation, logging |
|
|
234
247
|
| [Troubleshooting](docs/troubleshooting.md) | Common issues and debugging |
|
|
@@ -236,6 +249,7 @@ See [CLI Reference](docs/cli-reference.md) for all flags and examples.
|
|
|
236
249
|
| [Core Concepts](docs/concepts.md) | Flattening, $value, primitives, determinism |
|
|
237
250
|
| [Agent Skill Artifact](docs/agent-skill.md) | Release ZIP for consumer-project AI agents |
|
|
238
251
|
| [Version Migration](docs/migration.md) | Upgrading between package versions |
|
|
252
|
+
| [1.2.0 Release Notes](https://github.com/TechSpokes/typescript-wsdl-client/blob/main/docs/releases/v1.2.0.md) | Composition, consumer compatibility and bounded RPC qualification |
|
|
239
253
|
| [XML/JSON Mapping Contract](docs/mapping-contract.md) | Checked keyed, namespace, nil and group representations |
|
|
240
254
|
| [Product Corpus](docs/corpus.md) | Representative public inputs and their executable evidence |
|
|
241
255
|
|
|
@@ -259,7 +273,7 @@ See [CLI Reference](docs/cli-reference.md) for all flags and examples.
|
|
|
259
273
|
| [Direct Structural Matching](https://github.com/TechSpokes/typescript-wsdl-client/blob/main/docs/content-model-matching.md) | Internal structure-only matcher, evidence and scalar handoff |
|
|
260
274
|
| [Architecture](https://github.com/TechSpokes/typescript-wsdl-client/blob/main/docs/architecture.md) | Internal pipeline for contributors |
|
|
261
275
|
| [Reference Validation](https://github.com/TechSpokes/typescript-wsdl-client/blob/main/docs/reference-validation.md) | Internal evidence contract and Node tooling migration inventory |
|
|
262
|
-
| [NOAA NDFD Qualification](https://github.com/TechSpokes/typescript-wsdl-client/blob/main/docs/noaa-ndfd-qualification.md) |
|
|
276
|
+
| [NOAA NDFD Qualification](https://github.com/TechSpokes/typescript-wsdl-client/blob/main/docs/noaa-ndfd-qualification.md) | All-12 owned RPC execution, ten live successes and two live GML refusals |
|
|
263
277
|
| [File Naming And Path Organization](https://github.com/TechSpokes/typescript-wsdl-client/blob/main/docs/file-naming-and-path-organization.md) | Contributor convention for taxonomy paths and fixture retrieval |
|
|
264
278
|
|
|
265
279
|
### Decisions and research
|
package/dist/checkedPlanning.js
CHANGED
|
@@ -166,10 +166,8 @@ export async function prepareCheckedComponentPlan(source, choices = {}, outputs)
|
|
|
166
166
|
&& namespace !== XSD_NAMESPACE)
|
|
167
167
|
missing.set(namespace, child.source);
|
|
168
168
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (missing.size)
|
|
172
|
-
unresolved = [...(intake.namespaceLocations ?? []), ...[...missing.keys()].sort().map(namespace => ({ namespace, location: 'REQUIRED_SCHEMA_LOCATION' }))];
|
|
169
|
+
// @why XSD 1.0 section 4.2.3 permits locationless imports. Native construction
|
|
170
|
+
// decides whether their components are needed; namespace URIs are never fetch locations.
|
|
173
171
|
if (!blockers.length && config) {
|
|
174
172
|
if (!Object.hasOwn(explicit, 'standardLibraries') && input.schemas.some(s => syntaxElements(s.syntax, XSD_NAMESPACE, 'import')
|
|
175
173
|
.some(n => syntaxAttribute(n, 'namespace') === XSD_NAMESPACE && syntaxAttribute(n, 'schemaLocation') === undefined))) {
|
|
@@ -188,6 +186,31 @@ export async function prepareCheckedComponentPlan(source, choices = {}, outputs)
|
|
|
188
186
|
}
|
|
189
187
|
}
|
|
190
188
|
for (const refusal of native.refusals) {
|
|
189
|
+
if (['whole-source-qualification', 'source-preparation'].includes(refusal.rule) && missing.size) {
|
|
190
|
+
const referenced = new Set();
|
|
191
|
+
const pending = documents.map(document => document.root);
|
|
192
|
+
while (pending.length) {
|
|
193
|
+
const node = pending.pop();
|
|
194
|
+
for (const attribute of node.attributes)
|
|
195
|
+
if (attribute.name.namespace === ''
|
|
196
|
+
&& ['type', 'ref', 'base', 'itemType', 'memberTypes', 'substitutionGroup', 'element'].includes(attribute.name.local)) {
|
|
197
|
+
for (const token of attribute.value.trim().split(/[\t\r\n ]+/)) {
|
|
198
|
+
try {
|
|
199
|
+
referenced.add(resolveLexicalQName(token, node).namespace);
|
|
200
|
+
}
|
|
201
|
+
catch { /* Native diagnostics retain malformed references. */ }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
pending.push(...syntaxElements(node));
|
|
205
|
+
}
|
|
206
|
+
const needed = [...missing].filter(([namespace]) => referenced.has(namespace));
|
|
207
|
+
for (const [namespace, at] of needed)
|
|
208
|
+
add('missing-dependency', 'native-preparation', `locationless-import:${namespace}`, 'Supply an authorized schema path for required components in this namespace.', `checkedComponents.namespaceLocations[${JSON.stringify(namespace)}].location`, [refusal.selector], at);
|
|
209
|
+
if (needed.length) {
|
|
210
|
+
unresolved = [...(intake.namespaceLocations ?? []), ...needed.map(([namespace]) => namespace).sort().map(namespace => ({ namespace, location: 'REQUIRED_SCHEMA_LOCATION' }))];
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
191
214
|
if (refusal.rule.startsWith('ambiguous-record-path:')) {
|
|
192
215
|
add('ambiguous-selection', 'native-preparation', refusal.rule, 'Supply recordPath using expanded names to select one declared repeated complex record in this companion source.', 'checkedComponents.recordProjections[].recordPath', [refusal.selector], undefined, 'mapping-and-artifact');
|
|
193
216
|
continue;
|
package/dist/cli.js
CHANGED
|
@@ -50,6 +50,7 @@ function loadStreamConfigOrExit(filePath) {
|
|
|
50
50
|
}
|
|
51
51
|
// Process command line arguments, removing the first two elements (node executable and script path)
|
|
52
52
|
const rawArgs = hideBin(process.argv);
|
|
53
|
+
const hasServicesConfig = rawArgs.some(argument => argument === "--services-config" || argument.startsWith("--services-config="));
|
|
53
54
|
// Available commands
|
|
54
55
|
const COMMANDS = ["compile", "client", "openapi", "gateway", "app", "pipeline", "assess"];
|
|
55
56
|
// ---------------------------------------------------------------------------
|
|
@@ -613,10 +614,25 @@ if (rawArgs[0] === "app") {
|
|
|
613
614
|
});
|
|
614
615
|
process.exit(0);
|
|
615
616
|
}
|
|
616
|
-
if (rawArgs[0] === "pipeline") {
|
|
617
|
+
if (rawArgs[0] === "pipeline" && hasServicesConfig) {
|
|
618
|
+
const argv = await yargs(rawArgs.slice(1)).version(false).scriptName("wsdl-tsc pipeline")
|
|
619
|
+
.option("services-config", { type: "string", demandOption: true, desc: "Multi-service generation configuration; exclusive with single-source pipeline flags" })
|
|
620
|
+
.strict().help().parse();
|
|
621
|
+
try {
|
|
622
|
+
const { loadMultiServiceConfig, runMultiServicePipeline } = await import("./multiServicePipeline.js");
|
|
623
|
+
const result = await runMultiServicePipeline(await loadMultiServiceConfig(argv["services-config"]));
|
|
624
|
+
success(`Composed gateway written to ${result.pluginFile}`);
|
|
625
|
+
success(`Combined OpenAPI specification written to ${result.openapiFile}`);
|
|
626
|
+
}
|
|
627
|
+
catch (error) {
|
|
628
|
+
handleCLIError(error);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (rawArgs[0] === "pipeline" && !hasServicesConfig) {
|
|
617
632
|
const pipelineArgv = await yargs(rawArgs.slice(1))
|
|
618
633
|
.version(false)
|
|
619
634
|
.scriptName("wsdl-tsc pipeline")
|
|
635
|
+
.option("services-config", { type: "string", desc: "Compose independent service pipelines from JSON; exclusive with single-source flags" })
|
|
620
636
|
.usage("$0 --wsdl-source <file|url> [--client-dir <dir>] [--openapi-file <path>] [--gateway-dir <dir> --gateway-service-name <slug> --gateway-version-prefix <slug>] [--catalog-file <path>] [options]")
|
|
621
637
|
.option("wsdl-source", { type: "string", demandOption: true, desc: "Path or URL to the WSDL" })
|
|
622
638
|
.option("model", { type: "string", choices: ["legacy", "checked-components-v1"], default: "legacy", desc: "Generation model; components derive unambiguous WSDL selections" })
|
|
@@ -66,6 +66,18 @@ export function generateClient(outFile, compiled) {
|
|
|
66
66
|
const soapOptions = soapImport("IOptions");
|
|
67
67
|
const soapSecurity = soapImport("ISecurity");
|
|
68
68
|
const soapFactory = soapImport("createClientAsync");
|
|
69
|
+
const soapHttpClient = soapImport("HttpClient");
|
|
70
|
+
const soapExOptions = soapImport("IExOptions");
|
|
71
|
+
const aliases = new Map(compiled.aliases.map(alias => [alias.name, alias.tsType]));
|
|
72
|
+
const scalarTypes = Object.fromEntries(compiled.aliases.flatMap(alias => {
|
|
73
|
+
let type = alias.tsType;
|
|
74
|
+
const visited = new Set([alias.name]);
|
|
75
|
+
while (aliases.has(type) && !visited.has(type)) {
|
|
76
|
+
visited.add(type);
|
|
77
|
+
type = aliases.get(type);
|
|
78
|
+
}
|
|
79
|
+
return ["string", "number", "boolean", "bigint"].includes(type) || type.endsWith("[]") ? [[alias.name, type]] : [];
|
|
80
|
+
}));
|
|
69
81
|
// Track whether any operation opts into streaming, so we can emit the
|
|
70
82
|
// supporting runtime helpers only when they're actually used.
|
|
71
83
|
let anyStream = false;
|
|
@@ -97,12 +109,14 @@ export function generateClient(outFile, compiled) {
|
|
|
97
109
|
* Streams records from the ${m} operation of the ${clientName}.${opDocStr}${secHintsStr}
|
|
98
110
|
*
|
|
99
111
|
* @param args - The request arguments for the ${m} operation.
|
|
112
|
+
* @param options - Optional cancellation signal and total stream deadline in milliseconds.
|
|
100
113
|
* @returns A promise resolving to a streaming response whose \`records\` is an
|
|
101
114
|
* async iterable of ${recordTs} objects parsed from the SOAP body as
|
|
102
115
|
* chunks arrive. Iteration pulls bytes on demand.
|
|
103
116
|
*/
|
|
104
117
|
async ${m}<HeadersType = Record<string, string>>(
|
|
105
|
-
args: ${inTs}
|
|
118
|
+
args: ${inTs},
|
|
119
|
+
options?: StreamCallOptions
|
|
106
120
|
): Promise<StreamOperationResponse<T.${recordTs}, HeadersType>> {
|
|
107
121
|
return this.callStream<${inTs}, T.${recordTs}, HeadersType>(
|
|
108
122
|
args,
|
|
@@ -112,7 +126,8 @@ export function generateClient(outFile, compiled) {
|
|
|
112
126
|
${JSON.stringify(inputElementLocal)},
|
|
113
127
|
${JSON.stringify(inputElementNs)},
|
|
114
128
|
${JSON.stringify(op.soapAction ?? "")},
|
|
115
|
-
${JSON.stringify(op.stream.recordPath)}
|
|
129
|
+
${JSON.stringify(op.stream.recordPath)},
|
|
130
|
+
options
|
|
116
131
|
);
|
|
117
132
|
}`;
|
|
118
133
|
methods.push(methodTemplate);
|
|
@@ -144,7 +159,11 @@ export function generateClient(outFile, compiled) {
|
|
|
144
159
|
// inside this template literal (which would flag `this`, `await`, `yield`
|
|
145
160
|
// outside-of-function false positives).
|
|
146
161
|
const streamMethodsBlock = anyStream
|
|
147
|
-
? "\n" + loadRuntimeSource("clientStreamMethods.tpl.txt")
|
|
162
|
+
? "\n" + loadRuntimeSource("clientStreamMethods.tpl.txt")
|
|
163
|
+
.replaceAll("__SOAP_CLIENT_TYPE__", soapClient.local)
|
|
164
|
+
.replaceAll("__SOAP_HTTP_CLIENT__", soapHttpClient.local)
|
|
165
|
+
.replaceAll("__SOAP_EX_OPTIONS__", soapExOptions.local)
|
|
166
|
+
.replaceAll("__SOAP_SECURITY__", soapSecurity.local)
|
|
148
167
|
: "";
|
|
149
168
|
// noinspection JSFileReferences,JSUnresolvedReference,CommaExpressionJS,JSDuplicatedDeclaration,ReservedWordAsName,JSCommentMatchesSignature,JSValidateTypes,JSIgnoredPromiseFromCall,BadExpressionStatementJS,ES6UnusedImports,JSUnnecessarySemicolon,UnreachableCodeJS,JSUnusedLocalSymbols
|
|
150
169
|
const classTemplate = `// noinspection JSAnnotator
|
|
@@ -153,13 +172,14 @@ export function generateClient(outFile, compiled) {
|
|
|
153
172
|
* Generated ${clientName} client class.
|
|
154
173
|
* This class wraps the node-soap client and provides strongly-typed methods for each operation.
|
|
155
174
|
*/
|
|
156
|
-
import {${soapFactory.specifier}} from "soap";
|
|
157
|
-
import type {${soapClient.specifier}, ${soapOptions.specifier}, ${soapSecurity.specifier}} from "soap";
|
|
175
|
+
import {${soapFactory.specifier}${anyStream ? `, ${soapHttpClient.specifier}` : ""}} from "soap";
|
|
176
|
+
import type {${soapClient.specifier}, ${soapOptions.specifier}, ${soapSecurity.specifier}${anyStream ? `, ${soapExOptions.specifier}` : ""}} from "soap";
|
|
158
177
|
import type * as T from "./types${suffix}";
|
|
159
178
|
import type {${clientName}DataTypes} from "./utils${suffix}";
|
|
160
179
|
import {${clientConstant}_DATA_TYPES} from "./utils${suffix}";${anyStream ? `
|
|
161
180
|
import {parseRecords, type RecordParseSpec} from "./streamXml${suffix}";
|
|
162
|
-
import type {StreamOperationResponse} from "./operations${suffix}"
|
|
181
|
+
import type {StreamOperationResponse, StreamCallOptions} from "./operations${suffix}";
|
|
182
|
+
export type {StreamCallOptions} from "./operations${suffix}";` : ""}
|
|
163
183
|
|
|
164
184
|
/**
|
|
165
185
|
* Represents the response structure for ${clientName} operations.
|
|
@@ -214,6 +234,8 @@ export class ${clientName} {
|
|
|
214
234
|
protected attributesKeyOut: string;
|
|
215
235
|
protected client?: ${soapClient.local};
|
|
216
236
|
protected dataTypes: ${clientName}DataTypes = ${clientConstant}_DATA_TYPES;
|
|
237
|
+
protected scalarTypes: Readonly<Record<string, string>> = ${JSON.stringify(scalarTypes)};
|
|
238
|
+
${anyStream ? ` protected streamRequestOptions: ${soapExOptions.local};\n` : ""}
|
|
217
239
|
|
|
218
240
|
/**
|
|
219
241
|
* Creates a new ${clientName} instance.
|
|
@@ -224,17 +246,20 @@ export class ${clientName} {
|
|
|
224
246
|
* @param [options.security] - Optional SOAP security settings to apply.
|
|
225
247
|
* @param [options.attributesKeyIn] - Key name for input attribute bags (default: "$attributes").
|
|
226
248
|
* @param [options.attributesKeyOut] - Key name for output attribute bags (default: "attributes").
|
|
227
|
-
*/
|
|
249
|
+
${anyStream ? ` * @param [options.streamRequestOptions] - Stream HTTP options such as verified HTTPS agents, separate from WSDL options.\n` : ""} */
|
|
228
250
|
constructor(options: {
|
|
229
251
|
source: string,
|
|
230
252
|
options?: ${soapOptions.local},
|
|
231
253
|
security?: ${soapSecurity.local},
|
|
232
254
|
attributesKeyIn?: string,
|
|
233
|
-
attributesKeyOut?: string
|
|
255
|
+
attributesKeyOut?: string${anyStream ? `,
|
|
256
|
+
/** HTTP request options for stream calls, including HTTPS agents and timeouts; WSDL options stay separate. */
|
|
257
|
+
streamRequestOptions?: ${soapExOptions.local}` : ""}
|
|
234
258
|
}) {
|
|
235
259
|
this.source = options.source;
|
|
236
260
|
this.attributesKeyIn = options.attributesKeyIn ?? "$attributes";
|
|
237
261
|
this.attributesKeyOut = options.attributesKeyOut ?? "attributes";
|
|
262
|
+
${anyStream ? ` this.streamRequestOptions = options.streamRequestOptions ?? {};\n` : ""}
|
|
238
263
|
if (options.options) {
|
|
239
264
|
this.options = options.options;
|
|
240
265
|
}
|
|
@@ -402,13 +427,28 @@ ${methodsBody}
|
|
|
402
427
|
*
|
|
403
428
|
* @param node - The raw SOAP response node.
|
|
404
429
|
* @param typeName - Optional metadata key to guide deserialization.
|
|
430
|
+
* @param nonNillable - Whether parent schema metadata rules out a nil child, permitting empty-object recovery.
|
|
405
431
|
* @returns The deserialized object matching your DTO shape.
|
|
432
|
+
* @throws TypeError for invalid boolean, numeric, or bigint lexical values.
|
|
406
433
|
*/
|
|
407
|
-
protected fromSoapResult(node: any, typeName?: string): any {
|
|
408
|
-
//
|
|
434
|
+
protected fromSoapResult(node: any, typeName?: string, nonNillable = false): any {
|
|
435
|
+
// @why node-soap collapses empty complex elements to null; only a declared non-nillable child permits recovering its object DTO.
|
|
436
|
+
// @constraints Nullable children and unknown/custom metadata retain null, including explicit xsi:nil responses.
|
|
437
|
+
if (node === null && nonNillable && typeName && Object.hasOwn(this.dataTypes.Attributes, typeName)
|
|
438
|
+
&& Object.keys(this.dataTypes.ChildrenTypes[typeName] ?? {}).length === 0) return {};
|
|
409
439
|
if (node == null) {
|
|
410
440
|
return node;
|
|
411
441
|
}
|
|
442
|
+
if (typeName && Object.hasOwn(this.scalarTypes, typeName)) typeName = this.scalarTypes[typeName];
|
|
443
|
+
if (typeName?.endsWith("[]")) {
|
|
444
|
+
let itemType = typeName.slice(0, -2);
|
|
445
|
+
if (itemType.startsWith("(") && itemType.endsWith(")")) itemType = itemType.slice(1, -1);
|
|
446
|
+
if (typeof node === "string") {
|
|
447
|
+
const lexical = node.replace(/^[\\t\\r\\n ]+|[\\t\\r\\n ]+$/g, "");
|
|
448
|
+
return lexical === "" ? [] : lexical.split(/[\\t\\r\\n ]+/).map(item => this.fromSoapResult(item, itemType));
|
|
449
|
+
}
|
|
450
|
+
if (Array.isArray(node)) return node.map(item => this.fromSoapResult(item, itemType));
|
|
451
|
+
}
|
|
412
452
|
const childrenTypes: Readonly<Record<string, string>> = (typeName && this.dataTypes?.ChildrenTypes?.[typeName]) || {};
|
|
413
453
|
const attributeTypes: Readonly<Record<string, string>> = (typeName && this.dataTypes?.AttributeTypes?.[typeName]) || {};
|
|
414
454
|
// XML boolean attributes arrive as lexical strings even when element booleans are parsed by node-soap.
|
|
@@ -418,8 +458,22 @@ ${methodsBody}
|
|
|
418
458
|
if (lexical === "false" || lexical === "0") return false;
|
|
419
459
|
throw new TypeError("Invalid XML boolean response value");
|
|
420
460
|
}
|
|
461
|
+
if (typeof node === "string" && (typeName === "number" || typeName === "bigint")) {
|
|
462
|
+
const lexical = node.replace(/^[\\t\\r\\n ]+|[\\t\\r\\n ]+$/g, "");
|
|
463
|
+
if (typeName === "bigint") {
|
|
464
|
+
if (!/^[+-]?\\d+$/.test(lexical)) throw new TypeError("Invalid XML bigint response value");
|
|
465
|
+
return BigInt(lexical);
|
|
466
|
+
}
|
|
467
|
+
if (!/^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?$/.test(lexical)) {
|
|
468
|
+
throw new TypeError("Invalid XML numeric response value");
|
|
469
|
+
}
|
|
470
|
+
const number = Number(lexical);
|
|
471
|
+
if (!Number.isFinite(number)) throw new TypeError("Non-finite XML numeric response value");
|
|
472
|
+
return number;
|
|
473
|
+
}
|
|
421
474
|
// A simple-content complex type retains its object shape when no attributes are present on the wire.
|
|
422
475
|
if (typeof node !== "object") {
|
|
476
|
+
if (node === "" && typeName && Object.hasOwn(this.dataTypes.Attributes, typeName) && Object.keys(childrenTypes).length === 0) return {};
|
|
423
477
|
return childrenTypes.$value ? {$value: this.fromSoapResult(node, childrenTypes.$value)} : node;
|
|
424
478
|
}
|
|
425
479
|
// Recursively process array elements
|
|
@@ -429,11 +483,12 @@ ${methodsBody}
|
|
|
429
483
|
|
|
430
484
|
// Get child type mapping for recursive processing with correct types
|
|
431
485
|
const repeatedElements: readonly string[] = (typeName && this.dataTypes?.RepeatedElements?.[typeName]) || [];
|
|
486
|
+
const nonNillableElements: readonly string[] = (typeName && this.dataTypes?.NonNillableElements?.[typeName]) || [];
|
|
432
487
|
const result: any = {};
|
|
433
488
|
|
|
434
489
|
// Preserve text content for mixed XML elements
|
|
435
|
-
if ("$value" in node) {
|
|
436
|
-
result.$value = (node
|
|
490
|
+
if ("$value" in node && (childrenTypes.$value || node.$value !== "")) {
|
|
491
|
+
result.$value = this.fromSoapResult(node.$value, childrenTypes.$value);
|
|
437
492
|
}
|
|
438
493
|
|
|
439
494
|
// Extract attributes from various node-soap attribute containers
|
|
@@ -455,10 +510,11 @@ ${methodsBody}
|
|
|
455
510
|
// Recursively convert child elements with their specific type info
|
|
456
511
|
const childType: string | undefined = childrenTypes[k];
|
|
457
512
|
// node-soap can return a scalar when repetition is declared on a wrapping sequence (#141).
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
513
|
+
const repeated = repeatedElements.includes(k);
|
|
514
|
+
const value = v != null && !Array.isArray(v) && repeated ? [v] : v;
|
|
515
|
+
result[k] = repeated && Array.isArray(value)
|
|
516
|
+
? value.map(node => this.fromSoapResult(node, childType, nonNillableElements.includes(k)))
|
|
517
|
+
: this.fromSoapResult(value, childType, nonNillableElements.includes(k));
|
|
462
518
|
}
|
|
463
519
|
|
|
464
520
|
return result;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Generates a fully-typed operations interface for the SOAP client.
|
|
5
5
|
* This standalone interface enables mocking and testing without importing
|
|
6
6
|
* the concrete SOAP client class or its runtime dependencies.
|
|
7
|
+
* @since Unreleased
|
|
7
8
|
*/
|
|
8
9
|
import { writeGeneratedSource } from "../generation/writeGeneratedSource.js";
|
|
9
10
|
import { deriveClientName, pascal } from "../util/tools.js";
|
|
@@ -31,18 +32,25 @@ export function generateOperations(outFile, compiled) {
|
|
|
31
32
|
.map(line => line.replace(/\*\//g, "*\\/"));
|
|
32
33
|
// Collect type names used in method signatures for the import statement
|
|
33
34
|
const importedTypes = new Set();
|
|
35
|
+
const operationTypes = compiled.operations.map(operation => ({ operation,
|
|
36
|
+
input: operation.inputTypeName ?? (operation.inputElement ? pascal(operation.inputElement.local) : undefined),
|
|
37
|
+
output: operation.outputTypeName ?? (operation.outputElement ? pascal(operation.outputElement.local) : undefined),
|
|
38
|
+
}));
|
|
39
|
+
const reservedTypes = new Set(["Promise", "AsyncIterable", "AbortSignal", "StreamCallOptions", "StreamOperationResponse", `${clientName}Operations`]);
|
|
40
|
+
// @constraints Qualify model references when their public names overlap generated helpers or native transport types.
|
|
41
|
+
const namespaceModels = operationTypes.some(({ operation, input, output }) => [input, output, operation.stream?.recordTypeName].some(name => name !== undefined && reservedTypes.has(name)));
|
|
42
|
+
const modelType = (name) => namespaceModels ? `T.${name}` : name;
|
|
34
43
|
const methods = [];
|
|
35
44
|
// Does any operation opt into streaming? If so we also emit the
|
|
36
45
|
// StreamOperationResponse helper type below.
|
|
37
46
|
let anyStream = false;
|
|
38
|
-
for (const op of
|
|
39
|
-
const inTypeName = op.inputTypeName ?? (op.inputElement ? pascal(op.inputElement.local) : undefined);
|
|
40
|
-
const outTypeName = op.outputTypeName ?? (op.outputElement ? pascal(op.outputElement.local) : undefined);
|
|
47
|
+
for (const { operation: op, input: inTypeName, output: outTypeName } of operationTypes) {
|
|
41
48
|
if (!inTypeName && !outTypeName) {
|
|
42
49
|
continue;
|
|
43
50
|
}
|
|
44
|
-
|
|
45
|
-
const
|
|
51
|
+
// @constraints WSDL DTOs may be named Record and shadow the built-in utility type.
|
|
52
|
+
const inTs = inTypeName ? modelType(inTypeName) : "{[key: string]: unknown}";
|
|
53
|
+
const outTs = outTypeName ? modelType(outTypeName) : "unknown";
|
|
46
54
|
if (inTypeName)
|
|
47
55
|
importedTypes.add(inTypeName);
|
|
48
56
|
if (outTypeName)
|
|
@@ -56,8 +64,9 @@ export function generateOperations(outFile, compiled) {
|
|
|
56
64
|
const recordTs = op.stream.recordTypeName;
|
|
57
65
|
importedTypes.add(recordTs);
|
|
58
66
|
methods.push(`${docBlock} ${op.name}(\n` +
|
|
59
|
-
` args: ${inTs}
|
|
60
|
-
`
|
|
67
|
+
` args: ${inTs},\n` +
|
|
68
|
+
` options?: StreamCallOptions\n` +
|
|
69
|
+
` ): Promise<StreamOperationResponse<${modelType(recordTs)}>>;\n`);
|
|
61
70
|
}
|
|
62
71
|
else {
|
|
63
72
|
methods.push(`${docBlock} ${op.name}(\n` +
|
|
@@ -68,16 +77,26 @@ export function generateOperations(outFile, compiled) {
|
|
|
68
77
|
// Build sorted import list for deterministic output
|
|
69
78
|
const sortedImports = Array.from(importedTypes).sort();
|
|
70
79
|
const typeImport = sortedImports.length > 0
|
|
71
|
-
?
|
|
80
|
+
? namespaceModels
|
|
81
|
+
? `import type * as T from "./types${suffix}";\n\n`
|
|
82
|
+
: `import type {\n${sortedImports.map((t) => ` ${t},`).join("\n")}\n} from "./types${suffix}";\n\n`
|
|
72
83
|
: "";
|
|
73
84
|
// Emit the StreamOperationResponse helper type iff at least one operation
|
|
74
85
|
// is stream-configured. Kept inside operations.ts so that mocks and gateway
|
|
75
86
|
// code can import it without pulling in the SOAP runtime. The raw template
|
|
76
87
|
// lives in a sibling .tpl file so the IDE does not try to parse embedded
|
|
77
|
-
// TypeScript
|
|
78
|
-
// JavaScript assignment expressions.
|
|
88
|
+
// TypeScript generic defaults as JavaScript assignment expressions.
|
|
79
89
|
const streamHelper = anyStream
|
|
80
|
-
?
|
|
90
|
+
? `/**
|
|
91
|
+
* Per-call stream cancellation and total request/response deadline.
|
|
92
|
+
* @since Unreleased
|
|
93
|
+
*/
|
|
94
|
+
export interface StreamCallOptions {
|
|
95
|
+
signal?: AbortSignal;
|
|
96
|
+
timeoutMs?: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
` + loadRuntimeSource("operationsStreamHelper.tpl.txt").replace(/__CLIENT_NAME__/g, clientName)
|
|
81
100
|
: "";
|
|
82
101
|
const content = `/**
|
|
83
102
|
* Typed operations interface for the ${clientName} service.
|
|
@@ -48,7 +48,9 @@ export function generateUtils(outFile, compiled) {
|
|
|
48
48
|
return max === "unbounded" || (typeof max === "number" && max > 1);
|
|
49
49
|
}).sort(),
|
|
50
50
|
]));
|
|
51
|
-
const metas = JSON.stringify({ Attributes: attrSpec, AttributeTypes: compiled.meta.attrType ?? {}, ChildrenTypes: childType, RepeatedElements: repeatedElements
|
|
51
|
+
const metas = JSON.stringify({ Attributes: attrSpec, AttributeTypes: compiled.meta.attrType ?? {}, ChildrenTypes: childType, RepeatedElements: repeatedElements,
|
|
52
|
+
NonNillableElements: Object.fromEntries(Object.entries(compiled.meta.propMeta ?? {}).sort(([a], [b]) => a.localeCompare(b))
|
|
53
|
+
.map(([name, properties]) => [name, Object.keys(properties).filter(key => properties[key].nillable === false).sort()])) }, null, 2);
|
|
52
54
|
const dataTypes = `/**
|
|
53
55
|
* ${clientName} WSDL data types for proper serialization/deserialization.
|
|
54
56
|
* Used to distinguish between XML attributes and elements during conversion.
|
|
@@ -62,6 +64,8 @@ export interface ${clientName}DataTypes {
|
|
|
62
64
|
ChildrenTypes: Record<string, Readonly<Record<string, string>>>;
|
|
63
65
|
/** Maps type names to repeated child properties; optional for custom metadata compatibility. */
|
|
64
66
|
RepeatedElements?: Record<string, readonly string[]>;
|
|
67
|
+
/** Distinguishes empty complex nodes collapsed to null by node-soap from declared nullable children. */
|
|
68
|
+
NonNillableElements?: Record<string, readonly string[]>;
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
export const ${clientConstant}_DATA_TYPES: ${clientName}DataTypes = ${metas} as const;\n`;
|
|
@@ -1,19 +1,11 @@
|
|
|
1
|
+
/** Internal, language-neutral S04 contract. S05 resolves and composes these references. */
|
|
2
|
+
import type { Identity, NodeId, SymbolRole } from "./declarationIdentity.js";
|
|
3
|
+
export { globalId, scopedId } from "./declarationIdentity.js";
|
|
4
|
+
export type { Identity, NodeId, SymbolRole } from "./declarationIdentity.js";
|
|
1
5
|
import type { ExpandedName, SyntaxAttribute, SyntaxElement, SyntaxSource } from "../loader/orderedSyntax.js";
|
|
2
6
|
export declare const GRAPH_MODEL: "xsd10-faithful-v1";
|
|
3
7
|
export declare const DEFAULT_GRAPH_NODES = 100000;
|
|
4
8
|
export declare const isBuiltinType: (local: string) => boolean;
|
|
5
|
-
export type NodeId = string;
|
|
6
|
-
export type SymbolRole = "element" | "attribute" | "type" | "group" | "attributeGroup" | "message" | "portType" | "binding" | "service";
|
|
7
|
-
export type Identity = Readonly<{
|
|
8
|
-
kind: "global";
|
|
9
|
-
role: SymbolRole;
|
|
10
|
-
name: ExpandedName;
|
|
11
|
-
} | {
|
|
12
|
-
kind: "scoped";
|
|
13
|
-
owner: NodeId;
|
|
14
|
-
path: string;
|
|
15
|
-
role: string;
|
|
16
|
-
}>;
|
|
17
9
|
export type GraphContext = Readonly<{
|
|
18
10
|
namespaces: Readonly<Record<string, string>>;
|
|
19
11
|
baseUri: string;
|
|
@@ -190,8 +182,5 @@ export declare class GraphError extends Error {
|
|
|
190
182
|
readonly source?: SyntaxSource | undefined;
|
|
191
183
|
constructor(category: "invalid-schema" | "resource-limit", message: string, source?: SyntaxSource | undefined);
|
|
192
184
|
}
|
|
193
|
-
export declare function globalId(role: SymbolRole, name: ExpandedName): NodeId;
|
|
194
|
-
export declare function scopedId(owner: NodeId, path: string, role: string): NodeId;
|
|
195
185
|
export declare function wsdlReferenceRoles(node: SyntaxElement): Record<string, SymbolRole>;
|
|
196
186
|
export declare function deepFreeze<T>(value: T): T;
|
|
197
|
-
export {};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
export { globalId, scopedId } from "./declarationIdentity.js";
|
|
3
2
|
import { WSDL_NAMESPACE } from "../loader/orderedSyntax.js";
|
|
4
3
|
export const GRAPH_MODEL = "xsd10-faithful-v1";
|
|
5
4
|
export const DEFAULT_GRAPH_NODES = 100_000;
|
|
@@ -15,12 +14,6 @@ export class GraphError extends Error {
|
|
|
15
14
|
this.name = "GraphError";
|
|
16
15
|
}
|
|
17
16
|
}
|
|
18
|
-
export function globalId(role, name) {
|
|
19
|
-
return JSON.stringify(["global", role, name.namespace, name.local]);
|
|
20
|
-
}
|
|
21
|
-
export function scopedId(owner, path, role) {
|
|
22
|
-
return "scoped:" + createHash("sha256").update(JSON.stringify([owner, path, role])).digest("hex");
|
|
23
|
-
}
|
|
24
17
|
export function wsdlReferenceRoles(node) {
|
|
25
18
|
if (node.name.namespace !== WSDL_NAMESPACE)
|
|
26
19
|
return {};
|
|
@@ -101,6 +101,9 @@ export declare function decodeComponentRecordXml(outer: DirectOperationContract,
|
|
|
101
101
|
signal?: AbortSignal;
|
|
102
102
|
limits?: DirectXmlLimits;
|
|
103
103
|
uri?: string;
|
|
104
|
+
componentResponseEncoding?: {
|
|
105
|
+
charset?: string;
|
|
106
|
+
};
|
|
104
107
|
selectPayload?: (root: SyntaxElement) => {
|
|
105
108
|
payload: SyntaxElement;
|
|
106
109
|
mapping: ComponentRecordProjection | ComponentValueMapping;
|
|
@@ -46,6 +46,8 @@ export function createComponentRecordProjection(outer, companion, supplied, opti
|
|
|
46
46
|
try {
|
|
47
47
|
const result = budget.guard(() => {
|
|
48
48
|
check();
|
|
49
|
+
if ('parts' in outer.output || 'parts' in companion.output)
|
|
50
|
+
return failure('unsupported', 'record-projection-requires-document-output');
|
|
49
51
|
if (outer.basis !== 'native-component-premises-v1' || companion.basis !== 'native-component-premises-v1'
|
|
50
52
|
|| outer.correspondence !== 'dated-XSD-2004-components-v1' || companion.correspondence !== 'dated-XSD-2004-components-v1')
|
|
51
53
|
stop('component-record-source-authority');
|
|
@@ -75,6 +77,8 @@ export function createComponentRecordProjection(outer, companion, supplied, opti
|
|
|
75
77
|
if (mapping.kind !== 'component-mapping')
|
|
76
78
|
return mapping;
|
|
77
79
|
const projection = inspectComponentValueMapping(mapping, companion).projection;
|
|
80
|
+
if (!projection)
|
|
81
|
+
return failure('unsupported', 'record-projection-requires-document-mapping');
|
|
78
82
|
const records = new Map(projection.records.map(record => [record.type.id, record]));
|
|
79
83
|
if (association && !Object.hasOwn(requested, 'recordPath')) {
|
|
80
84
|
const candidates = [];
|
|
@@ -226,6 +230,8 @@ export function inspectComponentRecordValue(value, projection) {
|
|
|
226
230
|
*/
|
|
227
231
|
export function validateComponentRecordApplication(projection, application, options = {}) {
|
|
228
232
|
const owned = inspectComponentRecordProjection(projection), { context } = inspectDirectOperation(owned.companion);
|
|
233
|
+
if ('parts' in owned.mapping.root)
|
|
234
|
+
throw new TypeError('Record projection requires a document mapping');
|
|
229
235
|
return expandProjectionValue(context, owned.mapping.root.element, application, options);
|
|
230
236
|
}
|
|
231
237
|
/** Decode once and validate the declared wrapper plus its explicitly selected companion payload.
|
|
@@ -253,6 +259,8 @@ export function decodeComponentRecordXml(outer, projection, bytes, options = {})
|
|
|
253
259
|
return assessObservedComponentPayload(outer, mapping, observation, selected.payload, options);
|
|
254
260
|
}
|
|
255
261
|
const wrapper = selected.payload;
|
|
262
|
+
if ('parts' in outer.output)
|
|
263
|
+
throw new ScopeStop('unsupported', 'record-projection-requires-document-output');
|
|
256
264
|
if (!same(wrapper.name, outer.output.element.name))
|
|
257
265
|
throw new ScopeStop('mismatch', 'record-wrapper-root');
|
|
258
266
|
if (wrapper.attributes.length || wrapper.children.some(node => node.kind === 'text' && /[^\t\r\n ]/.test(node.value)))
|
|
@@ -284,6 +292,8 @@ export function decodeComponentRecordXml(outer, projection, bytes, options = {})
|
|
|
284
292
|
const decoded = assessObservedComponentPayload(owned.companion, owned.mapping, observation, payload, options);
|
|
285
293
|
if (decoded.kind !== 'component-decoded')
|
|
286
294
|
return decoded;
|
|
295
|
+
if (decoded.validated.kind !== 'projection-value')
|
|
296
|
+
throw new TypeError('Record projection requires a document value receipt');
|
|
287
297
|
const recordCount = countRecords(observation, payload, projection.spec.recordPath, check);
|
|
288
298
|
const validated = Object.freeze({ kind: 'component-record-value', scope: 'records-with-declared-wrapper-scope',
|
|
289
299
|
admitted: false, value: decoded.validated.value, recordCount, companion: decoded.validated });
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type NodeId = string;
|
|
2
|
+
export type SymbolRole = "element" | "attribute" | "type" | "group" | "attributeGroup" | "message" | "portType" | "binding" | "service";
|
|
3
|
+
export type Identity = Readonly<{
|
|
4
|
+
kind: "global";
|
|
5
|
+
role: SymbolRole;
|
|
6
|
+
name: Readonly<{
|
|
7
|
+
namespace: string;
|
|
8
|
+
local: string;
|
|
9
|
+
}>;
|
|
10
|
+
} | {
|
|
11
|
+
kind: "scoped";
|
|
12
|
+
owner: NodeId;
|
|
13
|
+
path: string;
|
|
14
|
+
role: string;
|
|
15
|
+
}>;
|
|
16
|
+
/** Encode XML symbol identity independently of source location and emitted language names. */
|
|
17
|
+
export declare function globalId(role: SymbolRole, name: Readonly<{
|
|
18
|
+
namespace: string;
|
|
19
|
+
local: string;
|
|
20
|
+
}>): NodeId;
|
|
21
|
+
/** Encode a declaration within its owner's scope using the caller's structural path convention. */
|
|
22
|
+
export declare function scopedId(owner: NodeId, path: string, role: string): NodeId;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/** Encode XML symbol identity independently of source location and emitted language names. */
|
|
3
|
+
export function globalId(role, name) {
|
|
4
|
+
return JSON.stringify(["global", role, name.namespace, name.local]);
|
|
5
|
+
}
|
|
6
|
+
/** Encode a declaration within its owner's scope using the caller's structural path convention. */
|
|
7
|
+
export function scopedId(owner, path, role) {
|
|
8
|
+
return "scoped:" + createHash("sha256").update(JSON.stringify([owner, path, role])).digest("hex");
|
|
9
|
+
}
|