controller
Advanced tools
Comparing version 0.6.2 to 1.0.0
@@ -1,60 +0,84 @@ | ||
var isRegExp = require('util').isRegExp; | ||
var join = require('path').join; | ||
var express = require('express'); | ||
var _ = require('underscore'); | ||
var methods = require('methods'); | ||
const isRegExp = require('util').isRegExp; | ||
const join = require('path').join; | ||
const Router = require('router'); | ||
const RouterLayer = require('router/lib/layer'); | ||
const _ = require('underscore'); | ||
const methods = require('methods'); | ||
const cuid = require('cuid'); | ||
const parseurl = require('parseurl'); | ||
var Controller = module.exports = function Controller() { | ||
if (!(this instanceof Controller)) { | ||
return new Controller(); | ||
} | ||
const createAnonymousGroupName = () => `anonymous-middleware-group-${cuid()}` | ||
var self = this; | ||
this.routes = [], | ||
this.actions = {}, | ||
this.middlewares = [], | ||
this.chainCache = {}, | ||
this.app = express(); | ||
// Make controller look like a server | ||
Object.defineProperty(this, 'handle', { | ||
get: function() { | ||
return this.app.handle.bind(this.app); | ||
} | ||
}); | ||
// Make sure Connect doesn't try to eat our route | ||
Object.defineProperty(this, 'route', { | ||
enumerable: true, configurable: true, | ||
writable: false, value: this.createRoute | ||
}); | ||
this._controllerInit = function controllerInit(req, res, next) { | ||
var route = _.find(self.routes, function(route) { | ||
return route.method === req.route.method | ||
&& route.path === req.route.path | ||
&& route.self === self; | ||
module.exports = function createController() { | ||
// an array of routes from URL paths or RegExps to actions. | ||
// [ { method: 'get', path: '/user/:id', action: 'getUser', controller: 'controllerId' } ] | ||
const routes = []; | ||
// a map of actions indexed by their names | ||
// { getUser: { groups: ['checkAuth', 'accessors'], handler: () => {} } } | ||
const actions = {}; | ||
// an array of middlewares. each middleware is a function, with "controller" and | ||
// "scope" properties (scope is an array of groups). | ||
const middlewares = []; | ||
// a cache of middleware chains so that a chain does not have to be calculated | ||
// every time a route is called. | ||
const chainCache = {}; | ||
const id = cuid(); | ||
const router = Router(); | ||
const Controller = (req, res, next) => { | ||
router(req, res, next); | ||
}; | ||
Controller.actions = actions; | ||
Controller.routes = routes; | ||
Controller.middlewares = middlewares; | ||
Controller.router = Controller.app = router; | ||
Controller._controllerId = id; | ||
// middleware entry point, the inline middleware that is passed to router | ||
const injectRouteScope = (req, res, next) => { | ||
// find a route that matches this route | ||
const pathname = parseurl(req).pathname; | ||
const route = _.find(routes, function(route) { | ||
var match = false; | ||
try { match = route.layer.match(pathname) } catch (e) {} | ||
return route.method === req.method.toLowerCase() | ||
&& match | ||
&& route.controller === id; | ||
}); | ||
if (route) { | ||
req.action = route.action; | ||
req._action = self.actions[req.action]; | ||
// the metadata for the action, for example { groups: [], handler: ()=>{} } | ||
const action = actions[route.action]; | ||
req.action = { | ||
key: route.action, | ||
groups: action.groups, | ||
handler: action.handler | ||
}; | ||
} | ||
// create a scope ['all', ...scopes defined as a part of action..., 'actionName'] | ||
var scope = ['all']; | ||
if (req._action) { | ||
scope = scope.concat(req._action.groups); | ||
scope.push(req.action); | ||
if (req.action) { | ||
scope = scope.concat(req.action.groups); | ||
scope.push(req.action.key); | ||
} | ||
var key = req.route.path + '-' + scope.join(','); | ||
if (!self.chainCache[key]) { | ||
self.chainCache[key] = collectMiddlewares.call(self, scope); | ||
req.route.callbacks = _.reject(req.route.callbacks, function(mw) { | ||
return !!mw.scope && mw.self == self; | ||
}); | ||
var chain = self.chainCache[key]; | ||
req.route.callbacks.splice.apply(req.route.callbacks, | ||
[1, 0].concat(chain)); | ||
// check if the middleware chain has been calculated yet. If not, or if it's | ||
// been invalidated, we calculate it again, clear the existing Controller | ||
// middleware from the route's stack, and add it again. | ||
const key = req.route.path + '-,' + scope.join(','); | ||
if (!chainCache[key]) { | ||
const chain = chainCache[key] = getMiddlewareMatchingScope(scope); | ||
// first we remove any existing middleware | ||
const stack = req.route.stack; | ||
for (let i = 0; i < stack.length; ++i) { | ||
const mw = stack[i].handle; | ||
if (!mw.scope) continue; | ||
stack.splice(i--, 1); | ||
} | ||
const layerChain = chain.map(mw => RouterLayer('/', {}, mw)); | ||
stack.splice.apply(stack, [1, 0].concat(layerChain)); | ||
} | ||
@@ -64,156 +88,144 @@ | ||
}; | ||
} | ||
const getMiddlewareMatchingScope = (scope) => { | ||
const inScope = group => ~scope.indexOf(group); | ||
return _.sortBy( | ||
collectMiddlewaresMatchingScope(Controller, scope), | ||
(mw) => scope.indexOf(_.find(mw.scope, inScope)) | ||
); | ||
} | ||
const collectMiddlewaresMatchingScope = (controller, scope, base = []) => { | ||
if (controller.parent && controller.parent._controllerId) { | ||
base = collectMiddlewaresMatchingScope(controller.parent, scope, base); | ||
} | ||
// Have run into production problems where controllers are created from different | ||
// instances of the Controller library due to weird dependency installations - | ||
// which means that we can't reference the `Controller` var and do `instanceof`, | ||
// we have to set some kind of constant and check against it. At least, that | ||
// seems like the most sensible way to me. | ||
Object.defineProperty(Controller.prototype, '_type', { | ||
enumerable: false, configurable: false, writable: false, value: '_controller_express_ext' | ||
}); | ||
const inScope = (group) => ~scope.indexOf(group); | ||
return base.concat(controller.middlewares.filter(mw => mw.scope.some(inScope))); | ||
} | ||
const addSubController = function (route, controller) { | ||
if (controller._controllerId) controller.parent = Controller; | ||
router.use(route, controller); | ||
return Controller; | ||
}; | ||
Controller.middleware = Controller.use = function (route, controller) { | ||
// just assume we're mounting a subcontroller/app to start with... | ||
if (typeof route != 'string' && !isRegExp(route)) { | ||
controller = route; | ||
route = '/'; | ||
} | ||
if (controller && controller._controllerId) return addSubController(route, controller); | ||
else if (route == '/') { route = controller; controller = undefined } | ||
// oh, not a controller? ok, proceed as normal... | ||
const args = _.flatten([].slice.call(arguments), true); | ||
const scope = []; | ||
const fns = []; | ||
function collectMiddlewares(scope) { | ||
var inScope = function(group) { return ~scope.indexOf(group); }; | ||
return _.sortBy( | ||
_collectMiddlewares.call(this, scope), | ||
function(mw) { return scope.indexOf(_.find(mw.scope, inScope)) } | ||
); | ||
} | ||
while (args.length) { | ||
const arg = args.shift(); | ||
if (arg == null) continue; | ||
(typeof arg === 'function' ? fns : scope).push(arg); | ||
} | ||
function _collectMiddlewares(scope, base) { | ||
if (!base) base = []; | ||
if (this.parent && this.parent._type && this.parent._type == this._type) | ||
base = _collectMiddlewares.call(this.parent, scope, base); | ||
if (!scope.length) scope.push('all'); | ||
fns.forEach(function(fn) { | ||
fn.scope = scope; | ||
fn.controller = id; | ||
middlewares.push(fn); | ||
}); | ||
var inScope = function(group) { return ~scope.indexOf(group); }; | ||
return base.concat( | ||
_.chain(this.middlewares) | ||
.filter(function(mw) { return mw.scope.some(inScope) }) | ||
.value() | ||
); | ||
} | ||
// Purge any related caches. | ||
Object.keys(chainCache).forEach(function(chain) { | ||
var groups = chain.split(','); | ||
if (!!groups.some(group => ~scope.indexOf(group))) { | ||
delete chainCache[chain]; | ||
} | ||
}); | ||
Controller.prototype.addSubController = function(route, controller) { | ||
if (typeof route != 'string') { | ||
controller = route; | ||
route = '/'; | ||
return Controller; | ||
} | ||
Controller.route = function (method, path, action) { | ||
method = method.toLowerCase(); | ||
router[method](path, injectRouteScope, function(req, res, next) { | ||
if (!actions[action]) { | ||
next(new Error('Unhandled action - ' + method + ' ' + action)); | ||
} else { | ||
actions[action].handler.call(Controller, req, res, next); | ||
} | ||
}); | ||
routes.push({ | ||
method, | ||
path, | ||
action, | ||
layer: router.stack[router.stack.length - 1], | ||
controller: id | ||
}); | ||
if (controller instanceof Controller) controller.parent = this; | ||
this.app.use(route, controller); | ||
}; | ||
Controller.prototype.middleware = Controller.prototype.use = | ||
function middleware(route, controller) { | ||
// just assume we're mounting a subcontroller/app to start with... | ||
if (typeof route != 'string' && route.handle) { | ||
controller = route; | ||
route = '/'; | ||
return Controller; | ||
} | ||
if (controller && controller.handle) | ||
return this.addSubController(route, controller); | ||
// oh, not a controller/app? ok, proceed as normal... | ||
var args = _.flatten([].slice.call(arguments), true), | ||
scope = [], fns = [], self = this; | ||
while (args.length) { | ||
var arg = args.shift(); | ||
(typeof arg === 'function' ? fns : scope).push(arg); | ||
} | ||
if (!scope.length) scope.push('all'); | ||
var isAll = !!~scope.indexOf('all'); | ||
fns.forEach(function(fn) { | ||
fn.scope = scope; | ||
fn.self = self; | ||
self.middlewares.push(fn); | ||
}); | ||
// Purge any related caches. | ||
Object.keys(this.chainCache).forEach(function(chain) { | ||
var groups = chain.split(','); | ||
if (!!groups.some(function(group) { return ~scope.indexOf(group) })) { | ||
delete self.chainCache[chain]; | ||
Controller.define = function (name, groups, handler) { | ||
if (typeof groups == 'function') handler = groups, groups = []; | ||
if (Array.isArray(groups)) { | ||
//clear old anonymous middlewares if we're overwriting | ||
if (actions[name]) { | ||
for (let i = 0; i < middlewares.length; ++i) { | ||
if (!middlewares[i].anonymous || middlewares[i].scope[0] != name) continue; | ||
middlewares.splice(i--, 1); | ||
} | ||
} | ||
groups = groups.filter((group) => { | ||
if (typeof group == "function") { | ||
group.anonymous = true; | ||
Controller.use(name, group); | ||
return false; | ||
} else { | ||
return true; | ||
} | ||
}) | ||
} | ||
}); | ||
return this; | ||
} | ||
actions[name] = { groups, handler }; | ||
Controller.prototype.createRoute = function route(method, path, action) { | ||
method = method.toLowerCase(); | ||
var self = this; | ||
this.routes.push({ method: method, path: path, action: action, self: self}); | ||
this.app[method](path, this._controllerInit, function(req, res, next) { | ||
if (!self.actions[action]) { | ||
next(new Error('Unhandled action - ' + method + ' ' + action)); | ||
} else { | ||
self.actions[action].handler.call(self, req, res, next); | ||
} | ||
return Controller; | ||
} | ||
methods.forEach(function(method) { | ||
Controller[method] = function() { | ||
return Controller.route.apply(null, [method].concat([].slice.call(arguments))); | ||
}; | ||
}); | ||
return this; | ||
} | ||
Controller.prototype.define = function define(name, groups, handler) { | ||
if (typeof groups == 'function') handler = groups, groups = []; | ||
if (Array.isArray(groups)) { | ||
var self = this; | ||
//clear old anonymous middlewares if we're overwriting | ||
if (this.actions[name]) { | ||
self.middlewares.forEach(function(middleware) { | ||
if (middleware.anonymous && middleware.scope[0] == name) | ||
self.middlewares = _.without(self.middlewares, middleware); | ||
}); | ||
} | ||
groups = groups.filter(function(group) { | ||
if (typeof group == "function") { | ||
group.anonymous = true; | ||
self.use(name, group); | ||
return false; | ||
Controller.direct = function (method, path /* [mw/g], fn */) { | ||
const args = [].slice.call(arguments); | ||
const groups = []; | ||
const id = createAnonymousGroupName(); | ||
args.shift(); args.shift(); | ||
const handler = args.pop(); | ||
var item; | ||
while (args.length) { | ||
item = args.shift(); | ||
if (typeof item === 'string') { | ||
groups.push(item); | ||
} else { | ||
return true; | ||
const anonGroup = createAnonymousGroupName(); | ||
groups.push(anonGroup); | ||
Controller.use(anonGroup, item); | ||
} | ||
}) | ||
} | ||
} | ||
Controller.define(id, groups, handler); | ||
Controller.route(method, path, id); | ||
this.actions[name] = { groups: groups, handler: handler }; | ||
return this; | ||
} | ||
methods.forEach(function(method) { | ||
Controller.prototype[method] = function() { | ||
return this.route.apply(this, [method].concat([].slice.call(arguments))); | ||
}; | ||
}); | ||
Controller.prototype.direct = function(method, path /* [mw/g], fn */) { | ||
var args = [].slice.call(arguments), | ||
groups = [], item, id = createAnonymousGroupName(); | ||
args.shift(); args.shift(); | ||
var handler = args.pop(); | ||
while (args.length) { | ||
item = args.shift(); | ||
if (typeof item === 'string') { | ||
groups.push(item); | ||
} else { | ||
var anonGroup = createAnonymousGroupName(); | ||
groups.push(anonGroup); | ||
this.middleware(anonGroup, item); | ||
} | ||
return Controller; | ||
} | ||
this.define(id, groups, handler); | ||
this.route(method, path, id); | ||
return this; | ||
return Controller; | ||
} | ||
var createAnonymousGroupName = (function() { | ||
var anonSeed = Math.floor(Math.random() * Math.pow(10, 10)); | ||
return function() { return 'anonymous-middleware-group-' + ++anonSeed } | ||
})(); |
{ | ||
"name": "controller", | ||
"version": "0.6.2", | ||
"version": "1.0.0", | ||
"description": "an action controller for express", | ||
@@ -14,10 +14,13 @@ "main": "lib/controller.js", | ||
"devDependencies": { | ||
"supertest": "~0.1.2", | ||
"mocha": "*" | ||
"express": "^4.16.2", | ||
"mocha": "^4.0.1", | ||
"supertest": "^3.0.0" | ||
}, | ||
"dependencies": { | ||
"express": "~3.4.4", | ||
"underscore": "~1.3.3", | ||
"methods": "0.0.1" | ||
"cuid": "^1.3.8", | ||
"methods": "^1.1.2", | ||
"parseurl": "^1.3.2", | ||
"router": "^1.3.2", | ||
"underscore": "^1.8.3" | ||
} | ||
} |
@@ -76,3 +76,3 @@ var assert = require('assert'); | ||
c0.app.use('/second/', c); | ||
c0.use('/second/', c); | ||
app.use('/first/', c0); | ||
@@ -79,0 +79,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
29802
523
1
5
3
5
+ Addedcuid@^1.3.8
+ Addedparseurl@^1.3.2
+ Addedrouter@^1.3.2
+ Addedarray-flatten@3.0.0(transitive)
+ Addedbrowser-fingerprint@0.0.1(transitive)
+ Addedcore-js@1.2.7(transitive)
+ Addedcuid@1.3.8(transitive)
+ Addeddebug@2.6.9(transitive)
+ Addedmethods@1.1.2(transitive)
+ Addedms@2.0.0(transitive)
+ Addednode-fingerprint@0.0.2(transitive)
+ Addedparseurl@1.3.3(transitive)
+ Addedpath-to-regexp@0.1.7(transitive)
+ Addedrouter@1.3.8(transitive)
+ Addedsetprototypeof@1.2.0(transitive)
+ Addedunderscore@1.13.7(transitive)
+ Addedutils-merge@1.0.1(transitive)
- Removedexpress@~3.4.4
- Removedbatch@0.5.0(transitive)
- Removedbuffer-crc32@0.2.1(transitive)
- Removedbytes@0.2.1(transitive)
- Removedcommander@1.3.2(transitive)
- Removedconnect@2.12.0(transitive)
- Removedcookie@0.1.0(transitive)
- Removedcookie-signature@1.0.1(transitive)
- Removedcore-util-is@1.0.3(transitive)
- Removeddebug@0.8.1(transitive)
- Removedexpress@3.4.8(transitive)
- Removedfresh@0.2.0(transitive)
- Removedinherits@2.0.4(transitive)
- Removedisarray@0.0.1(transitive)
- Removedkeypress@0.1.0(transitive)
- Removedmerge-descriptors@0.0.1(transitive)
- Removedmethods@0.0.10.1.0(transitive)
- Removedmime@1.2.11(transitive)
- Removedmkdirp@0.3.5(transitive)
- Removedmultiparty@2.2.0(transitive)
- Removednegotiator@0.3.0(transitive)
- Removedpause@0.0.1(transitive)
- Removedqs@0.6.6(transitive)
- Removedrange-parser@0.0.4(transitive)
- Removedraw-body@1.1.2(transitive)
- Removedreadable-stream@1.1.14(transitive)
- Removedsend@0.1.4(transitive)
- Removedstream-counter@0.2.0(transitive)
- Removedstring_decoder@0.10.31(transitive)
- Removeduid2@0.0.3(transitive)
- Removedunderscore@1.3.3(transitive)
Updatedmethods@^1.1.2
Updatedunderscore@^1.8.3