var assign = require('object-assign') var bodyParser = require('body-parser'); var methodOverride = require('method-override'); var Q = require('q'); var pluralize = require('pluralize'); var OKQuery = require('okquery'); /** * OKAdminView! */ function OKAdminView(options) { if (!(this instanceof OKAdminView)) return new OKAdminView(options); if (!options.app) throw new Error('No Express app provided to OKAdminView'); if (!options.express) throw new Error('No Express provided to OKAdminView'); if (!options.resourceConfig) throw new Error('No resourceConfig provided to OKAdminView'); if (!options.resourceCache) throw new Error('No resourceCache provided to OKAdminView'); if (!options.templateProvider) throw new Error('No templateProvider provided to OKAdminView'); if (!options.meta) throw new Error('No meta query provided to OKAdminView'); var app = options.app; var express = options.express; var meta = options.meta; var resourceCache = this._resourceCache = options.resourceCache; var resourceConfig = this._resourceConfig = options.resourceConfig; var provider = options.templateProvider; // Load templates var templates = this._templates = ['index', 'resource', 'resource_new'].reduce(function(cache, name) { var template = provider.getTemplate(name); if (!template) throw new Error('Admin theme does not have needed template: "' + name + '"'); cache[name] = template; return cache; }, {}); // OKAdmin middleware is a router, so mounts on 'use' Object.defineProperty(this, 'mount', { value: 'use', writable: false, enumerable: true }); // Resources which apper on the index are a function of // the resource configuration. var indexQueries = this._indexQueries = Object.keys(resourceConfig) .map(function(key) { var config = resourceConfig[key]; var type = config.type; var staticData = config.static || {}; var resource = resourceCache.get(config.type); if (!resource) throw new Error('Something weird is going on'); var id = staticData[resource.idField]; resource = resourceCache.get(type, id) || resource; if (resource.bound) { // Resource instances implement the query API return OKQuery({resource: resource});; } else { return OKQuery({resource: resource, query: config.query}) } }); this._middleware = createMiddleware(this); function createMiddleware(view) { var router = express.Router({ strict: app.get('strict routing') }); // Parse form data router.use(bodyParser.urlencoded({extended: true})); // HTML forms only support POST and GET methods // We extend this by adding hidden input fields to the forms which // specify the actual method desired. router.use(methodOverride(function(req, res) { // Parse out the hidden field if (req.body && typeof req.body === 'object' && '_method' in req.body) { var method = req.body._method; delete req.body._method return method } })); router.get('/', function readIndex(req, res, next) { fetchIndexTemplateData(meta, indexQueries).then(function(data) { view.renderIndex(req, res, data); }).fail(errorHandler(req, res)); }); router.get('/:type/new/', function createResourceView(req, res, next) { var type = req.params.type || ''; var resource = resourceCache.get(type); if (!resource) { errorHandler(req, res)(new Error('No such resource ' + type)); } else { meta.get().then(function(metadata) { view.renderResourceNew(req, res, { meta: metadata, resource: { type: resource.type, spec: resource.spec } }); }).fail(errorHandler(req, res)); } }); router.get('/:type/:id/', function readResource(req, res, next) { var type = req.params.type || ''; var id = req.params.id || ''; var resource = resourceCache.get(type, id); if (!resource) { errorHandler(req, res)(new Error('No such resource')); } else { var query = OKQuery({ resource: resource, query: id }); fetchResourceTemplateData(meta, query, getResourceTemplateData).then(function(data) { if (!data) { resourceMissingHandler(req, res)() } else { view.renderResource(req, res, data); } }).fail(errorHandler(req, res)); } }); router.post('/:type/', function createResource(req, res, next) { var type = req.params.type; var resource = resourceCache.get(type); var data = req.body; if (!resource) { errorHandler(req, res)(new Error('No such resource ' + type)); } else { meta.get().then(function(metadata) { try { resource.assertValid(data); resource.create(data).then(function(created) { res.redirect(303, data[resource.idField]); }).fail(errorHandler(req, res)); } catch (errors) { var templateData = getResourceTemplateData(metadata, resource, data); view.renderResource(req, res, assign(templateData, {errors: errors})); } }).fail(errorHandler(req, res));; } }); router.put('/:type/:id/', function updateResource(req, res, next) { var type = req.params.type; var id = req.params.id; var data = req.body; var resource = resourceCache.get(type, id); if (!resource) { errorHandler(req, res)(new Error('No such resource ' + type)); } else { // TODO Prob should make metadata synchronous... meta.get().then(function(metadata) { try { resource.assertValid(data); resource.update(id, data).then(function(updated) { res.redirect(303, '../' + updated[resource.idField]); }).fail(errorHandler(req, res)); } catch (errors) { var templateData = getResourceTemplateData(metadata, resource, data); view.renderResource(req, res, assign(templateData, {errors: errors})); } }).fail(errorHandler(req, res)); } }); return router; } } /** * Get template data for a single resource */ function getResourceTemplateData(meta, resource, data) { meta = meta || {}; resource = resource || {}; data = data || {}; var spec = Object.keys(resource.spec).reduce(function(cache, prop) { var value = data[prop]; cache[prop].value = value; return cache; }, resource.spec); return { meta: meta, resource: { id: data[resource.idField], type: resource.type, spec: spec } }; } OKAdminView.prototype.middleware = function() { return this._middleware; }; OKAdminView.prototype.renderIndex = function(req, res, data) { data = data || {}; this._templates['index'].render(data).then(function(rendered) { res.send(rendered); }).fail(errorHandler(req, res)); }; OKAdminView.prototype.renderResource = function(req, res, data) { data = data || {}; this._templates['resource'].render(data).then(function(rendered) { res.send(rendered); }).fail(errorHandler(req, res)); }; OKAdminView.prototype.renderResourceNew = function(req, res, data) { data = data || {meta: {}, resource: {}}; this._templates['resource_new'].render(data).then(function(rendered) { res.send(rendered); }).fail(errorHandler(req, res)); }; /** * Annotate template data with schema info */ function fetchIndexTemplateData(meta, queries) { return Q.Promise(function(resolve, reject) { Q.all([meta.get()].concat(queries.map(function(query) { return query.get(); }))).then(function(results) { var meta = results.shift(); var resources = results.reduce(function(cache, result, i) { if (!result) return cache; var resource = queries[i].resource; // We want the raw object spec var spec = resource.spec; var key = pluralize(resource.type); if (!cache[key]) { cache[key] = { type: resource.type, spec: spec, data: [] }; } if (result.length) { result.forEach(addToCache) } else { addToCache(result); } function addToCache(data) { // Report id to template under standard name data.id = data[resource.idField]; cache[key].data.push(data); } return cache; }, {}); resolve({ meta: meta, resources: resources }); }).fail(reject); }); } /** * Annotate template data with schema info */ function fetchResourceTemplateData(meta, query, fn) { fn = fn || function(m, r, d) { return {meta: m, resource: d}; }; return Q.Promise(function(resolve, reject) { meta.get().then(function(metadata) { query.get().then(function(data) { var resource = query.resource; resolve(fn(metadata, resource, data)); }).fail(reject); }).fail(reject) }); } /** * TODO Real error handling */ function errorHandler(req, res) { return function(err) { res.send(err.stack); }; } /** * TODO Real 404 handling */ function resourceMissingHandler(req, res) { return function() { res.status(404); res.send('404'); } } module.exports = OKAdminView;