backend-js
Version:
Backend-js is a layer built above expressjs to enable behaviours framework for nodejs applications.
1,423 lines (1,171 loc) • 45 kB
JavaScript
/*jslint node: true */
/*jshint esversion: 6 */
;
var express = require("express");
var paginate = require("express-paginate");
var Route = require("route-parser");
var debug = require("debug");
var {
unless
} = require("express-unless");
var vhost = require("vhost");
var define = require("define-js");
var parse = require("parseparams");
var {
URL,
URLSearchParams
} = require("url");
var crypto = require("crypto");
var {
BusinessBehaviourType,
BusinessBehaviour
} = require("behaviours-js");
var {
OFFLINESYNC,
OFFLINEACTION,
ONLINESYNC,
ONLINEACTION
} = BusinessBehaviourType;
var {
businessController
} = require("./controller.js");
var {
getLogBehaviour
} = require("./log.js");
var {
scheduleBehaviour
} = require("./schedule.js");
var {
getInputObjects,
setResponse,
respond,
getSignature,
setSignature,
getRequest
} = require("./utils.js");
debug.enable("backend:*");
debug = debug("backend:behaviour");
var backend = module.exports;
var app = backend.app = express();
backend.serve = express.static;
var join = backend.join = function (s1, s2) {
var fromIndex = s2.startsWith("/") ? 1 : 0;
var toIndex = s1.length;
if (s1.endsWith("/")) toIndex--;
s1 = s1.substr(0, toIndex) + "/";
s2 = s2.substr(fromIndex);
var url = new URL(...[
s2,
new URL(s1, "resolve://")
]);
if (url.protocol === "resolve:") {
var { pathname, search, hash } = url;
return pathname + search + hash;
}
return url.toString();
};
var compare = backend.compare = function () {
var [route1, route2] = arguments;
var route = route2;
var varying = !!(route1 && route1.path);
if (varying) {
varying = route1.path.indexOf(":") > -1;
varying |= route1.path.indexOf("*") > -1;
}
if (varying) route = route1;
if (route === route2) {
route2 = route1;
route1 = route;
}
if (route && route.path) {
route = new Route(route.path);
}
var path1 = route1 && route1.path;
var path2 = route2 && route2.path;
var method1 = (route1 && route1.method) || "";
method1 = method1.toLowerCase();
var method2 = (route2 && route2.method) || "";
method2 = method2.toLowerCase();
var matched = route instanceof Route;
if (matched) {
matched &= !!route.match(path2 || " ");
}
matched |= path1 === path2;
matched &= method1 === method2;
return matched;
};
var resolve = backend.resolve = function () {
var [prefix, suffix, path] = arguments;
var prefixed = typeof prefix === "string";
if (prefixed) {
prefixed &= path.startsWith(prefix);
}
if (prefixed && typeof suffix === "string") {
return join(prefix, suffix);
} else return suffix || prefix;
};
var defaultPrefix = "/";
var types = {
database: OFFLINESYNC,
database_with_action: OFFLINEACTION,
integration: ONLINESYNC,
integration_with_action: ONLINEACTION
};
var routers = {};
var events = {};
var emitters = {};
var behaviours = {
behaviours: {
method: "GET",
path: "/behaviours"
}
};
var BEHAVIOURS = {};
var otherDefaults = {};
var defaultTenants = {};
var defaultOperations = {};
var defaultRemotes = {};
var FetchBehaviours = {};
var LogBehaviours = {};
var upgradePlugins = {};
backend.behaviour = function (path, config) {
var no_app = typeof app !== "function";
if (!no_app) {
no_app |= typeof app.use !== "function";
}
if (no_app) throw new Error("Invalid express app");
if (typeof path === "object") {
config = path;
path = config.path;
}
if (typeof config !== "object" || !config) {
config = {};
}
var no_operations = !config.operations;
if (!no_operations) {
let { operations } = config;
no_operations |= typeof operations !== "object";
}
if (no_operations) config.operations = {};
return function (options, getConstructor) {
if (typeof options !== "object") {
throw new Error("Invalid definition object");
}
if (typeof options.inherits === "function") {
let { prototype } = options.inherits;
if (!(prototype instanceof BusinessBehaviour)) {
throw new Error("Super behaviour should " +
"inherit from BusinessBehaviour");
}
options = Object.assign(Object.keys(...[
BEHAVIOURS
]).reduce(function (ȯptions, name) {
let { constructor } = BEHAVIOURS[name];
if (constructor == options.inherits) {
return BEHAVIOURS[name].options;
}
return ȯptions;
}, {}), options);
}
no_operations = !options.operations;
if (!no_operations) {
let { operations } = options;
no_operations |= typeof operations !== "object";
}
if (no_operations) options.operations = {};
options.operations = Object.assign(...[
{},
defaultOperations,
config.operations,
options.operations
]);
var no_type = typeof options.type !== "string";
if (!no_type) {
no_type |= types[options.type] === undefined;
}
if (no_type) options.type = "database";
var no_version = typeof options.version !== "string";
if (!no_version) {
no_version |= options.version.length === 0;
}
if (no_version) {
throw new Error("Invalid behaviour version");
}
if (typeof getConstructor !== "function") {
throw new Error("Invalid constructor");
}
if (!Array.isArray(options.events)) {
if (options.events) {
debug("Events should be array or use event");
}
options.events = [];
}
if (typeof options.event === "function") {
options.events.push(options.event);
}
options.events = options.events.filter(...[
function (event) {
let valid = typeof event === "function";
if (!valid) {
valid = typeof event === "string";
if (valid) {
valid &= event.length > 0;
}
}
return valid;
}
]);
var no_tenants = !config.tenants;
if (!no_tenants) {
let { tenants: c_tenants } = config;
no_tenants |= typeof c_tenants !== "object";
}
if (no_tenants) config.tenants = {};
var tenants = Object.assign(...[
{},
defaultTenants,
config.tenants
]);
if (typeof options.database !== "function") {
let { database } = options;
options.database = function (req) {
if (database) return database;
return Object.keys(...[
tenants
]).reduce(function (tenant, key) {
if (tenant) return tenant;
let typeOf = typeof tenants[key];
if (req && typeOf === "function") {
if (tenants[key](req)) {
return tenant = key;
}
}
if (typeOf === "string") {
tenants[key] = {
host: tenants[key]
};
}
if (req && [
"object", "string"
].indexOf(typeOf) > -1) {
let {
host, path: päth, method, id
} = tenants[key];
let tenantID = req.get(...[
"Behaviour-Tenant"
]);
if (id == tenantID) {
return tenant = key;
}
let same_host = true;
if (host) vhost(...[
host, function () { }
])(...[req, , function () {
same_host = false;
}]);
if (same_host && (compare({
path: päth, method
}, {
path: päth && req.path,
method: method && req.method
}) || key == tenantID)) {
return tenant = key;
}
return tenant;
}
return undefined;
}, undefined);
};
}
var BehaviourConstructor = define(...[
getConstructor
]).extend(getLogBehaviour(...[
options,
config,
types,
BEHAVIOURS,
defaultTenants,
defaultRemotes,
FetchBehaviours,
LogBehaviours,
function (room) {
return emitters[room];
}
])).defaults({
type: types[options.type]
});
if (options.fetcher) {
var fetcher = "";
if (typeof options.fetcher === "string") {
fetcher = options.fetcher;
}
FetchBehaviours[fetcher] = BehaviourConstructor;
}
if (options.logger) {
var logger = "";
if (typeof options.logger === "string") {
logger = options.logger;
}
LogBehaviours[logger] = BehaviourConstructor;
}
var no_schedule = config.schedule === false;
no_schedule |= otherDefaults.schedule === false;
if (!no_schedule) {
let { schedule } = config;
if (!schedule) {
schedule = otherDefaults.schedule;
}
if (!schedule) {
schedule = !!process.env.SCHEDULE;
}
if (schedule) {
scheduleBehaviour(...[
options,
BehaviourConstructor,
types,
FetchBehaviours
]);
}
}
var named = typeof options.name === "string";
if (named) {
named &= options.name.length > 0;
}
var uniquelyNamed = function () {
let { skipSameRoutes } = config;
if (named && BEHAVIOURS[
options.name
] && skipSameRoutes !== true) {
throw new Error("Duplicate behavior name: " +
options.name + ". Make sure names are " +
"unique and not numerical");
}
return named && !BEHAVIOURS[options.name];
}();
if (uniquelyNamed) {
if (options.name === "behaviours") {
throw new Error("behaviours is a reserved name");
}
var middleware = typeof options.path === "string";
if (middleware) {
middleware &= options.path.length > 0;
}
var routing = middleware;
routing &= typeof options.method === "string";
if (routing) {
let method = options.method.toLowerCase();
routing &= typeof app[method] === "function";
}
var polling = routing && Object.keys(...[
types
]).indexOf(options.type) > 1;
if (!Array.isArray(options.plugins)) {
options.plugins = [];
}
if (typeof options.plugin === "function") {
options.plugins.push(options.plugin);
}
var request_plugins = [
function (req, res, next) {
req.tenant = options.database(req);
next();
},
...options.plugins.filter(function (plugin) {
let valid = typeof plugin === "function";
if (valid) {
valid &= parse(plugin)[0] !== "out";
}
return valid;
})
];
var upgradePlugin = request_plugins.find(...[
function (plugin) {
let [last] = parse(plugin).reverse();
return last === "head";
}
]);
if (upgradePlugin) {
upgradePlugins[options.name] = upgradePlugin;
}
var response_plugin = options.plugins.reduce(...[
function (response_plugin, plugin) {
let valid = typeof plugin === "function";
if (valid) {
valid &= parse(plugin)[0] === "out";
}
if (valid) return plugin;
return response_plugin;
},
undefined
]);
let prefix;
var pathing = typeof path === "string";
if (pathing) {
pathing &= path.length > 0;
}
if (pathing) {
if (config.overwritePath) {
prefix = path;
} else prefix = join(defaultPrefix, path);
} else {
var no_overwrite = !config.overwritePath;
no_overwrite &= defaultPrefix !== "/";
if (no_overwrite) {
prefix = defaultPrefix;
}
}
if (options.events.length > 0 && join(...[
prefix,
options.path
]) == join(prefix, "/events")) {
throw new Error("Invalid path. " +
join(prefix, options.path) +
" is reserved route");
}
BEHAVIOURS[options.name] = {
options: Object.assign({ prefix }, options),
constructor: BehaviourConstructor
};
var behaviour_runner = function () {
var [
req,
res,
next,
inputObjects
] = arguments;
let onClose, database = req.tenant;
var signature = getSignature(req);
var response = {
behaviour: options.name,
version: options.version
};
if (polling) {
let time = new Date(signature).getTime();
response.signature = time;
setSignature(req, res, next, response);
if (typeof signature === "number") return;
}
if (options.paginate) {
inputObjects.paginate = true;
inputObjects.page = req.query.page;
inputObjects.limit = req.query.limit;
}
var behaviour = new BehaviourConstructor({
name: options.name,
type: types[options.type],
priority: options.priority || 0,
timeout: options.timeout,
inputObjects
}, function (name, room) {
if (!req.session) return;
var event = events[name];
if (event && event[room]) {
let { id } = req.session;
let client = event[room][id];
if (client) return client.id;
}
}, function () {
return database;
});
behaviour.isCompleted = function () {
return req.complete;
};
var behaviour_callback = function () {
var [
result,
error
] = arguments;
var request = getRequest(...[
req,
res,
next,
response
]);
if (!request) {
if (polling) setResponse(...[
behaviour_callback.bind(...[
null,
result,
error
]),
response
]);
return;
}
if (polling) delete response.signature;
var failing = typeof error === "object";
failing |= typeof error === "function";
failing |= typeof result !== "object";
var failed = false;
if (failing) {
let typeOf = typeof error;
let responding = typeOf === "function";
failed = responding && error(...[
request.req,
request.res,
request.next
]);
}
if (failing && !failed) {
if (error && !error.behaviour) {
error.behaviour = options.name;
}
if (error && !error.version) {
error.version = options.version;
}
request.next(...[
error || new Error("Error" +
" while executing " +
options.name +
" behaviour, version " +
options.version + "!")
]);
} else if (!failed) {
let typeOf = typeof result;
let responding = typeOf === "object";
let responded = 0;
if (response_plugin) {
responded = response_plugin(...[
result,
request.req,
request.res,
request.next
]);
responding &= !responded;
}
if (responding && responded === 0) {
response.response = result;
if (options.paginate) {
let {
modelObjects: page
} = result;
if (page) {
response.response = page;
}
}
let { length } = options.events;
let eventful = length > 0;
eventful &= !!req.session;
if (eventful) {
let _ = crypto.randomBytes(48);
let token = _.toString("base64");
response.events_token = token;
({ events: _ } = options);
response.events = _.map(...[
function (event) {
let room = event;
_ = typeof event;
if (_ === "function") {
room = event(...[
options.name,
inputObjects
]);
}
let { stringify } = JSON;
let jsonify = !!room;
_ = typeof room;
jsonify &= _ === "object";
if (jsonify) {
room = stringify(room);
}
if (database) {
let { keys } = Object;
let tenant = keys(...[
tenants
]).sort().indexOf(...[
database
]);
room = stringify({
tenant,
event: room
});
}
return room;
}
]).filter(function (room) {
_ = typeof room;
let valid = _ === "string";
if (valid) {
valid &= !!room.trim();
}
if (valid) {
var event = events[
options.name
];
if (!event) {
event = events[
options.name
] = {};
}
if (!event[room]) {
event[room] = {};
}
let { id } = req.session;
event[room][id] = {
token,
date: new Date(),
count: 0
};
return true;
}
return false;
});
}
if (options.paginate) {
let {
pageCount: page
} = result;
if (typeof page !== "number") {
page = 1;
}
let _ = paginate.hasNextPages(...[
request.req
])(page);
response.has_more = _;
}
let { returns } = options;
if (typeof returns !== "function") {
if (!setResponse(...[
returns,
!routing,
request,
response
])) request.next();
} else returns(...[
request.req,
request.res,
result,
request.req.error,
function (outputObjects) {
respond(...[
request.res,
outputObjects
]);
}
]);
} else if (responding) {
request.next();
}
}
if (onClose) {
req.socket.removeListener(...[
"close", onClose
]);
}
};
var fetching = "";
if (typeof options.fetching === "string") {
fetching = options.fetching;
}
var FetchBehaviour = FetchBehaviours[fetching];
if (options.fetcher) {
FetchBehaviour = BehaviourConstructor;
}
let { queue } = options;
if (typeof queue === "function") {
queue = queue(options.name, inputObjects);
}
var cancel = businessController(...[
options.name,
queue,
database,
options.storage,
options.fetcher || options.fetching,
FetchBehaviour,
options.memory,
options.operations,
function () {
return req;
}
]).runBehaviour(...[
behaviour,
options.paginate ? function () {
var [
property,
superProperty
] = arguments;
let page = {
modelObjects: "modelObjects",
pageCount: "pageCount"
};
let map = { options };
if (typeof map === "function") {
var mapped = map(...[
property,
superProperty
]);
if (mapped) return mapped;
}
return page[property];
} : options.map,
behaviour_callback
]);
req.socket.on("close", onClose = function () {
let _ = typeof cancel;
var cancelling = _ === "function";
cancelling &= !polling;
cancelling &= !res.writableEnded;
if (cancelling) {
cancel();
debug("Request aborted and '" +
options.name + "' behaviour" +
" cancelled");
}
});
};
var request_handler = function (req, res, next) {
if (typeof options.parameters !== "function") {
if (!routing || req.complete) {
getInputObjects(...[
options.parameters,
Object.keys(behaviours).map(...[
function (name) {
let {
[name]: ȯptions
} = behaviours, suffix;
if (ȯptions) {
suffix = ȯptions.path;
}
return resolve(...[
prefix,
suffix,
req.path
]);
}]
),
req,
function (inputObjects) {
behaviour_runner(...[
req,
res,
next,
inputObjects
]);
}
]);
} else req.socket.on(...[
"end",
request_handler.bind(null, req, res, next)
]);
} else options.parameters(...[
req,
res,
function (inputObjects, er) {
req.error = er;
if (req.complete) behaviour_runner(...[
req,
res,
next,
inputObjects
]); else throw new Error("Parameters" +
" callback function called before" +
" all request data consumed");
}
]);
};
let filtering = Array.isArray(options.unless);
filtering |= Array.isArray(options.for);
if (filtering) {
request_handler.unless = unless;
request_handler = request_handler.unless({
custom(req) {
var exceptions = [];
if (Array.isArray(options.for)) {
exceptions = options.for;
} else options.for = undefined;
if (Array.isArray(options.unless)) {
exceptions = options.unless;
} else options.unless = undefined;
exceptions = exceptions.filter(...[
function (name) {
let {
[name]: ȯptions
} = behaviours, suffix, method;
if (ȯptions) {
suffix = ȯptions.path;
method = ȯptions.method;
}
return compare({
path: resolve(...[
prefix,
suffix,
req.path
]),
method
}, {
path: req.path,
method: req.method
});
}
]).length;
if (options.unless) {
return exceptions > 0;
}
return exceptions === 0;
}
});
}
filtering = typeof options.host === "string";
if (filtering) {
filtering &= options.host.length > 0;
}
if (filtering) {
request_handler = vhost(...[
options.host,
request_handler
]);
let plugins = request_plugins;
request_plugins = plugins.map(...[
function (plugin) {
return vhost(...[
options.host, plugin
]);
}
]);
} else {
let plugins = request_plugins;
request_plugins = plugins.map(...[
function (plugin) {
return function (req, res, next) {
plugin(req, res, next);
};
}
]);
}
if (routing) {
var names = Object.keys(behaviours);
let { skipSameRoutes } = config;
if (!skipSameRoutes && names.some(...[
function (name) {
let {
[name]: ȯptions
} = behaviours;
return compare({
path: ȯptions.path,
method: ȯptions.method
}, {
path: options.path,
method: options.method
});
}
])) {
throw new Error("Duplicated behavior" +
" path: " + options.path);
}
var router = app;
let prefixing = typeof prefix === "string";
if (prefixing) {
prefixing &= prefix.length > 0;
}
if (prefixing) {
router = routers[prefix];
if (!router) {
router = express.Router({
caseSensitive: true,
mergeParams: true,
strict: true
});
router.use(paginate.middleware(10, 50));
app.use(prefix, router);
routers[prefix] = router;
}
}
router[
options.method.toLowerCase()
].bind(router)(...[
options.path,
...request_plugins,
request_handler
]);
behaviours[options.name] = {
version: options.version,
method: options.method,
path: options.path,
host: options.host,
events: options.events.length > 0,
prefix,
origins: options.origins,
maxAge: options.maxAge,
parameters: function () {
let { parameters } = options;
if (typeof parameters !== "function") {
return parameters;
}
}(),
returns: function () {
let { returns } = options;
if (typeof returns !== "function") {
return returns;
}
}()
};
} else {
if (Object.keys(behaviours).length > 1) {
throw new Error(options.name + " is " +
"defined after a route!");
}
if (middleware) {
var route = options.path;
let prefixing = typeof prefix === "string";
if (prefixing) {
prefixing &= prefix.length > 0;
}
if (prefixing) {
route = join(prefix, options.path);
}
app.use(...[
route,
...request_plugins,
request_handler
]);
} else app.use(...[
...request_plugins,
request_handler
]);
}
} else BEHAVIOURS[Object.keys(BEHAVIOURS).length + 1] = {
options,
constructor: BehaviourConstructor
};
return BehaviourConstructor;
};
};
backend.BehavioursServer = function () {
var [
prefix, parser, remotes, operations, tenants, defaults
] = arguments;
if (defaults && typeof defaults.schedule === "boolean") {
otherDefaults.schedule = defaults.schedule;
}
if (tenants && typeof tenants === "object") {
defaultTenants = tenants;
}
if (operations && typeof operations === "object") {
defaultOperations = operations;
}
if (remotes && typeof remotes === "object") {
defaultRemotes = remotes;
}
var default_prefixing = defaultPrefix === "/";
default_prefixing &= typeof prefix === "string";
if (default_prefixing) {
default_prefixing &= prefix.length > 0;
}
if (default_prefixing) defaultPrefix = prefix;
if (!prefix) prefix = defaultPrefix;
var prefixing = typeof prefix === "string";
if (prefixing) {
prefixing &= prefix.length > 0;
}
app.get(function () {
if (prefixing) return join(prefix, "/behaviours");
return "/behaviours";
}(), function (_, res) {
respond(res, behaviours, parser);
});
var validate_path = function (behaviour, path) {
var behaviour_prefix = behaviour.prefix;
if (!behaviour_prefix) {
behaviour_prefix = defaultPrefix;
}
var behaviour_path = behaviour.path || "/";
var routing = typeof behaviour.method === "string";
if (routing) {
let method = behaviour.method.toLowerCase();
routing &= typeof app[method] === "function";
}
if (!routing) {
behaviour_path = join(behaviour_path, "/*path");
}
return compare({
path: resolve(...[
behaviour_prefix,
behaviour_path,
path
])
}, { path });
};
var validate_host = function (host, req) {
let same_host = true;
let filtering = typeof host === "string";
if (filtering) {
filtering &= host.length > 0;
}
if (filtering) vhost(host, function () { })(...[
req, , function () {
same_host = false;
}
]);
return same_host;
};
this.upgrade = function (req, socket, head) {
var names = Object.keys(BEHAVIOURS);
var [
path,
query
] = (req.originalUrl || req.url).split("?");
if (query) {
query = new URLSearchParams(...[
query
]).toString();
if (names.indexOf(query.behaviour) > -1) {
names = [query.behaviour];
}
}
for (var i = 0; i < names.length; i++) {
if (!upgradePlugins[names[i]]) continue;
let behaviour = BEHAVIOURS[names[i]].options;
let upgrading = validate_host(...[
behaviour.host,
req
]);
if (upgrading) {
upgrading = validate_path(behaviour, path);
}
if (upgrading) {
upgradePlugins[names[i]](...[
req,
socket, ,
head
]);
return true;
}
}
return false;
};
this.validate = function (path, query) {
var name = query.behaviour;
var named = typeof name === "string";
if (named) {
named &= name.length > 0;
}
if (named) {
let behaviour;
if (BEHAVIOURS[name]) {
behaviour = BEHAVIOURS[name].options;
}
let eventful = !!behaviour;
if (eventful) {
eventful &= !!behaviour.events;
}
if (eventful && compare({
path: resolve(...[
behaviour.prefix,
"/events",
path
])
}, { path })) return;
}
return new Error("Not found");
};
this.connect = function (socket) {
let client;
let name = socket.handshake.auth.behaviour;
if (!name) {
name = socket.handshake.query.behaviour;
}
let token = socket.handshake.auth.token;
if (!token) {
token = socket.handshake.query.token;
}
let id = (socket.handshake.session || {}).id;
var authenticating = typeof name === "string";
if (authenticating) {
authenticating &= name.length > 0;
}
authenticating &= typeof token === "string";
if (authenticating) {
authenticating &= token.length > 0;
}
if (authenticating) {
let behaviour;
if (BEHAVIOURS[name]) {
behaviour = BEHAVIOURS[name].options;
}
let eventful = !!behaviour;
if (eventful) {
eventful &= !!behaviour.events;
}
if (eventful && validate_host(...[
behaviour.host,
socket.request
])) {
var joined = false;
var event = events[name];
if (event) socket.on(...[
"join " + name,
function (room) {
if (event[room]) {
client = event[room][id];
}
if (client) {
if (client.id !== socket.id) {
client.count++;
}
var { date: dt } = client;
dt = dt.getTime();
dt = new Date().getTime() - dt;
var authenticated = dt < 60000;
if (client.token !== token) {
authenticated = false;
}
if (client.count !== 1) {
authenticated = false;
}
if (authenticated) {
var room_events = emitters[
room
];
if (!room_events) {
room_events = emitters[
room
] = {};
}
var ëmitters = room_events[
name
];
if (!ëmitters) {
ëmitters = room_events[
name
] = [];
}
if (!ëmitters.find(...[
function () {
var [{
name: e_id
}] = arguments;
var {
name: nsp_id
} = socket.nsp;
return e_id == nsp_id;
}
])) {
ëmitters.push(socket.nsp);
}
if (client.id !== socket.id) {
client.id = socket.id;
socket.join(room);
joined = true;
}
}
}
}
]);
setTimeout(function () {
if (!joined) socket.disconnect(true);
}, 60000);
socket.once("disconnect", function () {
if (client) {
client.count--;
if (client.count === 0) {
client.date = new Date();
}
}
});
return;
}
}
socket.disconnect(true);
};
};
backend.routes = behaviours;