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.
This commit is contained in:
Aron Carroll 2012-04-30 23:20:03 +01:00
parent 84d4292d99
commit fe5eae899c
3 changed files with 252 additions and 243 deletions

View File

@ -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) {

View File

@ -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('<script>', bin.javascript.trim(), '</script>');
}
}
// 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('<script src="/js/render/edit.js"></script>');
}
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('</body>');
last = parts.pop();
formatted = parts.join('</body>') + insert.join('\n') + '\n</body>' + last;
}
if (formatted.indexOf('%css%') > -1) {
formatted = formatted.replace(/%css%/g, bin.css);
} else {
insert = '<style>' + bin.css + '</style>';
parts = formatted.split('</head>');
last = parts.pop();
formatted = parts.join('</head>') + insert + '</head>' + 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(/<html[^>]*>/, 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('<script>', bin.javascript.trim(), '</script>');
}
if (!options || options.edit !== false) {
insert.push('<script src="/js/render/edit.js"></script>');
}
if (app.production) {
insert.push(analytics);
}
// Append scripts to the bottom of the page.
if (insert.length) {
parts = formatted.split('</body>');
last = parts.pop();
formatted = parts.join('</body>') + insert.join('\n') + '\n</body>' + last;
}
if (formatted.indexOf('%css%') > -1) {
formatted = formatted.replace(/%css%/g, bin.css);
} else {
insert = '<style>' + bin.css + '</style>';
parts = formatted.split('</head>');
last = parts.pop();
formatted = parts.join('</head>') + insert + '</head>' + 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(/<html[^>]*>/, 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
};

View File

@ -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();
};
}