@nicodoggie/node-kiwi-tcms-api
Version:
Vibe-coded Node.js wrapper for Kiwi TCMS XML-RPC API. Use at your own risk.
227 lines • 9.15 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AxiosXmlRpcClient = void 0;
const axios_1 = __importDefault(require("axios"));
const axios_cookiejar_support_1 = require("axios-cookiejar-support");
const tough_cookie_1 = require("tough-cookie");
const xml2js_1 = require("xml2js");
class AxiosXmlRpcClient {
client;
url;
cookieJar;
constructor(config) {
this.url = config.url;
this.cookieJar = new tough_cookie_1.CookieJar();
// Create axios instance with cookie support
this.client = (0, axios_cookiejar_support_1.wrapper)(axios_1.default.create({
timeout: config.timeout || 30000,
headers: {
'Content-Type': 'text/xml',
'User-Agent': 'Node.js Kiwi TCMS Client (axios)',
...config.headers,
},
// Axios handles redirects automatically
maxRedirects: 5,
validateStatus: (status) => status >= 200 && status < 300,
// Enable cookie jar for session management
jar: this.cookieJar,
withCredentials: true,
}));
console.log('🍪 AxiosXmlRpcClient initialized with cookie support');
}
async methodCall(method, params = []) {
const xmlPayload = this.buildXmlRpcRequest(method, params);
try {
const response = await this.client.post(this.url, xmlPayload);
if (!response.data || typeof response.data !== 'string') {
throw new Error('Invalid response: expected XML string');
}
return this.parseXmlRpcResponse(response.data);
}
catch (error) {
if (error.response) {
throw new Error(`XML-RPC call failed: ${error.response.status} ${error.response.statusText}`);
}
throw error;
}
}
buildXmlRpcRequest(method, params) {
let xml = '<?xml version="1.0"?>\n<methodCall>\n';
xml += ` <methodName>${this.escapeXml(method)}</methodName>\n`;
xml += ' <params>\n';
for (const param of params) {
xml += ' <param>\n';
xml += this.serializeValue(param, ' ');
xml += ' </param>\n';
}
xml += ' </params>\n';
xml += '</methodCall>';
return xml;
}
serializeValue(value, indent) {
let xml = `${indent}<value>`;
if (value === null || value === undefined) {
xml += '<nil/>';
}
else if (typeof value === 'string') {
xml += `<string>${this.escapeXml(value)}</string>`;
}
else if (typeof value === 'number') {
if (Number.isInteger(value)) {
xml += `<int>${value}</int>`;
}
else {
xml += `<double>${value}</double>`;
}
}
else if (typeof value === 'boolean') {
xml += `<boolean>${value ? '1' : '0'}</boolean>`;
}
else if (value instanceof Date) {
xml += `<dateTime.iso8601>${value.toISOString()}</dateTime.iso8601>`;
}
else if (Array.isArray(value)) {
xml += '<array><data>\n';
for (const item of value) {
xml += this.serializeValue(item, indent + ' ');
}
xml += `${indent}</data></array>`;
}
else if (typeof value === 'object') {
xml += '<struct>\n';
for (const [key, val] of Object.entries(value)) {
xml += `${indent} <member>\n`;
xml += `${indent} <name>${this.escapeXml(key)}</name>\n`;
xml += this.serializeValue(val, indent + ' ');
xml += `${indent} </member>\n`;
}
xml += `${indent}</struct>`;
}
xml += '</value>\n';
return xml;
}
async parseXmlRpcResponse(xmlString) {
return new Promise((resolve, reject) => {
(0, xml2js_1.parseString)(xmlString, { explicitArray: false, mergeAttrs: true }, (err, result) => {
if (err) {
reject(new Error(`XML parsing error: ${err.message}`));
return;
}
try {
if (result.methodResponse) {
if (result.methodResponse.fault) {
// Handle fault response
const fault = this.parseXmlRpcValue(result.methodResponse.fault.value);
throw new Error(`XML-RPC Fault: ${fault.faultString} (Code: ${fault.faultCode})`);
}
if (result.methodResponse.params) {
// Handle successful response
const params = result.methodResponse.params;
if (params.param) {
const value = Array.isArray(params.param) ? params.param[0] : params.param;
return resolve(this.parseXmlRpcValue(value.value));
}
}
// Handle empty response
resolve(null);
}
else {
reject(new Error('Invalid XML-RPC response format'));
}
}
catch (error) {
reject(error);
}
});
});
}
parseXmlRpcValue(valueObj) {
if (!valueObj || typeof valueObj !== 'object') {
return valueObj;
}
// Handle different XML-RPC types
if (valueObj.string !== undefined) {
return valueObj.string;
}
if (valueObj.int !== undefined) {
return parseInt(valueObj.int);
}
if (valueObj.i4 !== undefined) {
return parseInt(valueObj.i4);
}
if (valueObj.double !== undefined) {
return parseFloat(valueObj.double);
}
if (valueObj.boolean !== undefined) {
return valueObj.boolean === '1' || valueObj.boolean === 1;
}
if (valueObj['dateTime.iso8601'] !== undefined) {
return new Date(valueObj['dateTime.iso8601']);
}
if (valueObj.array !== undefined) {
if (valueObj.array.data && valueObj.array.data.value) {
const values = Array.isArray(valueObj.array.data.value)
? valueObj.array.data.value
: [valueObj.array.data.value];
return values.map((v) => this.parseXmlRpcValue(v));
}
return [];
}
if (valueObj.struct !== undefined) {
const result = {};
if (valueObj.struct.member) {
const members = Array.isArray(valueObj.struct.member)
? valueObj.struct.member
: [valueObj.struct.member];
for (const member of members) {
if (member.name && member.value !== undefined) {
result[member.name] = this.parseXmlRpcValue(member.value);
}
}
}
return result;
}
// Handle XML-RPC nil values - convert to JavaScript null
if (valueObj.nil !== undefined) {
return null;
}
// Handle text nodes or direct values - preserve objects when possible
if (typeof valueObj === 'string') {
return valueObj;
}
// If it's an object but not a recognized XML-RPC type, check if it's a direct value
// This handles cases where xml2js might parse values differently
if (typeof valueObj === 'object' && valueObj !== null) {
// Check if this is a text node (common in xml2js output)
if ('_' in valueObj && Object.keys(valueObj).length === 1) {
return valueObj._;
}
// Check if this is an XML-RPC nil value (alternative representation)
if ('nil' in valueObj) {
return null;
}
// If it has multiple properties or no special structure, treat as a complex object
// Recursively process all properties to handle nested structures
const result = {};
for (const [key, value] of Object.entries(valueObj)) {
result[key] = this.parseXmlRpcValue(value);
}
return result;
}
// For any other types (number, boolean, etc), return as-is
return valueObj;
}
escapeXml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
}
exports.AxiosXmlRpcClient = AxiosXmlRpcClient;
//# sourceMappingURL=axios-xmlrpc-client.js.map