mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-26 04:13:30 +03:00
6023d90089
no-issue
177 lines
5.7 KiB
JavaScript
177 lines
5.7 KiB
JavaScript
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;
|