Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-Claude) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
187 lines
7.0 KiB
Markdown
187 lines
7.0 KiB
Markdown
# 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&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 (`&`)
|
|
- 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><SINVOICE><FLD NAME="NUM">INV001</FLD></SINVOICE></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.
|