spike(soap): validate SOAP library against X3 WSDL

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-Claude)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-03-10 16:57:26 +00:00
parent c5a1b800fc
commit a7ae8148a3
3 changed files with 1028 additions and 0 deletions

186
spike/soap-spike-results.md Normal file
View File

@@ -0,0 +1,186 @@
# 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
<tns:getDescription>
<callContext>
<codeLang>ENG</codeLang>
<codeUser></codeUser>
<password></password>
<poolAlias>SEED</poolAlias>
<poolId></poolId>
<requestConfig>adxwss.optreturn=JSON&amp;adxwss.beautify=true</requestConfig>
</callContext>
<publicName>SIH</publicName>
</tns:getDescription>
```
Key observations:
- CAdxCallContext fields are correctly nested inside `<callContext>`
- `requestConfig` value is properly XML-escaped (`&amp;`)
- 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 `<resultXml>`:
```xml
<resultXml>&lt;SINVOICE&gt;&lt;FLD NAME=&quot;NUM&quot;&gt;INV001&lt;/FLD&gt;&lt;/SINVOICE&gt;</resultXml>
```
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 `<codeUser></codeUser>` and `<password></password>`
## 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 = `<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:wss="http://www.adonix.com/WSS"
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
<soap:Body>
<wss:read soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<callContext xsi:type="wss:CAdxCallContext">
<codeLang xsi:type="xsd:string">ENG</codeLang>
<poolAlias xsi:type="xsd:string">SEED</poolAlias>
...
</callContext>
...
</wss:read>
</soap:Body>
</soap:Envelope>`;
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.