UNPKG

@stuntman/client

Version:

Stuntman - HTTP proxy / mock API client

163 lines 6.69 kB
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