const moleculer = require('moleculer'); /** * @typedef {object} Service */ /** * @template Type * @typedef {function(new: Type, object)} Class<Type> */ /** * Creates a naive "proxy" method which calls a moleculer service's method * passing the first argument as the `params` to the moleculer service call * It also binds the ctx correctly to allow for nested service calls * * @param {moleculer.Context} ctx * @param {string} serviceName - The name of the service in the moleculer cluster * @param {string} methodName - The name of the moleculer "action" on the service * @param {string} serviceVersion - The version of the service in the moleculer cluster * * @returns {(params: object) => Promise<any>} */ function proxy(ctx, serviceName, methodName, serviceVersion) { return async params => ctx.call(`${serviceVersion}.${serviceName}.${methodName}`, params); } /** * Get the method names of the service * * @param {Class<any>} Class * * @returns {string[]} A list of the methods names for Class */ function getClassMethods(Class) { return Reflect.ownKeys(Class.prototype).reduce((methods, key) => { if (typeof Class.prototype[key] !== 'function') { return methods; } if (key === 'constructor' || key.toString().startsWith('_')) { return methods; } return methods.concat(key); }, []); } /** * createServiceProxy * * @param {moleculer.Context} ctx * @param {string} serviceName * @param {string} serviceVersion * @returns {Promise<object>} */ async function createServiceProxy(ctx, serviceName, serviceVersion) { await ctx.broker.waitForServices([{ name: serviceName, version: serviceVersion }]); /** @type {{action: {name: string, rawName: string}}[]} */ const actionsList = await ctx.call('$node.actions'); const serviceMethods = actionsList.filter((obj) => { const isValidAction = obj && obj.action; if (!isValidAction) { ctx.broker.logger.debug(`Recieved invalid action ${JSON.stringify(obj)}`); return false; } const belongsToService = obj.action.name.startsWith(`${serviceVersion}.${serviceName}.`); return belongsToService; }).map(obj => obj.action.rawName); return serviceMethods.reduce((serviceProxy, methodName) => { ctx.broker.logger.debug(`Creating proxy ${serviceName}.${methodName}`); return Object.assign(serviceProxy, { [methodName]: proxy(ctx, serviceName, methodName, serviceVersion) }); }, []); } /** * @typedef {Object} ServiceDefinition * @prop {string} name * @prop {string} version */ /** * Create a ServiceSchema compatible with moleculer * * @template {{_init(): Promise<void>}} Type * * @param {object} params The Service to proxy via moleculer * @param {Class<Type>} params.Service The Service to proxy via moleculer * @param {string} params.name The name of the service in moleculer * @param {Object.<string, ServiceDefinition>} [params.serviceDeps] A map of dependencies with a key of the param name and value of the moleculer service * @param {Object.<string, any>} [params.staticDeps] Any static dependencies which do not need to be proxied by moleculer * @param {boolean} [params.forceSingleton=false] Forces the wrapper to only ever create once instance of Service * @param {string} [params.version='1'] Forces the wrapper to only ever create once instance of Service * * @returns {moleculer.ServiceSchema} */ function createMoleculerServiceSchema({Service, name, serviceDeps = null, staticDeps = null, forceSingleton = false, version = '1'}) { const methods = getClassMethods(Service); /** * Creates an instance of the service - wiring and mapping any dependencies * * @param {moleculer.Context} ctx * @returns {Promise<Type>} */ async function getDynamicServiceInstance(ctx) { const instanceDeps = Object.create(serviceDeps); if (serviceDeps) { for (const dep in serviceDeps) { const serviceDefinition = serviceDeps[dep]; const serviceName = serviceDefinition.name; const serviceVersion = serviceDefinition.version; const serviceProxy = await createServiceProxy(ctx, serviceName, serviceVersion); instanceDeps[dep] = serviceProxy; } } instanceDeps.logging = ctx.broker.logger; Object.assign(instanceDeps, staticDeps); const service = new Service(instanceDeps); return service; } let singleton = null; /** * Ensures that the Service is only instantiated once * * @param {moleculer.Context} ctx * @returns {Promise<Type>} */ async function getSingletonServiceInstance(ctx) { if (singleton) { return singleton; } singleton = await getDynamicServiceInstance(ctx); if (singleton._init) { await singleton._init(); } return singleton; } const getServiceInstance = (!serviceDeps || forceSingleton) ? getSingletonServiceInstance : getDynamicServiceInstance; /** @type moleculer.ServiceActionsSchema */ const actions = { ping() { return 'pong'; } }; for (const method of methods) { /** @type {(ctx: moleculer.Context) => Promise<any>} */ actions[method] = async function (ctx) { const service = await getServiceInstance(ctx); return service[method](ctx.params); }; } return { name, version, actions, async started() { const ctx = new moleculer.Context(this.broker, null); await getServiceInstance(ctx); } }; } module.exports = createMoleculerServiceSchema;