UNPKG

efficy-enterprise-api

Version:

The Efficy Enterprise API is developed for server-side usage in a Node.js environment (e.g. for integrations) and also bundled for usage inside an Efficy browser session for client-side JSON RPC requests.

350 lines (305 loc) 11.1 kB
import nodeFetch from 'node-fetch'; import CrmEnv from "./crm-env.mjs"; import setCookie from './remote-objects/utils/set-cookie.mjs'; import findDeep from './remote-objects/utils/findDeep.mjs' /** * Class with low-level JSON RPC functions * @see {@link https://stackoverflow.com/questions/17575790/environment-detection-node-js-or-browser} * @property {CrmEnv} crmEnv * @property {array} remoteObjects * @property {number} requestCounter * @property {number} threadId * @property {object} lastResponseObject * @property {function} logFunction * @property {function} errorFunction */ class RemoteAPI { #name = "RemoteAPI"; crmEnv; remoteObjects; requestCounter = 0; threadId = 1; lastResponseObject; logFunction; errorFunction; /** * Construct a RemoteAPI object * @param {CrmEnv} [crmEnv=undefined] - When empty, uses the Efficy context of the browser * @param {function} [logFunction=undefined] - Your (custom) log function to call for requests and responses, e.g. console.log * @param {number} [threadId=undefined] - Unique thread ID for logging purposes */ constructor(crmEnv = new CrmEnv(), logFunction, threadId) { if (crmEnv && typeof crmEnv !== "object") throw new TypeError(`${this.#name}.constructor::crmEnv is not an Object`); if (logFunction && typeof logFunction !== "function") throw new TypeError(`${this.#name}.constructor::logFunction is not a function`); if (threadId) this.threadId = threadId; this.crmEnv = crmEnv; this.remoteObjects = []; this.logFunction = logFunction; this.#setFetchOptions(); } #setFetchOptions() { try { if (this.crmEnv.apiKey) { this.#fetchOptions.headers["X-Efficy-ApiKey"] = this.crmEnv.apiKey; } else if (this.crmEnv.user && this.crmEnv.pwd) { this.#fetchOptions.headers["X-Efficy-User"] = this.crmEnv.user; this.#fetchOptions.headers["X-Efficy-Pwd"] = this.crmEnv.pwd; } if (this.crmEnv.logOff) { this.#fetchOptions.headers["X-Efficy-Logoff"] = true; } } catch(ex) { this.throwError(`${this.#name}.readEnv::${ex.message}`) } } #fetchOptions = { method: "POST", headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, credentials: 'include' // Always send user credentials (cookies, basic http auth, etc..), even for cross-origin calls. } /** * Execute all assembled and queued RPC operations */ async executeBatch() { const requestObject = []; const responseObject = []; try { requestObject.push(...this.remoteObjects.map(item => item.asJsonRpc())); } catch(ex) { this.throwError(`${this.#name}.executeBatch::asJsonRpc\n${ex.message}`); } // Nothing to execute, ignore silently if (!requestObject.length) return; try { const response = await this.post(requestObject); if (Array.isArray(response)) { // Do nothing, all good! } else if (typeof response === "object" && this.getRpcException(response)) { const ex = this.getRpcException(response); this.throwError(`${ex["#error"].errorcode} - ${ex["#error"].errorstring}`); } else if (!response) { throw new TypeError(`${this.#name}.executeBatch::empty response`); } else { throw new TypeError(`${this.#name}.executeBatch::responseObject is not an Array`); } responseObject.push(...response); this.lastResponseObject = responseObject; } catch(ex) { this.throwError(`${this.#name}.executeBatch::${ex.message}`); } // Add response info to operations and remove executed operations (handled or not) const items = this.remoteObjects; var index = items.length while (index--) { const operation = items[index]; const respOper = responseObject.find(respOper => respOper["#id"] === operation.id); if (!respOper) this.throwError(`${this.#name}.executeBatch::cannot find response for queued operation [${index}/${items.length}]`); Object.assign(operation.responseObject, respOper); operation.afterExecute(); items.splice(index, 1); } // Error handling const ex = this.getRpcException(responseObject); if (ex) this.throwError(`${ex["#error"].errorcode} - ${ex["#error"].errorstring}`); } throwError(message) { if (typeof this.errorFunction === "function") { this.errorFunction(message); } else { throw Error(message); } } /** * Logoff the remote session */ logoff() { this.crmEnv.logOff = true; this.#setFetchOptions(); } /** @private */ registerObject(object) { this.remoteObjects.push(object); } /** @private */ async post(requestObject) { var response, responseBody, responseObject; try { const request = Object.assign(this.#fetchOptions, {body: JSON.stringify(requestObject)}); const requestUrl = `${this.crmEnv.url}/crm/json${this.crmEnv.customer ? "?customer=" + encodeURIComponent(this.crmEnv.customer) : ""}`; const rql = new RequestLog(this.requestCounter++, this.logFunction, this.threadId); rql.setRequest(requestUrl, request.method, requestObject); if (this.crmEnv.cookieHeader) { request.headers["Cookie"] = this.crmEnv.cookieHeader; rql.sessionId = this.crmEnv.shortSessionId; } rql.log(); response = await fetch(requestUrl, request); try { responseBody = await response.text(); responseObject = JSON.parse(responseBody || "[]"); } catch(ex) { this.throwError(`invalid JSON response from resource '${requestUrl}'`) } rql.setResponse(response, responseObject); rql.exception = this.getRpcException(responseObject); // @ts-ignore if (typeof response.headers.raw === "function") { // @ts-ignore const cookies = setCookie(response.headers.raw()['set-cookie'], null); if (cookies.length > 0) { this.crmEnv.cookies = cookies; this.sessionId = this.crmEnv.shortSessionId; } } rql.log(); if (rql.exception?.error === true) { const ex = rql.exception; this.throwError(`/json: ${ex?.code || ex?.errorcode} - ${ex?.message || ex?.errorstring} - ${ex?.detail}`); } return responseObject; } catch(ex) { this.throwError(`${this.#name}.post::${ex.message}`); } } /** @private */ findDataSetArray(resp, dataSetName = "dataset") { if (typeof resp !== "object") return; const result = findDeep(resp, {"#class": dataSetName}); if (!result || typeof result["#data"] !== "object" || result["#data"] === null) return; if (Array.isArray(result["#data"])) { return result["#data"]; // Efficy Enterprise } else if (Array.isArray(result["#data"]["data"])) { return result["#data"]["data"]; // Efficy U (with earlier bug) } } findListArray(resp, listName = "stringlist") { return this.findDataSetArray(resp, listName); } /** @private */ findAttachment(resp, key) { if (typeof resp !== "object") return; return findDeep(resp, {"@name": "attachment", "key": key}); } /** @private */ findFunc(resp, name) { if (typeof resp !== "object" || !Array.isArray(resp["@func"])) return; return resp["@func"].find(item => item["@name"] === name); } /** @private */ findFunc2(resp, name, name2, value2) { if (typeof resp !== "object" || !Array.isArray(resp["@func"])) return; return resp["@func"].find(item => item["@name"] === name && item[name2] === value2); } /** @private */ findFuncArray(resp, name) { var result = this.findDataSetArray(this.findFunc(resp, name)); return Array.isArray(result) ? result : null; } /** @private */ findFuncArray2(resp, name, name2, value2) { var result = this.findDataSetArray(this.findFunc2(resp, name, name2, value2)); return Array.isArray(result) ? result : null; } /** @private */ findFuncCategoryArray(resp, category) { var result = this.findDataSetArray(this.findFunc2(resp, "category", "category", category)); return Array.isArray(result) ? result : null; } /** @private */ findFuncDetailArray(resp, detail) { var result = this.findDataSetArray(this.findFunc2(resp, "detail", "detail", detail)); return Array.isArray(result) ? result : null; } /** @private */ getRpcException(responseObject) { if (Array.isArray(responseObject)) { return responseObject.find(operation => operation["@name"] === "exception"); } else if (typeof responseObject === "object" && typeof responseObject["#error"] === "object") { const errorObject = responseObject["#error"]; errorObject.error = true; return errorObject; } else if (typeof responseObject === "object" && responseObject["error"] === true) { return { "#error": { error: true, errorcode: responseObject.code, errorstring: responseObject.message } } } } }; class RequestLog { d_request = new Date(); d_response; elapsed_ms; sessionId; requestId; method = ""; statusCode = 0; statusText = ""; requestUrl = ""; exception; // Not exposed when doing JSON.stringify(this) #requestObject; #responseObject; #logFunc; constructor(id, logFunction, threadId) { this.requestId = id; this.#logFunc = logFunction; this.threadId = threadId; } get requestObject() { return this.#requestObject; } get responseObject() { return this.#responseObject; } countFuncItems(rpcObject) { return Array.isArray(rpcObject) && rpcObject.length > 0 && Array.isArray(rpcObject[0]["@func"]) ? rpcObject[0]["@func"].length : 0; } setRequest(url, method, requestObject) { this.requestUrl = url; this.method = method; this.#requestObject = requestObject; } setResponse(resp, responseObject) { this.d_response = new Date(); this.statusCode = parseInt(resp.status, 10); this.statusText = resp.statusText; this.elapsed_ms = this.d_response.getTime() - this.d_request.getTime() this.#responseObject = this.#cloneAndClean(responseObject); } log() { const prefix = `${this.threadId},${this.method}-${[this.requestId]}`; if (typeof this.#logFunc === "function") { if (this.statusCode > 0) { this.#logFunc(`<${prefix},${this.statusCode} ${this.statusText} (${this.elapsed_ms} ms)${this.sessionId ? `,${this.sessionId}`: ","}${this.exception ? ",EXCEPTION_RPC": ""}`, this); } else { this.#logFunc(`>${prefix},FUNCS-${this.countFuncItems(this.#requestObject)}`, this); } } } #cloneAndClean(object) { const data = JSON.parse(JSON.stringify(object)); // Remove long data strings -> TODO, clean multiple hits const result = findDeep(data, {"encodingkind": "MIME64"}); if (result && typeof result["@data"] === "string" && result["@data"].length > 100) { result["@data"] = "**CLEANED**"; } return data; } }; /* * Platform agnostic solution for the definition of fetch. Lib node-fetch is excluded by rollup ignore plugin */ const isNode = (typeof process !== "undefined" && process?.versions?.node ? true : false); if (isNode) { // @ts-ignore globalThis.fetch = nodeFetch; } export default RemoteAPI;