import {config} from '../config.js';
import {login} from "./basicauth.js";
import {trackingHeader} from "./matomo-tracking.js";
import {appendFile} from 'fs/promises';
import {
BusinessObjectTestRequest,
BusinessObjectTestResponse,
ConnectorTestRequest,
ConnectorTestResponse,
CreateLoginMethodRequest,
GenericApiResponse,
RFCWizardCreateCallsPayload,
RFCWizardDetailsResponse,
RFCWizardSearchOptions,
SAPSystem,
SAPSystemListResponse,
SimplifierApiResponse,
SimplifierBusinessObjectDetails,
SimplifierBusinessObjectFunction,
SimplifierConnectorCallDetails,
SimplifierConnectorCallsResponse,
SimplifierConnectorCallUpdate,
SimplifierConnectorDetails,
SimplifierConnectorListResponse,
SimplifierConnectorUpdate,
SimplifierDataType,
SimplifierDataTypesResponse,
SimplifierDataTypeUpdate, SimplifierInstance, SimplifierInstanceSettings,
SimplifierLogEntryDetails,
SimplifierLoginMethodDetailsRaw,
SimplifierLoginMethodsResponse,
SimplifierLogListOptions,
SimplifierLogListResponse,
SimplifierLogPagesResponse,
SimplifierOAuth2ClientsResponse,
UnwrappedSimplifierApiResponse,
UpdateLoginMethodRequest,
} from './types.js';
/**
* Client for interacting with Simplifier Low Code Platform REST API
*
* This client will need to be enhanced with SimplifierToken.
* The SimplifierToken acts as a session key that needs to be:
* - Obtained daily by the user
* - Configured in environment variables
* - Included in API requests as authentication header
*/
export class SimplifierClient {
private baseUrl: string;
private simplifierToken?: string | undefined;
constructor() {
this.baseUrl = config.simplifierBaseUrl;
}
getBaseUrl(): string { return this.baseUrl; }
/**
* Log HTTP request details to file if HTTP_REQUEST_LOG_FILE is configured
* Sanitizes sensitive information like SimplifierToken
*/
private async logRequest(url: string, options: RequestInit): Promise<void> {
if (!config.httpRequestLogFile) {
return;
}
try {
const timestamp = new Date().toISOString();
const method = options.method || 'GET';
// Sanitize headers to hide SimplifierToken
const sanitizedHeaders: Record<string, string> = {};
if (options.headers) {
const headers = options.headers as Record<string, string>;
for (const [key, value] of Object.entries(headers)) {
if (key.toLowerCase() === 'simplifiertoken') {
sanitizedHeaders[key] = '***REDACTED***';
} else {
sanitizedHeaders[key] = value;
}
}
}
// Format body (truncate if too large)
let bodyStr = '';
if (options.body) {
const body = typeof options.body === 'string' ? options.body : JSON.stringify(options.body);
//bodyStr = body.length > 10000 ? body.substring(0, 10000) + '...[truncated]' : body;
bodyStr = body;
}
const logEntry = {
timestamp,
method,
url,
headers: sanitizedHeaders,
body: bodyStr
};
await appendFile(config.httpRequestLogFile, JSON.stringify(logEntry) + '\n');
} catch (error) {
// Silently fail - logging should not break requests
console.error('Failed to log HTTP request:', error);
}
}
private async getSimplifierToken(): Promise<string> {
if (!this.simplifierToken) {
if (config.simplifierToken) {
this.simplifierToken = config.simplifierToken;
} else if (config.credentialsFile) {
this.simplifierToken = await login();
}
}
return this.simplifierToken!;
}
/**
* Private method to execute HTTP request with common setup
* Returns raw Response object for different processing approaches
*/
private async executeRequest(urlPath: string, options: RequestInit = {}): Promise<Response> {
const url = `${this.baseUrl}${urlPath}`;
const simplifierToken = await this.getSimplifierToken();
const data = {
...options,
headers: {
'Content-Type': 'application/json',
'SimplifierToken': simplifierToken,
...options.headers,
},
}
// Log the request if logging is enabled
await this.logRequest(url, data);
const response: Response = await fetch(url, data);
if (!response.ok) {
const body = await response.text();
throw new Error(`HTTP ${response.status}: ${response.statusText}\n${body}`);
}
return response;
}
async executeRequestWithHandler<T>(
urlPath: string,
options: RequestInit,
handle: (response: Response) => T
): Promise<T> {
try {
const response = await this.executeRequest(urlPath, options);
return handle(response)
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed request ${options.method || "GET"} ${this.baseUrl}${urlPath}: ${error.message}`);
}
throw error;
}
}
/** For handling APIs that return JSON in the common Simplifier API format:
* ```
* { success: true, result: T } | {success: false, message?: string, error?: string }
* ```
*/
async makeRequest<T>(
urlPath: string,
options: RequestInit = {}
): Promise<T> {
return this.executeRequestWithHandler(urlPath, options, async (response: Response) => {
const oResponse = (await response.json()) as SimplifierApiResponse<T>;
if (oResponse.success === false) {
throw new Error(`Received error: ${oResponse.error || ""}${oResponse.message || ""}`);
}
return oResponse.result as T;
})
}
/** For handling APIs that return JSON, but don't wrap successful calls with `{success: true, result: ... }`.
* Errors are still expected to be of the form `{success: false, message?: string, error?: string }` */
async makeUnwrappedRequest<T extends object>(
urlPath: string,
options: RequestInit = {}
): Promise<T> {
return this.executeRequestWithHandler(urlPath, options, async (response: Response) => {
const oResponse = (await response.json()) as UnwrappedSimplifierApiResponse<T>;
if ('success' in oResponse && oResponse.success === false) {
throw new Error(`Received error: ${oResponse.error || ""}${oResponse.message || ""}`);
}
return (oResponse) as T;
})
}
/** For handling APIs that return plaintext results and errors */
async makePlaintextRequest(
urlPath: string,
options: RequestInit = {}
): Promise<string> {
return this.executeRequestWithHandler(urlPath, options, async (response: Response) => {
return await response.text();
})
}
async ping(): Promise<boolean> {
return this.makeUnwrappedRequest<{msg: string}>(`/client/2.0/ping`).then(response => response.msg === "pong");
}
async getServerBusinessObjects(trackingKey: string): Promise<SimplifierBusinessObjectDetails[]> {
return this.makeRequest("/UserInterface/api/businessobjects/server", { method: "GET",
headers: trackingHeader(trackingKey) });
}
async getServerBusinessObjectDetails(objectName: string, trackingKey: string): Promise<SimplifierBusinessObjectDetails> {
return this.makeRequest(`/UserInterface/api/businessobjects/server/${objectName}`, {
method: "GET",
headers: trackingHeader(trackingKey)
})
}
async deleteServerBusinessObject(objectName: string, trackingKey: string): Promise<string> {
const oResult = await this.makeUnwrappedRequest<GenericApiResponse>(`/UserInterface/api/businessobjects/server/${objectName}`, {
method: "DELETE",
headers: trackingHeader(trackingKey)
})
return oResult.message;
}
async getServerBusinessObjectFunction(objectName: string, functionName: string, trackingKey?: string): Promise<SimplifierBusinessObjectFunction> {
return this.makeRequest(`/UserInterface/api/businessobjects/server/${objectName}/functions/${functionName}?completions=false&dataTypes=true`, {
method: "GET",
headers: trackingHeader(trackingKey)
})
}
async getServerBusinessObjectFunctions(objectName: string, trackingKey: string): Promise<SimplifierBusinessObjectFunction[]> {
return this.makeRequest(`/UserInterface/api/businessobjects/server/${objectName}/functions`, {
method: "GET",
headers: trackingHeader(trackingKey)
})
}
async createServerBusinessObjectFunction(objectName: string, functionData: SimplifierBusinessObjectFunction): Promise<string> {
await this.makeRequest(`/UserInterface/api/businessobjects/server/${objectName}/functions`, {
method: "POST",
body: JSON.stringify(functionData)
});
return `Successfully created function '${functionData.name}' in Business Object '${objectName}'`;
}
async updateServerBusinessObjectFunction(objectName: string, functionName: string, functionData: SimplifierBusinessObjectFunction): Promise<string> {
await this.makeRequest(`/UserInterface/api/businessobjects/server/${objectName}/functions/${functionName}`, {
method: "PUT",
body: JSON.stringify(functionData)
});
return `Successfully updated function '${functionName}' in Business Object '${objectName}'`;
}
async testServerBusinessObjectFunction(objectName: string, functionName: string, testRequest: BusinessObjectTestRequest, trackingKey: string): Promise<BusinessObjectTestResponse> {
return await this.makeUnwrappedRequest(`/UserInterface/api/businessobjecttest/${objectName}/methods/${functionName}`, {
method: "POST",
body: JSON.stringify(testRequest),
headers: trackingHeader(trackingKey)
});
}
async deleteServerBusinessObjectFunction(objectName: string, functionName: string, trackingKey: string): Promise<string> {
const oResult = await this.makeUnwrappedRequest<GenericApiResponse>(`/UserInterface/api/businessobjects/server/${objectName}/functions/${functionName}`, {
method: "DELETE",
headers: trackingHeader(trackingKey)
})
return oResult.message;
}
async createServerBusinessObject(oData: SimplifierBusinessObjectDetails): Promise<string> {
return this.makeRequest(`/UserInterface/api/businessobjects/server`, { method: "POST", body: JSON.stringify(oData) })
.then(() => `Successfully created Business Object '${oData.name}'`)
}
async updateServerBusinessObject(oData: SimplifierBusinessObjectDetails): Promise<string> {
return this.makeRequest(`/UserInterface/api/businessobjects/server/${oData.name}`, { method: "PUT", body: JSON.stringify(oData) })
.then(() => `Successfully updated Business Object '${oData.name}'`);
}
async createConnector(oData: SimplifierConnectorUpdate): Promise<string> {
await this.makeRequest(`/UserInterface/api/connectors`, { method: "POST", body: JSON.stringify(oData) });
return `Successfully created Connector '${oData.name}'`;
}
async updateConnector(oData: SimplifierConnectorUpdate): Promise<string> {
await this.makeRequest(`/UserInterface/api/connectors/${oData.name}`, { method: "PUT", body: JSON.stringify(oData) });
return `Successfully updated Connector '${oData.name}'`;
}
async createConnectorCall(connectorName: string, oData: SimplifierConnectorCallUpdate): Promise<string> {
await this.makeRequest(`/UserInterface/api/connectors/${connectorName}/calls`, { method: "POST", body: JSON.stringify(oData) });
return `Successfully created Connector call '${connectorName}.${oData.name}'`;
}
async updateConnectorCall(connectorName: string, oData: SimplifierConnectorCallUpdate): Promise<string> {
await this.makeRequest(`/UserInterface/api/connectors/${connectorName}/calls/${oData.name}`, { method: "PUT", body: JSON.stringify(oData) });
return `Successfully updated Connector call '${connectorName}.${oData.name}'`;
}
async getSoapConnectorWSDL(connectorName: string, endpoint: string): Promise<string> {
const response = await this.executeRequest(`/UserInterface/api/connectors/${connectorName}/state/${endpoint}`, {
method: "POST",
body: '{"action": "download"}',
});
return response.json();
}
async searchPossibleRFCConnectorCalls(connectorName: string, filter: RFCWizardSearchOptions, trackingKey: string): Promise<string[]> {
const result = await this.makeUnwrappedRequest<{names: string[]}>(`/UserInterface/api/connectorCallWizard/${connectorName}/operations/search`, {
method: "POST",
body: JSON.stringify(filter),
headers: trackingHeader(trackingKey),
});
return result.names;
}
async viewRFCFunctions(connectorName: string, functionNames: string[], trackingKey: string): Promise<void> {
await this.makeUnwrappedRequest(`/UserInterface/api/connectorCallWizard/${connectorName}/operations/view`, {
method: "POST",
body: JSON.stringify({filter: functionNames.join(", ")}),
headers: trackingHeader(trackingKey),
});
}
async rfcWizardGetCallDetails(connectorName: string, functionNames: string[]): Promise<RFCWizardDetailsResponse> {
return this.makeUnwrappedRequest<RFCWizardDetailsResponse>(`/UserInterface/api/connectorCallWizard/${connectorName}/suggestions/detailed`, {
method: "POST",
body: JSON.stringify({callsRfc: functionNames}),
});
}
async rfcWizardCreateCalls(connectorName: string, calls: RFCWizardCreateCallsPayload): Promise<string> {
await this.makePlaintextRequest(`/UserInterface/api/connectorCallWizard/${connectorName}`, {
method: "POST",
body: JSON.stringify(calls),
});
return `Successfully created ${calls.callsRfc.length} calls: ${calls.callsRfc}.`;
}
async getDataTypes(trackingKey: string): Promise<SimplifierDataTypesResponse> {
return this.makeUnwrappedRequest("/UserInterface/api/datatypes?cacheIndex=true", {
method: "GET",
headers: trackingHeader(trackingKey)
});
}
async getSingleDataType(name: string, nameSpace: string | undefined, trackingKey: string): Promise<SimplifierDataType> {
const fullDataType = `${nameSpace ? nameSpace + '/' : ''}${name}`
return this.makeUnwrappedRequest(`/UserInterface/api/datatypes/${fullDataType}`, {
method: "GET",
headers: trackingHeader(trackingKey)
});
}
/**
* Get detailed information about a specific datatype by its full identifier (not the hash id).
*
* @param datatypeId - The fully qualified datatype identifier, which is namespace/datatypename.
* For root namespace (no namespace), use just the datatype name without slash.
* Examples:
* - "bo/SF_User/getUser_groups_Struct" (business object datatype with namespace)
* - "_ITIZ_B_BUS2038_DATA" (datatype in root namespace)
* @param trackingKey - The MCP tool or resource name for tracking purposes
* @returns Detailed datatype information including fields, category, and metadata
*/
async getDataTypeByName(datatypeId: string, trackingKey?: string): Promise<SimplifierDataType> {
return this.makeUnwrappedRequest(`/UserInterface/api/datatypes/${datatypeId}?woAutoGen=false&detailLevel=detailed`, {
method: "GET",
headers: trackingHeader(trackingKey)
});
}
async createDataType(datatypeDesc: SimplifierDataTypeUpdate): Promise<string> {
const fullDataType = `${datatypeDesc.nameSpace ? datatypeDesc.nameSpace + '/' : ''}${datatypeDesc.name}`
return this.makePlaintextRequest(`/UserInterface/api/datatypes`, { method: "POST", body: JSON.stringify(datatypeDesc) })
.then((id) => `Successfully created data type ${fullDataType} with id ${id}`);
}
async updateDataType(datatypeDesc: SimplifierDataTypeUpdate): Promise<string> {
const fullDataType = `${datatypeDesc.nameSpace ? datatypeDesc.nameSpace + '/' : ''}${datatypeDesc.name}`
return this.makePlaintextRequest(`/UserInterface/api/datatypes/${fullDataType}`, { method: "PUT", body: JSON.stringify(datatypeDesc) })
.then((id) => `Successfully updated data type ${fullDataType} with id ${id}`)
}
async deleteDataType(name: string, nameSpace: string | undefined, trackingKey: string): Promise<string> {
const fullDataType = `${nameSpace ? nameSpace + '/' : ''}${name}`
return this.makePlaintextRequest(`/UserInterface/api/datatypes/${fullDataType}`, {
method: "DELETE",
headers: trackingHeader(trackingKey)
});
}
// ========================================
// Connector API Methods
// ========================================
async listConnectors(trackingKey: string): Promise<SimplifierConnectorListResponse> {
return this.makeUnwrappedRequest(`/UserInterface/api/connectors`, {
method: "GET",
headers: trackingHeader(trackingKey)
});
}
async getConnector(name: string, trackingKey: string, withEndpointConfigurations: boolean = true): Promise<SimplifierConnectorDetails> {
const params = withEndpointConfigurations ? '' : '?withEndpointConfigurations=false';
return this.makeUnwrappedRequest(`/UserInterface/api/connectors/${name}${params}`, {
method: "GET",
headers: trackingHeader(trackingKey)
});
}
async listConnectorCalls(connectorName: string, trackingKey: string): Promise<SimplifierConnectorCallsResponse> {
return this.makeUnwrappedRequest(`/UserInterface/api/connectors/${connectorName}/calls`, {
method: "GET",
headers: trackingHeader(trackingKey)
});
}
async getConnectorCall(connectorName: string, callName: string, trackingKey?: string): Promise<SimplifierConnectorCallDetails> {
const response = await this.makeUnwrappedRequest<SimplifierConnectorCallDetails>(`/UserInterface/api/connectors/${connectorName}/calls/${callName}`, {
method: "GET",
headers: trackingHeader(trackingKey)
});
// Normalize category values from API - 'any' should be 'base'
if (response.connectorCallParameters) {
response.connectorCallParameters = response.connectorCallParameters.map(param => ({
...param,
dataType: {
...param.dataType,
category: param.dataType.category === 'any' ? 'base' : param.dataType.category
}
}));
}
return response;
}
async testConnectorCall(connectorName: string, callName: string, testRequest: ConnectorTestRequest, trackingKey: string): Promise<ConnectorTestResponse> {
return await this.makeUnwrappedRequest(`/UserInterface/api/connectortest/${connectorName}/calls/${callName}`, {
method: "POST",
body: JSON.stringify(testRequest),
headers: trackingHeader(trackingKey)
});
}
async deleteConnector(connectorName: string, trackingKey: string): Promise<string> {
const oResult = await this.makeUnwrappedRequest<GenericApiResponse>(`/UserInterface/api/connectors/${connectorName}`, {
method: "DELETE",
headers: trackingHeader(trackingKey)
})
return oResult.message;
}
async deleteConnectorCall(connectorName: string, callName: string, trackingKey: string): Promise<string> {
const oResult = await this.makeUnwrappedRequest<GenericApiResponse>(`/UserInterface/api/connectors/${connectorName}/calls/${callName}`, {
method: "DELETE",
headers: trackingHeader(trackingKey)
})
return oResult.message;
}
// ========================================
// LoginMethod API Methods
// ========================================
async listLoginMethods(trackingKey: string): Promise<SimplifierLoginMethodsResponse> {
return this.makeUnwrappedRequest(`/UserInterface/api/login-methods`, {
method: "GET",
headers: trackingHeader(trackingKey)
});
}
async listOAuth2Clients(trackingKey: string): Promise<SimplifierOAuth2ClientsResponse> {
return this.makeUnwrappedRequest(`/UserInterface/api/AuthSettings?mechanism=OAuth2`, {
method: "GET",
headers: trackingHeader(trackingKey)
});
}
async getLoginMethodDetails(name: string, trackingKey: string): Promise<SimplifierLoginMethodDetailsRaw> {
return this.makeUnwrappedRequest(`/UserInterface/api/login-methods/${name}`, {
method: "GET",
headers: trackingHeader(trackingKey)
});
}
async createLoginMethod(request: CreateLoginMethodRequest): Promise<string> {
await this.makeUnwrappedRequest(`/UserInterface/api/login-methods`, {
method: "POST",
body: JSON.stringify(request)
});
return `Successfully created Login Method '${request.name}'`;
}
async updateLoginMethod(name: string, request: UpdateLoginMethodRequest): Promise<string> {
await this.makeUnwrappedRequest(`/UserInterface/api/login-methods/${name}`, {
method: "PUT",
body: JSON.stringify(request)
});
return `Successfully updated Login Method '${name}'`;
}
// SAP system API methods
async getSapSystem(systemName: string, trackingKey: string): Promise<SAPSystem> {
return this.makeUnwrappedRequest(`/UserInterface/api/sapSystem/${systemName}`, {
method: "GET",
headers: trackingHeader(trackingKey),
})
}
async listSapSystems(trackingKey: string): Promise<SAPSystemListResponse> {
return this.makeUnwrappedRequest(`/UserInterface/api/sapSystem`, {
method: "GET",
headers: trackingHeader(trackingKey),
})
}
async createSapSystem(sapSystemRequest: SAPSystem): Promise<string> {
await this.makeUnwrappedRequest(`/UserInterface/api/sapSystem`, {
method: "POST",
body: JSON.stringify(sapSystemRequest),
})
return `Successfully created SAP system '${sapSystemRequest.name}'`;
}
async updateSapSystem(sapSystemRequest: SAPSystem): Promise<string> {
await this.makeUnwrappedRequest(`/UserInterface/api/sapSystem/${sapSystemRequest.name}`, {
method: "PUT",
body: JSON.stringify(sapSystemRequest),
})
return `Successfully updated SAP system '${sapSystemRequest.name}'`;
}
async deleteSapSystem(systemName: string, trackingKey: string): Promise<string> {
const oResult = await this.makeUnwrappedRequest<GenericApiResponse>(`/UserInterface/api/sapSystem/${systemName}`, {
method: "DELETE",
headers: trackingHeader(trackingKey),
})
return oResult.message;
}
// Logging API methods
async listLogEntriesPaginated(pageNo: number, pageSize: number, trackingKey: string, options?: SimplifierLogListOptions): Promise<SimplifierLogListResponse> {
const params = this.optionsToQueryParams(options)
const queryString = params.toString();
const url = queryString
? `/UserInterface/api/logging/list/page/${pageNo}/pagesize/${pageSize}?${queryString}`
: `/UserInterface/api/logging/list/page/${pageNo}/pagesize/${pageSize}`;
return await this.makeUnwrappedRequest(url, {
method: "GET",
headers: trackingHeader(trackingKey)
});
}
async getLogPages(pageSize: number = 50, options?: SimplifierLogListOptions): Promise<SimplifierLogPagesResponse> {
const params = this.optionsToQueryParams(options)
params.append('pagesize', pageSize.toString())
return await this.makeUnwrappedRequest(`/UserInterface/api/logging/pages?${params}`);
}
async getLogEntry(id: string, trackingKey: string): Promise<SimplifierLogEntryDetails> {
return await this.makeUnwrappedRequest(`/UserInterface/api/logging/entry/${id}`, {
method: "GET",
headers: trackingHeader(trackingKey)
});
}
optionsToQueryParams(options?: SimplifierLogListOptions): URLSearchParams {
const params = new URLSearchParams();
if (options?.logLevel !== undefined) params.append('logLevel', options.logLevel.toString());
if (options?.since) params.append('since', options.since);
if (options?.from) params.append('from', options.from);
if (options?.until) params.append('until', options.until);
return params;
}
// Instance settings
async getInstanceSettings(trackingKey: string): Promise<SimplifierInstance[]> {
const oInstanceSettings: SimplifierInstanceSettings = await this.makeUnwrappedRequest(`/UserInterface/api/InstanceSettings`, {
method: "GET",
headers: trackingHeader(trackingKey)
});
return oInstanceSettings.instanceSettings;
}
}