@salaxy/node
Version:
Salaxy general plain JavaScript / TypeScript libraries with Node.js -ajax component (Palkkaus.fi).
375 lines (371 loc) • 15.4 kB
JavaScript
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