UNPKG

@chord-ts/rpc

Version:

💎 Cutting edge transport framework vanishing borders between frontend and backend

317 lines (316 loc) • 11.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Composer = void 0; exports.toRPC = toRPC; exports.rpc = rpc; exports.depends = depends; exports.val = val; require("reflect-metadata"); const specs_1 = require("../specs"); const validators_1 = require("../validators"); class Composer { constructor(models, config) { var _a; this.adapter = null; this.config = config ?? {}; (_a = this.config).validator ?? (_a.validator = validators_1.ZodAdapter); this.models = models; for (const [key, Model] of Object.entries(models)) { this[key] = Model; } this.middlewares = []; } static init(models, config) { return new Composer(models, config); } static upsertMethod(desc) { const key = `${desc.target.constructor.name}.${desc.key.toString()}`; const old = Composer.methods.get(key) ?? { validators: { in: {}, out: {} } }; const merged = { ...old, ...desc, key }; merged.validators.in = { ...old.validators.in, ...desc.validators?.in }; Composer.methods.set(key, merged); } static addProp({ key, target }) { const targetName = `${target.constructor.name}`; const oldProps = Composer.props.get(targetName) ?? []; Composer.props.set(targetName, oldProps.concat({ key, target })); } get clientType() { return {}; } use(middleware) { if (middleware.name === 'backendAdapter') { this.adapter = middleware; return; } this.middlewares.push(middleware); } createScoped(ctx) { return Object.fromEntries(Object.entries(this.models).map(([k, v]) => { return [ k, new Proxy(v, { get: (target, prop) => (...args) => { return target[prop].call({ ...target, ctx }, ...args); } }) ]; })); } get stringified() { const allMethods = []; for (const [service, instance] of Object.entries(this.models)) { for (const method of Reflect.ownKeys(Reflect.getPrototypeOf(instance))) { if (method === 'constructor') continue; allMethods.push(`${service}.${method.toString()}`); } } function serviceGet(target, prop, receiver) { function check(target, prop) { if (prop === Symbol.isConcatSpreadable) { return true; } if (Reflect.has(target, prop)) { return Reflect.get(target, prop, receiver); } return null; } function methodGet(target, prop, receiver) { const res = check(target, prop); if (res !== null) return res; return target.find((method) => method.endsWith(prop)); } const res = check(target, prop); if (res !== null) return res; return new Proxy(target.filter((v) => v.startsWith(prop)), { get: methodGet }); } return new Proxy(allMethods, { get: serviceGet }); } getSchema(route) { route = route ?? this.config?.route; if (!route) { throw new EvalError('No route provided during Composer initialization or Schema generation'); } const methods = {}; const modelsSet = new Set(); for (const [key, info] of Composer.methods.entries()) { modelsSet.add(info.target.constructor.name); const { argsType, returnType } = info.metadata; methods[key] = { argsType, returnType }; } const models = Array.from(modelsSet); return { methods, route, models }; } async initCtx(event) { const ctx = { body: null }; if (this.adapter) { await this.adapter(event, ctx, () => { }); return ctx; } console.warn('\x1b[33mNo "adapter" middleware specified. Trying to parse request automatically\n'); if (event.jsonrpc && event.method) { ctx.body = event; return ctx; } if (event?.body && event.method) { return event; } if (typeof event.json === 'function') { ctx.body = await event.json(); return ctx; } const fields = Object.getOwnPropertyNames(event); if (fields.includes('request')) { ctx.body = await event['request'].json(); } return ctx; } async exec(event) { const ctx = await this.initCtx(event); const { body } = ctx; if (!Array.isArray(body)) { return this.execProcedure(event, ctx, body); } const batch = []; for (const req of body) { batch.push(this.execProcedure(event, ctx, req)); } return Promise.all(batch); } async runMiddlewares(middlewares, event, ctx) { ctx ?? (ctx = {}); let lastMiddlewareResult; let middlewareIndex = -1; let error; async function next() { middlewareIndex++; if (middlewareIndex >= middlewares.length || error) return; const middleware = middlewares[middlewareIndex]; lastMiddlewareResult = await middleware(event, ctx, next).catch((e) => (error = e)); } await next(); if (middlewareIndex <= middlewares.length - 1) { return { ctx, res: lastMiddlewareResult, error }; } return { ctx, res: undefined, error }; } async execProcedure(event, ctx, req) { if (!req?.method) { return (0, specs_1.buildError)({ code: specs_1.ErrorCode.InvalidRequest, message: 'Wrong invocation. Method and Args must be defined', data: [] }); } let { method, params } = req; const [cls, func] = method.split('.'); method = `${this.models[cls].constructor.name}.${func}`; const methodDesc = Composer.methods.get(method); if (!method || !methodDesc) { const msg = `Error: Cannot find method: "${method}"\nHave you marked it with @rpc() decorator?`; console.error('\x1b[31m' + msg + `\nRegistered methods: ${JSON.stringify(Composer.methods.entries())}`); return (0, specs_1.buildError)({ code: specs_1.ErrorCode.MethodNotFound, message: msg, data: [] }); } const { target, descriptor, use, argNames, validators } = methodDesc; const targetInstance = new target.constructor(); try { let res, error; ({ ctx, res, error } = await this.runMiddlewares(this.middlewares.concat(use), event, { ...ctx, methodDesc })); if (error) { return (0, specs_1.buildError)({ code: specs_1.ErrorCode.InvalidParams, message: error.message ?? error.body?.message ?? '', data: error.data }); } if (res) return res; const ctxProp = Composer.props.get(target.constructor.name)?.find((d) => d.key === 'ctx'); if (ctxProp) { Reflect.defineProperty(targetInstance, ctxProp.key, { configurable: true, enumerable: true, writable: true, value: ctx }); } if (!Array.isArray(params)) { params = argNames.map((key) => params[key]); } if (this.config?.validator && validators.in) { let errors = []; for (const [i, param] of params.entries()) { const validator = validators.in[i]; const { success, error } = this.config.validator.validate(validator, param); if (!error) continue; errors = errors.concat(error.issues); } if (errors.length) { return (0, specs_1.buildError)({ code: specs_1.ErrorCode.InvalidParams, message: 'Validation error', data: errors }); } } const result = await descriptor.value.apply(targetInstance, params); return (0, specs_1.buildResponse)({ request: req, result }); } catch (e) { if (e?.status?.toString()?.startsWith('3')) { console.log('throw'); throw e; } (this.config?.onError ?? console.error)(e, req); return (0, specs_1.buildError)({ code: specs_1.ErrorCode.InternalError, message: e?.message ?? e?.body?.message ?? '', data: [e] }); } } } exports.Composer = Composer; Composer.methods = new Map(); Composer.props = new Map(); const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/gm; const ARGUMENT_NAMES = /([^\s,]+)/g; function getParamNames(func) { const fnStr = func.toString().replace(STRIP_COMMENTS, ''); let result = fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')')).match(ARGUMENT_NAMES); if (result === null) result = []; return result; } function getMetadata(target, key) { return { argsType: Reflect.getMetadata('design:paramtypes', target, key)?.map((a) => a?.name), returnType: Reflect.getMetadata('design:returntype', target, key)?.name }; } function toRPC(instance) { const proto = Reflect.getPrototypeOf(instance); for (const key of Reflect.ownKeys(proto)) { if (key === 'constructor') continue; const descriptor = Reflect.getOwnPropertyDescriptor(proto, key); if (!(descriptor.value instanceof Function)) continue; const metadata = getMetadata(proto, key); Composer.upsertMethod({ key, descriptor, metadata, target: instance, use: [], argNames: [] }); } return instance; } function rpc(config) { return function (target, key, descriptor) { const metadata = getMetadata(target, key); const argNames = getParamNames(descriptor.value); let use = config?.use ?? []; if (!Array.isArray(use)) { use = [use]; } if (config?.in && !Array.isArray(config?.in)) { config.in = [config.in]; } Composer.upsertMethod({ key, descriptor, metadata, target, use, argNames }); }; } function depends() { return function (target, key) { Composer.addProp({ key, target }); if (key === 'ctx') return; throw TypeError("Dependency injection is supported only for 'ctx' property right now.\n" + `Remove ${String(key)} property inside ${target.constructor.name}`); }; } function val(validator) { return (target, key, parameterIndex) => { Composer.upsertMethod({ target, key, validators: { in: { [parameterIndex]: validator } } }); }; }