UNPKG

mappersmith

Version:

It is a lightweight rest client for node.js and the browser

1 lines 81.3 kB
{"version":3,"sources":["../../src/method-descriptor.ts","../../src/utils/index.ts","../../src/manifest.ts","../../src/request.ts","../../src/client-builder.ts","../../src/response.ts","../../src/version.ts","../../src/mappersmith.ts","../../src/gateway/timeout-error.ts","../../src/gateway/gateway.ts","../../src/gateway/xhr.ts","../../src/gateway/fetch.ts","../../src/index.ts"],"sourcesContent":["import type { Headers, RequestParams, ParameterEncoderFn, Params } from './types'\nimport type { Middleware } from './middleware/index'\n\nexport interface MethodDescriptorParams {\n allowResourceHostOverride?: boolean\n authAttr?: string\n binary?: boolean\n bodyAttr?: string\n headers?: Headers\n headersAttr?: string\n host: string\n hostAttr?: string\n method?: string\n middleware?: Array<Middleware>\n middlewares?: Array<Middleware>\n parameterEncoder?: ParameterEncoderFn\n params?: Params\n path: string | ((args: RequestParams) => string)\n pathAttr?: string\n queryParamAlias?: Record<string, string>\n signalAttr?: string\n timeoutAttr?: string\n}\n\n/**\n * @typedef MethodDescriptor\n * @param {MethodDescriptorParams} params\n * @param {boolean} params.allowResourceHostOverride\n * @param {Function} params.parameterEncoder\n * @param {String} params.authAttr - auth attribute name. Default: 'auth'\n * @param {boolean} params.binary\n * @param {String} params.bodyAttr - body attribute name. Default: 'body'\n * @param {Headers} params.headers\n * @param {String} params.headersAttr - headers attribute name. Default: 'headers'\n * @param {String} params.host\n * @param {String} params.hostAttr - host attribute name. Default: 'host'\n * @param {String} params.method\n * @param {Middleware[]} params.middleware\n * @param {Middleware[]} params.middlewares - alias for middleware\n * @param {RequestParams} params.params\n * @param {String|Function} params.path\n * @param {String} params.pathAttr. Default: 'path'\n * @param {Object} params.queryParamAlias\n * @param {Number} params.signalAttr - signal attribute name. Default: 'signal'\n * @param {Number} params.timeoutAttr - timeout attribute name. Default: 'timeout'\n */\nexport class MethodDescriptor {\n public readonly allowResourceHostOverride: boolean\n public readonly authAttr: string\n public readonly binary: boolean\n public readonly bodyAttr: string\n public readonly headers?: Headers\n public readonly headersAttr: string\n public readonly host: string\n public readonly hostAttr: string\n public readonly method: string\n public readonly middleware: Middleware[]\n public readonly parameterEncoder: ParameterEncoderFn\n public readonly params?: RequestParams\n public readonly path: string | ((args: RequestParams) => string)\n public readonly pathAttr: string\n public readonly queryParamAlias: Record<string, string>\n public readonly signalAttr: string\n public readonly timeoutAttr: string\n\n constructor(params: MethodDescriptorParams) {\n this.allowResourceHostOverride = params.allowResourceHostOverride || false\n this.binary = params.binary || false\n this.headers = params.headers\n this.host = params.host\n this.method = params.method || 'get'\n this.parameterEncoder = params.parameterEncoder || encodeURIComponent\n this.params = params.params\n this.path = params.path\n this.queryParamAlias = params.queryParamAlias || {}\n\n this.authAttr = params.authAttr || 'auth'\n this.bodyAttr = params.bodyAttr || 'body'\n this.headersAttr = params.headersAttr || 'headers'\n this.hostAttr = params.hostAttr || 'host'\n this.pathAttr = params.pathAttr || 'path'\n this.signalAttr = params.signalAttr || 'signal'\n this.timeoutAttr = params.timeoutAttr || 'timeout'\n\n const resourceMiddleware = params.middleware || params.middlewares || []\n this.middleware = resourceMiddleware\n }\n}\n\nexport default MethodDescriptor\n","import type { Primitive, NestedParam, Hash, NestedParamArray } from '../types'\n\nlet _process: NodeJS.Process,\n getNanoSeconds: (() => number) | undefined,\n loadTime: number | undefined\n\ntry {\n // eslint-disable-next-line no-eval\n _process = eval(\n 'typeof __TEST_WEB__ === \"undefined\" && typeof process === \"object\" ? process : undefined'\n )\n} catch (e) {} // eslint-disable-line no-empty\n\nconst hasProcessHrtime = () => {\n return typeof _process !== 'undefined' && _process !== null && _process.hrtime\n}\n\nif (hasProcessHrtime()) {\n getNanoSeconds = () => {\n const hr = _process.hrtime()\n return hr[0] * 1e9 + hr[1]\n }\n loadTime = getNanoSeconds()\n}\n\nconst R20 = /%20/g\n\nconst isNeitherNullNorUndefined = <T>(x: T | undefined | null): x is T =>\n x !== null && x !== undefined\n\nexport const validKeys = (entry: Record<string, unknown>) =>\n Object.keys(entry).filter((key) => isNeitherNullNorUndefined(entry[key]))\n\nexport const buildRecursive = (\n key: string,\n value: Primitive | NestedParam | NestedParamArray,\n suffix = '',\n encoderFn = encodeURIComponent\n): string => {\n if (Array.isArray(value)) {\n return value.map((v) => buildRecursive(key, v, suffix + '[]', encoderFn)).join('&')\n }\n\n if (typeof value !== 'object') {\n return `${encoderFn(key + suffix)}=${encoderFn(value)}`\n }\n\n return Object.keys(value)\n .map((nestedKey) => {\n const nestedValue = value[nestedKey]\n if (isNeitherNullNorUndefined(nestedValue)) {\n return buildRecursive(key, nestedValue, suffix + '[' + nestedKey + ']', encoderFn)\n }\n return null\n })\n .filter(isNeitherNullNorUndefined)\n .join('&')\n}\n\nexport const toQueryString = (\n entry: undefined | null | Primitive | NestedParam,\n encoderFn = encodeURIComponent\n) => {\n if (!isPlainObject(entry)) {\n return entry\n }\n\n return Object.keys(entry)\n .map((key) => {\n const value = entry[key]\n if (isNeitherNullNorUndefined(value)) {\n return buildRecursive(key, value, '', encoderFn)\n }\n return null\n })\n .filter(isNeitherNullNorUndefined)\n .join('&')\n .replace(R20, '+')\n}\n\n/**\n * Gives time in milliseconds, but with sub-millisecond precision for Browser\n * and Nodejs\n */\nexport const performanceNow = () => {\n if (hasProcessHrtime() && getNanoSeconds !== undefined) {\n const now = getNanoSeconds()\n if (now !== undefined && loadTime !== undefined) {\n return (now - loadTime) / 1e6\n }\n }\n\n return Date.now()\n}\n\n/**\n * borrowed from: {@link https://gist.github.com/monsur/706839}\n * XmlHttpRequest's getAllResponseHeaders() method returns a string of response\n * headers according to the format described here:\n * {@link http://www.w3.org/TR/XMLHttpRequest/#the-getallresponseheaders-method}\n * This method parses that string into a user-friendly key/value pair object.\n */\nexport const parseResponseHeaders = (headerStr: string) => {\n const headers: Hash = {}\n if (!headerStr) {\n return headers\n }\n\n const headerPairs = headerStr.split('\\u000d\\u000a')\n for (let i = 0; i < headerPairs.length; i++) {\n const headerPair = headerPairs[i]\n // Can't use split() here because it does the wrong thing\n // if the header value has the string \": \" in it.\n const index = headerPair.indexOf('\\u003a\\u0020')\n if (index > 0) {\n const key = headerPair.substring(0, index).toLowerCase().trim()\n const val = headerPair.substring(index + 2).trim()\n headers[key] = val\n }\n }\n return headers\n}\n\nexport const lowerCaseObjectKeys = (obj: Hash) => {\n return Object.keys(obj).reduce((target, key) => {\n target[key.toLowerCase()] = obj[key]\n return target\n }, {} as Hash)\n}\n\nconst hasOwnProperty = Object.prototype.hasOwnProperty\nexport const assign =\n Object.assign ||\n function (target: Hash) {\n for (let i = 1; i < arguments.length; i++) {\n // eslint-disable-next-line prefer-rest-params\n const source = arguments[i]\n for (const key in source) {\n if (hasOwnProperty.call(source, key)) {\n target[key] = source[key]\n }\n }\n }\n return target\n }\n\nconst toString = Object.prototype.toString\nexport const isPlainObject = (value: unknown): value is Record<string, unknown> => {\n return (\n toString.call(value) === '[object Object]' &&\n Object.getPrototypeOf(value) === Object.getPrototypeOf({})\n )\n}\n\nexport const isObject = (value: unknown): value is Record<string, unknown> => {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * borrowed from: {@link https://github.com/davidchambers/Base64.js}\n */\nconst CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='\nexport const btoa = (input: object | Primitive | null) => {\n let output = ''\n let map = CHARS\n const str = String(input)\n for (\n // initialize result and counter\n let block = 0, charCode: number, idx = 0;\n // if the next str index does not exist:\n // change the mapping table to \"=\"\n // check if d has no fractional digits\n str.charAt(idx | 0) || ((map = '='), idx % 1);\n // \"8 - idx % 1 * 8\" generates the sequence 2, 4, 6, 8\n output += map.charAt(63 & (block >> (8 - (idx % 1) * 8)))\n ) {\n charCode = str.charCodeAt((idx += 3 / 4))\n if (charCode > 0xff) {\n throw new Error(\n \"[Mappersmith] 'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.\"\n )\n }\n block = (block << 8) | charCode\n }\n return output\n}\n","import { MethodDescriptor, MethodDescriptorParams } from './method-descriptor'\nimport { assign } from './utils/index'\nimport type { ParameterEncoderFn } from './types'\nimport type { GatewayConfiguration } from './gateway/types'\nimport type { Gateway } from './gateway/index'\nimport { Context, Middleware, MiddlewareDescriptor, MiddlewareParams } from './middleware/index'\n\nexport interface GlobalConfigs {\n context: Context\n middleware: Middleware[]\n Promise: PromiseConstructor | null\n fetch: typeof fetch | null\n gateway: typeof Gateway | null\n gatewayConfigs: GatewayConfiguration\n maxMiddlewareStackExecutionAllowed: number\n}\n\nexport type ResourceTypeConstraint = {\n [resourceName: string]: {\n [methodName: string]: Omit<MethodDescriptorParams, 'host'> & { host?: string }\n }\n}\n\nexport interface ManifestOptions<Resources extends ResourceTypeConstraint> {\n allowResourceHostOverride?: boolean\n authAttr?: string\n bodyAttr?: string\n clientId?: string\n gatewayConfigs?: Partial<GatewayConfiguration>\n headersAttr?: string\n host: string\n hostAttr?: string\n middleware?: Middleware[]\n parameterEncoder?: ParameterEncoderFn\n resources?: Resources\n signalAttr?: string\n timeoutAttr?: string\n /**\n * @deprecated - use `middleware` instead\n */\n middlewares?: Middleware[]\n ignoreGlobalMiddleware?: boolean\n}\n\nexport type Method = { name: string; descriptor: MethodDescriptor }\ntype EachResourceCallbackFn = (name: string, methods: Method[]) => void\ntype EachMethodCallbackFn = (name: string) => Method\ntype CreateMiddlewareParams = Partial<Omit<MiddlewareParams, 'resourceName' | 'resourceMethod'>> &\n Pick<MiddlewareParams, 'resourceName' | 'resourceMethod'>\n/**\n * @typedef Manifest\n * @param {Object} obj\n * @param {Object} obj.gatewayConfigs - default: base values from mappersmith\n * @param {Object} obj.ignoreGlobalMiddleware - default: false\n * @param {Object} obj.resources - default: {}\n * @param {Array} obj.middleware or obj.middlewares - default: []\n * @param {Object} globalConfigs\n */\nexport class Manifest<Resources extends ResourceTypeConstraint> {\n public allowResourceHostOverride: boolean\n public authAttr?: string\n public bodyAttr?: string\n public clientId: string | null\n public context: Context\n public gatewayConfigs: GatewayConfiguration\n public headersAttr?: string\n public host: string\n public hostAttr?: string\n public middleware: Middleware[]\n public parameterEncoder: ParameterEncoderFn\n public resources: Resources\n public signalAttr?: string\n public timeoutAttr?: string\n\n constructor(\n options: ManifestOptions<Resources>,\n { gatewayConfigs, middleware = [], context = {} }: GlobalConfigs\n ) {\n this.allowResourceHostOverride = options.allowResourceHostOverride || false\n this.authAttr = options.authAttr\n this.bodyAttr = options.bodyAttr\n this.clientId = options.clientId || null\n this.context = context\n this.gatewayConfigs = assign({}, gatewayConfigs, options.gatewayConfigs)\n this.headersAttr = options.headersAttr\n this.host = options.host\n this.hostAttr = options.hostAttr\n this.parameterEncoder = options.parameterEncoder || encodeURIComponent\n this.resources = options.resources || ({} as Resources)\n this.signalAttr = options.signalAttr\n this.timeoutAttr = options.timeoutAttr\n\n // TODO: deprecate obj.middlewares in favor of obj.middleware\n const clientMiddleware = options.middleware || options.middlewares || []\n\n if (options.ignoreGlobalMiddleware) {\n this.middleware = clientMiddleware\n } else {\n this.middleware = [...clientMiddleware, ...middleware]\n }\n }\n\n public eachResource(callback: EachResourceCallbackFn) {\n Object.keys(this.resources).forEach((resourceName) => {\n const methods = this.eachMethod(resourceName, (methodName) => ({\n name: methodName,\n descriptor: this.createMethodDescriptor(resourceName, methodName),\n }))\n\n callback(resourceName, methods)\n })\n }\n\n private eachMethod(resourceName: string, callback: EachMethodCallbackFn) {\n return Object.keys(this.resources[resourceName]).map(callback)\n }\n\n public createMethodDescriptor(resourceName: string, methodName: string) {\n const definition = this.resources[resourceName][methodName]\n if (!definition || !['string', 'function'].includes(typeof definition.path)) {\n throw new Error(\n `[Mappersmith] path is undefined for resource \"${resourceName}\" method \"${methodName}\"`\n )\n }\n return new MethodDescriptor(\n assign(\n {\n host: this.host,\n allowResourceHostOverride: this.allowResourceHostOverride,\n parameterEncoder: this.parameterEncoder,\n bodyAttr: this.bodyAttr,\n headersAttr: this.headersAttr,\n authAttr: this.authAttr,\n timeoutAttr: this.timeoutAttr,\n hostAttr: this.hostAttr,\n signalAttr: this.signalAttr,\n },\n definition\n )\n )\n }\n\n /**\n * @param {Object} args\n * @param {String|Null} args.clientId\n * @param {String} args.resourceName\n * @param {String} args.resourceMethod\n * @param {Object} args.context\n * @param {Boolean} args.mockRequest\n *\n * @return {Array<Object>}\n */\n public createMiddleware(args: CreateMiddlewareParams) {\n const createInstance = (middlewareFactory: Middleware) => {\n const defaultDescriptor: MiddlewareDescriptor = {\n __name: middlewareFactory.name || middlewareFactory.toString(),\n response(next) {\n return next()\n },\n /**\n * @since 2.27.0\n * Replaced the request method\n */\n prepareRequest(next) {\n return this.request ? next().then((req) => this.request?.(req)) : next()\n },\n }\n\n const middlewareParams = assign(args, {\n clientId: this.clientId,\n context: assign({}, this.context),\n })\n\n return assign(defaultDescriptor, middlewareFactory(middlewareParams))\n }\n\n const { resourceName: name, resourceMethod: method } = args\n const resourceMiddleware = this.createMethodDescriptor(name, method).middleware\n const middlewares = [...resourceMiddleware, ...this.middleware]\n\n return middlewares.map(createInstance)\n }\n}\n\nexport default Manifest\n","import { MethodDescriptor } from './method-descriptor'\nimport { toQueryString, lowerCaseObjectKeys, assign } from './utils/index'\nimport type {\n Auth,\n Body,\n Headers,\n NestedParam,\n NestedParamArray,\n Primitive,\n RequestParams,\n} from './types'\n\nconst REGEXP_DYNAMIC_SEGMENT = /{([^}?]+)\\??}/\nconst REGEXP_OPTIONAL_DYNAMIC_SEGMENT = /\\/?{([^}?]+)\\?}/g\nconst REGEXP_TRAILING_SLASH = /\\/$/\n\nexport type RequestContext = Record<string, unknown>\n\n/**\n * Removes the object type without removing Record types in the union\n */\nexport type ExcludeObject<T> = T extends object ? (object extends T ? never : T) : T\n\n/**\n * @typedef Request\n * @param {MethodDescriptor} methodDescriptor\n * @param {RequestParams} requestParams, defaults to an empty object ({})\n * @param {RequestContext} request context store, defaults to an empty object ({})\n */\nexport class Request {\n public methodDescriptor: MethodDescriptor\n public requestParams: RequestParams\n private requestContext: RequestContext\n\n constructor(\n methodDescriptor: MethodDescriptor,\n requestParams: RequestParams = {},\n requestContext: RequestContext = {}\n ) {\n this.methodDescriptor = methodDescriptor\n this.requestParams = requestParams\n this.requestContext = requestContext\n }\n\n private isParam(key: string) {\n return (\n key !== this.methodDescriptor.headersAttr &&\n key !== this.methodDescriptor.bodyAttr &&\n key !== this.methodDescriptor.authAttr &&\n key !== this.methodDescriptor.timeoutAttr &&\n key !== this.methodDescriptor.hostAttr &&\n key !== this.methodDescriptor.signalAttr &&\n key !== this.methodDescriptor.pathAttr\n )\n }\n\n public params() {\n const params = assign({}, this.methodDescriptor.params, this.requestParams)\n\n return Object.keys(params).reduce((obj, key) => {\n if (this.isParam(key)) {\n obj[key] = params[key]\n }\n return obj\n }, {} as RequestParams)\n }\n\n /**\n * Returns the request context; a key value object.\n * Useful to pass information from upstream middleware to a downstream one.\n */\n public context<T extends RequestContext = RequestContext>() {\n return this.requestContext as T\n }\n\n /**\n * Returns the HTTP method in lowercase\n */\n public method() {\n return this.methodDescriptor.method.toLowerCase()\n }\n\n /**\n * Returns host name without trailing slash\n * Example: 'http://example.org'\n */\n public host() {\n const { allowResourceHostOverride, hostAttr, host } = this.methodDescriptor\n const originalHost = allowResourceHostOverride\n ? this.requestParams[hostAttr] || host || ''\n : host || ''\n\n if (typeof originalHost === 'string') {\n return originalHost.replace(REGEXP_TRAILING_SLASH, '')\n }\n\n return ''\n }\n\n /**\n * Returns path with parameters and leading slash.\n * Example: '/some/path?param1=true'\n *\n * @throws {Error} if any dynamic segment is missing.\n * Example:\n * Imagine the path '/some/{name}', the error will be similar to:\n * '[Mappersmith] required parameter missing (name), \"/some/{name}\" cannot be resolved'\n */\n public path() {\n const { pathAttr: mdPathAttr, path: mdPath } = this.methodDescriptor\n const originalPath = (this.requestParams[mdPathAttr] as RequestParams['path']) || mdPath || ''\n const params = this.params()\n\n let path: string\n if (typeof originalPath === 'function') {\n path = originalPath(params)\n if (typeof path !== 'string') {\n throw new Error(\n `[Mappersmith] method descriptor function did not return a string, params=${JSON.stringify(\n params\n )}`\n )\n }\n } else {\n path = originalPath\n }\n\n // RegExp with 'g'-flag is stateful, therefore defining it locally\n const regexp = new RegExp(REGEXP_DYNAMIC_SEGMENT, 'g')\n\n const dynamicSegmentKeys = []\n let match\n while ((match = regexp.exec(path)) !== null) {\n dynamicSegmentKeys.push(match[1])\n }\n\n for (const key of dynamicSegmentKeys) {\n const pattern = new RegExp(`{${key}\\\\??}`, 'g')\n const value = params[key]\n if (value != null && typeof value !== 'object') {\n path = path.replace(pattern, this.methodDescriptor.parameterEncoder(value))\n delete params[key]\n }\n }\n\n path = path.replace(REGEXP_OPTIONAL_DYNAMIC_SEGMENT, '')\n\n const missingDynamicSegmentMatch = path.match(REGEXP_DYNAMIC_SEGMENT)\n if (missingDynamicSegmentMatch) {\n throw new Error(\n `[Mappersmith] required parameter missing (${missingDynamicSegmentMatch[1]}), \"${path}\" cannot be resolved`\n )\n }\n\n const aliasedParams = Object.keys(params).reduce(\n (aliased, key) => {\n const aliasedKey = this.methodDescriptor.queryParamAlias[key] || key\n const value = params[key]\n if (value != null) {\n /**\n * Here we use `ExcludeObject` to surgically remove the `object` type from `value`.\n * We need it as `object` is too broad to be useful, whereas `value` is also typed\n * as NestedParam, which is the correct shape for param objects.\n */\n aliased[aliasedKey] = value as ExcludeObject<typeof value>\n }\n return aliased\n },\n {} as Record<string, Primitive | NestedParam | NestedParamArray>\n )\n\n const queryString = toQueryString(aliasedParams, this.methodDescriptor.parameterEncoder)\n if (typeof queryString === 'string' && queryString.length !== 0) {\n const hasQuery = path.includes('?')\n path += `${hasQuery ? '&' : '?'}${queryString}`\n }\n\n // https://www.rfc-editor.org/rfc/rfc1738#section-3.3\n if (path[0] !== '/' && path.length > 0) {\n path = `/${path}`\n }\n\n return path\n }\n\n /**\n * Returns the template path, without params, before interpolation.\n * If path is a function, returns the result of request.path()\n * Example: '/some/{param}/path'\n */\n public pathTemplate() {\n const path = this.methodDescriptor.path\n\n const prependSlash = (str: string) => (str[0] !== '/' ? `/${str}` : str)\n\n if (typeof path === 'function') {\n return prependSlash(path(this.params()))\n }\n\n return prependSlash(path)\n }\n\n /**\n * Returns the full URL\n * Example: http://example.org/some/path?param1=true\n *\n */\n public url() {\n return `${this.host()}${this.path()}`\n }\n\n /**\n * Returns an object with the headers. Header names are converted to\n * lowercase\n */\n public headers() {\n const headerAttr = this.methodDescriptor.headersAttr\n const headers = (this.requestParams[headerAttr] || {}) as Headers\n\n if (typeof headers === 'function') {\n return headers\n }\n\n const mergedHeaders = { ...this.methodDescriptor.headers, ...headers } as Headers\n return lowerCaseObjectKeys(mergedHeaders) as Headers\n }\n\n /**\n * Utility method to get a header value by name\n */\n public header<T extends string | number | boolean>(name: string): T | undefined {\n const key = name.toLowerCase()\n\n if (key in this.headers()) {\n return this.headers()[key] as T\n }\n\n return undefined\n }\n\n public body() {\n return this.requestParams[this.methodDescriptor.bodyAttr] as Body | undefined\n }\n\n public auth() {\n return this.requestParams[this.methodDescriptor.authAttr] as Auth | undefined\n }\n\n public timeout() {\n return this.requestParams[this.methodDescriptor.timeoutAttr] as number | undefined\n }\n\n public signal() {\n return this.requestParams[this.methodDescriptor.signalAttr] as AbortSignal | undefined\n }\n\n /**\n * Enhances current request returning a new Request\n * @param {RequestParams} extras\n * @param {Object} extras.auth - it will replace the current auth\n * @param {String|Object} extras.body - it will replace the current body\n * @param {Headers} extras.headers - it will be merged with current headers\n * @param {String} extras.host - it will replace the current timeout\n * @param {RequestParams} extras.params - it will be merged with current params\n * @param {Number} extras.timeout - it will replace the current timeout\n * @param {Object} requestContext - Use to pass information between different middleware.\n */\n public enhance(extras: RequestParams, requestContext?: RequestContext) {\n const authKey = this.methodDescriptor.authAttr\n const bodyKey = this.methodDescriptor.bodyAttr\n const headerKey = this.methodDescriptor.headersAttr\n const hostKey = this.methodDescriptor.hostAttr\n const timeoutKey = this.methodDescriptor.timeoutAttr\n const pathKey = this.methodDescriptor.pathAttr\n const signalKey = this.methodDescriptor.signalAttr\n\n // Note: The result of merging an instance of RequestParams with instance of Params\n // is simply a RequestParams with even more [param: string]'s on it.\n const requestParams: RequestParams = assign({}, this.requestParams, extras.params)\n\n const headers = this.requestParams[headerKey] as Headers | undefined\n const mergedHeaders = assign({}, headers, extras.headers)\n requestParams[headerKey] = mergedHeaders\n\n extras.auth && (requestParams[authKey] = extras.auth)\n extras.body && (requestParams[bodyKey] = extras.body)\n extras.host && (requestParams[hostKey] = extras.host)\n extras.timeout && (requestParams[timeoutKey] = extras.timeout)\n extras.path && (requestParams[pathKey] = extras.path)\n extras.signal && (requestParams[signalKey] = extras.signal)\n\n const nextContext = { ...this.requestContext, ...requestContext }\n\n return new Request(this.methodDescriptor, requestParams, nextContext)\n }\n\n /**\n * Is the request expecting a binary response?\n */\n public isBinary() {\n return this.methodDescriptor.binary\n }\n}\n\nexport default Request\n","import {\n Manifest,\n ManifestOptions,\n GlobalConfigs,\n Method,\n ResourceTypeConstraint,\n} from './manifest'\nimport { Response } from './response'\nimport { Request, RequestContext } from './request'\nimport type { MiddlewareDescriptor, RequestGetter, ResponseGetter } from './middleware/index'\nimport { Gateway } from './gateway/index'\nimport type { Params } from './types'\n\nexport type AsyncFunction = (params?: Params, context?: RequestContext) => Promise<Response>\n\nexport type AsyncFunctions<HashType> = {\n [Key in keyof HashType]: AsyncFunction\n}\n\nexport type Client<ResourcesType> = {\n [ResourceKey in keyof ResourcesType]: AsyncFunctions<ResourcesType[ResourceKey]>\n}\n\ninterface RequestPhaseFailureContext {\n middleware: string | null\n returnedInvalidRequest: boolean\n abortExecution: boolean\n}\n\nconst isFactoryConfigured = <T>(factory: () => T | null): factory is () => T => {\n if (!factory || !factory()) {\n return false\n }\n return true\n}\n\n/**\n * @typedef ClientBuilder\n * @param {Object} manifestDefinition - manifest definition with at least the `resources` key\n * @param {Function} GatewayClassFactory - factory function that returns a gateway class\n */\nexport class ClientBuilder<Resources extends ResourceTypeConstraint> {\n public Promise: PromiseConstructor\n public manifest: Manifest<Resources>\n public GatewayClassFactory: () => typeof Gateway\n public maxMiddlewareStackExecutionAllowed: number\n\n constructor(\n manifestDefinition: ManifestOptions<Resources>,\n GatewayClassFactory: () => typeof Gateway | null,\n configs: GlobalConfigs\n ) {\n if (!manifestDefinition) {\n throw new Error(`[Mappersmith] invalid manifest (${manifestDefinition})`)\n }\n\n if (!isFactoryConfigured(GatewayClassFactory)) {\n throw new Error('[Mappersmith] gateway class not configured (configs.gateway)')\n }\n\n if (!configs.Promise) {\n throw new Error('[Mappersmith] Promise not configured (configs.Promise)')\n }\n\n this.Promise = configs.Promise\n this.manifest = new Manifest(manifestDefinition, configs)\n this.GatewayClassFactory = GatewayClassFactory\n this.maxMiddlewareStackExecutionAllowed = configs.maxMiddlewareStackExecutionAllowed\n }\n\n public build() {\n const client: Client<Resources> = { _manifest: this.manifest } as never\n\n this.manifest.eachResource((resourceName: keyof Resources, methods) => {\n client[resourceName] = this.buildResource(resourceName, methods)\n })\n\n return client\n }\n\n private buildResource<T, K extends keyof T = keyof T>(resourceName: K, methods: Method[]) {\n type Resource = AsyncFunctions<T[K]>\n const initialResourceValue: Partial<Resource> = {}\n\n const resource = methods.reduce((resource, method) => {\n const resourceMethod = (requestParams?: Params, context?: RequestContext) => {\n const request = new Request(method.descriptor, requestParams, context)\n // `resourceName` can be `PropertyKey`, making this `string | number | Symbol`, therefore the string conversion\n // to stop type bleeding.\n return this.invokeMiddlewares(String(resourceName), method.name, request)\n }\n return {\n ...resource,\n [method.name]: resourceMethod,\n }\n }, initialResourceValue)\n\n // @hint: This type assert is needed as the compiler cannot be made to understand that the reduce produce a\n // non-partial result on a partial input. This is due to a shortcoming of the type signature for Array<T>.reduce().\n // @link: https://github.com/microsoft/TypeScript/blob/v3.7.2/lib/lib.es5.d.ts#L1186\n return resource as Resource\n }\n\n private invokeMiddlewares(resourceName: string, resourceMethod: string, initialRequest: Request) {\n const middleware = this.manifest.createMiddleware({ resourceName, resourceMethod })\n const GatewayClass = this.GatewayClassFactory()\n const gatewayConfigs = this.manifest.gatewayConfigs\n const requestPhaseFailureContext: RequestPhaseFailureContext = {\n middleware: null,\n returnedInvalidRequest: false,\n abortExecution: false,\n }\n\n const getInitialRequest = () => this.Promise.resolve(initialRequest)\n const chainRequestPhase = (next: RequestGetter, middleware: MiddlewareDescriptor) => () => {\n const abort = (error: Error) => {\n requestPhaseFailureContext.abortExecution = true\n throw error\n }\n\n return this.Promise.resolve()\n .then(() => middleware.prepareRequest(next, abort))\n .then((request: unknown) => {\n if (request instanceof Request) {\n return request\n }\n\n // FIXME: Here be dragons: prepareRequest is typed as Promise<Response | void>\n // but this code clearly expects it can be something else... anything.\n // Hence manual cast to `unknown` above.\n requestPhaseFailureContext.returnedInvalidRequest = true\n const typeValue = typeof request\n const prettyType =\n typeValue === 'object' || typeValue === 'function'\n ? // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (request as any).name || typeValue\n : typeValue\n\n throw new Error(\n `[Mappersmith] middleware \"${middleware.__name}\" should return \"Request\" but returned \"${prettyType}\"`\n )\n })\n .catch((e) => {\n requestPhaseFailureContext.middleware = middleware.__name || null\n throw e\n })\n }\n\n const prepareRequest = middleware.reduce(chainRequestPhase, getInitialRequest)\n let executions = 0\n\n const executeMiddlewareStack = () =>\n prepareRequest()\n .catch((e) => {\n const { returnedInvalidRequest, abortExecution, middleware } = requestPhaseFailureContext\n if (returnedInvalidRequest || abortExecution) {\n throw e\n }\n\n const error = new Error(\n `[Mappersmith] middleware \"${middleware}\" failed in the request phase: ${e.message}`\n )\n error.stack = e.stack\n throw error\n })\n .then((finalRequest) => {\n executions++\n\n if (executions > this.maxMiddlewareStackExecutionAllowed) {\n throw new Error(\n `[Mappersmith] infinite loop detected (middleware stack invoked ${executions} times). Check the use of \"renew\" in one of the middleware.`\n )\n }\n\n const renew = executeMiddlewareStack\n const chainResponsePhase =\n (previousValue: ResponseGetter, currentValue: MiddlewareDescriptor) => () => {\n // Deliberately putting this on two separate lines - to get typescript to not return \"any\"\n const nextValue = currentValue.response(previousValue, renew, finalRequest)\n return nextValue\n }\n const callGateway = () => new GatewayClass(finalRequest, gatewayConfigs).call()\n const execute = middleware.reduce(chainResponsePhase, callGateway)\n return execute()\n })\n\n return new this.Promise<Response>((resolve, reject) => {\n executeMiddlewareStack()\n .then((response) => resolve(response))\n .catch(reject)\n })\n }\n}\n\nexport default ClientBuilder\n","import { lowerCaseObjectKeys } from './utils/index'\nimport { Request } from './request'\nimport type { Headers } from './types'\n\nexport const REGEXP_CONTENT_TYPE_JSON = /^application\\/(json|.*\\+json)/\n\nexport interface ResponseParams {\n readonly status?: number\n readonly rawData?: string\n readonly headers?: Headers\n readonly error?: Error\n}\n\n/**\n * @typedef Response\n * @param {Request} originalRequest, for auth it hides the password\n * @param {Integer} responseStatus\n * @param {String} responseData, defaults to null\n * @param {Object} responseHeaders, defaults to an empty object ({})\n * @param {Array<Error>} errors, defaults to an empty array ([])\n */\ntype SerializableJSON = number | string | boolean | null | Record<string, unknown>\nexport type ParsedJSON = SerializableJSON | SerializableJSON[]\nexport class Response<DataType extends ParsedJSON = ParsedJSON> {\n public readonly originalRequest: Request\n public readonly responseStatus: number\n public readonly responseData: string | null\n public readonly responseHeaders: Headers\n // eslint-disable-next-line no-use-before-define\n public readonly errors: Array<Error | string>\n public timeElapsed: number | null\n\n constructor(\n originalRequest: Request,\n responseStatus: number,\n responseData?: string | null,\n responseHeaders?: Headers,\n errors?: Array<Error | string>\n ) {\n const auth = originalRequest.requestParams && originalRequest.requestParams.auth\n if (auth) {\n const maskedAuth = { ...auth, password: '***' }\n this.originalRequest = originalRequest.enhance({ auth: maskedAuth })\n } else {\n this.originalRequest = originalRequest\n }\n\n this.responseStatus = responseStatus\n this.responseData = responseData ?? null\n this.responseHeaders = responseHeaders || {}\n this.errors = errors || []\n this.timeElapsed = null\n }\n\n public request() {\n return this.originalRequest\n }\n\n public status() {\n // IE sends 1223 instead of 204\n if (this.responseStatus === 1223) {\n return 204\n }\n\n return this.responseStatus\n }\n\n /**\n * Returns true if status is greater or equal 200 or lower than 400\n */\n public success() {\n const status = this.status()\n return status >= 200 && status < 400\n }\n\n /**\n * Returns an object with the headers. Header names are converted to\n * lowercase\n */\n public headers() {\n return lowerCaseObjectKeys(this.responseHeaders)\n }\n\n /**\n * Utility method to get a header value by name\n */\n public header<T extends string | number | boolean>(name: string): T | undefined {\n const key = name.toLowerCase()\n\n if (key in this.headers()) {\n return this.headers()[key] as T\n }\n\n return undefined\n }\n\n /**\n * Returns the original response data\n */\n public rawData() {\n return this.responseData\n }\n\n /**\n * Returns the response data, if \"Content-Type\" is \"application/json\"\n * it parses the response and returns an object.\n * Friendly reminder:\n * - JSON.parse() can return null, an Array or an object.\n */\n public data<T = DataType>() {\n if (this.isContentTypeJSON() && this.responseData !== null) {\n try {\n return JSON.parse(this.responseData) as T\n } catch (e) {} // eslint-disable-line no-empty\n }\n\n return this.responseData as unknown as T\n }\n\n public isContentTypeJSON() {\n const contentType = this.header<string>('content-type')\n\n if (contentType === undefined) {\n return false\n }\n\n return REGEXP_CONTENT_TYPE_JSON.test(contentType)\n }\n\n /**\n * Returns the last error instance that caused the request to fail\n */\n public error() {\n const lastError = this.errors[this.errors.length - 1] || null\n\n if (typeof lastError === 'string') {\n return new Error(lastError)\n }\n\n return lastError\n }\n\n /**\n * Enhances current Response returning a new Response\n *\n * @param {Object} extras\n * @param {Integer} extras.status - it will replace the current status\n * @param {String} extras.rawData - it will replace the current rawData\n * @param {Object} extras.headers - it will be merged with current headers\n * @param {Error} extras.error - it will be added to the list of errors\n */\n public enhance(extras: ResponseParams) {\n const mergedHeaders = { ...this.headers(), ...(extras.headers || {}) }\n const enhancedResponse = new Response<DataType>(\n this.request(),\n extras.status || this.status(),\n extras.rawData || this.rawData(),\n mergedHeaders,\n extras.error ? [...this.errors, extras.error] : [...this.errors]\n )\n enhancedResponse.timeElapsed = this.timeElapsed\n\n return enhancedResponse\n }\n}\n\nexport default Response\n","export const version = '2.45.0'\n","import { ClientBuilder, Client } from './client-builder'\nimport { assign } from './utils/index'\nimport { GlobalConfigs, ManifestOptions, ResourceTypeConstraint } from './manifest'\nimport { Context } from './middleware/index'\n\n/**\n * Can be used to test for `instanceof Response`\n */\nexport { Response } from './response'\n\nexport { version } from './version'\n\nexport const configs: GlobalConfigs = {\n context: {},\n middleware: [],\n Promise: typeof Promise === 'function' ? Promise : null,\n fetch: typeof fetch === 'function' ? fetch : null,\n\n /**\n * The maximum amount of executions allowed before it is considered an infinite loop.\n * In the response phase of middleware, it's possible to execute a function called \"renew\",\n * which can be used to rerun the middleware stack. This feature is useful in some scenarios,\n * for example, re-fetching an invalid access token.\n\n * This configuration is used to detect infinite loops, don't increase this value too much\n * @default 2\n */\n maxMiddlewareStackExecutionAllowed: 2,\n\n /**\n * Gateway implementation, it defaults to \"lib/gateway/xhr\" for browsers and\n * \"lib/gateway/http\" for node\n */\n gateway: null,\n gatewayConfigs: {\n /**\n * Setting this option will fake PUT, PATCH and DELETE requests with a HTTP POST. It will\n * add \"_method\" and \"X-HTTP-Method-Override\" with the original requested method\n * @default false\n */\n emulateHTTP: false,\n\n /**\n * Setting this option will return HTTP status 408 (Request Timeout) when a request times\n * out. When \"false\", HTTP status 400 (Bad Request) will be used instead.\n * @default false\n */\n enableHTTP408OnTimeouts: false,\n\n XHR: {\n /**\n * Indicates whether or not cross-site Access-Control requests should be made using credentials\n * such as cookies, authorization headers or TLS client certificates.\n * Setting withCredentials has no effect on same-site requests\n *\n * https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials\n *\n * @default false\n */\n withCredentials: false,\n\n /**\n * For additional configurations to the XMLHttpRequest object.\n * @param {XMLHttpRequest} xhr\n * @default null\n */\n configure: null,\n },\n\n HTTP: {\n /**\n * Enable this option to evaluate timeout on entire request durations,\n * including DNS resolution and socket connection.\n *\n * See original nodejs issue: https://github.com/nodejs/node/pull/8101\n *\n * @default false\n */\n useSocketConnectionTimeout: false,\n /**\n * For additional configurations to the http/https module\n * For http: https://nodejs.org/api/http.html#http_http_request_options_callback\n * For https: https://nodejs.org/api/https.html#https_https_request_options_callback\n *\n * @param {object} options\n * @default null\n */\n configure: null,\n onRequestWillStart: null,\n onRequestSocketAssigned: null,\n onSocketLookup: null,\n onSocketConnect: null,\n onSocketSecureConnect: null,\n onResponseReadable: null,\n onResponseEnd: null,\n },\n\n Fetch: {\n /**\n * Indicates whether the user agent should send cookies from the other domain in the case of cross-origin\n * requests. This is similar to XHR’s withCredentials flag, but with three available values (instead of two):\n *\n * \"omit\": Never send cookies.\n * \"same-origin\": Only send cookies if the URL is on the same origin as the calling script.\n * \"include\": Always send cookies, even for cross-origin calls.\n *\n * https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials\n *\n * @default \"omit\"\n */\n credentials: 'omit',\n },\n },\n}\n\n/**\n * @deprecated Shouldn't be used, not safe for concurrent use.\n * @param {Object} context\n */\nexport const setContext = (context: Context) => {\n console.warn(\n 'The use of setContext is deprecated - you need to find another way to pass data between your middlewares.'\n )\n configs.context = assign(configs.context, context)\n}\n\nexport default function forge<Resources extends ResourceTypeConstraint>(\n manifest: ManifestOptions<Resources>\n): Client<Resources> {\n const GatewayClassFactory = () => configs.gateway\n return new ClientBuilder(manifest, GatewayClassFactory, configs).build()\n}\n\nexport { forge }\n","export const isTimeoutError = (e: Error) => {\n return e && e.name === 'TimeoutError'\n}\n\nexport const createTimeoutError = (message: string) => {\n const error = new Error(message)\n error.name = 'TimeoutError'\n return error\n}\n","import { performanceNow, toQueryString, isPlainObject } from '../utils/index'\nimport { configs as defaultConfigs } from '../mappersmith'\nimport { Request } from '../request'\nimport { Response } from '../response'\nimport { isTimeoutError } from './timeout-error'\nimport type { GatewayConfiguration } from './types'\nimport type { Primitive } from '../types'\n\nconst REGEXP_EMULATE_HTTP = /^(delete|put|patch)/i\n\nexport class Gateway {\n public request: Request\n public configs: GatewayConfiguration\n public successCallback: (res: Response) => void\n public failCallback: (res: Response) => void\n\n constructor(request: Request, configs: GatewayConfiguration) {\n this.request = request\n this.configs = configs\n this.successCallback = function () {\n return undefined\n }\n this.failCallback = function () {\n return undefined\n }\n }\n\n public get() {\n throw new Error('Not implemented')\n }\n\n public head() {\n throw new Error('Not implemented')\n }\n\n public post() {\n throw new Error('Not implemented')\n }\n\n public put() {\n throw new Error('Not implemented')\n }\n\n public patch() {\n throw new Error('Not implemented')\n }\n\n public delete() {\n throw new Error('Not implemented')\n }\n\n options() {\n return this.configs\n }\n\n shouldEmulateHTTP() {\n return this.options().emulateHTTP && REGEXP_EMULATE_HTTP.test(this.request.method())\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n call(): Promise<any> {\n const timeStart = performanceNow()\n if (!defaultConfigs.Promise) {\n throw new Error('[Mappersmith] Promise not configured (configs.Promise)')\n }\n return new defaultConfigs.Promise((resolve, reject) => {\n this.successCallback = (response) => {\n response.timeElapsed = performanceNow() - timeStart\n resolve(response)\n }\n\n this.failCallback = (response) => {\n response.timeElapsed = performanceNow() - timeStart\n reject(response)\n }\n\n try {\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n this[this.request.method()].apply(this, arguments) // eslint-disable-line prefer-spread,prefer-rest-params\n } catch (e: unknown) {\n const err: Error = e as Error\n this.dispatchClientError(err.message, err)\n }\n })\n }\n\n dispatchResponse(response: Response) {\n response.success() ? this.successCallback(response) : this.failCallback(response)\n }\n\n dispatchClientError(message: string, error: Error) {\n if (isTimeoutError(error) && this.options().enableHTTP408OnTimeouts) {\n this.failCallback(new Response(this.request, 408, message, {}, [error]))\n } else {\n this.failCallback(new Response(this.request, 400, message, {}, [error]))\n }\n }\n\n prepareBody(method: string, headers: Record<string, Primitive>) {\n let body = this.request.body()\n\n if (this.shouldEmulateHTTP()) {\n body = body || {}\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n isPlainObject(body) && ((body as any)['_method'] = method)\n headers['x-http-method-override'] = method\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const bodyString = toQueryString(body as any)\n\n if (bodyString) {\n // If it's not simple, let the browser (or the user) set it\n if (isPlainObject(body)) {\n headers['content-type'] = 'application/x-www-form-urlencoded;charset=utf-8'\n }\n }\n\n return bodyString\n }\n}\n\nexport default Gateway\n","import { Gateway } from './gateway'\nimport Response from '../response'\nimport type { Method } from './types'\nimport type { Headers } from '../types'\nimport { assign, parseResponseHeaders, btoa } from '../utils/index'\nimport { createTimeoutError } from './timeout-error'\n\nlet toBase64: (data: string) => string\ntry {\n toBase64 = window.btoa\n} catch {\n toBase64 = btoa\n}\n\nexport class XHR extends Gateway {\n private canceled = false\n private timer?: ReturnType<typeof setTimeout>\n\n get() {\n const xmlHttpRequest = this.createXHR()\n xmlHttpRequest.open('GET', this.request.url(), true)\n this.setHeaders(xmlHttpRequest, {})\n this.configureTimeout(xmlHttpRequest)\n this.configureBinary(xmlHttpRequest)\n this.configureAbort(xmlHttpRequest)\n xmlHttpRequest.send()\n }\n\n head() {\n const xmlHttpRequest = this.createXHR()\n xmlHttpRequest.open('HEAD', this.request.url(), true)\n this.setHeaders(xmlHttpRequest, {})\n this.configureTimeout(xmlHttpRequest)\n this.configureBinary(xmlHttpRequest)\n this.configureAbort(xmlHttpRequest)\n xmlHttpRequest.send()\n }\n\n post() {\n this.performRequest('post')\n }\n\n put() {\n this.performRequest('put')\n }\n\n patch() {\n this.performRequest('patch')\n }\n\n delete() {\n this.performRequest('delete')\n }\n\n configureBinary(xmlHttpRequest: XMLHttpRequest) {\n if (this.request.isBinary()) {\n xmlHttpRequest.responseType = 'blob'\n }\n }\n\n configureTimeout(xmlHttpRequest: XMLHttpRequest) {\n this.canceled = false\n this.timer = undefined\n\n const timeout = this.request.timeout()\n\n if (timeout) {\n xmlHttpRequest.timeout = timeout\n xmlHttpRequest.addEventListener('timeout', () => {\n this.canceled = true\n this.timer && clearTimeout(this.timer)\n const error = createTimeoutError(`Timeout (${timeout}ms)`)\n this.dispatchClientError(error.message, error)\n })\n\n // PhantomJS doesn't support timeout for XMLHttpRequest\n this.timer = setTimeout(() => {\n this.canceled = true\n const error = createTimeoutError(`Timeout (${timeout}ms)`)\n this.dispatchClientError(error.message, error)\n }, timeout + 1)\n }\n }\n\n configureAbort(xmlHttpRequest: XMLHttpRequest) {\n const signal = this.request.signal()\n if (signal) {\n signal.addEventListener('abort', () => {\n xmlHttpRequest.abort()\n })\n xmlHttpRequest.addEventListener('abort', () => {\n this.dispatchClientError(\n 'The operation was aborted',\n new Error('The operation was aborted')\n )\n })\n }\n }\n\n configureCallbacks(xmlHttpRequest: XMLHttpRequest) {\n xmlHttpRequest.addEventListener('load', () => {\n if (this.canceled) {\n return\n }\n\n this.timer && clearTimeout(this.timer)\n this.dispatchResponse(this.createResponse(xmlHttpRequest))\n })\n\n