UNPKG

@salaxy/node

Version:

Salaxy general plain JavaScript / TypeScript libraries with Node.js -ajax component (Palkkaus.fi).

375 lines (371 loc) 15.4 kB
import { Configs, OAuth2, OAuthGrantType, Test, Accounts, Workers, Calculator, Calculations, Taxcards, Reports, Files, CalendarEvents } from '@salaxy/core'; import axios from 'axios'; /** * Provides wrapper methods for communicating with the Palkkaus.fi API * The request-promise access to the server methods: GET, POST and DELETE * with different return types and authentication / error events. * This is implementation is the default for server-side running of the Salaxy library code. */ class AjaxNode { /** * Creates a new instance of AjaxNode */ constructor() { /** * The server address - root of the server. This is settable field. * Will probably be changed to a configuration object in the final version. */ this.serverAddress = "https://test-api.salaxy.com"; /** * Fixes stack trace in Axios * Fixes this issue: https://github.com/axios/axios/issues/2387 * Taken from: https://github.com/rafaelalmeidatk/TIL/issues/4 */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types this.axiosCatch = (stackTrace) => (error) => { // TODO: We should go through all the errors from the server so we could simplify this. let errorMessage = error.message; if (error.response) { errorMessage = `Server error (${error.response.status})`; if (error.response.data) { if (error.response.data.allErrors && error.response.data.allErrors.length) { errorMessage += ": " + error.response.data.allErrors[0]; } else if (error.response.data.message) { errorMessage += ": " + error.response.data.message; } if (error.response.data.stack) { errorMessage += "\nServer stack:\n" + error.response.data.stack.replace(/\\r\\n/g, "\n"); if (errorMessage.indexOf("--- End of stack trace from previous location where exception was thrown ---")) { errorMessage = errorMessage.substr(0, errorMessage.indexOf("--- End of stack trace from previous location where exception was thrown ---")); } } else { const responseBody = JSON.stringify(error.response.data, null, 2); errorMessage += `\nResponse body:\n${responseBody}`; } } } error.message = errorMessage; error.stack = stackTrace; throw error; }; const config = Configs.current; if (config) { // apiServer if (config.apiServer) { this.serverAddress = config.apiServer; } // useCredentials if (config.useCredentials != null) { this.useCredentials = config.useCredentials; } } } /** Gets the Server address that is used as bases to the HTML queries. E.g. 'https://test-api.salaxy.com' */ getServerAddress() { return this.serverAddress; } /** Gets the API address with version information. E.g. 'https://test-api.salaxy.com/v02/api' */ getApiAddress() { return this.serverAddress + "/v02/api"; } /** * Gets a JSON-message from server using the API * * @param method - The API method is the url path after the api version segments (e.g. '/v02/api') * and starts with a forward slash, e.g. '/calculator/new', or a full URL address. * * @returns A Promise with result. Standard Promise rejection to be used for error handling. */ getJSON(method) { const token = this.getCurrentToken(); const options = { headers: {}, responseType: "json", method: "GET", url: this.getUrl(method), withCredentials: (token) ? false : this.useCredentials, }; if (token) { const authHeaderKey = "Authorization"; options.headers[authHeaderKey] = "Bearer " + token; } const stackTrace = this.getStackTrace(); return axios.request(options) .then((response) => { return response.data; }).catch(this.axiosCatch(stackTrace)); } /** * Gets a HTML-message from server using the API * * @param method - The API method is the url path after the api version segments (e.g. '/v02/api') * and starts with a forward slash, e.g. '/calculator/new', or a full URL address. * * @returns A Promise with result html. Standard Promise rejection to be used for error handling. */ getHTML(method) { const token = this.getCurrentToken(); const options = { headers: {}, method: "GET", responseType: "text", url: this.getUrl(method), withCredentials: (token) ? false : this.useCredentials, }; if (token) { const authHeaderKey = "Authorization"; options.headers[authHeaderKey] = "Bearer " + token; } const stackTrace = this.getStackTrace(); return axios.request(options) .then((value) => { return value.data; }) .catch(this.axiosCatch(stackTrace)); } /** * POSTS data to server and receives back a JSON-message. * * @param method - The API method is the url path after the api version segments (e.g. '/v02/api') * and starts with a forward slash, e.g. '/calculator/new', or a full URL address. * @param data - The data that is posted to the server. * * @returns A Promise with result. Standard Promise rejection to be used for error handling. */ postJSON(method, data) { const token = this.getCurrentToken(); const options = { data, headers: {}, responseType: "json", method: "POST", url: this.getUrl(method), withCredentials: (token) ? false : this.useCredentials, }; if (token) { const authHeaderKey = "Authorization"; options.headers[authHeaderKey] = "Bearer " + token; } const stackTrace = this.getStackTrace(); return axios.request(options) .then((value) => { return value.data; }) .catch(this.axiosCatch(stackTrace)); } /** * POSTS data to server and receives back HTML. * * @param method - The API method is the url path after the api version segments (e.g. '/v02/api') * and starts with a forward slash, e.g. '/calculator/new', or a full URL address. * @param data - The data that is posted to the server. * * @returns A Promise with result. Standard Promise rejection to be used for error handling. */ postHTML(method, data) { const token = this.getCurrentToken(); const options = { data, headers: { "Content-Type": "text/plain" }, method: "POST", responseType: "text", url: this.getUrl(method), withCredentials: (token) ? false : this.useCredentials, }; if (token) { const authHeaderKey = "Authorization"; options.headers[authHeaderKey] = "Bearer " + token; } const stackTrace = this.getStackTrace(); return axios.request(options) .then((value) => { return value.data; }) .catch(this.axiosCatch(stackTrace)); } /** * Sends a DELETE-message to server using the API * * @param method - The API method is the url path after the api version segments (e.g. '/v02/api') * and starts with a forward slash, e.g. '/calculator/new', or a full URL address. * * @returns A Promise with result. Standard Promise rejection to be used for error handling. */ remove(method) { const token = this.getCurrentToken(); const options = { headers: {}, responseType: "json", method: "DELETE", url: this.getUrl(method), withCredentials: (token) ? false : this.useCredentials, }; if (token) { const authHeaderKey = "Authorization"; options.headers[authHeaderKey] = "Bearer " + token; } const stackTrace = this.getStackTrace(); return axios.request(options) .then((value) => { return value.data; }) .catch(this.axiosCatch(stackTrace)); } /** * Returns current JWT token. */ getCurrentToken() { return this.token; } /** * Sets token. * @param token - JWT token. */ setCurrentToken(token) { this.token = token; } /** * Gets a new stacktrace at the current location. * Fixes this issue: https://github.com/axios/axios/issues/2387 * Taken from: https://github.com/rafaelalmeidatk/TIL/issues/4 */ getStackTrace() { const { stack } = new Error(); let split = stack.split("\n"); // Remove the above "new Error" line from the stack trace if (split[1].includes("at getStackTrace")) { split = [split[0], ...split.splice(2)]; } return split.join("\n"); } /** If missing, append the API server address to the given url method string */ getUrl(method) { if (!method || method.trim() === "") { return null; } if (method.toLowerCase().startsWith("http")) { return method; } if (method.toLowerCase().startsWith("/v")) { return this.getServerAddress() + method; } return this.getApiAddress() + method; } } /** * Helpers for creating and runing tests in Node (server) environment. * Unit tests, System tests and demonstrations written in test format. * Tested and designed for Mocha / Chai, but should apply to other frameworks as well. */ class TestHelper { /** * Creates a new TestHelper with server configured, but not authenticated (call authenticate() to do that). * Anonymous mode is enough for many unit testing scenarios but not for e.g. CRUD operations. * We do not create an authenticated connection in constructor as a best practice. * * If suite callback context is specified, the timeout is set to config.timeout or 30000 as default. * The default number may be changed later without it being a breaking change, so set in config if you need it to be something specific. * * @param config Configuration for the test helper. * @param suiteCallBackContext Callback context is the **this** of 'describe(\"\", function ()' in Mocha. * See Mocha.ISuiteCallbackContext for details. * NOTE that you cannot use 'describe(\"\", () =>', because that will pass the wrong context (this). * @example * ```js * import { config } from "../config"; * * describe("Basic tests demonstrate the use of tester methods.", function() { * const sxyHelper = new TestHelper(config, this); * ``` */ constructor(config, suiteCallBackContext) { /** * Initializes the authenticated connection to the test server according to config. * Before calling this method, the ajax connection is anonymous. * Note that in Mocha / Chai, you can attach the method directly to before(), see examples below. * * @example * ```js * // Mocha / Chai testing * describe("Calculations basic CRUD operations.", function() { * const testHelper = new TestHelper(config, this); * before("Authenticate", testHelper.authenticate); *``` * * @example * ```js * const testHelper = new TestHelper(config); * testHelper.authenticate().then(() => { * // Some other initialization code here. * }); * ``` */ this.authenticate = () => { const oauth = new OAuth2(this.ajax); if (!this.config) { throw new Error("No config set before calling authanticate()."); } if (!this.config.username || !this.config.password) { throw new Error("Authenticate called without setting a username or password."); } const request = { grant_type: OAuthGrantType.Password, username: this.config.username, password: this.config.password, }; this.ajax.serverAddress = this.getServer(); return oauth.token(request).then((oauthResponse) => { this.ajax.setCurrentToken(oauthResponse.access_token); return oauthResponse; }); }; this.config = config; if (suiteCallBackContext) { if (!suiteCallBackContext.timeout) { throw new Error("SuiteCallBackContext does not have a timeout() method. Are you using 'describe(\"\", () =>' instead of 'describe(\"\", function ()'?"); } suiteCallBackContext.timeout((config || {}).timeout || 30000); } else { throw new Error("There is no parameter for suiteCallBackContext. Typically, this is because you are calling 'describe('', () =>' insted of 'describe('', function ()'."); } this.ajax = new AjaxNode(); this.ajax.serverAddress = this.getServer(); } /** Shortcut to get the Salaxy OpenApi wrappers with the initialized ajax. */ get api() { if (!this.ajax) { throw new Error("Ajax has not been initialized!"); } return { test: new Test(this.ajax), accounts: new Accounts(this.ajax), workers: new Workers(this.ajax), calculator: new Calculator(this.ajax), calculations: new Calculations(this.ajax), taxcards: new Taxcards(this.ajax), reports: new Reports(this.ajax), files: new Files(this.ajax), calendarEvents: new CalendarEvents(this.ajax), }; } getServer() { if (this.config && this.config.serverUrl) { return this.config.serverUrl; } switch ((this.config || {}).server) { case "localhost": return "http://localhost:82"; case "prod": return "https://secure.salaxy.com"; case "auto": case "test": default: return "https://test-api.salaxy.com"; } } } export { AjaxNode, TestHelper }; //# sourceMappingURL=index.esm.js.map