vue-express-mongo-boilerplate
Version:
Express NodeJS application server boilerplate with Mongo and VueJS
624 lines (543 loc) • 13.2 kB
JavaScript
"use strict";
let logger = require("./logger");
let config = require("../config");
let response = require("./response");
let tokgen = require("../libs/tokgen");
let C = require("./constants");
let Sockets = require("./sockets");
let _ = require("lodash");
let hash = require("object-hash");
let Services; // circular reference
/**
* Context class for requests
*/
class Context {
/**
* Constructor of Context
*
* @param {any} called service
*/
constructor(service) {
this.id = tokgen();
this.createdAt = Date.now(); // TODO: HF timer
this.service = service; // service instance
this.io = service.io; // namespace IO
this.app = null; // ExpressJS app
this.req = null; // req from ExpressJS router
this.res = null; // res from ExpressJS router
this.action = null; // action of service
this.t = null; // i18n translate method
this.user = null; // logged in user
this.socket = null; // socket from socket.io session
this.params = []; // params from ExpressJS REST or websocket or GraphQL args
this.model = null; // model from `modelResolvers` (This is a plain JSON object, not a Mongo doc!)
this.modelID = null; // `id` of model from `modelResolvers`
this.provider = "internal"; // `internal`, `rest`, `socket` or `graphql`
this.validationErrors = [];
if (!Services)
Services = require("./services");
}
/**
* Get a service by name of service
*
* @param {any} serviceName
* @returns {Service}
*
* @memberOf Context
*/
services(serviceName) {
return Services.get(serviceName);
}
/**
* Create a new Context from a REST request
*
* @param {any} service
* @param {any} action
* @param {any} app
* @param {any} req
* @param {any} res
* @returns
*/
static CreateFromREST(service, action, app, req, res) {
let ctx = new Context(service);
ctx.provider = "rest";
ctx.app = app;
ctx.req = req;
ctx.res = res;
ctx.t = req.t;
ctx.user = req.user;
ctx.params = _.defaults({}, req.query, req.params, req.body);
ctx.action = action;
return ctx;
}
/**
* Create a new Context from a websocket request
*
* @param {any} service
* @param {any} action
* @param {any} app
* @param {any} socket
* @param {any} data
* @returns
*/
static CreateFromSocket(service, action, app, socket, data) {
let ctx = new Context(service);
ctx.provider = "socket";
ctx.app = app;
ctx.socket = socket;
ctx.t = app.t;
ctx.user = socket.request.user;
ctx.params = data || {};
ctx.action = action;
return ctx;
}
/**
* Create a new Context from a GraphQL request
*
* @param {any} service
* @param {any} action
* @param {any} root
* @param {any} args
* @param {any} context
* @returns
*/
static CreateFromGraphQL(service, action, root, args, context) {
let ctx = new Context(service);
ctx.provider = "graphql";
ctx.t = context.t;
ctx.params = args;
ctx.user = context.user;
ctx.action = action;
context.ctx = ctx;
return ctx;
}
/**
* Create a new Context for initialize services
*
* @param {any} service
* @param {any} app
* @param {any} db
* @returns
*/
static CreateToServiceInit(service) {
let ctx = new Context(service);
ctx.provider = "";
ctx.app = service.$app;
return ctx;
}
/**
* Initialize new Context from self
*
* @param {any} params
* @returns
*
* @memberOf Context
*/
copy(params, appendParams) {
let newCtx = _.defaults(new Context(this.service), this);
newCtx.provider = "internal";
if (appendParams === true)
newCtx.params = _.defaults(this.params, params);
else
newCtx.params = params;
return newCtx;
}
/**
* Return the response time in milliseconds
*
* @returns {Number}
*
* @memberOf Context
*/
responseTime() {
return Date.now() - this.createdAt;
}
/**
* Resolve model from request by id/code
*
* @returns
*
* @memberOf Context
*/
resolveModel() {
if (_.isFunction(this.service.modelResolver)) {
let idParamName = this.service.$settings.idParamName || "id";
let id = this.params[idParamName];
if (id != null) {
return this.service.modelResolver.call(this.service, this, id).then( (model) => {
this.model = model;
return model;
});
}
}
return Promise.resolve(null);
}
/**
* Check the ctx.model exists. If not we throw a BAD_REQUEST exception
*
* @param {any} errorMessage
* @returns
*
* @memberOf Context
*/
assertModelIsExist(errorMessage) {
if (!this.model)
throw this.errorBadRequest(C.ERR_MODEL_NOT_FOUND, errorMessage);
return true;
}
/**
* Check permission of request
*
* @returns
*
* @memberOf Context
*/
checkPermission() {
let permission = this.action.permission || this.service.$settings.permission || C.PERM_LOGGEDIN;
if (permission == C.PERM_PUBLIC)
return Promise.resolve();
return Promise.resolve()
// check logged in
.then(() => {
if (!this.user)
this.errorUnauthorized();
})
// check role
.then(() => {
if (permission == C.PERM_ADMIN && this.isAdmin()) {
this.errorForbidden();
}
else if (permission == C.PERM_USER && this.user.roles.indexOf(C.ROLE_USER) == -1) {
this.errorForbidden();
}
})
// check owner
.then(() => {
if (permission == C.PERM_OWNER && _.isFunction(this.service.$schema.ownerChecker)) {
return this.service.$schema.ownerChecker(this).catch((err) => {
if (_.isObject(err))
throw err;
else
this.errorForbidden(C.ERR_ONLY_OWNER_CAN_EDIT_AND_DELETE, this.t("app:YouAreNotTheOwner"));
});
}
});
}
/**
* Broadcast a websocket message
*
* @param {any} cmd command of message
* @param {any} data data of message
*
* @memberOf Context
*/
broadcast(cmd, data) {
if (this.io) {
let path = "/" + this.service.namespace + "/" + cmd;
logger.debug("Send WS broadcast message to '" + path + "':", data);
this.io.emit(path, data);
}
}
/**
* Send a message back to the requester
*
* @param {any} cmd command of message
* @param {any} data data of message
*
* @memberOf Context
*/
emitUser(cmd, data) {
if (!this.socket && this.user) {
// If not socket (come from REST), but has user, we try to find it
this.socket = _.find(Sockets.userSockets, (socket) => {
return socket.request.user._id == this.user._id;
});
}
if (this.socket) {
let path = "/" + this.service.namespace + "/" + cmd;
logger.debug("Send WS message to " + this.socket.request.user.username + " '" + path + "':", data);
this.socket.emit(path, data);
}
}
/**
* Broadcast a message to a role
*
* @param {any} cmd command of message
* @param {any} data data of message
* @param {any} role If the `role` is not specified, we use the role of service
* @returns
*
* @memberOf Context
*/
emit(cmd, data, role) {
if (!role)
role = this.service.$settings.role;
// If not definied we will send a broadcast
if (!role) {
let path = "/" + this.service.namespace + "/" + cmd;
logger.debug("Send WS broadcast message to '" + path + "':", data);
if (this.socket)
this.socket.broadcast.emit(path, data);
else
this.io.emit(path, data);
return;
}
if (this.io) {
let path = "/" + this.service.namespace + "/" + cmd;
logger.debug("Send WS message to '" + role + "' role '" + path);
_.each(Sockets.userSockets, (socket) => {
let user = socket.request.user;
if (user && user.roles && user.roles.indexOf(role) !== -1) {
// If requested via socket we omit the requester user
if (this.provider == "socket" && user == this.user) return;
logger.debug("Send WS message to " + user.username + " '" + path + "':", data);
socket.emit(path, data);
}
});
}
}
/**
* Check the context has the `name` parameter
*
* @param {any} name
* @returns {boolean}
*
* @memberOf Context
*/
hasParam(name, errorMessage) {
return this.params[name] != null;
}
/**
* Validate the requested parameters
*
* @param {any} name
* @param {any} errorMessage
* @returns
*
* @memberOf Context
*/
validateParam(name, errorMessage) {
let self = this;
let validator = {
name: name,
value: null,
errors: []
};
/**
* Check has no errors yet
*
* @returns
*/
validator.noError = function() {
return validator.errors.length == 0;
};
/**
* Add a new validation error
*
* @param {any} message
*/
validator.addError = function(message) {
validator.errors.push(message);
self.validationErrors.push(message);
};
/**
* Close the validation. If no error set back the parameter value to this.params
*
* @returns
*/
validator.end = function() {
if (validator.noError())
self.params[validator.name] = validator.value;
return validator.value;
};
/**
* Throw exception if has validation error
*
* @returns
*/
validator.throw = function() {
if (!validator.noError())
throw new Error(validator.errors.join(" "));
return validator.value;
};
/**
* Assert the parameter is not empty
*
* @param {any} errorMessage
* @returns
*/
validator.notEmpty = function(errorMessage) {
if (validator.value == null || validator.value === "")
validator.addError(errorMessage || `Parameter '${name}' is empty!`); // i18n
if (_.isArray(validator.value) && validator.value.length == 0)
validator.addError(errorMessage || `Parameter '${name}' is empty!`); // i18n
return validator;
};
/**
* Assert the parameter is a Number
*
* @param {any} errorMessage
* @returns
*/
validator.isNumber = function(errorMessage) {
if (validator.value != null)
return _.isNumber(validator.value);
// We don't check if it is not null
return true;
};
/**
* Trim the content of parameter
*
* @returns
*/
validator.trim = function() {
if (validator.noError() && validator.value != null)
validator.value = validator.value.trim();
return validator;
};
let value = this.params[name];
if (value != null)
validator.value = value;
//else
// validator.addError(errorMessage || `Parameter '${name}' missing!`); // i18n
return validator;
}
/**
* Check has validation errors
*
* @returns
*
* @memberOf Context
*/
hasValidationErrors() {
return this.validationErrors.length > 0;
}
/**
* Generate and throw a new BAD_REQUEST response error
*
* @param {any} type type of error
* @param {any} msg message of error (localized)
*
* @memberOf Context
*/
errorBadRequest(type, msg) {
let err = new Error(msg);
err = _.defaults(response.BAD_REQUEST);
if (type)
err.type = type;
if (msg)
err.message = msg;
throw err;
}
/**
* Generate and throw a new FORBIDDEN response error
*
* @param {any} type type of error
* @param {any} msg message of error (localized)
*
* @memberOf Context
*/
errorForbidden(type, msg) {
let err = new Error(msg);
err = _.defaults(response.FORBIDDEN);
if (type)
err.type = type;
if (msg)
err.message = msg;
throw err;
}
/**
* Generate and throw a new UNAUTHORIZED response error
*
* @param {any} type type of error
* @param {any} msg message of error (localized)
*
* @memberOf Context
*/
errorUnauthorized(type, msg) {
let err = new Error(msg);
err = _.defaults(response.UNAUTHORIZED);
if (type)
err.type = type;
if (msg)
err.message = msg;
throw err;
}
/**
* Send notification from data changes via websocket
*
* @param {any} type type of changes
* @param {any} json Changed JSON object
* @param {any} role affected role
*
* @memberOf Context
*/
notifyChanges(type, json, role) {
let response = {
status: 200,
event: type,
data: json
};
if (this.user) {
let personService = this.services("persons");
response.user = personService.toJSON(this.user);
}
this.emit(type, response, role);
}
/**
* Process limit, offset and sort params from request
* and use them in the query
*
* Example:
* GET /posts?offset=20&limit=10&sort=-votes,createdAt
*
* @param {query} query Mongo query object
* @return {query}
*
* @memberOf Context
*/
queryPageSort(query) {
if (this.params) {
if (this.params.limit)
query.limit(this.params.limit);
if (this.params.offset)
query.skip(this.params.offset);
if (this.params.sort)
query.sort(this.params.sort.replace(/,/, " "));
}
return query;
}
/**
* Check the request is authenticated.
*
* @param {any} role
* @returns
*
* @memberOf Context
*/
isAuthenticated(role) {
return this.user != null;
}
/**
* Check the request come from a user who has the required role
*
* @param {any} role required role
* @returns
*
* @memberOf Context
*/
hasRole(role) {
return this.user && this.user.roles.indexOf(role) != -1;
}
/**
* Check the requester user is an admin (has "admin" role)
*
* @returns {boolean}
*
* @memberOf Context
*/
isAdmin() {
return this.user && this.hasRole(C.ROLE_ADMIN);
}
}
module.exports = Context;