UNPKG

allserver

Version:

Multi-protocol simple RPC server and [optional] client. Boilerplate-less. Opinionated. Minimalistic. DX-first.

194 lines (168 loc) 7.14 kB
const assert = require("assert"); const { isObject, isBoolean, isFunction, uniq } = require("../util"); module.exports = require("stampit")({ name: "Allserver", deepProps: { before: null, after: null, }, props: { procedures: {}, transport: null, logger: console, introspection: true, callsCount: 0, }, init({ procedures, transport, introspection, before, after, logger }) { this.procedures = procedures || this.procedures; this.transport = transport || this.transport || require("./HttpTransport")(); this.logger = logger || this.logger; this.introspection = introspection != null ? introspection : this.introspection; if (before) this.before = uniq([].concat(this.before).concat(before).filter(isFunction)); if (after) this.after = uniq([].concat(this.after).concat(after).filter(isFunction)); this._validateProcedures(); }, methods: { _validateProcedures() { assert(isObject(this.procedures), "'procedures' must be an object"); assert(Object.values(this.procedures).every(isFunction), "All procedures must be functions"); }, async _introspect(ctx) { const allow = isFunction(this.introspection) ? this.introspection(ctx) : this.introspection; if (!allow) return; const obj = {}; for (const [key, value] of Object.entries(this.procedures)) obj[key] = typeof value; ctx.introspection = obj; ctx.result = { success: true, code: "ALLSERVER_INTROSPECTION", message: "Introspection as JSON string", procedures: JSON.stringify(ctx.introspection), }; await this.transport.prepareIntrospectionReply(ctx); }, /** * This method does not throw. * The `ctx.procedure` is the function to call. * @param ctx * @return {Promise<void>} * @private */ async _callProcedure(ctx) { if (!isFunction(ctx.procedure)) { ctx.result = { success: false, code: "ALLSERVER_PROCEDURE_NOT_FOUND", message: `Procedure '${ctx.procedureName}' not found`, }; await this.transport.prepareNotFoundReply(ctx); return; } let result; try { result = await ctx.procedure(ctx.arg, ctx); } catch (err) { const code = err.code || "ALLSERVER_PROCEDURE_ERROR"; this.logger.error(err, code); ctx.error = err; ctx.result = { success: false, code, message: `'${err.message}' error in '${ctx.procedureName}' procedure`, }; await this.transport.prepareProcedureErrorReply(ctx); return; } if (result === undefined) { ctx.result = { success: true, code: "SUCCESS", message: "Success" }; } else if (!result || !isBoolean(result.success)) { ctx.result = { success: true, code: "SUCCESS", message: "Success", [ctx.procedureName]: result, }; } else { ctx.result = result; } }, async _callMiddlewares(ctx, middlewareType, next) { const runMiddlewares = async (middlewares) => { if (!middlewares?.length) { // no middlewares to run if (next) return await next(); return; } const middleware = middlewares[0]; async function handleMiddlewareResult(result) { if (result !== undefined) { ctx.result = result; // Do not call any more middlewares } else { await runMiddlewares(middlewares.slice(1)); } } try { if (middleware.length > 1) { // This middleware accepts more than one argument await middleware.call(this, ctx, handleMiddlewareResult); } else { const result = await middleware.call(this, ctx); await handleMiddlewareResult(result); } } catch (err) { const code = err.code || "ALLSERVER_MIDDLEWARE_ERROR"; this.logger.error(err, code); ctx.error = err; ctx.result = { success: false, code, message: `'${err.message}' error in '${middlewareType}' middleware`, }; // Do not call any more middlewares if (next) return await next(); } }; const middlewares = [].concat(this[middlewareType]).filter(isFunction); return await runMiddlewares(middlewares); }, async handleCall(ctx) { ctx.callNumber = this.callsCount; this.callsCount += 1; ctx.procedureName = this.transport.getProcedureName(ctx); ctx.isIntrospection = this.transport.isIntrospection(ctx); if (!ctx.isIntrospection && ctx.procedureName) ctx.procedure = this.procedures[ctx.procedureName]; if (!ctx.arg) ctx.arg = {}; if (!ctx.arg._) ctx.arg._ = {}; if (!ctx.arg._.procedureName) ctx.arg._.procedureName = ctx.procedureName; await this._callMiddlewares(ctx, "before", async () => { if (!ctx.result) { if (ctx.isIntrospection) { await this._introspect(ctx); } else { await this._callProcedure(ctx); } } // Warning! This call might overwrite an existing result. await this._callMiddlewares(ctx, "after"); }); return this.transport.reply(ctx); }, start() { return this.transport.startServer({ allserver: this }); }, stop() { return this.transport.stopServer(); }, }, statics: { defaults({ procedures, transport, logger, introspection, before, after } = {}) { if (before != null) before = (Array.isArray(before) ? before : [before]).filter(isFunction); if (after != null) after = (Array.isArray(after) ? after : [after]).filter(isFunction); return this.compose({ props: { procedures, transport, logger, introspection }, deepProps: { before, after }, }); }, }, });