wires-domain
Version:
Restfull service with dependency injection
557 lines (502 loc) • 15.8 kB
JavaScript
var pathToRegexp = require('path-to-regexp');
var _ = require('lodash');
var Require = require('./_require');
var Promise = require("promise");
var logger = require("log4js").getLogger("domain");
var Promise = require("promise");
var Convinience = require("./_convenience");
var shortid = require('shortid');
var moment = require("moment")
var RestFul = [];
// ETAG
//
//
//
global.__wires_etags__ = global.__wires_etags__ || {};
var ETagMemoryStorage = {
generate: function() {
return shortid.generate();
},
update: function(key, tag) {
return new Promise(function(resolve, reject) {
global.__wires_etags__[key] = tag;
return resolve({
key: tag
});
});
},
status: function(key, tag) {
return new Promise(function(resolve, reject) {
return resolve({
modified: global.__wires_etags__[key] === undefined ? true : global.__wires_etags__[key] !== tag,
current: global.__wires_etags__[key]
});
})
}
}
var getETagStorageProvider = function() {
return new Promise(function(resolve, reject) {
if (Require.isServiceRegistered('ETagCustomStorage')) {
return Require.require(function(ETagCustomStorage) {
return resolve(_.assignIn(ETagCustomStorage, ETagMemoryStorage));
});
}
return resolve(ETagMemoryStorage);
});
}
var eTagProvider = {
generate: function(key) {
return getETagStorageProvider().then(function(provider) {
var tag = provider.generate();
return provider.update(key, tag);
});
},
status: function(key, tag) {
var provider;
return getETagStorageProvider().then(function(_provider) {
provider = _provider;
return provider.status(key, tag);
});
}
}
Require.service('$eTag', function() {
return eTagProvider;
});
var getResourceCandidate = function(resources, startIndex, url) {
for (var i = startIndex; i < resources.length; i++) {
var item = resources[i];
var keys = [];
var re = pathToRegexp(item.path, keys);
params = re.exec(url);
if (params) {
return {
params: params,
keys: keys,
handler: item.handler,
nextIndex: i + 1
};
}
}
};
// Register local services
// Will be available only on rest service construct
var restLocalServices = function(info, params, req, res) {
var services = {
$req: req,
$res: res,
$params: params,
// Next function tries to get next
$next: function() {
var resources = RestFul;
var data = getResourceCandidate(resources, info.nextIndex, req.path);
if (data) {
return callCurrentResource(data, req, res);
}
}
};
// Helper to validate required arguments
// Helper to validate required arguments
var required = function() {
var err;
var self = this;
_.each(arguments, function(item) {
if (_.isString(item)) {
if (!self[item]) {
err = {
status: 400,
message: item + " is required"
};
return false;
}
}
// If it's a dictionary with options
if (_.isPlainObject(item)) {
_.each(item, function(funcValidate, k) {
// Assume k - is query's argument
// v should be a function
if (_.isFunction(funcValidate) && _.isString(k)) {
self[k] = funcValidate(self[k]);
}
});
}
});
if (err) {
throw err;
}
};
var __get = function(opt, defaultValue) {
var s = opt.split("@");
var name = s[0];
var p = s[1];
var isRequired = false;
var intRequested = false;
var xpathSplit = name.split('.');
var value;
var spitError = function(code, message) {
throw {
status: code || 400,
message: message,
validation: true
};
}
if (xpathSplit.length > 1) {
value = this[xpathSplit[0]];
if (_.isPlainObject(value)) {
var valueValid = true;
for (var i = 1; i < xpathSplit.length; i++) {
if (valueValid === true) {
var x = xpathSplit[i];
if (value !== undefined) {
value = value[x];
} else {
valueValid = false
}
}
}
} else {
value = undefined;
}
} else {
value = this[name];
}
var params = {};
if (p !== undefined) {
params = Convinience.parse(p, {
cache: true,
dict: true
});
};
if (params.required && (value === undefined || value === "")) {
spitError(400, params.required.attrs[0] || (name + " is required"))
}
if (params.bool) {
if (value === undefined) {
return false;
}
value = value.toString();
if (value === "1" || value === "true") {
return true;
}
return false;
}
if (params.min) {
var minSymols = (params.min.attrs[0] * 1 || 0);
if (value === undefined || value.toString().length < minSymols) {
var eMessage = params.min.attrs[1] || "Expected to have at least " + minSymols + " in " + name;
spitError(400, eMessage)
}
}
if (params.max) {
var maxSymbols = (params.max.attrs[0] * 1 || 255);
if (value === undefined || value.toString().length > maxSymbols) {
var eMessage = params.max.attrs[1] || "Expected to have not more than " + maxSymbols + " in " + name;
spitError(400, eMessage);
}
}
// momentjs
if (params.moment) {
var format = params.moment.attrs[0];
var eMessage = params.moment.attrs[1] || "Invalid moment format.";
if (value !== undefined) {
try {
return moment(value, format);
} catch (e) {
spitError(400, eMessage);
}
} else {
spitError(400, eMessage);
}
}
if (params.email) {
if (value !== undefined) {
var eMessage = params.email.attrs[0] || "Email is in wrong format";
var re =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if (!re.test(value)) {
spitError(400, eMessage);
}
}
}
if (params.phone) {
var validateTelephoneRegEx =
/(00|\+)(9[976]\d|8[987530]\d|6[987]\d|5[90]\d|42\d|3[875]\d|2[98654321]\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|4[987654310]|3[9643210]|2[70]|7|1)\d{3,14}$/;
if (!validateTelephoneRegEx.test(value)) {
var eMessage = params.phone.attrs[0] || "Phone is in wrong format";
spitError(400, eMessage);
}
}
// Integer validation
if (params.int) {
if (value !== undefined) {
var eMessage = params.int.attrs[0] || (name + " is in wrong format (int required)");
value = value.toString();
if (!value.match(/^\d+$/)) {
spitError(400, eMessage);
}
value = value * 1;
}
}
if (params.date) {
if (value !== undefined) {
var eMessage = params.date.attrs[0] || (name + " is in wrong format");
value = value.toString();
try {
value = new Date(value)
} catch (e) {
spitError(400, eMessage);
}
}
}
if (_.isFunction(defaultValue)) {
return defaultValue(value)
}
return value !== undefined ? value : defaultValue;
}
services.$jsonp = function(cbname) {
req.__jsonp_callback__ = cbname || "callback";
};
// Body
services.$body = {
require: required.bind(req.body),
attrs: req.body,
get: __get.bind(req.body),
getAttributes: function() {
return req.body;
}
};
services.$cors = function(domains) {
res.header("Access-Control-Allow-Origin", domains || "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Session");
};
// Query
services.$query = {
require: required.bind(req.query),
attrs: req.query,
get: __get.bind(req.query),
getAttributes: function() {
return req.query;
}
};
// Assertion codes
services.$assert = {
bad_request: function(message) {
throw {
status: 400,
message: message || "Bad request"
};
},
handle: function(message, code) {
throw {
status: code || 400,
handler: message || "errr.bad_request"
};
},
validation: function(message) {
throw {
status: code || 400,
validation: true,
message: message || "errr.bad_request"
};
},
unauthorized: function(message) {
throw {
status: 401,
message: message || "Unauthorized"
};
},
not_found: function(message) {
throw {
status: 404,
message: message || "Not Found"
};
}
};
return services;
};
var getAssertHandler = function(_locals) {
return new Promise(function(resolve, reject) {
if (Require.isServiceRegistered("WiresAssertHandler")) {
return Require.require('WiresAssertHandler', _locals, function(WiresAssertHandler) {
return resolve(WiresAssertHandler);
});
}
return resolve();
})
}
var callCurrentResource = function(info, req, res) {
// Extract params
var mergedParams = {};
var params = info.params;
var handler = info.handler;
_.each(info.keys, function(data, index) {
var i = index + 1;
if (params[i] !== null && params[i] !== undefined) {
var parameterValue = params[i];
if (parameterValue.match(/^\d{1,4}$/)) {
parameterValue = parseInt(parameterValue);
}
mergedParams[data.name] = parameterValue;
}
});
// Define method name
var method = req.method.toLowerCase();
var corsDomains = "*";
// check for cors option request
if (method === "options" && handler.cors === true) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Session");
return res.send({});
}
// setting cors headers for any other method
if (handler.cors) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Session");
}
// Allow to define free style method for access
if (mergedParams.action) {
method = mergedParams.action;
}
// Define parse options
var parseOptions;
if (_.isPlainObject(handler)) {
if (handler[method]) {
parseOptions = {
source: handler[method],
target: handler[method],
instance: handler
};
}
}
if (_.isFunction(handler)) {
parseOptions = handler;
}
// If there is nothing to execute
if (!parseOptions) {
return res.status(501).send({
error: 501,
message: "Not implemented"
});
}
var executeInteceptor = function() {
return new Promise(function(resolve, reject) {
if (!handler.intercept)
return resolve();
if (_.isString(handler.intercept)) {
return Require.require(handler.intercept, function(remoteInterceptor) {
return Require.require(remoteInterceptor,
restLocalServices(info, mergedParams, req, res));
}).then(resolve).catch(reject);
}
if (_.isFunction(handler.intercept)) {
return Require.require(handler.intercept, restLocalServices(info, mergedParams, req, res)).then(resolve).catch(reject);
}
return resolve();
});
}
var requireAndCallDestination = function() {
executeInteceptor().then(function(additionalServices) {
var _localServices = restLocalServices(info, mergedParams, req, res);
if (additionalServices && _.isPlainObject(additionalServices)) {
_localServices = _.merge(_localServices, additionalServices);
}
return Require.require(parseOptions, _localServices).then(function(result) {
if (result !== undefined) {
return res.send(result);
}
})
}).catch(function(e) {
var err = {
status: 500,
message: "Error"
};
logger.fatal(e.stack || e);
// If we have a direct error
if (e.stack) {
return res.status(500).send({
status: 500,
message: "Server Error"
});
}
if (_.isObject(e)) {
var status = e.status || 500;
e.message = e.message || "Server Error";
return getAssertHandler(restLocalServices(info, mergedParams, req, res)).then(function(
assertHandler) {
if (assertHandler) {
return assertHandler(e);
}
return e;
}).then(function(result) {
return res.status(status).send(result !== undefined ? result : err);
}).catch(function(e) {
logger.fatal(e.stack || e);
return res.status(500).send({
status: 500,
message: "Server Error"
});
});
}
res.status(err.status).send(err);
});
};
if (handler.eTag && method === 'get') {
var tagName = _.isFunction(handler.eTag) ? handler.eTag(req, res) : handler.eTag;
if (!tagName) {
return requireAndCallDestination();
}
var tag = req.headers['if-none-match'];
var ps = _.merge(req.query, mergedParams);
for (var k in ps) {
var v = ps[k];
if (v !== undefined) {
tagName = tagName.split('$' + k).join(v);
}
}
return eTagProvider.status(tagName, tag).then(function(status) {
if (status.modified === true) {
if (status.current) {
res.setHeader('ETag', status.current);
}
return requireAndCallDestination();
} else {
if (status.current) {
res.setHeader('ETag', status.current);
}
return res.status(304).send('');
}
});
} else {
return requireAndCallDestination();
}
};
var express = function(req, res, next) {
var resources = RestFul;
var data = getResourceCandidate(resources, 0, req.path);
if (!data) {
return next();
}
return callCurrentResource(data, req, res);
};
var Path = function() {
var handlers = [];
var path;
_.each(arguments, function(item) {
if (!path) {
path = item;
} else {
handlers.push(item);
}
});
_.each(handlers, function(handler) {
RestFul.push({
path: path,
handler: handler
});
});
};
module.exports = {
express: function() {
return express;
},
path: Path
};