From 6f23c7163ad72616e89b67d37245b13c23b38130 Mon Sep 17 00:00:00 2001 From: Erik Ziegler Date: Tue, 7 Jun 2016 19:32:30 +0200 Subject: [PATCH] Fixing iron:router issue with latest Chrome version --- LesionTracker/.meteor/versions | 4 +- OHIFViewer/.meteor/versions | 4 +- Packages/router-middleware-stack/.gitignore | 6 + Packages/router-middleware-stack/History.md | 3 + Packages/router-middleware-stack/LICENSE.txt | 21 ++ Packages/router-middleware-stack/README.md | 18 ++ Packages/router-middleware-stack/circle.yml | 57 ++++ .../examples/basic/.meteor/.gitignore | 1 + .../examples/basic/.meteor/identifier | 1 + .../examples/basic/.meteor/packages | 10 + .../examples/basic/.meteor/release | 1 + .../examples/basic/.meteor/versions | 44 +++ .../examples/basic/basic.js | 29 ++ .../router-middleware-stack/lib/handler.js | 118 ++++++++ .../lib/middleware_stack.js | 282 ++++++++++++++++++ Packages/router-middleware-stack/package.js | 32 ++ .../gagarin/RouterMiddlewareStackTests.js | 17 ++ .../tests/tinytests/handler_test.js | 54 ++++ .../tests/tinytests/middleware_stack_test.js | 182 +++++++++++ .../tests/tinytests/notes.js | 194 ++++++++++++ 20 files changed, 1074 insertions(+), 4 deletions(-) create mode 100644 Packages/router-middleware-stack/.gitignore create mode 100644 Packages/router-middleware-stack/History.md create mode 100644 Packages/router-middleware-stack/LICENSE.txt create mode 100644 Packages/router-middleware-stack/README.md create mode 100644 Packages/router-middleware-stack/circle.yml create mode 100644 Packages/router-middleware-stack/examples/basic/.meteor/.gitignore create mode 100644 Packages/router-middleware-stack/examples/basic/.meteor/identifier create mode 100644 Packages/router-middleware-stack/examples/basic/.meteor/packages create mode 100644 Packages/router-middleware-stack/examples/basic/.meteor/release create mode 100644 Packages/router-middleware-stack/examples/basic/.meteor/versions create mode 100644 Packages/router-middleware-stack/examples/basic/basic.js create mode 100644 Packages/router-middleware-stack/lib/handler.js create mode 100644 Packages/router-middleware-stack/lib/middleware_stack.js create mode 100644 Packages/router-middleware-stack/package.js create mode 100644 Packages/router-middleware-stack/tests/gagarin/RouterMiddlewareStackTests.js create mode 100644 Packages/router-middleware-stack/tests/tinytests/handler_test.js create mode 100644 Packages/router-middleware-stack/tests/tinytests/middleware_stack_test.js create mode 100644 Packages/router-middleware-stack/tests/tinytests/notes.js diff --git a/LesionTracker/.meteor/versions b/LesionTracker/.meteor/versions index d7c058931..7dcaebfa6 100644 --- a/LesionTracker/.meteor/versions +++ b/LesionTracker/.meteor/versions @@ -26,8 +26,8 @@ clinical:hipaa-audit-log@2.4.2 clinical:hipaa-logger@1.0.1 clinical:router@2.0.17 clinical:router-location@2.0.14 -clinical:router-middleware-stack@2.0.13 -clinical:router-url@2.0.15 +clinical:router-middleware-stack@2.1.2 +clinical:router-url@2.1.0 clinical:theming@0.4.10 coffeescript@1.0.11 cornerstone@0.0.1 diff --git a/OHIFViewer/.meteor/versions b/OHIFViewer/.meteor/versions index 013be8bed..96391e615 100644 --- a/OHIFViewer/.meteor/versions +++ b/OHIFViewer/.meteor/versions @@ -14,8 +14,8 @@ callback-hook@1.0.4 check@1.1.0 clinical:router@2.0.17 clinical:router-location@2.0.14 -clinical:router-middleware-stack@2.0.13 -clinical:router-url@2.0.15 +clinical:router-middleware-stack@2.1.2 +clinical:router-url@2.1.0 coffeescript@1.0.11 cornerstone@0.0.1 ddp@1.2.2 diff --git a/Packages/router-middleware-stack/.gitignore b/Packages/router-middleware-stack/.gitignore new file mode 100644 index 000000000..e9cd9c9a9 --- /dev/null +++ b/Packages/router-middleware-stack/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +.npm +.build* +smart.lock +versions.json +.versions diff --git a/Packages/router-middleware-stack/History.md b/Packages/router-middleware-stack/History.md new file mode 100644 index 000000000..9b60cf9c4 --- /dev/null +++ b/Packages/router-middleware-stack/History.md @@ -0,0 +1,3 @@ +v1.0.11 / 2015-10-09 +================== + * Support Meteor 1.2 diff --git a/Packages/router-middleware-stack/LICENSE.txt b/Packages/router-middleware-stack/LICENSE.txt new file mode 100644 index 000000000..5acce6251 --- /dev/null +++ b/Packages/router-middleware-stack/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 EventedMind + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Packages/router-middleware-stack/README.md b/Packages/router-middleware-stack/README.md new file mode 100644 index 000000000..a872c938f --- /dev/null +++ b/Packages/router-middleware-stack/README.md @@ -0,0 +1,18 @@ +Clinical Router MiddlewareStack +============================================================================== + +Client and server middleware support inspired by Connect. + +=============================== +#### Installation + +This package is a dependency of ``clinical:router``. Just install the router package, and you'll get this included. + +````bash +meteor add clinical:router +```` + +=============================== +#### Licensing + +![MIT License](https://img.shields.io/badge/license-MIT-blue.svg) diff --git a/Packages/router-middleware-stack/circle.yml b/Packages/router-middleware-stack/circle.yml new file mode 100644 index 000000000..981eedd3e --- /dev/null +++ b/Packages/router-middleware-stack/circle.yml @@ -0,0 +1,57 @@ +## Customize the test machine +machine: + + # Timezone + timezone: + America/Los_Angeles # Set the timezone + + # Add some environment variables + environment: + CIRCLE_ENV: test + CXX: g++-4.8 + DISPLAY: :99.0 + NPM_PREFIX: /home/ubuntu/nvm/v0.10.33 + +#general: +# build_dir: helloworld + +## Customize dependencies +dependencies: + cache_directories: + - ~/.meteor # relative to the user's home directory + - ~/nvm/v0.10.33/lib/node_modules/starrynight + - ~/nvm/v0.10.33/bin/starrynight + + pre: + # Install Starrynight unless it is cached + - if [ ! -e ~/nvm/v0.10.33/bin/starrynight ]; then npm install -g starrynight; else echo "Starrynight seems to be cached"; fi; + # Install Meteor + - mkdir -p ${HOME}/.meteor + # If Meteor is already cached, do not need to build it again. + - if [ ! -e ${HOME}/.meteor/meteor ]; then curl https://install.meteor.com | /bin/sh; else echo "Meteor seems to be cached"; fi; + # Link the meteor executable into /usr/bin + - sudo ln -s $HOME/.meteor/meteor /usr/bin/meteor + # Check if the helloworld directory already exists, if it doesn't, create the helloworld app + - if [ ! -e ${HOME}/helloworld ]; then meteor create --release METEOR@1.1.0.3 helloworld; else echo "helloworld app seems to be cached"; fi; + + override: + - cd helloworld + - cd helloworld && ls -la + - cd helloworld && rm helloworld.* + - cd helloworld && meteor add anti:gagarin@0.4.11 session meteor-platform clinical:theming clinical:default-theme clinical:router clinical:router-default-config clinical:active-layout clinical:active-layout-reset clinical:active-layout-pagescreen-config grove:less + - cd helloworld && git clone http://github.com/clinical-meteor/router-middleware-stack packages/router-middleware-stack + - cd helloworld && meteor add clinical:router clinical:router-middleware-stack + - cd helloworld/packages && ls -la + - cd helloworld/packages/router-middleware-stack && ls -la + - cd helloworld/packages/router-middleware-stack/tests && ls -la + - cd helloworld/packages/router-middleware-stack/tests/gagarin && ls -la + - cd helloworld && starrynight autoconfig + +## Customize test commands +test: + pre: + - cd helloworld && meteor: + background: true + - sleep 80 + override: + - cd helloworld && starrynight run-tests --type package-verification diff --git a/Packages/router-middleware-stack/examples/basic/.meteor/.gitignore b/Packages/router-middleware-stack/examples/basic/.meteor/.gitignore new file mode 100644 index 000000000..408303742 --- /dev/null +++ b/Packages/router-middleware-stack/examples/basic/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/Packages/router-middleware-stack/examples/basic/.meteor/identifier b/Packages/router-middleware-stack/examples/basic/.meteor/identifier new file mode 100644 index 000000000..205b5e536 --- /dev/null +++ b/Packages/router-middleware-stack/examples/basic/.meteor/identifier @@ -0,0 +1 @@ +ofqtruq93rux231sdq \ No newline at end of file diff --git a/Packages/router-middleware-stack/examples/basic/.meteor/packages b/Packages/router-middleware-stack/examples/basic/.meteor/packages new file mode 100644 index 000000000..15a504144 --- /dev/null +++ b/Packages/router-middleware-stack/examples/basic/.meteor/packages @@ -0,0 +1,10 @@ +# Meteor packages used by this project, one per line. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +standard-app-packages +autopublish +insecure +iron:middleware-stack + diff --git a/Packages/router-middleware-stack/examples/basic/.meteor/release b/Packages/router-middleware-stack/examples/basic/.meteor/release new file mode 100644 index 000000000..621e94f0e --- /dev/null +++ b/Packages/router-middleware-stack/examples/basic/.meteor/release @@ -0,0 +1 @@ +none diff --git a/Packages/router-middleware-stack/examples/basic/.meteor/versions b/Packages/router-middleware-stack/examples/basic/.meteor/versions new file mode 100644 index 000000000..bd185f5a6 --- /dev/null +++ b/Packages/router-middleware-stack/examples/basic/.meteor/versions @@ -0,0 +1,44 @@ +application-configuration@1.0.0 +autopublish@1.0.0 +autoupdate@1.0.0 +binary-heap@1.0.0 +blaze-tools@1.0.0 +blaze@1.0.0 +callback-hook@1.0.0 +check@1.0.0 +ctl-helper@1.0.0 +ctl@1.0.0 +deps@1.0.0 +ejson@1.0.0 +follower-livedata@1.0.0 +geojson-utils@1.0.0 +html-tools@1.0.0 +htmljs@1.0.0 +id-map@1.0.0 +insecure@1.0.0 +iron:core@0.1.0 +iron:middleware-stack@0.1.0 +iron:url@0.1.0 +jquery@1.0.0 +json@1.0.0 +livedata@1.0.0 +logging@1.0.0 +meteor@1.0.0 +minifiers@1.0.0 +minimongo@1.0.0 +mongo-livedata@1.0.1 +observe-sequence@1.0.0 +ordered-dict@1.0.0 +random@1.0.0 +reactive-dict@1.0.0 +reload@1.0.0 +retry@1.0.0 +routepolicy@1.0.0 +session@1.0.0 +spacebars-compiler@1.0.0 +spacebars@1.0.0 +standard-app-packages@1.0.0 +templating@1.0.0 +ui@1.0.0 +underscore@1.0.0 +webapp@1.0.0 diff --git a/Packages/router-middleware-stack/examples/basic/basic.js b/Packages/router-middleware-stack/examples/basic/basic.js new file mode 100644 index 000000000..23d1b92e6 --- /dev/null +++ b/Packages/router-middleware-stack/examples/basic/basic.js @@ -0,0 +1,29 @@ +stack = new Iron.MiddlewareStack; + +printUrls = function (name, thisArg) { + console.log('%c***********************', 'color: orange;'); + console.log("<" + name + "> this.originalUrl: " + JSON.stringify(thisArg.originalUrl)); + console.log("<" + name + "> this.url: " + JSON.stringify(thisArg.url)); + console.log('%c***********************', 'color: orange;'); +}; + +stack.push(function m1 () { + printUrls('m1', this); + this.next(); +}); + +stack.push(function m2 () { + printUrls('m2', this); + this.next(); +}); + +stack.push('/admin', function adminApp () { + printUrls('adminApp', this); +}, {mount: true}); + +console.log('%cDispatch "/"', 'color: blue;'); +stack.dispatch('/', {}); + +console.log(''); +console.log('%cDispatch "/admin"', 'color: blue;'); +stack.dispatch('/admin', {}); diff --git a/Packages/router-middleware-stack/lib/handler.js b/Packages/router-middleware-stack/lib/handler.js new file mode 100644 index 000000000..816c95055 --- /dev/null +++ b/Packages/router-middleware-stack/lib/handler.js @@ -0,0 +1,118 @@ +var Url = Iron.Url; + +Handler = function (path, fn, options) { + if (_.isFunction(path)) { + options = options || fn || {}; + fn = path; + path = '/'; + + // probably need a better approach here to differentiate between + // Router.use(function () {}) and Router.use(MyAdminApp). In the first + // case we don't want to count it as a viable server handler when we're + // on the client and need to decide whether to go to the server. in the + // latter case, we DO want to go to the server, potentially. + this.middleware = true; + + if (typeof options.mount === 'undefined') + options.mount = true; + } + + // if fn is a function then typeof fn => 'function' + // but note we can't use _.isObject here because that will return true if the + // fn is a function OR an object. + if (typeof fn === 'object') { + options = fn; + fn = options.action || 'action'; + } + + options = options || {}; + + this.options = options; + this.mount = options.mount; + this.method = (options.method && options.method.toLowerCase()) || false; + + // should the handler be on the 'client', 'server' or 'both'? + // XXX can't we default this to undefined in which case it's run in all + // environments? + this.where = options.where || 'client'; + + // if we're mounting at path '/foo' then this handler should also handle + // '/foo/bar' and '/foo/bar/baz' + if (this.mount) + options.end = false; + + // set the name + if (options.name) + this.name = options.name; + else if (typeof path === 'string' && path.charAt(0) !== '/') + this.name = path; + else if (typeof path === 'string' && path !== '/') + this.name = path.split('/').slice(1).join('.'); + + // if the path is explicitly set on the options (e.g. legacy router support) + // then use that + // otherwise use the path argument which could also be a name + path = options.path || path; + + if (typeof path === 'string' && path.charAt(0) !== '/') + path = '/' + path; + + this.path = path; + this.compiledUrl = new Url(path, options); + + if (_.isString(fn)) { + this.handle = function handle () { + // try to find a method on the current thisArg which might be a Controller + // for example. + var func = this[fn]; + + if (typeof func !== 'function') + throw new Error("No method named " + JSON.stringify(fn) + " found on handler."); + + return func.apply(this, arguments); + }; + } else if (_.isFunction(fn)) { + // or just a regular old function + this.handle = fn; + } +}; + +/** + * Returns true if the path matches the handler's compiled url, method + * and environment (e.g. client/server). If no options.method or options.where + * is provided, then only the path will be used to test. + */ +Handler.prototype.test = function (path, options) { + options = options || {}; + + var isUrlMatch = this.compiledUrl.test(path); + var isMethodMatch = true; + var isEnvMatch = true; + + if (this.method && options.method) + isMethodMatch = this.method == options.method.toLowerCase(); + + if (options.where) + isEnvMatch = this.where == options.where; + + return isUrlMatch && isMethodMatch && isEnvMatch; +}; + +Handler.prototype.params = function (path) { + return this.compiledUrl.params(path); +}; + +Handler.prototype.resolve = function (params, options) { + return this.compiledUrl.resolve(params, options); +}; + +/** + * Returns a new cloned Handler. + * XXX problem is here because we're not storing the original path. + */ +Handler.prototype.clone = function () { + var clone = new Handler(this.path, this.handle, this.options); + // in case the original function had a name + clone.name = this.name; + return clone; +}; diff --git a/Packages/router-middleware-stack/lib/middleware_stack.js b/Packages/router-middleware-stack/lib/middleware_stack.js new file mode 100644 index 000000000..c3f7f99d0 --- /dev/null +++ b/Packages/router-middleware-stack/lib/middleware_stack.js @@ -0,0 +1,282 @@ +var Url = Iron.Url; +var assert = Iron.utils.assert; +var defaultValue = Iron.utils.defaultValue; + +/** + * Connect inspired middleware stack that works on the client and the server. + * + * You can add handlers to the stack for various paths. Those handlers can run + * on the client or server. Then you can dispatch into the stack with a + * given path by calling the dispatch method. This goes down the stack looking + * for matching handlers given the url and environment (client/server). If we're + * on the client and we should make a trip to the server, the onServerDispatch + * callback is called. + * + * The middleware stack supports the Connect API. But it also allows you to + * specify a context so we can have one context object (like a Controller) that + * is a consistent context for each handler function called on a dispatch. + * + */ +MiddlewareStack = function () { + this._stack = []; + this.length = 0; +}; + +MiddlewareStack.prototype._create = function (path, fn, options) { + var handler = new Handler(path, fn, options); + var name = handler.name; + + if (name) { + if (_.has(this._stack, name)) { + throw new Error("Handler with name '" + name + "' already exists."); + } + this._stack[name] = handler; + } + + return handler; +}; + +MiddlewareStack.prototype.findByName = function (name) { + return this._stack[name]; +}; + +/** + * Push a new handler onto the stack. + */ +MiddlewareStack.prototype.push = function (path, fn, options) { + var handler = this._create(path, fn, options); + this._stack.push(handler); + this.length++; + return handler; +}; + +MiddlewareStack.prototype.append = function (/* fn1, fn2, [f3, f4]... */) { + var self = this; + var args = _.toArray(arguments); + var options = {}; + + if (typeof args[args.length-1] === 'object') + options = args.pop(); + + _.each(args, function (fnOrArray) { + if (typeof fnOrArray === 'undefined') + return; + else if (typeof fnOrArray === 'function') + self.push(fnOrArray, options); + else if (_.isArray(fnOrArray)) + self.append.apply(self, fnOrArray.concat([options])); + else + throw new Error("Can only append functions or arrays to the MiddlewareStack"); + }); + + return this; +}; + +/** + * Insert a handler at a specific index in the stack. + * + * The index behavior is the same as Array.prototype.splice. If the index is + * greater than the stack length the handler will be appended at the end of the + * stack. If the index is negative, the item will be inserted "index" elements + * from the end. + */ +MiddlewareStack.prototype.insertAt = function (index, path, fn, options) { + var handler = this._create(path, fn, options); + this._stack.splice(index, 0, handler); + this.length = this._stack.length; + return this; +}; + +/** + * Insert a handler before another named handler. + */ +MiddlewareStack.prototype.insertBefore = function (name, path, fn, options) { + var beforeHandler; + var index; + + if (!(beforeHandler = this._stack[name])) + throw new Error("Couldn't find a handler named '" + name + "' on the path stack"); + + index = _.indexOf(this._stack, beforeHandler); + this.insertAt(index, path, fn, options); + return this; +}; + +/** + * Insert a handler after another named handler. + * + */ +MiddlewareStack.prototype.insertAfter = function (name, path, fn, options) { + var handler; + var index; + + if (!(handler = this._stack[name])) + throw new Error("Couldn't find a handler named '" + name + "' on the path stack"); + + index = _.indexOf(this._stack, handler); + this.insertAt(index + 1, path, fn, options); + return this; +}; + +/** + * Return a new MiddlewareStack comprised of this stack joined with other + * stacks. Note the new stack will not have named handlers anymore. Only the + * handlers are cloned but not the name=>handler mapping. + */ +MiddlewareStack.prototype.concat = function (/* stack1, stack2, */) { + var ret = new MiddlewareStack; + var concat = Array.prototype.concat; + var clonedThisStack = EJSON.clone(this._stack); + var clonedOtherStacks = _.map(_.toArray(arguments), function (s) { return EJSON.clone(s._stack); }); + ret._stack = concat.apply(clonedThisStack, clonedOtherStacks); + ret.length = ret._stack.length; + return ret; +}; + +/** + * Dispatch into the middleware stack, allowing the handlers to control the + * iteration by calling this.next(); + */ +MiddlewareStack.prototype.dispatch = function dispatch (url, context, done) { + var self = this; + var originalUrl = url; + + assert(typeof url === 'string', "Requires url"); + assert(typeof context === 'object', "Requires context object"); + + url = Url.normalize(url || '/'); + + defaultValue(context, 'request', {}); + defaultValue(context, 'response', {}); + defaultValue(context, 'originalUrl', url); + + //defaultValue(context, 'location', Url.parse(originalUrl)); + defaultValue(context, '_method', context.method); + defaultValue(context, '_handlersForEnv', {client: false, server: false}); + defaultValue(context, '_handled', false); + + defaultValue(context, 'isHandled', function () { + return context._handled; + }); + + defaultValue(context, 'willBeHandledOnClient', function () { + return context._handlersForEnv.client; + }); + + defaultValue(context, 'willBeHandledOnServer', function () { + return context._handlersForEnv.server; + }); + + var wrappedDone = function () { + if (done) { + try { + done.apply(this, arguments); + } catch (err) { + // if we catch an error at this point in the stack we don't want it + // handled in the next() iterator below. So we'll mark the error to tell + // the next iterator to ignore it. + err._punt = true; + + // now rethrow it! + throw err; + } + } + }; + + var index = 0; + + var next = Meteor.bindEnvironment(function boundNext (err) { + var handler = self._stack[index++]; + + // reset the url + context.url = context.request.url = context.originalUrl; + + if (!handler) + return wrappedDone.call(context, err); + + if (!handler.test(url, {method: context._method})) + return next(err); + + // okay if we've gotten this far the handler matches our url but we still + // don't know if this is a client or server handler. Let's track that. + // XXX couldn't the environment be something else like cordova? + var where = Meteor.isClient ? 'client' : 'server'; + + // track that we have a handler for the given environment so long as it's + // not middleware created like this Router.use(function () {}). We'll assume + // that if the handler is of that form we don't want to make a trip to + // the client or the server for it. + if (!handler.middleware) + context._handlersForEnv[handler.where] = true; + + // but if we're not actually on that env, skip to the next handler. + if (handler.where !== where) + return next(err); + + // get the parameters for this url from the handler's compiled path + // XXX removing for now + //var params = handler.params(context.location.href); + //context.request.params = defaultValue(context, 'params', {}); + //_.extend(context.params, params); + + // so we can call this.next() + // XXX this breaks with things like request.body which require that the + // iterator be saved for the given function call. + context.next = next; + + if (handler.mount) { + var mountpath = Url.normalize(handler.compiledUrl.pathname); + var newUrl = url.substr(mountpath.length, url.length); + newUrl = Url.normalize(newUrl); + context.url = context.request.url = newUrl; + } + + try { + // + // The connect api says a handler signature (arity) can look like any of: + // + // 1) function (req, res, next) + // 2) function (err, req, res, next) + // 3) function (err) + var arity = handler.handle.length + var req = context.request; + var res = context.response; + + // function (err, req, res, next) + if (err && arity === 4) + return handler.handle.call(context, err, req, res, next); + + // function (req, res, next) + if (!err && arity < 4) + return handler.handle.call(context, req, res, next); + + // default is function (err) so punt the error down the stack + // until we either find a handler who likes to deal with errors or we call + // out + return next(err); + } catch (err) { + if (err._punt) + // ignore this error and throw it down the stack + throw err; + else + // see if the next handler wants to deal with the error + next(err); + } finally { + // we'll put this at the end because some middleware + // might want to decide what to do based on whether we've + // been handled "yet". If we set this to true before the handler + // is called, there's no way for the handler to say, if we haven't been + // handled yet go to the server, for example. + context._handled = true; + context.next = null; + } + }); + + next(); + + context.next = null; + return context; +}; + +Iron = Iron || {}; +Iron.MiddlewareStack = MiddlewareStack; diff --git a/Packages/router-middleware-stack/package.js b/Packages/router-middleware-stack/package.js new file mode 100644 index 000000000..c399f28d3 --- /dev/null +++ b/Packages/router-middleware-stack/package.js @@ -0,0 +1,32 @@ +Package.describe({ + name: 'clinical:router-middleware-stack', + summary: 'Client and server middleware support inspired by Connect.', + version: '2.1.2', + git: 'https://github.com/clinical-meteor/clinical-router-middleware-stack' +}); + +Package.on_use(function (api) { + api.versionsFrom('1.1.0.3'); + + api.use('underscore'); + api.use('ejson'); + + api.use('iron:core@1.0.11'); + api.imply('iron:core'); + + api.use('clinical:router-url@2.1.0'); + + api.add_files('lib/handler.js'); + api.add_files('lib/middleware_stack.js'); + + api.export('MiddlewareStack'); + api.export('Handler', {testOnly: true}); +}); + +Package.on_test(function (api) { + api.use('clinical:router-middleware-stack'); + api.use('tinytest'); + api.use('test-helpers'); + api.add_files('tests/tinytests/handler_test.js'); + api.add_files('tests/tinytests/middleware_stack_test.js'); +}); diff --git a/Packages/router-middleware-stack/tests/gagarin/RouterMiddlewareStackTests.js b/Packages/router-middleware-stack/tests/gagarin/RouterMiddlewareStackTests.js new file mode 100644 index 000000000..d9e954a49 --- /dev/null +++ b/Packages/router-middleware-stack/tests/gagarin/RouterMiddlewareStackTests.js @@ -0,0 +1,17 @@ + +describe('clinical:router-middleware-stack', function () { + var server = meteor(); + var client = browser(server); + + it('MiddlewareStack should exist on the client', function () { + return client.execute(function () { + expect(MiddlewareStack).to.exist; + }); + }); + + it('MiddlewareStack should exist on the client', function () { + return server.execute(function () { + expect(MiddlewareStack).to.exist; + }); + }); +}); diff --git a/Packages/router-middleware-stack/tests/tinytests/handler_test.js b/Packages/router-middleware-stack/tests/tinytests/handler_test.js new file mode 100644 index 000000000..67272bfe6 --- /dev/null +++ b/Packages/router-middleware-stack/tests/tinytests/handler_test.js @@ -0,0 +1,54 @@ +Tinytest.add('MiddlewareStack - handler basics', function (test) { + var handler; + var fn = function myName () {}; + var opts = {}; + // constructor options + + // middleware handler + handler = new Handler(fn); + test.equal(handler.handle, fn); + test.equal(handler.name, 'myName', 'name is "myName"'); + test.equal(handler.where, 'client'); + test.isTrue(handler.test('/')); + test.isTrue(handler.test('/match/everything')); + + opts.name = 'newName'; + handler = new Handler('/items/:id', fn, opts); + test.equal(handler.handle, fn); + test.equal(handler.name, 'newName', 'name is "newName"'); + test.equal(handler.options, opts); + test.isTrue(handler.test('/items/1')); + test.isTrue(handler.test('/items/2')); + + + handler = new Handler('/items/:id?', fn, opts); + test.isTrue(handler.test('/items/1')); + test.isTrue(handler.test('/items')); + + handler = new Handler(/.*/, fn, opts); + test.isTrue(handler.test('/')); + test.isTrue(handler.test('/foo')); + + var called = false; + var thisArg = { + methodName: function () {called = true;} + }; + handler = new Handler('/items/:id', 'methodName'); + handler.handle.call(thisArg); + test.isTrue(called); + + handler = new Handler('/items/:id', fn); + var params = handler.params('/items/5'); + test.equal(params.id, "5"); +}); + +Tinytest.add('Handler - test', function (test) { + var handler = new Handler('/testme', function () {}, { + where: 'server', + method: 'GET' + }); + + test.isTrue(handler.test('/testme', {where: 'server', method: 'GET'})); + test.isFalse(handler.test('/testme', {where: 'client', method: 'GET'})); + test.isFalse(handler.test('/testme', {where: 'server', method: 'POST'})); +}); diff --git a/Packages/router-middleware-stack/tests/tinytests/middleware_stack_test.js b/Packages/router-middleware-stack/tests/tinytests/middleware_stack_test.js new file mode 100644 index 000000000..9a1c3e60a --- /dev/null +++ b/Packages/router-middleware-stack/tests/tinytests/middleware_stack_test.js @@ -0,0 +1,182 @@ +Tinytest.add('MiddlewareStack - handler names and paths', function (test) { + var handler; + + // path is a name + handler = new Handler('home', {}); + test.equal(handler.name, 'home', 'name is "home"'); + test.equal(handler.path, '/home', 'path is "/home"'); + + // path is an option + handler = new Handler('home', {path: '/foo'}); + test.equal(handler.name, 'home', 'name is "home"'); + test.equal(handler.path, '/foo', 'path is "/foo"'); + + handler = new Handler('/home', {path: '/bar'}); + test.equal(handler.path, '/bar', 'path is "/bar"'); + + handler = new Handler('/home', {path: '/bar', name: 'foo'}); + test.equal(handler.path, '/bar', 'path is "/bar"'); + test.equal(handler.name, 'foo', 'name is "foo"'); +}); + +Tinytest.add('MiddlewareStack - create and find by name', function (test) { + // basically just test that a handler gets created and keyed by name if thee's + // a name. Also test duplicate named handlers throws an error. + + var stack = new Iron.MiddlewareStack; + stack._create('/items', function () {}, {name: 'items'}); + test.isTrue(stack.findByName('items')); + + test.throws(function () { + // same name + stack._create('/items', function () {}, {name: 'items'}); + }); +}); + +Tinytest.add('MiddlewareStack - push', function (test) { + var stack = new Iron.MiddlewareStack; + var fns = [function () {}, function () {}]; + stack.push(fns[0]); + test.equal(stack._stack[0].handle, fns[0]); + stack.push(fns[1]); + test.equal(stack._stack[1].handle, fns[1]); +}); + +Tinytest.add('MiddlewareStack - insertAt', function (test) { + var stack = new Iron.MiddlewareStack; + var fns = [function () {}, function () {}, function () {}]; + stack.push(fns[0]); + stack.push(fns[2]); + + stack.insertAt(1, fns[1]); + test.equal(stack._stack[1].handle, fns[1]); +}); + +Tinytest.add('MiddlewareStack - insertBefore', function (test) { + var stack = new Iron.MiddlewareStack; + var fns = [function one() {}, function two() {}, function three() {}]; + stack.push(fns[0]); + stack.push(fns[2]); + stack.insertBefore('three', fns[1]); + test.equal(stack._stack[1].handle, fns[1]); +}); + +Tinytest.add('MiddlewareStack - insertAfter ', function (test) { + var stack = new Iron.MiddlewareStack; + var fns = [function one() {}, function two() {}, function three() {}]; + stack.push(fns[0]); + stack.push(fns[2]); + stack.insertAfter('one', fns[1]); + test.equal(stack._stack[1].handle, fns[1]); +}); + +Tinytest.add('MiddlewareStack - dispatch iteration with this.next', function (test) { + var stack = new Iron.MiddlewareStack; + var calls = []; + + if (Meteor.isClient) { + stack.push(function m1 () { + calls.push('m1'); + this.next(); + }); + + stack.push(function m2 () { + calls.push('m2'); + // no call to next + }); + + stack.push(function m3 () { + calls.push('m3'); + }); + + stack.dispatch('/', {}); + test.equal(calls.length, 2, "call length is two"); + test.equal(calls[0], 'm1', "m1 called"); + test.equal(calls[1], 'm2', "m2 called"); + } + + if (Meteor.isServer) { + stack.push(function m1 () { + calls.push('m1'); + this.next(); + }, {where: 'server'}); + + stack.push(function m2 () { + calls.push('m2'); + // no call to next + }, {where: 'server'}); + + stack.push(function m3 () { + calls.push('m3'); + }, {where: 'server'}); + + stack.dispatch('/', {}); + test.equal(calls.length, 2, "call length is two"); + test.equal(calls[0], 'm1', "m1 called"); + test.equal(calls[1], 'm2', "m2 called"); + } +}); + +Tinytest.add('MiddlewareStack - dispatch callback', function (test) { + var stack = new Iron.MiddlewareStack; + var calls = []; + + if (Meteor.isClient) { + stack.push(function m1 () { + calls.push('m1'); + this.next(); + }); + + stack.dispatch('/', {}, function () { + calls.push('done'); + }); + + test.equal(calls.length, 2, "call length is two"); + test.equal(calls[0], 'm1', "m1 called"); + test.equal(calls[1], 'done', "done called"); + } + + if (Meteor.isServer) { + stack.push(function m1 () { + calls.push('m1'); + this.next(); + }, {where: 'server'}); + + stack.dispatch('/', {}, function () { + calls.push('done'); + }); + + test.equal(calls.length, 2, "call length is two"); + test.equal(calls[0], 'm1', "m1 called"); + test.equal(calls[1], 'done', "done called"); + } +}); + +if (Meteor.isServer) { + var Fiber = Npm.require('fibers'); + Tinytest.addAsync('MiddlewareStack - async next maintains fibers', function (test, done) { + var envVar = new Meteor.EnvironmentVariable; + + envVar.withValue(true, function () { + var stack = new Iron.MiddlewareStack; + + test.isTrue(envVar.getOrNullIfOutsideFiber()); + stack.push(function(req, res, next) { + // break out of the current fiber + setTimeout(function() { + next(); + }, 0); + }, {where: 'server'}); + + stack.push(function(req, res, next) { + test.isTrue(envVar.getOrNullIfOutsideFiber()); + this.next(); + }, {where: 'server'}); + + stack.dispatch('/', {}, function () { + test.isTrue(envVar.getOrNullIfOutsideFiber()); + done(); + }); + }); + }); +} diff --git a/Packages/router-middleware-stack/tests/tinytests/notes.js b/Packages/router-middleware-stack/tests/tinytests/notes.js new file mode 100644 index 000000000..48324132b --- /dev/null +++ b/Packages/router-middleware-stack/tests/tinytests/notes.js @@ -0,0 +1,194 @@ +Tinytest.add('MiddlewareStack - dispatch', function (test) { + var stack = new Iron.MiddlewareStack; + var calls = {}; + var call = function (name) { + calls[name] = calls[name] || 0; + calls[name]++; + }; + + // client middleware + stack.push(function m1 (req, res, next) { + call('m1'); + next(); + }); + + stack.push(function m2 (req, res, next) { + call('m2'); + this.next(); + }); + + // client handlers + var params; + stack.push('/items/:id', function item (req, res, next) { + call('item'); + params = this.params; + next(); + }); + + // same path is okay as long as name is different + stack.push('/items/:id', function itemTwo (req, res, next) { + call('item2'); + }); + + // server handler + stack.push('/server', function server (req, res, next) { + call('server'); + }, { where: 'server' }); + + var thisArg = {}; + + if (Meteor.isClient) { + stack.dispatch('/', {}); + test.equal(calls.m1, 1, 'm1 not called'); + test.equal(calls.m2, 1, 'm2 not called'); + test.isFalse(calls.item); + test.isFalse(calls.item2); + test.isFalse(calls.server); + + stack.dispatch('/items/1', thisArg); + test.equal(calls.m1, 2, 'm1 not called'); + test.equal(calls.m2, 2, 'm2 not called'); + test.equal(calls.item, 1, 'item not called'); + test.equal(calls.item2, 1, 'item2 not called'); + test.isFalse(calls.server); + + var params = thisArg.params; + test.equal(params.id, "1"); + + stack.onServerDispatch(function () { + call('serverDispatch'); + }); + + stack.dispatch('/server', {}); + test.equal(calls.serverDispatch, 1); + } + + if (Meteor.isServer) { + stack.dispatch('/server', {}); + test.isFalse(calls.m1); + test.isFalse(calls.m2); + test.equal(calls.server, 1); + } +}); + +Tinytest.add('MiddlewareStack - dispatch error handling', function (test) { + if (Meteor.isClient) { + var stack = new Iron.MiddlewareStack; + var calls = []; + stack.push(function (req, res, next) { + calls.push(1); + throw new Error('test'); + }); + + stack.push(function (req, res, next) { + calls.push(2); + next(); + }); + + stack.dispatch('/', { + next: function (err) { + calls.push(3); + test.equal(err.message, 'test'); + } + }); + + test.equal(calls.length, 2); + + // first fn throws an error + test.equal(calls[0], 1); + + // rest of middleware skipped + test.equal(calls[1], 3); + } +}); + +Tinytest.add('MiddlewareStack - mounting paths', function (test) { + var stack = new Iron.MiddlewareStack; + var calls = []; + + if (Meteor.isClient) { + // add a mounted handler at mountpath /foo + stack.push('/foo', function (req, res, next) { + calls.push({args: EJSON.clone(arguments), thisArg: EJSON.clone(this)}); + next(); + }, {mount: true}); + + // add a regular handler at /foo/bar/baz + stack.push('/foo/bar/baz', function (req, res, next) { + calls.push({args: EJSON.clone(arguments), thisArg: EJSON.clone(this)}); + }); + + stack.dispatch('/foo/bar/baz', {}); + + // make sure it got called + test.equal(calls.length, 2); + + // this.url + test.equal(calls[0].thisArg.url, '/bar/baz', 'url wrong on controller'); + test.equal(calls[0].thisArg.originalUrl, '/foo/bar/baz', 'originalUrl wrong on controller'); + + // fn (req, res, next) + // req.url + test.equal(calls[0].args[0].url, '/bar/baz', 'url wrong on req'); + test.equal(calls[0].args[0].originalUrl, '/foo/bar/baz', 'originalUrl wrong on req'); + + // now make sure the url is set back to the originalUrl for non mounted + // handlers. + test.equal(calls[1].thisArg.url, '/foo/bar/baz'); + test.equal(calls[1].thisArg.originalUrl, '/foo/bar/baz'); + + // fn (req, res, next) + // req.url + test.equal(calls[1].args[0].url, '/foo/bar/baz'); + test.equal(calls[1].args[0].originalUrl, '/foo/bar/baz'); + } + + if (Meteor.isServer) { + stack.push('/foo', function (req, res, next) { + calls.push({args: EJSON.clone(arguments), thisArg: EJSON.clone(this)}); + next(); + }, {mount: true, where: 'server'}); + + // add a regular handler at /foo/bar/baz + stack.push('/foo/bar/baz', function (req, res, next) { + calls.push({args: EJSON.clone(arguments), thisArg: EJSON.clone(this)}); + }, {where: 'server'}); + + stack.dispatch('/foo/bar/baz', {}); + + // make sure it got called + test.equal(calls.length, 2); + + // this.url + test.equal(calls[0].thisArg.url, '/bar/baz'); + test.equal(calls[0].thisArg.originalUrl, '/foo/bar/baz'); + + // fn (req, res, next) + // req.url + test.equal(calls[0].args[0].url, '/bar/baz'); + test.equal(calls[0].args[0].originalUrl, '/foo/bar/baz'); + + // now make sure the url is set back to the originalUrl for non mounted + // handlers. + test.equal(calls[1].thisArg.url, '/foo/bar/baz'); + test.equal(calls[1].thisArg.originalUrl, '/foo/bar/baz'); + + // fn (req, res, next) + // req.url + test.equal(calls[1].args[0].url, '/foo/bar/baz'); + test.equal(calls[1].args[0].originalUrl, '/foo/bar/baz'); + } +}); + +Tinytest.add('MiddlewareStack - append with options', function (test) { + var fn1 = function () {}; + var fn2 = function () {}; + var stack = new Iron.MiddlewareStack; + stack.append(fn1, fn2, {where: 'server'}); + + test.equal(stack._stack[0].where, 'server'); + test.equal(stack._stack[1].where, 'server'); +}); + +//TODO concat +//TODO append