# SOAP Spike Results — Sage X3 WSDL Validation ## Date: 2026-03-10 ## Method - Created a mock WSDL (`mock-x3.wsdl`) replicating X3's `CAdxWebServiceXmlCC` structure - Mock uses RPC/encoded binding style with SOAP 1.1, `soapenc:Array` types, Adonix/WSS namespace - Ran a local mock SOAP server + client using `soap@1.8.0` to validate all 5 questions - All 6 tests passed (5 questions + 1 bonus envelope inspection) ## Validation Questions ### Q1: Can `soap` parse X3's WSDL? **YES** The `soap` library successfully parses a WSDL with: - `style="rpc"` and `use="encoded"` binding - `soapenc:Array` complex types (ArrayOfCAdxMessage, ArrayOfCAdxParamKeyValue, etc.) - Complex nested types (CAdxCallContext, CAdxResultXml) - Multiple operations with different message signatures All 5 operations discovered via `client.describe()`: - `getDescription`, `read`, `query`, `save`, `run` Each operation's input/output types were correctly introspected including the CAdxCallContext parameter structure. ### Q2: Can it construct getDescription with CAdxCallContext? **YES** The generated SOAP envelope correctly contains: ```xml ENG SEED adxwss.optreturn=JSON&adxwss.beautify=true SIH ``` Key observations: - CAdxCallContext fields are correctly nested inside `` - `requestConfig` value is properly XML-escaped (`&`) - RPC namespace `http://www.adonix.com/WSS` is present as `tns` - Uses `tns:getDescription` wrapper element (RPC style, not document/literal) ### Q3: Does adxwss.optreturn=JSON work? **PARTIALLY TESTABLE** What we confirmed: - `resultXml` is always a **string field** in the SOAP response - The `soap` library passes the string value through without parsing it - When the server puts JSON inside `resultXml`, `JSON.parse(resultXml)` works - When the server puts XML inside `resultXml`, we need `fast-xml-parser` What requires a real X3 server to fully validate: - Whether X3 actually returns valid JSON when `adxwss.optreturn=JSON` is set - The exact JSON structure X3 produces (field names, nesting) **Implementation approach**: Always try `JSON.parse(resultXml)` first, fall back to `fast-xml-parser`. ### Q4: What format does resultXml come in? **STRING** Key findings from the spike: | Field | Type returned by soap lib | Notes | |-------|---------------------------|-------| | `resultXml` | `string` | Raw string, NOT parsed. We parse it ourselves. | | `status` | `string` | Returns "1" not `1`. Must `parseInt()`. | | `messages` | `undefined` | Empty arrays come back as undefined. Must default to `[]`. | | `technicalInfos` | `undefined` | Same as messages — default to `[]`. | The raw SOAP response correctly HTML-encodes XML inside ``: ```xml <SINVOICE><FLD NAME="NUM">INV001</FLD></SINVOICE> ``` The `soap` library correctly decodes this back to the original XML string. ### Q5: Does Basic Auth work at HTTP level? **YES** - `client.setSecurity(new soap.BasicAuthSecurity(user, password))` works - Auth header is added at HTTP transport level (not in SOAP body) - CAdxCallContext `codeUser` and `password` should remain empty strings for V12 - The SOAP envelope correctly shows empty `` and `` ## Recommendation: **Use `soap` library** The `soap@1.8.0` library is the right choice for Sage X3 SOAP integration. ### Why `soap` library wins: 1. Correctly parses RPC/encoded WSDL with `soapenc:Array` types 2. Generates proper SOAP 1.1 envelopes with CAdxCallContext 3. Built-in `BasicAuthSecurity` matches X3 V12 auth pattern 4. `client.describe()` enables runtime operation discovery 5. Handles XML entity encoding/decoding in resultXml automatically 6. Promise-based API via `*Async` methods (getDescriptionAsync, readAsync, etc.) ### Still needed: `fast-xml-parser` as companion - Parse `resultXml` content when X3 returns XML (no JSON flag, or flag unsupported) - Fallback for any edge cases where JSON parsing fails ### Caveats discovered: 1. `status` returns as string "1", not number — always `parseInt(status, 10)` 2. Empty arrays (messages, technicalInfos) return as `undefined` — always default to `[]` 3. The soap lib does NOT add `xsi:type` or `encodingStyle` attributes to individual elements — X3 may or may not require these (test with real server) 4. No `soapenc:arrayType` attribute on array elements in the request — monitor for compatibility ## Implementation Notes ### SOAP client creation pattern: ```typescript const client = await soap.createClientAsync(wsdlUrl, {}); client.setSecurity(new soap.BasicAuthSecurity(user, password)); client.setEndpoint(soapEndpoint); ``` ### Calling X3 operations: ```typescript const [response] = await client.readAsync({ callContext: { codeLang: "ENG", codeUser: "", password: "", poolAlias: "SEED", poolId: "", requestConfig: "adxwss.optreturn=JSON", }, publicName: "SIH", objectKeys: [{ key: "NUM", value: "INV001" }], }); const status = parseInt(response.readReturn.status, 10); const resultXml = response.readReturn.resultXml; const data = tryParseJson(resultXml) ?? parseXml(resultXml); const messages = response.readReturn.messages ?? []; ``` ### Response normalization: ```typescript function normalizeResult(raw: any): SoapResult { return { status: parseInt(raw.status, 10), data: tryParseJson(raw.resultXml) ?? parseXml(raw.resultXml), messages: (raw.messages ?? []).map(normalizeMessage), technicalInfos: (raw.technicalInfos ?? []).map(normalizeTechInfo), }; } ``` ## Fallback Plan: Raw HTTP POST If the `soap` library fails with a real X3 server (e.g., missing `xsi:type` attributes cause rejection), the fallback is: ```typescript const envelope = ` ENG SEED ... ... `; const response = await fetch(soapEndpoint, { method: "POST", headers: { "Content-Type": "text/xml; charset=utf-8", "SOAPAction": '""', "Authorization": `Basic ${btoa(`${user}:${password}`)}`, }, body: envelope, }); ``` This approach gives full control over `xsi:type` annotations and `encodingStyle` attributes, but requires manual XML construction and parsing. Use only if the `soap` library proves incompatible with the real X3 server.