@chord-ts/rpc
Version:
💎 Cutting edge transport framework vanishing borders between frontend and backend
317 lines (316 loc) • 11.6 kB
JavaScript
;
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 } } });
};
}