@techspokes/typescript-wsdl-client 1.4.0 → 1.4.1

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 CHANGED
@@ -258,6 +258,7 @@ See [CLI Reference](docs/cli-reference.md) for all flags and examples.
258
258
  | [Agent Skill Artifact](docs/agent-skill.md) | Release ZIP for consumer-project AI agents |
259
259
  | [Version Migration](docs/migration.md) | Upgrading between package versions |
260
260
  | [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 |
261
+ | [1.4.1 Release Notes](https://github.com/TechSpokes/typescript-wsdl-client/blob/main/docs/releases/v1.4.1.md) | Composable checked OpenAPI documentation |
261
262
  | [1.4.0 Release Notes](https://github.com/TechSpokes/typescript-wsdl-client/blob/main/docs/releases/v1.4.0.md) | Contract corrections, offline inspection and SOAP 1.1 protocol faults |
262
263
  | [1.3.0 Release Notes](https://github.com/TechSpokes/typescript-wsdl-client/blob/main/docs/releases/v1.3.0.md) | Checked SOAP 1.2 execution and preserved SOAP 1.1 compatibility |
263
264
  | [XML/JSON Mapping Contract](docs/mapping-contract.md) | Checked keyed, namespace, nil and group representations |
@@ -0,0 +1,14 @@
1
+ import type { DirectOperationSelector } from '../compiler/directOperation.js';
2
+ type Schema = Readonly<Record<string, unknown>>;
3
+ /** Lower one issued shape while its original reference scope is still available.
4
+ * @param root Generator-owned shape, with at most one root resource and definition dictionary.
5
+ * @param selector Outer operation selector, independent of companion projection contracts.
6
+ * @param role Direction or declared-fault role owning the shape.
7
+ * @param grouped Whether published configured definition names must be preserved.
8
+ * @param components Destination dictionary shared by the operation document.
9
+ * @param check Generation cancellation and semantic resource-accounting checkpoint.
10
+ * @returns A new documentation schema; runtime schemas and literal annotations remain unchanged.
11
+ * @throws TypeError For unexpected resources, references, missing descendants or component collisions.
12
+ */
13
+ export declare function lowerCheckedDocumentation(root: Schema, selector: DirectOperationSelector, role: string, grouped: boolean, components: Record<string, Schema>, check: () => void): Schema;
14
+ export {};
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Relocates generator-owned shape resources into OpenAPI documentation components.
3
+ * @since 1.4.1
4
+ * @constraints This is not a general JSON Schema resource bundler.
5
+ */
6
+ import { createHash } from 'node:crypto';
7
+ const maps = new Set(['properties', 'patternProperties', 'dependentSchemas']);
8
+ const singles = new Set(['additionalProperties', 'additionalItems', 'contains', 'not', 'if', 'then', 'else', 'propertyNames', 'unevaluatedProperties', 'unevaluatedItems']);
9
+ const arrays = new Set(['allOf', 'anyOf', 'oneOf', 'prefixItems']);
10
+ const escape = (value) => encodeURIComponent(value.replace(/~/g, '~0').replace(/\//g, '~1'));
11
+ /** Lower one issued shape while its original reference scope is still available.
12
+ * @param root Generator-owned shape, with at most one root resource and definition dictionary.
13
+ * @param selector Outer operation selector, independent of companion projection contracts.
14
+ * @param role Direction or declared-fault role owning the shape.
15
+ * @param grouped Whether published configured definition names must be preserved.
16
+ * @param components Destination dictionary shared by the operation document.
17
+ * @param check Generation cancellation and semantic resource-accounting checkpoint.
18
+ * @returns A new documentation schema; runtime schemas and literal annotations remain unchanged.
19
+ * @throws TypeError For unexpected resources, references, missing descendants or component collisions.
20
+ */
21
+ export function lowerCheckedDocumentation(root, selector, role, grouped, components, check) {
22
+ check();
23
+ if (root.$id !== undefined && (typeof root.$id !== 'string' || !/^urn:checked-component:shape:[a-f0-9]{64}$/.test(root.$id))) {
24
+ throw new TypeError('Unexpected checked documentation resource');
25
+ }
26
+ const definitions = (root.definitions ?? {});
27
+ // @why #467 Original resource IDs encode selection, not contract equivalence or component ownership.
28
+ const owner = createHash('sha256').update(JSON.stringify([selector.portType.namespace, selector.portType.local,
29
+ selector.binding.namespace, selector.binding.local, selector.operation, role])).digest('hex');
30
+ const names = new Map(Object.keys(definitions).sort().map(name => [name, grouped ? name : `Checked_${owner}_${name}`]));
31
+ const reference = (value) => {
32
+ if (typeof value !== 'string' || !value.startsWith('#/definitions/'))
33
+ throw new TypeError('Unexpected checked documentation reference');
34
+ let pointer;
35
+ try {
36
+ pointer = decodeURIComponent(value.slice(1));
37
+ }
38
+ catch {
39
+ throw new TypeError('Invalid checked documentation pointer');
40
+ }
41
+ const parts = pointer.slice(1).split('/').map(part => {
42
+ if (/~(?![01])/.test(part))
43
+ throw new TypeError('Invalid checked documentation pointer');
44
+ return part.replace(/~1/g, '/').replace(/~0/g, '~');
45
+ });
46
+ const name = names.get(parts[1]);
47
+ if (!name)
48
+ throw new TypeError('Unresolved checked documentation definition');
49
+ let target = root;
50
+ for (const part of parts) {
51
+ check();
52
+ if (!target || typeof target !== 'object' || !Object.hasOwn(target, part))
53
+ throw new TypeError('Unresolved checked documentation descendant');
54
+ target = target[part];
55
+ }
56
+ if (typeof target !== 'boolean' && (!target || typeof target !== 'object' || Array.isArray(target))) {
57
+ throw new TypeError('Checked documentation reference must target a schema');
58
+ }
59
+ return '#/components/schemas/' + escape(name) + parts.slice(2).map(part => '/' + escape(part)).join('');
60
+ };
61
+ const visit = (value, isRoot = false) => {
62
+ check();
63
+ if (typeof value === 'boolean')
64
+ return value;
65
+ if (!value || typeof value !== 'object' || Array.isArray(value))
66
+ throw new TypeError('Unexpected checked documentation schema');
67
+ const record = value, result = Object.create(null);
68
+ for (const [key, item] of Object.entries(record)) {
69
+ check();
70
+ if (key === '$id' && isRoot)
71
+ continue;
72
+ if (key === 'definitions' && isRoot)
73
+ continue;
74
+ if (['$id', '$anchor', '$dynamicAnchor', '$dynamicRef', '$recursiveRef', '$recursiveAnchor', '$defs', 'definitions'].includes(key)) {
75
+ throw new TypeError('Unexpected checked documentation resource');
76
+ }
77
+ if (key === '$ref')
78
+ result[key] = reference(item);
79
+ else if (maps.has(key) || key === 'dependencies') {
80
+ const dictionary = Object.create(null);
81
+ for (const [name, child] of Object.entries(item)) {
82
+ check();
83
+ dictionary[name] = key === 'dependencies' && Array.isArray(child) ? child : visit(child);
84
+ }
85
+ result[key] = dictionary;
86
+ }
87
+ else if (arrays.has(key) || key === 'items' && Array.isArray(item))
88
+ result[key] = item.map(child => visit(child));
89
+ else if (singles.has(key) || key === 'items')
90
+ result[key] = visit(item);
91
+ // @constraints Unknown annotations are literal data, including keyword-shaped application examples.
92
+ else
93
+ result[key] = item;
94
+ }
95
+ return result;
96
+ };
97
+ const lowered = visit(root, true);
98
+ for (const [original, name] of names) {
99
+ const resolved = visit(definitions[original]);
100
+ if (Object.hasOwn(components, name) && JSON.stringify(components[name]) !== JSON.stringify(resolved)) {
101
+ throw new TypeError(`applicationGroups OpenAPI component collision ${name}`);
102
+ }
103
+ components[name] = resolved;
104
+ }
105
+ return lowered;
106
+ }
@@ -7,6 +7,7 @@ import { mkdir, writeFile } from 'node:fs/promises';
7
7
  import path from 'node:path';
8
8
  import { componentSourceCapsuleBytes, inspectReconstructedComponentSource, inspectComponentSourceService } from '../compiler/directSourceCapsule.js';
9
9
  import { lowerComponentShape } from '../generation/componentShape.js';
10
+ import { lowerCheckedDocumentation } from './checkedDocumentation.js';
10
11
  import { checkSemanticBudget } from '../compiler/semanticBudget.js';
11
12
  import { inspectDirectOperation } from '../compiler/directOperation.js';
12
13
  import { inspectComponentValueMapping } from '../compiler/directValues.js';
@@ -31,15 +32,21 @@ async function generateGatewayFromReconstruction(expected, options) {
31
32
  const output = expected.mappings.find(mapping => mapping.direction.kind === 'output');
32
33
  if (!input || !output)
33
34
  throw new TypeError('Issued input and output mappings required');
34
- const schema = (mapping, role) => {
35
- const { budget } = inspectDirectOperation(expected.contract).context;
36
- return lowerComponentShape(expected.contract, mapping, role, () => {
37
- checkSemanticBudget(budget);
38
- if (options.signal?.aborted)
39
- throw new Error('Checked gateway generation cancelled');
40
- }).schema;
35
+ const { budget } = inspectDirectOperation(expected.contract).context;
36
+ const check = () => {
37
+ checkSemanticBudget(budget);
38
+ if (options.signal?.aborted)
39
+ throw new Error('Checked gateway generation cancelled');
41
40
  };
41
+ const schema = (mapping, role) => lowerComponentShape(expected.contract, mapping, role, check).schema;
42
+ const components = Object.create(null);
43
+ const documented = (value, mapping, role) => lowerCheckedDocumentation(value, expected.contract.selector, role, mapping.kind === 'component-mapping'
44
+ && !!inspectComponentValueMapping(mapping, expected.contract).applicationGroups, components, () => {
45
+ budget.chargeWork();
46
+ check();
47
+ });
42
48
  const request = schema(input, 'Request'), response = schema(output, 'Response');
49
+ const documentation = { request: documented(request, input, 'Request'), response: documented(response, output, 'Response') };
43
50
  const soap12 = expected.contract.binding.version === '1.2';
44
51
  const nameSchema = { type: 'object', additionalProperties: false, required: ['namespace', 'local'],
45
52
  properties: { namespace: { type: 'string' }, local: { type: 'string' } } };
@@ -53,12 +60,14 @@ async function generateGatewayFromReconstruction(expected, options) {
53
60
  const faults = expected.mappings.filter(mapping => mapping.direction.kind === 'fault').map(mapping => {
54
61
  if (mapping.direction.kind !== 'fault')
55
62
  throw new TypeError('Declared fault mapping required');
56
- return { name: mapping.direction.name, value: schema(mapping, `Fault_${expected.mappings.filter(item => item.direction.kind === 'fault').indexOf(mapping)}`) };
63
+ const role = `Fault_${expected.mappings.filter(item => item.direction.kind === 'fault').indexOf(mapping)}`;
64
+ const value = schema(mapping, role);
65
+ return { name: mapping.direction.name, value, documented: documented(value, mapping, role) };
57
66
  });
58
- const faultVariants = faults.map(fault => ({
67
+ const faultVariants = (documentationOnly) => faults.map(fault => ({
59
68
  type: 'object', additionalProperties: false, required: ['fault', 'value'],
60
69
  properties: {
61
- value: fault.value,
70
+ value: documentationOnly ? fault.documented : fault.value,
62
71
  fault: {
63
72
  type: 'object', additionalProperties: false, required: ['name', 'code', 'string', ...(soap12 ? ['version', 'subcodes', 'reasons'] : [])],
64
73
  properties: soap12 ? { ...protocolFaultProperties, name: { const: fault.name } } : {
@@ -69,48 +78,17 @@ async function generateGatewayFromReconstruction(expected, options) {
69
78
  },
70
79
  },
71
80
  }));
72
- faultVariants.push({ type: 'object', additionalProperties: false, required: ['fault'], properties: {
81
+ const protocolFault = { type: 'object', additionalProperties: false, required: ['fault'], properties: {
73
82
  fault: { type: 'object', additionalProperties: false, required: ['version', 'code', 'string', ...(soap12 ? ['subcodes', 'reasons'] : [])],
74
83
  properties: soap12 ? protocolFaultProperties : { version: { const: '1.1' }, code: nameSchema, string: { type: 'string' }, actor: { type: 'string' } } },
75
- } });
76
- const fault = { oneOf: faultVariants };
84
+ } };
85
+ const fault = { oneOf: [...faultVariants(false), protocolFault] };
86
+ const documentedFault = { oneOf: [...faultVariants(true), protocolFault] };
77
87
  const schemas = { request: { $schema: draft07, ...request }, response: { $schema: draft07, ...response }, fault: { $schema: draft07, ...fault } };
78
- const grouped = expected.mappings.some(mapping => mapping.kind === 'component-mapping'
79
- && inspectComponentValueMapping(mapping, expected.contract).applicationGroups);
80
- const components = Object.create(null);
81
- const documented = (value, dictionary = false) => {
82
- if (Array.isArray(value))
83
- return value.map(item => documented(item));
84
- if (!value || typeof value !== 'object')
85
- return value;
86
- const record = value, result = Object.create(null);
87
- // @why Property names belong to the application; definitions and $id are keywords only at schema nodes.
88
- if (dictionary) {
89
- for (const [key, item] of Object.entries(record))
90
- result[key] = documented(item);
91
- return result;
92
- }
93
- if (Object.hasOwn(record, 'definitions'))
94
- for (const [key, definition] of Object.entries(record.definitions)) {
95
- const resolved = documented(definition);
96
- if (components[key] && JSON.stringify(components[key]) !== JSON.stringify(resolved))
97
- throw new TypeError(`applicationGroups OpenAPI component collision ${key}`);
98
- components[key] = resolved;
99
- }
100
- for (const [key, item] of Object.entries(record)) {
101
- if (key === 'definitions' || key === '$id')
102
- continue;
103
- result[key] = ['const', 'enum', 'default', 'examples'].includes(key) ? item
104
- : key === '$ref' && typeof item === 'string' && item.startsWith('#/definitions/') ? item.replace('#/definitions/', '#/components/schemas/')
105
- : documented(item, ['properties', 'patternProperties', 'dependencies', 'dependentSchemas'].includes(key));
106
- }
107
- return result;
108
- };
109
- const documentation = grouped ? { request: documented(request), response: documented(response), fault: documented(fault) } : { request, response, fault };
110
88
  const doc = {
111
89
  openapi: '3.1.1', jsonSchemaDialect: 'https://json-schema.org/draft/2020-12/schema',
112
90
  info: { title: expected.selection.service.local, version: '1.0.0' },
113
- ...(grouped ? { components: { schemas: components } } : {}),
91
+ ...(Object.keys(components).length ? { components: { schemas: components } } : {}),
114
92
  'x-checked-model': { model: 'checked-components-v1', scope: soap12 ? 'native-component-structured-soap12' : 'native-component-structured-soap11',
115
93
  ...(expected.responseProjection
116
94
  ? { responseProjection: { spec: expected.responseProjection.spec, delivery: 'buffered', retainedValue: 'complete-companion-payload' } } : {}),
@@ -123,7 +101,7 @@ async function generateGatewayFromReconstruction(expected, options) {
123
101
  requestBody: { required: true, content: { 'application/json': { schema: documentation.request } } },
124
102
  responses: { '200': { description: 'Independently validated SOAP response', content: { 'application/json': { schema: documentation.response } } },
125
103
  '400': { description: 'Invalid HTTP request; no SOAP request is dispatched' },
126
- '502': { description: 'SOAP fault or refused upstream response', content: { 'application/json': { schema: { oneOf: [documentation.fault,
104
+ '502': { description: 'SOAP fault or refused upstream response', content: { 'application/json': { schema: { oneOf: [documentedFault,
127
105
  { type: 'object', additionalProperties: false, required: ['kind', 'rule'], properties: { kind: { type: 'string' }, rule: { type: 'string' } } }] } } } },
128
106
  '500': { description: 'Incompatible artifacts or gateway configuration' } } } } },
129
107
  };
@@ -362,6 +362,8 @@ The generated aggregate plugin exports `createClients`, `MultiServiceClientOptio
362
362
 
363
363
  Compose existing OpenAPI 3.1 documents without generating clients or routers:
364
364
 
365
+ Checked documents regenerated with 1.4.1 or later support this API. Regeneration relocates generator-owned local schema resources while preserving configured names and native supplemental-validation metadata. Archived resource-bearing documents remain unsupported; composition does not replace native XML validation.
366
+
365
367
  ```typescript
366
368
  interface OpenApiCompositionEntry {key: string; document: unknown}
367
369
  interface OpenApiCompositionOptions {
@@ -255,6 +255,10 @@ Generated `buildServer` accepts runtime cancellation, timeout and XML limits. Or
255
255
 
256
256
  ## Validation and coverage evidence
257
257
 
258
+ Regenerate with 1.4.1 or later to compose checked OpenAPI documents through `composeOpenApiDocuments`. Documentation definitions are relocated into operation- and role-scoped components for ungrouped shapes; configured grouped names remain unchanged. Runtime draft-07 schemas, capsules and native supplemental validation retain their existing contracts.
259
+
260
+ Composition preserves structural JSON documentation, not complete native XML equivalence. Archived 1.4.0 documents with embedded schema resources still refuse composition; updating the runtime package alone does not repair those documents.
261
+
258
262
  OpenAPI documentation uses 3.1.1 with separate draft-07 HTTP runtime plans. Route-local Ajv disables coercion, default insertion and property removal, and preserves unrelated host policy. The native runtime explicitly converts validated HTTP values through the issued mapping, performs actual SOAP execution and independently validates response/fault XML. HTTP response validation also runs independently of serializer selection; JSON serialization preserves the validated values.
259
263
 
260
264
  `client/inventory.json` and the API result's `checked` field list every discovered portType operation, selected service/port/binding, requested/generated operation and unselected operations. Generation records `executed:false`; it does not claim a SOAP call occurred. Refused selection diagnostics name the requested operation and zero generated/executed operations. Unselected operations remain unassessed.
package/docs/migration.md CHANGED
@@ -6,6 +6,12 @@ Versions are listed newest first. Find your current version and follow the steps
6
6
 
7
7
  See the root [README](../README.md). Historical version sections below describe their original releases; 1.2.0 compatibility guidance supersedes earlier unchanged-output statements for this upgrade.
8
8
 
9
+ ## From 1.4.0 to 1.4.1
10
+
11
+ Regenerate checked specifications to compose them with `composeOpenApiDocuments`. Ungrouped documentation uses operation- and role-scoped components; configured grouped names remain stable. Archived 1.4.0 specifications containing embedded resources remain explicitly refused by composition.
12
+
13
+ Existing runtime artifacts remain usable. Runtime draft-07 validation, capsules and native supplemental validation are unchanged; structural documentation parity does not establish complete native XML equivalence.
14
+
9
15
  ## General Upgrade Steps
10
16
 
11
17
  These steps apply to every version upgrade:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techspokes/typescript-wsdl-client",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
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",