summaryrefslogtreecommitdiff
path: root/app/node_modules/okadminview/index.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/node_modules/okadminview/index.js')
-rw-r--r--app/node_modules/okadminview/index.js391
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;