@stuntman/client
Version:
Stuntman - HTTP proxy / mock API client
163 lines • 6.69 kB
JavaScript
import serializeJavascript from 'serialize-javascript';
import { ClientError } from './clientError.js';
import { stuntmanConfig } from '@stuntman/shared';
const SERIALIZE_JAVASCRIPT_OPTIONS = {
unsafe: true,
ignoreFunction: true,
};
const getFunctionParams = (func) => {
const funstr = func.toString();
const params = funstr.slice(funstr.indexOf('(') + 1, funstr.indexOf(')')).match(/([^\s,]+)/g) || new Array();
if (params.includes('=')) {
throw new Error('default argument values are not supported');
}
return params;
};
const serializeApiFunction = (fn, variables) => {
const variableInitializer = [];
const functionParams = getFunctionParams(fn);
if (variables) {
for (const varName of Object.keys(variables)) {
let varValue = variables[varName];
if (varValue === undefined || varValue === null || typeof varValue === 'number' || typeof varValue === 'boolean') {
varValue = `${varValue}`;
}
else if (typeof varValue === 'string') {
varValue = `${serializeJavascript(variables[varName], SERIALIZE_JAVASCRIPT_OPTIONS)}`;
}
else {
varValue = `eval('(${serializeJavascript(variables[varName], SERIALIZE_JAVASCRIPT_OPTIONS).replace(/'/g, "\\'")})')`;
}
variableInitializer.push(`const ${varName} = ${varValue};`);
}
}
const functionString = fn.toString();
const serializedHeader = `return ((${functionParams.map((_param, index) => `____arg${index}`).join(',')}) => {`;
const serializedParams = `${functionParams
.map((_param, index) => `const ${functionParams[index]} = ____arg${index};`)
.join('\n')}`;
const serializedVariables = `${variableInitializer.join('\n')}`;
// prettier-ignore
const serializedFunction = `return (${functionString.substring(0, functionString.indexOf('('))}()${functionString.substring(functionString.indexOf(')') + 1)})(); })(${functionParams.map((_param, index) => `____arg${index}`).join(',')})`;
if (!serializedParams && !serializedVariables) {
return `${serializedHeader}${serializedFunction}`;
}
return [serializedHeader, serializedParams, serializedVariables, serializedFunction].filter((x) => !!x).join('\n');
};
const keysOf = (obj) => {
return Array.from(Object.keys(obj));
};
const serializeRemotableFunctions = (obj) => {
const objectKeys = keysOf(obj);
if (!objectKeys || objectKeys.length === 0) {
return obj;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const output = {};
for (const key of objectKeys) {
if (typeof obj[key] === 'object') {
if ('localFn' in obj[key]) {
const remotableFunction = obj[key];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
output[key] = {
remoteFn: serializeApiFunction(remotableFunction.localFn, remotableFunction.localVariables),
localFn: remotableFunction.localFn.toString(),
localVariables: serializeJavascript(remotableFunction.localVariables, SERIALIZE_JAVASCRIPT_OPTIONS),
};
}
else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
output[key] = serializeRemotableFunctions(obj[key]);
}
}
else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
output[key] = obj[key];
}
}
return output;
};
export class Client {
// TODO websockets connection to API and hooks `onIntereceptedRequest`, `onInterceptedResponse`
options;
get baseUrl() {
return `${this.options.protocol}://${this.options.host}${this.options.port ? `:${this.options.port}` : ''}`;
}
constructor(options) {
this.options = {
...stuntmanConfig.client,
...options,
port: options?.port || (options?.protocol ? (options.protocol === 'https' ? 443 : 80) : stuntmanConfig.client.port),
};
}
async fetch(url, init) {
const controller = new AbortController();
const timeout = setTimeout(() => {
controller.abort();
}, this.options.timeout);
try {
const response = await fetch(url, {
...init,
headers: {
...(this.options.apiKey && { 'x-api-key': this.options.apiKey }),
...init?.headers,
},
signal: init?.signal ?? controller.signal,
});
if (!response.ok) {
const text = await response.text();
let json;
try {
json = JSON.parse(text);
}
catch {
// ignore
}
if (json && 'error' in json) {
throw new ClientError(json.error);
}
throw new Error(`Unexpected errror: ${text}`);
}
return response;
}
finally {
clearTimeout(timeout);
}
}
async getRules() {
const response = await this.fetch(`${this.baseUrl}/rules`);
return (await response.json());
}
async getRule(id) {
const response = await this.fetch(`${this.baseUrl}/rule/${encodeURIComponent(id)}`);
return (await response.json());
}
async disableRule(id) {
await this.fetch(`${this.baseUrl}/rule/${encodeURIComponent(id)}/disable`);
}
async enableRule(id) {
await this.fetch(`${this.baseUrl}/rule/${encodeURIComponent(id)}/enable`);
}
async removeRule(id) {
await this.fetch(`${this.baseUrl}/rule/${encodeURIComponent(id)}/remove`);
}
async addRule(rule) {
const serializedRule = serializeRemotableFunctions(rule);
const response = await this.fetch(`${this.baseUrl}/rule`, {
method: 'POST',
body: JSON.stringify(serializedRule),
headers: { 'content-type': 'application/json' },
});
return (await response.json());
}
async getTraffic(ruleOrIdOrLabel) {
const ruleId = typeof ruleOrIdOrLabel === 'object' ? ruleOrIdOrLabel.id : ruleOrIdOrLabel;
const response = await this.fetch(`${this.baseUrl}/traffic${ruleId ? `/${encodeURIComponent(ruleId)}` : ''}`);
return (await response.json());
}
}
//# sourceMappingURL=apiClient.js.map