diff --git a/docs/development/WritingTests/jest-test-case.md b/docs/development/WritingTests/jest-test-case.md new file mode 100644 index 000000000..e69de29bb diff --git a/servers/backend-server/package.json b/servers/backend-server/package.json index 77a022e78..7eea31358 100755 --- a/servers/backend-server/package.json +++ b/servers/backend-server/package.json @@ -91,7 +91,9 @@ "express": "^4.17.1", "graphql": "^14.7.0", "graphql-bigint": "^1.0.0", + "graphql-middleware": "^4.0.2", "graphql-nats-subscriptions": "^1.4.2", + "graphql-shield": "7.3.5", "graphql-subscriptions": "^1.2.0", "graphql-tools": "^4.0.8", "graphql-type-json": "^0.3.1", @@ -116,6 +118,9 @@ "pm2": "^4.2.1", "rimraf": "^3.0.0" }, + "peerDependencies": { + "@cdm-logger/core": "*" + }, "typescript": { "definition": "dist/main.d.ts" } diff --git a/servers/backend-server/src/middleware/moleculer-inter-namespace.ts b/servers/backend-server/src/middleware/moleculer-inter-namespace.ts new file mode 100644 index 000000000..2590e9b50 --- /dev/null +++ b/servers/backend-server/src/middleware/moleculer-inter-namespace.ts @@ -0,0 +1,60 @@ +import { ServiceBroker, Middleware } from 'moleculer'; +import * as _ from 'lodash'; +import { logger } from '@cdm-logger/server'; + +export const InterNamespaceMiddleware = function (options): Middleware { + if (!Array.isArray(options)) { + throw new Error('Must be an Array'); + } + + let thisBroker: ServiceBroker; + const brokers: { [key: string]: ServiceBroker } = {}; + + return { + created(broker: ServiceBroker) { + thisBroker = broker; + options.forEach((nsOpts) => { + if (_.isString(nsOpts)) { + nsOpts = { + namespace: nsOpts, + }; + } + const ns = nsOpts.namespace; + + const brokerOpts = _.defaultsDeep( + {}, + nsOpts, + { nodeID: null, middlewares: null, created: null, started: null }, + broker.options, + ); + brokers[ns] = new ServiceBroker(brokerOpts); + }); + }, + + started() { + return Promise.all(Object.values(brokers).map((b) => b.start())); + }, + + stopped() { + return Promise.all(Object.values(brokers).map((b) => b.stop())); + }, + + call(next) { + return function (actionName, params, opts = {}) { + if (_.isString(actionName) && actionName.includes('@')) { + const [action, namespace] = actionName.split('@'); + + if (brokers[namespace]) { + return brokers[namespace].call(action, params, opts); + } + if (namespace === thisBroker.namespace) { + return next(action, params, opts); + } + throw new Error(`Unknown namespace: ${namespace}`); + } + + return next(actionName, params, opts); + }; + }, + }; +}; diff --git a/servers/backend-server/src/stack-server.ts b/servers/backend-server/src/stack-server.ts index 213280a72..d0d23035a 100755 --- a/servers/backend-server/src/stack-server.ts +++ b/servers/backend-server/src/stack-server.ts @@ -1,45 +1,49 @@ import * as http from 'http'; import * as express from 'express'; +import { logger, logger as serverLogger } from '@cdm-logger/server'; +import { Feature } from '@common-stack/server-core'; +import { Container, ContainerModule, interfaces } from 'inversify'; +import { ServiceBroker } from 'moleculer'; +import { CommonType } from '@common-stack/core'; +import * as _ from 'lodash'; +import { applyMiddleware } from 'graphql-middleware'; +import { shield } from 'graphql-shield'; +import { CdmLogger } from '@cdm-logger/core'; import { expressApp } from './express-app'; import { GraphqlServer } from './server-setup/graphql-server'; import { config } from './config'; -import { logger as serverLogger } from '@cdm-logger/server'; import { ConnectionBroker } from './connectors/connection-broker'; -import { Feature } from '@common-stack/server-core'; -import { ContainerModule, interfaces, Container } from 'inversify'; -import { ServiceBroker, ServiceSettingSchema } from 'moleculer'; import * as brokerConfig from './config/moleculer.config'; import modules, { settings } from './modules'; import { GatewaySchemaBuilder } from './api/schema-builder'; import { WebsocketMultiPathServer } from './server-setup/websocket-multipath-update'; import { IModuleService } from './interfaces'; -import { CommonType } from '@common-stack/core'; -import * as _ from 'lodash'; -import {migrate} from './utils/migrations'; -import { CdmLogger } from '@cdm-logger/core'; -type ILogger = CdmLogger.ILogger; +import { migrate } from './utils/migrations'; +import { InterNamespaceMiddleware } from './middleware/moleculer-inter-namespace'; +// This is temp and will be replaced one we add support for rules in Feature +type ILogger = CdmLogger.ILogger; function startListening(port) { - let server = this; + const server = this; return new Promise((resolve) => { server.listen(port, resolve); }); } -const infraModule = - ({ broker, pubsub, logger }) => new ContainerModule((bind: interfaces.Bind) => { +const infraModule = ({ broker, pubsub, mongoClient, logger }) => + new ContainerModule((bind: interfaces.Bind) => { bind('Logger').toConstantValue(logger); bind(CommonType.LOGGER).toConstantValue(logger); bind('Environment').toConstantValue(config.NODE_ENV || 'development'); bind(CommonType.ENVIRONMENT).toConstantValue(config.NODE_ENV || 'development'); bind('PubSub').toConstantValue(pubsub); bind(CommonType.PUBSUB).toConstantValue(pubsub); - bind('MoleculerBroker').toConstantValue(broker); bind(CommonType.MOLECULER_BROKER).toConstantValue(broker); + bind('MoleculerBroker').toConstantValue(broker); + bind('MongoDBConnection').toConstantValue(mongoClient); }); - /** * Controls the lifecycle of the Application Server * @@ -47,66 +51,105 @@ const infraModule = * @class StackServer */ export class StackServer { + public httpServer: http.Server & { startListening?: (port) => void }; - public httpServer: http.Server & { startListening?: (port) => void; }; private app: express.Express; + private logger: ILogger; + private connectionBroker: ConnectionBroker; + + private mainserviceBroker: ServiceBroker; + private microserviceBroker: ServiceBroker; + private multiPathWebsocket: WebsocketMultiPathServer; private serviceContainer: Container; + private microserviceContainer: Container; constructor() { this.logger = serverLogger.child({ className: 'StackServer' }); } - public async initialize() { + public async initialize() { this.logger.info('StackServer initializing'); this.connectionBroker = new ConnectionBroker(brokerConfig.transporter, this.logger); const redisClient = this.connectionBroker.redisDataloaderClient; + const mongoClient = await this.connectionBroker.mongoConnection; // Moleculer Broker Setup - this.microserviceBroker = new ServiceBroker({ + this.mainserviceBroker = new ServiceBroker({ ...brokerConfig, + middlewares: [ + InterNamespaceMiddleware([ + { + namespace: 'api-admin', + transporter: brokerConfig.transporter, + }, + ]), + ], started: async () => { - // start DB migration - await migrate(mongoClient, this.serviceContainer); await modules.preStart(this.serviceContainer); if (config.NODE_ENV === 'development') { - await modules.microservicePreStart(this.microserviceContainer); + // await modules.microservicePreStart(this.micorserviceContainer); } await modules.postStart(this.serviceContainer); + await migrate(mongoClient, this.serviceContainer); + // start DB migration + if (config.NODE_ENV === 'development') { - await modules.microservicePostStart(this.microserviceContainer); + // await modules.microservicePostStart(this.micorserviceContainer); } }, - // created, - created: async () => { + // created, + async created() { + return Promise.resolve(); }, }); + if (config.NODE_ENV === 'development') { + this.microserviceBroker = new ServiceBroker({ + ...brokerConfig, + nodeID: 'node-broker-2', + started: async () => { + await modules.microservicePreStart(this.microserviceContainer); + await modules.microservicePostStart(this.microserviceContainer); + }, + // created, + created: async () => Promise.resolve(), + }); + } const pubsub = await this.connectionBroker.graphqlPubsub; const InfraStructureFeature = new Feature({ createContainerFunc: [ - () => infraModule({ - broker: this.microserviceBroker, - pubsub, logger: serverLogger, - })], + () => + infraModule({ + broker: this.mainserviceBroker, + pubsub, + mongoClient, + logger: serverLogger, + }), + ], + createServiceFunc: (container) => ({ moleculerBroker: container.get(CommonType.MOLECULER_BROKER) }), createHemeraContainerFunc: [ - () => infraModule({ - broker: this.microserviceBroker, - pubsub, logger: serverLogger, - })], + () => + infraModule({ + broker: this.mainserviceBroker, + pubsub, + mongoClient, + logger: serverLogger, + }), + ], }); - const allModules = new Feature(InfraStructureFeature, modules); - const executableSchema = await (new GatewaySchemaBuilder({ + const allModules = new Feature(InfraStructureFeature, modules as Feature); + let executableSchema = await new GatewaySchemaBuilder({ schema: allModules.schemas, resolvers: allModules.createResolvers({ pubsub, @@ -115,7 +158,16 @@ export class StackServer { }), directives: allModules.createDirectives({ logger: this.logger }), logger: serverLogger, - })).build(); + }).build(); + + executableSchema = applyMiddleware( + executableSchema, + // we can import rules from all modules and use lodash.merge to merge + // them all together before passing to graphQl shield + shield(modules.rules, { + allowExternalErrors: true, + }), + ); // set the service container this.serviceContainer = await allModules.createContainers({ ...settings, mongoConnection: mongoClient }); @@ -125,30 +177,31 @@ export class StackServer { serviceContext: createServiceContext, dataSource: allModules.createDataSource(), defaultPreferences: allModules.createDefaultPreferences(), - createContext: async (req, res) => await allModules.createContext(req, res), + createContext: async (req, res) => allModules.createContext(req, res), logger: serverLogger, schema: executableSchema, }; allModules.loadMainMoleculerService({ - broker: this.microserviceBroker, + broker: this.mainserviceBroker, container: this.serviceContainer, - settings: settings, + settings, }); if (config.NODE_ENV === 'development') { - this.microserviceContainer = await allModules.createHemeraContainers({ ...settings, mongoConnection: mongoClient }); + this.microserviceContainer = await allModules.createHemeraContainers({ + ...settings, + mongoConnection: mongoClient, + }); allModules.loadClientMoleculerService({ broker: this.microserviceBroker, container: this.microserviceContainer, - settings: settings, + settings, }); } - // intialize Servers + // initialize Servers this.httpServer = http.createServer(); this.app = await expressApp(serviceBroker, null, this.httpServer); - - this.httpServer.startListening = startListening.bind(this.httpServer); this.httpServer.on('request', this.app); this.httpServer.on('close', () => { @@ -162,14 +215,23 @@ export class StackServer { this.multiPathWebsocket = new WebsocketMultiPathServer(serviceBroker, redisClient, customWebsocket); this.httpServer = this.multiPathWebsocket.httpServerUpgrade(this.httpServer); } - const graphqlServer = new GraphqlServer(this.app, this.httpServer, redisClient, serviceBroker, !customWebsocketEnable); - + const graphqlServer = new GraphqlServer( + this.app, + this.httpServer, + redisClient, + serviceBroker, + !customWebsocketEnable, + ); await graphqlServer.initialize(); } public async start() { - await this.microserviceBroker.start(); + if (config.NODE_ENV === 'development') { + await Promise.all([this.mainserviceBroker.start(), this.microserviceBroker.start()]); + } else { + await this.mainserviceBroker.start(); + } } public async cleanup() { @@ -182,6 +244,9 @@ export class StackServer { if (this.connectionBroker) { await this.connectionBroker.stop(); } + if (this.mainserviceBroker) { + await this.mainserviceBroker.stop(); + } if (this.microserviceBroker) { await this.microserviceBroker.stop(); }