From fe5eae899cd071008a515e32f242b2ea93053c17 Mon Sep 17 00:00:00 2001 From: Aron Carroll Date: Mon, 30 Apr 2012 23:20:03 +0100 Subject: [PATCH] Move state onto Request instances via helper middleware This seems a little hairy but essentially avoids creating modules with factory functions which seems very clunky. We now provide various helper functions that are bound to the application state such as url, routing and models via the request object. This is massively overloading this object but seems to be the common way in Express apps to pass state into handlers. --- lib/app.js | 15 +- lib/handlers.js | 450 ++++++++++++++++++++++------------------------ lib/middleware.js | 30 ++++ 3 files changed, 252 insertions(+), 243 deletions(-) diff --git a/lib/app.js b/lib/app.js index 65ef3de4..7b0f585f 100644 --- a/lib/app.js +++ b/lib/app.js @@ -7,25 +7,17 @@ var express = require('express'), app = express(), options = require('./config'), store = require('./store')(options.store), + models = require('./models'), handlers = require('./handlers'), middleware = require('./middleware'), flattened; -app.store = store; +app.store = store; +app.models = models.createModels(store); app.templates = {}; app.PRODUCTION = 'production'; app.DEVELOPMENT = 'development'; -Object.defineProperties(app, { - production: { - get: function () { - return this.set('env') === this.PRODUCTION; - } - } -}); - -// TODO: Refactor this! -handlers = handlers(app); // Apply the keys from the config file. All nested properties are // space delimited to match the express style. @@ -48,6 +40,7 @@ app.use(express.errorHandler({showStack: true, dumpExceptions: true})); app.use(middleware.noslashes()); app.use(middleware.ajax()); app.use(middleware.jsonp()); +app.use(middleware.helpers(app)); // Create a Hogan/Mustache handler for templates. app.engine('html', function (path, options, fn) { diff --git a/lib/handlers.js b/lib/handlers.js index b3e20563..af987804 100644 --- a/lib/handlers.js +++ b/lib/handlers.js @@ -1,7 +1,7 @@ var async = require('asyncjs'), path = require('path'), utils = require('./utils'), - createBinModel = require('./models/bin'); + handlers; // Create a not found error object. function NotFound() { @@ -9,259 +9,245 @@ function NotFound() { } NotFound.prototype = Object.create(Error.prototype); -module.exports = function (app) { - var binModel = createBinModel(app.store), handlers; - - handlers = { - getDefault: function (req, res) { - handlers.renderFiles(req, res); - }, - getBin: function (req, res, next) { - handlers.render(req, res, req.bin); - }, - getBinPreview: function (req, res, next) { - var options = {edit: !req.param('quiet')}; - handlers.formatPreview(req.bin, options, function (err, formatted) { - if (err) { - next(err); - } - - if (formatted) { - res.send(formatted); - } else { - res.contentType('js'); - res.send(req.bin.javascript); - } - }); - }, - getBinSource: function (req, res) { - res.contentType('json'); - var output = JSON.stringify(handlers.templateFromBin(req.bin)); - if (!req.ajax) { - res.contentType('js'); - output = 'var template = ' + output; +module.exports = handlers = { + getDefault: function (req, res) { + handlers.renderFiles(req, res); + }, + getBin: function (req, res, next) { + handlers.render(req, res, req.bin); + }, + getBinPreview: function (req, res, next) { + var options = {edit: !req.param('quiet')}; + handlers.formatPreview(req.bin, req.helpers, options, function (err, formatted) { + if (err) { + next(err); } - res.send(output); - }, - getBinSourceFile: function (req, res) { - var format = req.params.format; - res.contentType(format); - if (format !== 'html') { - if (format === 'js' || format === 'json') { - format = 'javascript'; - } - res.send(req.bin[format]); + if (formatted) { + res.send(formatted); } else { - handlers.getBinPreview(req, res); + res.contentType('js'); + res.send(req.bin.javascript); } - }, - redirectToLatest: function (req, res) { - var path = req.path.replace('latest', req.bin.revision); - res.redirect(303, path); - }, - createBin: function (req, res, next) { - var data = utils.extract(req.body, 'html', 'css', 'javascript'); + }); + }, + getBinSource: function (req, res) { + res.contentType('json'); + var output = JSON.stringify(handlers.templateFromBin(req.bin)); + if (!req.ajax) { + res.contentType('js'); + output = 'var template = ' + output; + } + res.send(output); + }, + getBinSourceFile: function (req, res) { + var format = req.params.format; - binModel.create(data, function (err, result) { + res.contentType(format); + if (format !== 'html') { + if (format === 'js' || format === 'json') { + format = 'javascript'; + } + res.send(req.bin[format]); + } else { + handlers.getBinPreview(req, res); + } + }, + redirectToLatest: function (req, res) { + var path = req.path.replace('latest', req.bin.revision); + res.redirect(303, path); + }, + createBin: function (req, res, next) { + var data = utils.extract(req.body, 'html', 'css', 'javascript'); + + req.models.bin.create(data, function (err, result) { + if (err) { + return next(err); + } + handlers.renderCreated(req, res, result); + }); + }, + createRevision: function (req, res, next) { + var panel = req.param('panel'), + params = {}; + + if (req.param('method') === 'save') { + params = utils.extract(req.body, 'html', 'css', 'javascript'); + params.url = req.bin.url; + params.revision = req.bin.revision + 1; + req.models.bin.createRevision(params, function (err, result) { if (err) { return next(err); } handlers.renderCreated(req, res, result); }); - }, - createRevision: function (req, res, next) { - var panel = req.param('panel'), - params = {}; + } else if (req.param('method') === 'update') { + params[panel] = req.param('content'); + params.streamingKey = req.param('checksum'); + params.revision = req.param('revision'); + params.url = req.param('code'); - if (req.param('method') === 'save') { - params = utils.extract(req.body, 'html', 'css', 'javascript'); - params.url = req.bin.url; - params.revision = req.bin.revision + 1; - binModel.createRevision(params, function (err, result) { - if (err) { - return next(err); - } - handlers.renderCreated(req, res, result); - }); - } else if (req.param('method') === 'update') { - params[panel] = req.param('content'); - params.streamingKey = req.param('checksum'); - params.revision = req.param('revision'); - params.url = req.param('code'); + req.models.bin.updatePanel(panel, params, function (err, result) { + if (err) { + return next(err); + } + res.json({ok: true, error: false}); + }); + } else { + next(); + } + }, + notFound: function (req, res) { + var files = handlers.defaultFiles(); + files[2] = 'not_found.js'; + handlers.renderFiles(req, res, files); + }, + loadBin: function (req, res, next) { + var rev = parseInt(req.params.rev, 10) || 1, + query = {id: req.params.bin, revision: rev}; - binModel.updatePanel(panel, params, function (err, result) { - if (err) { - return next(err); - } - res.json({ok: true, error: false}); - }); + function complete(err, result) { + if (err) { + return next(new NotFound('Could not find bin: ' + req.params.bin)); } else { + req.bin = result; next(); } - }, - notFound: function (req, res) { - var files = handlers.defaultFiles(); - files[2] = 'not_found.js'; - handlers.renderFiles(req, res, files); - }, - loadBin: function (req, res, next) { - var rev = parseInt(req.params.rev, 10) || 1, - query = {id: req.params.bin, revision: rev}; + } - function complete(err, result) { + // TODO: Re-factor this logic. + if ((req.params.rev || req.path.indexOf('latest') === -1) && req.path.indexOf('save') === -1) { + req.models.bin.load(query, complete); + } else { + req.models.bin.latest(query, complete); + } + }, + render: function (req, res, bin) { + var template = handlers.templateFromBin(bin), + jsbin = handlers.jsbin(bin, req.helpers.production ? req.helpers.set('version') : 'debug'); + + req.helpers.analytics(function (err, analytics) { + res.render('index', { + tips: '{}', + revision: bin.revision || 1, + jsbin: JSON.stringify(jsbin), + json_template: JSON.stringify(template), + version: jsbin.version, + analytics: analytics, + 'production?': req.helpers.production + }); + }); + }, + renderFiles: function (req, res, files) { + files = files || handlers.defaultFiles(); + async.files(files, req.helpers.set('views')).readFile("utf8").toArray(function (err, results) { + if (!err) { + handlers.render(req, res, { + html: results[0].data, + css: results[1].data, + javascript: results[2].data + }); + } + }); + }, + renderCreated: function (req, res, bin) { + var permalink = req.helpers.urlForBin(bin), + editPermalink = req.helpers.editUrlForBin(bin); + + if (req.ajax) { + res.json({ + code: bin.url, + root: req.helpers.set('url full'), + created: (new Date()).toISOString(), // Should be part of bin. + revision: bin.revision, + url: permalink, + edit: editPermalink, + html: editPermalink, + js: editPermalink, + title: utils.titleForBin(bin), + allowUpdate: false, + checksum: req.streamingKey + }); + } else { + res.redirect(303, '/' + bin.url + '/' + bin.revision + '/edit'); + } + }, + jsbin: function (bin, version) { + return { + root: '', + version: version, + state: { + stream: false, + code: bin.url || null, + revision: bin.revision || 1 + } + }; + }, + templateFromBin: function (bin) { + return utils.extract(bin, 'html', 'css', 'javascript'); + }, + defaultFiles: function () { + return ['html', 'css', 'js'].map(function (ext) { + return 'default.' + ext; + }); + }, + formatPreview: function (bin, helpers, options, fn) { + var formatted = bin.html || '', + insert = [], parts, last, context; + + // TODO: Re implement this entire block with an HTML parser. + if (formatted) { + helpers.analytics(function (err, analytics) { if (err) { - return next(new NotFound('Could not find bin: ' + req.params.bin)); + return fn(err); + } + + if (formatted.indexOf('%code%') > -1) { + formatted = formatted.replace(/%code%/g, bin.javascript); } else { - req.bin = result; - next(); + insert.push(''); } - } - // TODO: Re-factor this logic. - if ((req.params.rev || req.path.indexOf('latest') === -1) && req.path.indexOf('save') === -1) { - binModel.load(query, complete); - } else { - binModel.latest(query, complete); - } - }, - render: function (req, res, bin) { - var template = handlers.templateFromBin(bin), - jsbin = handlers.jsbin(bin); + if (!options || options.edit !== false) { + insert.push(''); + } - handlers.analytics(function (err, analytics) { - res.render('index', { - tips: '{}', - revision: bin.revision || 1, - jsbin: JSON.stringify(jsbin), - json_template: JSON.stringify(template), - version: jsbin.version, - analytics: analytics, - 'production?': app.production - }); - }); - }, - renderFiles: function (req, res, files) { - files = files || handlers.defaultFiles(); - async.files(files, app.set('views')).readFile("utf8").toArray(function (err, results) { - if (!err) { - handlers.render(req, res, { - html: results[0].data, - css: results[1].data, - javascript: results[2].data + if (helpers.production) { + insert.push(analytics); + } + + // Append scripts to the bottom of the page. + if (insert.length) { + parts = formatted.split(''); + last = parts.pop(); + formatted = parts.join('') + insert.join('\n') + '\n' + last; + } + + if (formatted.indexOf('%css%') > -1) { + formatted = formatted.replace(/%css%/g, bin.css); + } else { + insert = ''; + parts = formatted.split(''); + last = parts.pop(); + formatted = parts.join('') + insert + '' + last; + } + + context = { + domain: helpers.set('url host'), + permalink: helpers.editUrlForBin(bin, true) + }; + + // Append attribution comment to header. + helpers.render('comment', context, function (err, comment) { + formatted = formatted.replace(/]*>/, function ($0) { + return $0 + '\n' + comment.trim(); }); - } - }); - }, - renderCreated: function (req, res, bin) { - var permalink = handlers.urlForBin(bin), - editPermalink = handlers.editUrlForBin(bin); - - if (req.ajax) { - res.json({ - code: bin.url, - root: app.set('url full'), - created: (new Date()).toISOString(), // Should be part of bin. - revision: bin.revision, - url: permalink, - edit: editPermalink, - html: editPermalink, - js: editPermalink, - title: utils.titleForBin(bin), - allowUpdate: false, - checksum: req.streamingKey + return fn(err || null, err ? undefined : formatted); }); - } else { - res.redirect(303, '/' + bin.url + '/' + bin.revision + '/edit'); - } - }, - jsbin: function (bin) { - return { - root: '', - version: app.set('environment') === 'production' ? app.set('version') : 'debug', - state: { - stream: false, - code: bin.url || null, - revision: bin.revision || 1 - } - }; - }, - urlForBin: function (bin, full) { - return app.set(full ? 'url full' : 'url prefix') + bin.url + '/' + bin.revision; - }, - editUrlForBin: function (bin, full) { - return handlers.urlForBin(bin, full) + '/edit'; - }, - templateFromBin: function (bin) { - return utils.extract(bin, 'html', 'css', 'javascript'); - }, - defaultFiles: function () { - return ['html', 'css', 'js'].map(function (ext) { - return 'default.' + ext; }); - }, - analytics: function (fn) { - app.render('analytics', {id: app.set('analytics id')}, fn); - }, - formatPreview: function (bin, options, fn) { - var formatted = bin.html || '', - insert = [], parts, last, context; - - // TODO: Re implement this entire block with an HTML parser. - if (formatted) { - handlers.analytics(function (err, analytics) { - if (err) { - return fn(err); - } - - if (formatted.indexOf('%code%') > -1) { - formatted = formatted.replace(/%code%/g, bin.javascript); - } else { - insert.push(''); - } - - if (!options || options.edit !== false) { - insert.push(''); - } - - if (app.production) { - insert.push(analytics); - } - - // Append scripts to the bottom of the page. - if (insert.length) { - parts = formatted.split(''); - last = parts.pop(); - formatted = parts.join('') + insert.join('\n') + '\n' + last; - } - - if (formatted.indexOf('%css%') > -1) { - formatted = formatted.replace(/%css%/g, bin.css); - } else { - insert = ''; - parts = formatted.split(''); - last = parts.pop(); - formatted = parts.join('') + insert + '' + last; - } - - context = { - domain: app.get('url host'), - permalink: handlers.editUrlForBin(bin, true) - }; - - // Append attribution comment to header. - app.render('comment', context, function (err, comment) { - formatted = formatted.replace(/]*>/, function ($0) { - return $0 + '\n' + comment.trim(); - }); - return fn(err || null, err ? undefined : formatted); - }); - }); - } else { - fn(null, formatted); - } - }, - NotFound: NotFound - }; - return handlers; + } else { + fn(null, formatted); + } + }, + NotFound: NotFound }; diff --git a/lib/middleware.js b/lib/middleware.js index 32830d7d..c9da1b95 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -37,6 +37,36 @@ module.exports = { if (req.path !== '/' && req.path.slice(-1) === '/') { res.redirect(301, req.path.slice(0, -1)); } + next(); + }; + }, + helpers: function (app) { + return function (req, res, next) { + req.store = app.store; + req.models = app.models; + + req.helpers = { + set: app.set.bind(app), + render: app.render.bind(app), + analytics: function (fn) { + app.render('analytics', {id: req.helpers.set('analytics id')}, fn); + }, + urlForBin: function (bin, full) { + return app.set(full ? 'url full' : 'url prefix') + bin.url + '/' + bin.revision; + }, + editUrlForBin: function (bin, full) { + return req.helpers.urlForBin(bin, full) + '/edit'; + } + }; + + Object.defineProperties(req.helpers, { + production: { + get: function () { + return this.set('env') === this.PRODUCTION; + } + } + }); + next(); }; }