Files
sage-mcp-server/src/clients/soap-client.ts

142 lines
4.3 KiB
TypeScript

import soap from 'soap';
import { XMLParser } from 'fast-xml-parser';
import type { Client } from 'soap';
import type {
SageConfig,
SoapReadOptions,
SoapQueryOptions,
SoapResult,
SoapMessage,
SoapTechInfo,
} from '../types/index.js';
export class SoapClient {
private config: SageConfig;
private client: Client | null = null;
private xmlParser: XMLParser;
constructor(config: SageConfig) {
this.config = config;
this.xmlParser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '@_',
});
}
private async getClient(): Promise<Client> {
if (!this.client) {
const wsdlUrl = `${this.config.url}/soap-wsdl/syracuse/collaboration/syracuse/CAdxWebServiceXmlCC?wsdl`;
this.client = await soap.createClientAsync(wsdlUrl, {});
this.client.setSecurity(
new soap.BasicAuthSecurity(this.config.user, this.config.password),
);
const soapEndpoint = `${this.config.url}/soap-generic/syracuse/collaboration/syracuse/CAdxWebServiceXmlCC`;
this.client.setEndpoint(soapEndpoint);
}
return this.client;
}
private buildCallContext() {
return {
codeLang: this.config.language,
codeUser: '',
password: '',
poolAlias: this.config.poolAlias,
poolId: '',
requestConfig: 'adxwss.optreturn=JSON&adxwss.beautify=false',
};
}
// SOAP caveat: status returns as STRING "1", empty arrays as undefined
private normalizeResult(raw: unknown): SoapResult {
const r = raw as Record<string, unknown> | undefined;
return {
status: parseInt(String(r?.status ?? '0'), 10),
data: this.parseResultXml(r?.resultXml),
messages: this.normalizeMessages(r?.messages),
technicalInfos: this.normalizeTechInfos(r?.technicalInfos),
};
}
private parseResultXml(resultXml: unknown): unknown {
if (!resultXml || typeof resultXml !== 'string') return null;
try {
return JSON.parse(resultXml);
} catch {
try {
return this.xmlParser.parse(resultXml);
} catch {
return resultXml;
}
}
}
private normalizeMessages(raw: unknown): SoapMessage[] {
if (!raw) return [];
const arr = Array.isArray(raw) ? raw : [raw];
return arr.map((m: Record<string, unknown>) => ({
type: parseInt(String(m?.type ?? '0'), 10),
message: String(m?.message ?? ''),
}));
}
private normalizeTechInfos(raw: unknown): SoapTechInfo[] {
if (!raw) return [];
const arr = Array.isArray(raw) ? raw : [raw];
return arr.map((t: Record<string, unknown>) => ({
type: String(t?.type ?? ''),
message: String(t?.message ?? ''),
}));
}
async read(options: SoapReadOptions): Promise<SoapResult> {
const client = await this.getClient();
const [response] = await client.readAsync({
callContext: this.buildCallContext(),
publicName: options.publicName,
objectKeys: Object.entries(options.key).map(([key, value]) => ({
key,
value,
})),
});
const res = response as Record<string, unknown>;
return this.normalizeResult(res?.readReturn ?? res);
}
async query(options: SoapQueryOptions): Promise<SoapResult> {
const client = await this.getClient();
const listSize = Math.min(options.listSize || 20, 200);
const [response] = await client.queryAsync({
callContext: this.buildCallContext(),
publicName: options.publicName,
objectKeys: [],
listSize,
});
const res = response as Record<string, unknown>;
return this.normalizeResult(res?.queryReturn ?? res);
}
async getDescription(publicName: string): Promise<SoapResult> {
const client = await this.getClient();
const [response] = await client.getDescriptionAsync({
callContext: this.buildCallContext(),
publicName,
});
const res = response as Record<string, unknown>;
return this.normalizeResult(res?.getDescriptionReturn ?? res);
}
async healthCheck(): Promise<{ status: string; latencyMs: number }> {
const start = Date.now();
try {
await this.getClient();
return { status: 'connected', latencyMs: Date.now() - start };
} catch (error) {
return {
status: `error: ${error instanceof Error ? error.message : String(error)}`,
latencyMs: Date.now() - start,
};
}
}
}