vue-express-mongo-boilerplate
Version:
Express NodeJS application server boilerplate with Mongo and VueJS
552 lines (437 loc) • 14.1 kB
JavaScript
;
let logger = require("./logger");
let config = require("../config");
let EventEmitter = require("events").EventEmitter;
let path = require("path");
let fs = require("fs");
let util = require("util");
let _ = require("lodash");
let chalk = require("chalk");
let express = require("express");
let C = require("./constants");
let Context = require("./context");
let auth = require("./auth/helper");
let response = require("./response");
let listEndpoints = require("express-list-endpoints");
let Table = require("cli-table2");
let GraphQLScalarType = require("graphql").GraphQLScalarType;
let Kind = require("graphql/language").Kind;
let Service = require("./service");
/* global WEBPACK_BUNDLE */
if (!WEBPACK_BUNDLE) require("require-webpack-compat")(module, require);
/**
* Service handler class
*/
class Services extends EventEmitter {
/**
* Constructor of Service
*/
constructor() {
super();
this.setMaxListeners(0); // turn off
this.app = null;
this.db = null;
this.services = {};
}
/**
* Load built-in and applogic services. Scan the folders
* and load service files
*
* @param {Object} app ExpressJS instance
* @param {Object} db Database instance
*/
loadServices(app, db) {
let self = this;
self.app = app;
self.db = db;
let addService = function(serviceSchema) {
let service = new Service(serviceSchema, app, db);
self.services[service.name] = service;
};
if (WEBPACK_BUNDLE || fs.existsSync(path.join(__dirname, "..", "services"))) {
logger.info("");
logger.info(chalk.bold("Search built-in services..."));
let modules = require.context("../services", true, /\.js$/);
if (modules) {
modules.keys().map(function(module) {
logger.info(" Load", path.relative(path.join(__dirname, "..", "services"), module), "service...");
addService(modules(module));
});
}
}
if (WEBPACK_BUNDLE || fs.existsSync(path.join(__dirname, "..", "applogic", "modules"))) {
logger.info("");
logger.info(chalk.bold("Search applogic services..."));
let modules = require.context("../applogic/modules", true, /service\.js$/);
if (modules) {
modules.keys().map(function(module) {
logger.info(" Load", path.relative(path.join(__dirname, "..", "applogic", "modules"), module), "service...");
addService(modules(module));
});
}
}
// Call `init` of services
_.forIn(self.services, (service) => {
if (_.isFunction(service.$schema.init)) {
service.$schema.init.call(service, Context.CreateToServiceInit(service));
}
});
}
/**
* Register actions of services as REST routes
*
* @param {Object} app ExpressJS instance
*/
registerRoutes(app) {
let self = this;
//logger.info("Register routes ", this.services);
_.forIn(this.services, (service, name) => {
if (service.$settings.rest !== false && service.actions) {
let router = express.Router();
// Trying authenticate with API key
router.use(auth.tryAuthenticateWithApiKey);
let idParamName = service.$settings.idParamName || "id";
let lastRoutes = [];
_.forIn(service.actions, (actionFunc, name) => {
let action = actionFunc.settings;
action.handler = actionFunc;
if (!_.isFunction(action.handler))
throw new Error(`Missing handler function in '${name}' action in '${service.name}' service!`);
// Make the request handler for action
let handler = (req, res) => {
let ctx = Context.CreateFromREST(service, action, app, req, res);
logger.debug(`Request via REST '${service.namespace}/${action.name}' (ID: ${ctx.id})`, ctx.params);
console.time("REST request");
self.emit("request", ctx);
let cacheKey = service.getCacheKey(action.name, ctx.params);
Promise.resolve()
// Resolve model if ID provided
.then(() => {
return ctx.resolveModel();
})
// Check permission
.then(() => {
return ctx.checkPermission();
})
// Call the action handler
.then(() => {
return action.handler(ctx);
})
// Response the result
.then((json) => {
res.append("Request-Id", ctx.id);
response.json(res, json);
})
// Response the error
.catch((err) => {
logger.error(err);
response.json(res, null, err);
})
.then(() => {
self.emit("response", ctx);
console.timeEnd("REST request");
//logger.debug("Response time:", ctx.responseTime(), "ms");
});
};
// Register handler to GET and POST method types
// So you can call the /api/namespace/action with these request methods.
//
// GET /api/namespace/vote?id=123
// POST /api/namespace/vote?id=123
router.get("/" + name, handler);
router.post("/" + name, handler);
// You can call with ID in the path
// GET /api/namespace/123/vote
// POST /api/namespace/123/vote
router.get("/:" + idParamName + "/" + name, handler);
router.post("/:" + idParamName + "/" + name, handler);
// Create default RESTful handlers
switch (name) {
// You can call the `find` action with
// GET /api/namespace/
case "find": {
router.get("/", handler);
break;
}
// You can call the `get` action with
// GET /api/namespace/?id=123
// or
// GET /api/namespace/123
case "get": {
// router.get("/:" + idParamName, handler);
lastRoutes.push({ method: "get", path: "/:" + idParamName, handler: handler });
break;
}
// You can call the `create` action with
// POST /api/namespace/
case "create": {
// router.post("/:" + idParamName, handler);
lastRoutes.push({ method: "post", path: "/:" + idParamName, handler: handler });
router.post("/", handler);
break;
}
// You can call the `update` action with
// PUT /api/namespace/?id=123
// or
// PATCH /api/namespace/?id=123
// or
// PUT /api/namespace/123
// or
// PATCH /api/namespace/123
case "update": {
// router.put("/:" + idParamName, handler);
lastRoutes.push({ method: "put", path: "/:" + idParamName, handler: handler });
// router.patch("/:" + idParamName, handler);
lastRoutes.push({ method: "patch", path: "/:" + idParamName, handler: handler });
router.put("/", handler);
router.patch("/", handler);
break;
}
// You can call the `remove` action with
// DELETE /api/namespace/?id=123
// or
// DELETE /api/namespace/123
case "remove": {
// router.delete("/:" + idParamName, handler);
lastRoutes.push({ method: "delete", path: "/:" + idParamName, handler: handler });
router.delete("/", handler);
break;
}
}
});
// Register '/:code' routes
lastRoutes.forEach((item) => {
router[item.method](item.path, item.handler);
});
// Register router to namespace
app.use("/api/" + service.namespace, router);
// Register a version namespace
if (service.version) {
app.use("/api/v" + service.version + "/" + service.namespace, router);
}
}
});
}
/**
* Register actions of services as socket.io event handlers
*
* @param {Object} IO Socket.IO object
* @param {Object} socketHandler Socket handler instance
*/
registerSockets(IO, socketHandler) {
let self = this;
_.forIn(this.services, (service, name) => {
if (service.ws !== false) {
service.socket = service.socket || {};
// get namespace IO
let io;
if (service.socket.nsp && service.socket.nsp !== "/") {
io = socketHandler.addNameSpace(service.socket.nsp, service.role);
}
else
io = IO;
service.io = io;
io.on("connection", function (socket) {
if (_.isFunction(service.socket.afterConnection)) {
service.socket.afterConnection.call(service, socket, io);
}
_.forIn(service.actions, (actionFunc, name) => {
let action = actionFunc.settings;
action.handler = actionFunc;
if (!_.isFunction(action.handler))
throw new Error(`Missing handler function in '${name}' action in '${service.name}' service!`);
let cmd = "/" + service.namespace + "/" + action.name;
let handler = (data, callback) => {
let ctx = Context.CreateFromSocket(service, action, self.app, socket, data);
logger.debug(`Request via WebSocket '${service.namespace}/${action.name}'`, ctx.params);
console.time("SOCKET request");
self.emit("request", ctx);
let cacheKey = service.getCacheKey(action.name, ctx.params);
Promise.resolve()
// Resolve model if ID provided
.then(() => {
return ctx.resolveModel();
})
// Check permission
.then(() => {
return ctx.checkPermission();
})
// Call the action handler
.then(() => {
return action.handler(ctx);
})
// Response the result
.then((json) => {
if (_.isFunction(callback)) {
callback(response.json(null, json));
}
})
// Response the error
.catch((err) => {
logger.error(err);
if (_.isFunction(callback)) {
callback(response.json(null, null, err));
}
})
.then(() => {
self.emit("response", ctx);
console.timeEnd("SOCKET request");
});
};
socket.on(cmd, handler);
if (service.version) {
socket.on("/v" + service.version + cmd, handler);
}
});
});
}
});
}
/**
* Get actions of services as GraphQL queries & mutations schema
*/
registerGraphQLSchema() {
let self = this;
let schemas = {
queries: [],
types: [],
mutations: [],
resolvers: []
};
_.forIn(this.services, (service, name) => {
if (service.$settings.graphql !== false && _.isObject(service.$schema.graphql)) {
let graphQL = service.$schema.graphql;
graphQL.resolvers = graphQL.resolvers || {};
let processResolvers = function(resolvers) {
_.forIn(resolvers, (resolver, name) => {
if (_.isString(resolver) && service.actions[resolver]) {
let handler = (root, args, context) => {
let actionFunc = service.actions[resolver];
let action = actionFunc.settings;
action.handler = actionFunc;
if (!_.isFunction(action.handler))
throw new Error(`Missing handler function in '${name}' action in '${service.name}' service!`);
let ctx = Context.CreateFromGraphQL(service, action, root, args, context);
logger.debug("Request via GraphQL", ctx.params, context.query);
console.time("GRAPHQL request");
self.emit("request", ctx);
let cacheKey = service.getCacheKey(action.name, ctx.params);
return Promise.resolve()
// Resolve model if ID provided
.then(() => {
return ctx.resolveModel();
})
// Check permission
.then(() => {
return ctx.checkPermission();
})
// Call the action handler
.then(() => {
return action.handler(ctx);
})
.catch((err) => {
logger.error(err);
throw err;
})
.then((json) => {
self.emit("response", ctx);
console.timeEnd("GRAPHQL request");
return json;
});
};
resolvers[name] = handler;
}
});
};
if (graphQL.resolvers.Query)
processResolvers(graphQL.resolvers.Query);
if (graphQL.resolvers.Mutation)
processResolvers(graphQL.resolvers.Mutation);
schemas.queries.push(graphQL.query);
schemas.types.push(graphQL.types);
schemas.mutations.push(graphQL.mutation);
schemas.resolvers.push(graphQL.resolvers);
}
});
// Merge Type Definitons
if (schemas.queries.length == 0) return null;
let mergedSchema = `
scalar Timestamp
type Query {
${schemas.queries.join("\n")}
}
${schemas.types.join("\n")}
type Mutation {
${schemas.mutations.join("\n")}
}
schema {
query: Query
mutation: Mutation
}
`;
// Merge Resolvers
let mergeModuleResolvers = function(baseResolvers) {
schemas.resolvers.forEach((module) => {
baseResolvers = _.merge(baseResolvers, module);
});
return baseResolvers;
};
return {
schema: [mergedSchema],
resolvers: mergeModuleResolvers({
Timestamp: {
__parseValue(value) {
return new Date(value); // value from the client
},
__serialize(value) {
return value.getTime(); // value sent to the client
},
__parseLiteral(ast) {
if (ast.kind === Kind.INT)
return parseInt(ast.value, 10); // ast value is always in string format
return null;
}
}
/* This version is not working
Copied from http://dev.apollodata.com/tools/graphql-tools/scalars.html
*/
/*
Timestamp: new GraphQLScalarType({
name: "Timestamp",
description: "Timestamp scalar type",
parseValue(value) {
return new Date(value); // value from the client
},
serialize(value) {
return value.getTime(); // value sent to the client
},
parseLiteral(ast) {
if (ast.kind === Kind.INT) {
return parseInt(ast.value, 10); // ast value is always in string format
}
return null;
},
}),*/
})
};
}
/**
* Get a service by name
* @param {String} serviceName Name of service
* @return {Object} Service instance
*/
get(serviceName) {
return this.services[serviceName];
}
/**
* Print service info to the console (in dev mode)
*
* @memberOf Services
*/
printServicesInfo() {
let endPoints = listEndpoints(this.app);
//logger.debug(endPoints);
}
}
// Export instance of class
module.exports = new Services();