var assign = require('object-assign'); var pluralize = require('pluralize'); var isarray = require('lodash.isarray'); var Q = require('q'); var OKQuery = require('okquery'); var OKResource = require('okresource'); // Routes for views over collections have a special pattern // containing a free variable e.g. :id var UNBOUND_ROUTE_PATTERN = /:([a-zA-Z\-_]+)/; /** * OKView! * Is supplied DB queries and a template and is responsible * for resolving the queries, throwing the data into the templates, * and sending the response. */ function OKView(options) { if (!(this instanceof OKView)) return new OKView(options); options = options || {}; if (!options.template) throw new Error('No template provided to OKView.'); if (!options.meta) throw new Error('No metadata provided to OKView'); if (!options.route) throw new Error('No route provided to OKView'); if (!options.errorHandler) throw new Error('No error handler provided to OKView'); var route = options.route; var mount = options.mount || 'get'; var error = this._error = options.errorHandler; this._template = options.template; var meta = this._meta = options.meta; var queries = this._queries = options.queries || []; // Whether this is a view for a specific resource or its // resource will be resolved later // TODO This bound / unbound thing can probably be expressed in a // less convoluted way. var unbound = this.unbound = !!UNBOUND_ROUTE_PATTERN.exec(route); Object.defineProperty(this, 'mount', { value: mount, writable: false, enumerable: true }); Object.defineProperty(this, 'route', { value: route, writable: false, enumerable: true }); this._middleware = unbound ? unboundMiddleware(this, meta, queries, error) : boundMiddleware(this, meta, queries, error); } OKView.prototype.middleware = function() { return this._middleware; }; OKView.prototype.render = function(req, res, data) { this._template.render(data).then(function(html) { res.send(html); }).fail(this._error(req, res, 500)); }; /** * Creates middleware for a view which does not * yet have a resource id associated with it */ function unboundMiddleware(view, meta, queries, error) { var paramName = getParamName(view.route); return function(req, res, next) { var id = req.params[paramName]; fetchTemplateData(meta, queries, id).then(function(data) { view.render(req, res, data); }).fail(failHandler(req, res, error)); }; } /** * Creates middleware for a view which already * has a resource id associated with it */ function boundMiddleware(view, meta, queries, error) { return function(req, res, next) { fetchTemplateData(meta, queries).then(function(data) { view.render(req, res, data); }).fail(failHandler(req, res, error)); }; } function failHandler(req, res, error) { return function (err) { // TODO Use custom exception type if (err.message === 'No resource found') { error(req, res, 404)(err); } else { error(req, res, 500)(err); } } } /** * Given a route with a free variable, return the * name of the variable, e.g. :id returns id */ function getParamName(route) { route = route || ''; var matches = UNBOUND_ROUTE_PATTERN.exec(route) || []; return matches[1]; } /** * Takes a meta data query and an array of resource queries * and returns a promise for an object merging all queried * data, pluralizing keys where necessary. * * Pretty convoluted, sorry. */ function fetchTemplateData(meta, queries, id) { // If there's only one query, we assume it is for a single // resource and will resolve errors if no data is found var single = queries && queries.length === 1; return Q.promise(function(resolve, reject) { return Q.all(queries.map(function(query) { return query.get(id); })).then(function(results) { if (single && !results[0]) { reject(new Error('No resource found')); } else { var normalized = results.reduce(function(cache, result, i) { // Could be just some rogue request if (!result) { return cache; } var query = queries[i] var resource = query.resource; var type = query.as || query.type; var manyResult = isarray(result); var groupBy = query.groupBy // If we have a lot of results for a certain type, // we pluralize the key and yield an array of results if (cache[type] || manyResult || groupBy) { var plural = pluralize(type); delete cache[type]; cache[plural] = []; // Pluralize grouped field if (query.groupBy) { result = Object.keys(result).reduce(function(acc, key) { acc[pluralize(key)] = result[key] return acc }, {}) } if (manyResult) { cache[plural] = cache[plural].concat(result); } else if (groupBy) { cache[plural] = result } else { cache[plural].push(result); } } else { cache[type] = result; } return cache; }, {meta: meta}); resolve(normalized); } }).fail(reject); }); } module.exports = OKView;