diff options
Diffstat (limited to 'app/node_modules/okadminview/index.js')
| -rw-r--r-- | app/node_modules/okadminview/index.js | 391 |
1 files changed, 262 insertions, 129 deletions
diff --git a/app/node_modules/okadminview/index.js b/app/node_modules/okadminview/index.js index 987fe51..3a9056f 100644 --- a/app/node_modules/okadminview/index.js +++ b/app/node_modules/okadminview/index.js @@ -1,10 +1,25 @@ -var assign = require('object-assign') +var assign = require('object-assign'); +var cloneDeep = require('lodash.clonedeep'); var bodyParser = require('body-parser'); var methodOverride = require('method-override'); +var session = require('express-session'); +var flash = require('connect-flash'); +var passport = require('passport'); +var DigestStrategy = require('passport-http').DigestStrategy; var Q = require('q'); var pluralize = require('pluralize'); var OKQuery = require('okquery'); +// Configure auth +passport.use(new DigestStrategy({qop: 'auth'}, + function authenticate(username, done) { + if (!process.env.OK_USER || !process.env.OK_PASS) { + return done(new Error('No user or pass configured on server')); + } else { + return done(null, process.env.OK_USER, process.env.OK_PASS); + } +})); + /** * OKAdminView! */ @@ -21,13 +36,20 @@ function OKAdminView(options) { if (!options.templateProvider) throw new Error('No templateProvider provided to OKAdminView'); if (!options.meta) - throw new Error('No meta query provided to OKAdminView'); + throw new Error('No metadata provided to OKAdminView'); + if (!options.errorHandler) + throw new Error('No error handler provided to OKAdminView'); + + var dashboardConfig = options.dashboardConfig || {} + var dashboardResourceConfig = dashboardConfig.resources || {} 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 resourceConfig = this._resourceConfig = cloneDeep(options.resourceConfig); var provider = options.templateProvider; + var error = this._error = options.errorHandler; + // Load templates var templates = this._templates = ['index', 'resource', 'resource_new'].reduce(function(cache, name) { @@ -37,6 +59,7 @@ function OKAdminView(options) { cache[name] = template; return cache; }, {}); + // OKAdmin middleware is a router, so mounts on 'use' Object.defineProperty(this, 'mount', { value: 'use', @@ -51,16 +74,32 @@ function OKAdminView(options) { var config = resourceConfig[key]; var type = config.type; var staticData = config.static || {}; + // Get parent level resource var resource = resourceCache.get(config.type); if (!resource) throw new Error('Something weird is going on'); - var id = staticData[resource.idField]; + var id = resource.getID(staticData); + // Check to see if there's a more specific instance resource = resourceCache.get(type, id) || resource; + var dashConf = dashboardResourceConfig[type] || {} + var groupBy = dashConf.groupBy + var sortBy = dashConf.sortBy + var descending = dashConf.descending if (resource.bound) { - // Resource instances implement the query API - return OKQuery({resource: resource});; + return OKQuery({ + resource: resource, + groupBy: groupBy, + sortBy: sortBy, + descending: descending + }) } else { - return OKQuery({resource: resource, query: config.query}) + return OKQuery({ + resource: resource, + query: config.query, + groupBy: groupBy, + sortBy: sortBy, + descending: descending + }) } }); @@ -71,6 +110,14 @@ function OKAdminView(options) { strict: app.get('strict routing') }); + // Enable basic sessions for flash messages + router.use(session({ + secret: 'okadmin', + resave: false, + saveUninitialized: false + })); + // Enable flash messaging + router.use(flash()); // Parse form data router.use(bodyParser.urlencoded({extended: true})); // HTML forms only support POST and GET methods @@ -85,27 +132,34 @@ function OKAdminView(options) { } })); + // This should really be mounted on the router, but can't be due to + // https://github.com/jaredhanson/passport-http/pull/16 + app.use('/admin/', passport.initialize()); + app.all('/admin/(:path*)?', passport.authenticate('digest', { + session: false + })); + router.get('/', function readIndex(req, res, next) { - fetchIndexTemplateData(meta, indexQueries).then(function(data) { - view.renderIndex(req, res, data); - }).fail(errorHandler(req, res)); + fetchIndexTemplateData(meta, indexQueries, dashboardConfig).then(function(data) { + view.renderIndex(req, res, assign(data, { + success: req.flash('success'), + errors: req.flash('errors') + })); + }).fail(error(req, res, 500)); }); - router.get('/:type/new/', function createResourceView(req, res, next) { + 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)); + error(req, res, 404)(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)); + fetchNewTemplateData(meta, resource, transformData).then(function(data) { + view.renderResourceNew(req, res, assign(data, { + success: req.flash('success'), + errors: req.flash('errors'), + })) + }).fail(error(req, res, 500)) } }); @@ -113,22 +167,28 @@ function OKAdminView(options) { 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)); - } - + var query = OKQuery({ + resource: resource, + query: id + }); + fetchResourceTemplateData(meta, query, transformData) + .then(function(data) { + if (!data) { + resourceMissingHandler(req, res)() + } else { + view.renderResource(req, res, assign(data, { + JSON: JSON, + success: req.flash('success'), + errors: req.flash('errors') + })); + } + }).fail(function(err) { + if (err.message === 'No resource data') { + error(req, res, 404)(new Error('No such resource')); + } else { + error(req, res, 500)(err); + } + }); }); router.post('/:type/', function createResource(req, res, next) { @@ -136,19 +196,47 @@ function OKAdminView(options) { var resource = resourceCache.get(type); var data = req.body; if (!resource) { - errorHandler(req, res)(new Error('No such resource ' + type)); + error(req, res, 400)(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));; + try { + resource.assertValid(data); + resource.create(data).then(function(created) { + req.flash('success', {action: 'create'}); + res.redirect(303, resource.getID(data)); + }).fail(error(req, res, 500)); + } catch (errors) { + var spec = resource.spec + var templateData = transformData(meta, spec, resource, data); + view.renderResource(req, res, assign(templateData, {errors: errors})); + } + } + }); + + router.put('/:type/__batch__/', function putBatch(req, res, next) { + var type = req.params.type; + var body = req.body || {}; + var resourcesJSON = body[type]; + var resource = resourceCache.get(type); + if (!resourcesJSON || !resourcesJSON.length) { + error(req, res, 400)(new Error('Bad request')); + } else if (!resource) { + error(req, res, 404)(new Error('No such resource')); + } else { + try { + var ids = []; + var resourcesParsed = resourcesJSON.map(function(resourceJSON) { + var data = JSON.parse(resourceJSON); + ids.push(resource.getID(data)); + return data; + }); + } catch (e) { + error(req, res, 500)(new Error('Resource batch contains invalid JSON')); + return; + } + resource.updateBatch(ids, resourcesParsed).then(function(results) { + req.flash('success', {action: 'batch_update'}); + res.redirect(303, '../..'); + }).fail(error(req, res, 500)); } }); @@ -158,20 +246,33 @@ function OKAdminView(options) { var data = req.body; var resource = resourceCache.get(type, id); if (!resource) { - errorHandler(req, res)(new Error('No such resource ' + type)); + error(req, res, 400)(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)); + try { + resource.assertValid(data); + resource.update(id, data).then(function(updated) { + req.flash('success', {action: 'update'}); + res.redirect(303, '../' + resource.getID(updated)); + }).fail(error(req, res, 500)); + } catch (errors) { + var spec = resource.spec + var templateData = transformData(meta, spec, resource, data); + view.renderResource(req, res, assign(templateData, {errors: errors})); + } + } + }); + + router.delete('/:type/:id/', function deleteResource(req, res, next) { + var type = req.params.type; + var id = req.params.id; + var resource = resourceCache.get(type, id); + if (!resource) { + error(req, res, 500)(new Error('No such resource ' + type)); + } else { + resource.destroy(id).then(function() { + req.flash('success', {action: 'delete'}); + res.redirect(303, '../..'); + }).fail(error(req, res, 500)); } }); @@ -180,21 +281,27 @@ function OKAdminView(options) { } /** - * Get template data for a single resource + * Yields formatted template data for a single resource */ -function getResourceTemplateData(meta, resource, data) { +function transformData(meta, spec, resource, data) { meta = meta || {}; resource = resource || {}; data = data || {}; - var spec = Object.keys(resource.spec).reduce(function(cache, prop) { + var spec = Object.keys(spec).reduce(function(cache, prop) { var value = data[prop]; - cache[prop].value = value; + var propSpec = cache[prop]; + // Decorate spec with actual resource values + propSpec.value = value; + // Some fields should not be shown to the user + if (propSpec.type === 'meta' || propSpec.static) { + propSpec.hidden = true; + } return cache; - }, resource.spec); + }, spec); return { meta: meta, resource: { - id: data[resource.idField], + id: resource.getID(data), type: resource.type, spec: spec } @@ -209,102 +316,128 @@ OKAdminView.prototype.renderIndex = function(req, res, data) { data = data || {}; this._templates['index'].render(data).then(function(rendered) { res.send(rendered); - }).fail(errorHandler(req, res)); + }).fail(this._error(req, res, 500)); }; OKAdminView.prototype.renderResource = function(req, res, data) { data = data || {}; this._templates['resource'].render(data).then(function(rendered) { res.send(rendered); - }).fail(errorHandler(req, res)); + }).fail(this._error(req, res, 500)); }; 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)); + }).fail(this._error(req, res, 500)); }; /** * 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) { +function fetchIndexTemplateData(meta, queries, dashboardConfig) { + var resourceConfig = dashboardConfig.resources || {} + + return Q.promise(function(resolve, reject) { + Q.all(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; + })).then(function(results) { + var templateData = results.reduce(function(acc, result, i) { + result = result.length ? result : [result] 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] = { + var key = pluralize(resource.type) + if (acc[key]) { + acc[key].data = acc[key].data.concat(result) + } else { + // We want the raw object spec + var spec = resource.spec; + var dashConf = resourceConfig[resource.type] || {} + var groupBy = dashConf.groupBy + var descending = dashConf.descending + acc[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); + data: result, + groupBy: groupBy, + descending: descending, + } } - - return cache; - }, {}); - + return acc + }, {}) resolve({ meta: meta, - resources: resources - }); - }).fail(reject); + resources: templateData + }) + }).fail(reject) }); } +function fetchNewTemplateData(meta, resource, transformFn) { + return Q.promise(function(resolve, reject) { + if (!resource.hasForeignKey) { + done({spec: resource.spec, resource: resource}) + } else { + fetchForeignKeyOptions(resource).then(done).fail(reject) + } + + function done(results) { + resolve(transformFn(meta, results.spec, results.resource, {})) + } + }) +} + /** * 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); +function fetchResourceTemplateData(meta, query, transformFn) { + return Q.promise(function(resolve, reject) { + query.get().then(function(data) { + if (!data) + return reject(new Error('No resource data')) + + var resource = query.resource + + if (resource.hasForeignKey) { + fetchForeignKeyOptions(resource).then(done).fail(reject) + } else { + done({spec: resource.spec, resource: resource}) + } + + function done(results) { + resolve(transformFn(meta, results.spec, results.resource, data)) + } }).fail(reject) - }); + }) } -/** - * TODO Real error handling - */ -function errorHandler(req, res) { - return function(err) { - res.send(err.stack); - }; -} +function fetchForeignKeyOptions(resource) { + var promises = Object.keys(resource.foreignKeys) + .map(fetchOptionsForKey) + var spec = resource.spec -/** - * TODO Real 404 handling - */ -function resourceMissingHandler(req, res) { - return function() { - res.status(404); - res.send('404'); + return Q.all(promises).then(done) + + function done() { + return Q.promise(function(resolve, reject) { + resolve({spec: spec, resource: resource}) + }) + } + + function fetchOptionsForKey(field) { + var relatedResourceType = resource.foreignKeys[field] + return resource.related(relatedResourceType).then(fillOptions) + + function fillOptions(results) { + return Q.promise(function(resolve, reject) { + spec[field].options = results.map(function(result) { + return result.id + }) + resolve() + }) + } } } + module.exports = OKAdminView; |
