mappersmith
Version:
It is a lightweight rest client for node.js and the browser
1 lines • 10.2 kB
Source Map (JSON)
{"version":3,"sources":["../../src/client-builder.ts"],"sourcesContent":["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"],"mappings":"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","names":["resource","middleware"]}