summaryrefslogtreecommitdiff
path: root/app/node_modules
diff options
context:
space:
mode:
Diffstat (limited to 'app/node_modules')
-rw-r--r--app/node_modules/okadminview/index.js391
-rw-r--r--app/node_modules/okadminview/package.json5
-rw-r--r--app/node_modules/okdb/index.js213
-rw-r--r--app/node_modules/okdb/package.json3
-rw-r--r--app/node_modules/okquery/index.js73
-rw-r--r--app/node_modules/okquery/package.json1
-rw-r--r--app/node_modules/okresource/index.js204
-rw-r--r--app/node_modules/okresource/package.json1
-rw-r--r--app/node_modules/okschema/index.js178
-rw-r--r--app/node_modules/okschema/package.json2
-rw-r--r--app/node_modules/okserver/index.js30
-rw-r--r--app/node_modules/okservices/index.js41
-rwxr-xr-xapp/node_modules/okservices/install.sh1
-rw-r--r--app/node_modules/okservices/oks3/index.js145
-rw-r--r--app/node_modules/okservices/oks3/package.json16
-rw-r--r--app/node_modules/okservices/oks3/upload.js79
-rw-r--r--app/node_modules/okservices/oktwitter/Readme.md6
-rw-r--r--app/node_modules/okservices/oktwitter/index.js48
-rw-r--r--app/node_modules/okservices/oktwitter/package.json13
-rw-r--r--app/node_modules/okservices/okwebhook/index.js83
-rw-r--r--app/node_modules/okservices/okwebhook/package.json11
-rw-r--r--app/node_modules/okservices/package.json8
-rw-r--r--app/node_modules/oktemplate/index.js74
-rw-r--r--app/node_modules/oktemplate/package.json1
-rw-r--r--app/node_modules/okview/index.js148
25 files changed, 1329 insertions, 446 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;
diff --git a/app/node_modules/okadminview/package.json b/app/node_modules/okadminview/package.json
index 4832db1..bdaefc5 100644
--- a/app/node_modules/okadminview/package.json
+++ b/app/node_modules/okadminview/package.json
@@ -10,8 +10,13 @@
"license": "None",
"dependencies": {
"body-parser": "^1.12.2",
+ "connect-flash": "^0.1.1",
+ "express-session": "^1.11.1",
+ "lodash.clonedeep": "^3.0.0",
"method-override": "^2.3.2",
"object-assign": "^2.0.0",
+ "passport": "^0.2.1",
+ "passport-http": "^0.2.2",
"pluralize": "^1.1.2",
"q": "^1.2.0"
}
diff --git a/app/node_modules/okdb/index.js b/app/node_modules/okdb/index.js
index 79ce1eb..ad8d9a7 100644
--- a/app/node_modules/okdb/index.js
+++ b/app/node_modules/okdb/index.js
@@ -1,6 +1,10 @@
-var Q = require('q');
+var assign = require('object-assign')
+var cloneDeep = require('lodash.clonedeep');
+var isobject = require('lodash.isobject');
var format = require('util').format;
var low = require('lowdb');
+var Q = require('q');
+
low.mixin(low.mixin(require('underscore-db')));
/**
@@ -10,14 +14,14 @@ low.mixin(low.mixin(require('underscore-db')));
function OKDB(options) {
if (!(this instanceof OKDB)) return new OKDB(options);
options = options || {};
- var type;
+ var db;
if (typeof options === 'string')
- type = options;
+ db = options;
else
- type = options.type;
- if (!type)
- throw new Error('No DB type provided');
- switch (type) {
+ db = options.db;
+ if (!db)
+ throw new Error('No DB db provided to OKDB');
+ switch (db) {
case 'fs':
return FSDB(options);
default:
@@ -27,81 +31,186 @@ function OKDB(options) {
/**
* DB implementation backed by a JSON file.
- * TODO Incomplete
*/
function FSDB(options) {
if (!(this instanceof FSDB)) return new FSDB(options);
options = options || {};
+ if (!options.schemas)
+ throw new Error('No schemas provided to FSDB')
+ this._schemas = options.schemas;
var name = options.name || 'db';
var filename = name + '.json';
this._db = low(filename);
}
-FSDB.prototype._resolve = function(data, error) {
- return Q.Promise(function resolvePromise(resolve, reject) {
- if (error) {
- reject(error);
- } else {
- resolve(data);
- }
- });
+FSDB.prototype.get = function(collection, id) {
+ var schema = this._schemas[collection];
+ if (!schema)
+ return resolve(null, new Error('No such collection type'));
+
+ var query = getQuery(schema, id);
+ if (!query)
+ return resolve(null, new Error('Bad query'));
+
+ var result = this._db(collection).find(query);
+ return resolve(result ? cloneDeep(result) : result);
};
-FSDB.prototype.get = function(collection, query) {
- if (!query) {
- return this._resolve(null, new Error('No query given'));
+/**
+ * Add a new document to the DB
+ */
+FSDB.prototype.insert = function(collection, data) {
+ var schema = this._schemas[collection];
+ var wrapped = this._db(collection);
+ if (!schema)
+ return resolve(null, new Error('No such collection type'));
+ // Get detached, clone deep, data sleep, beep beep
+ data = cloneDeep(data);
+ // Auto-increment fields
+ data = autoincrement(wrapped, schema, data);
+ // Record date created
+ // TODO Should this meta prop logic be moved out of the DB?
+ data.dateCreated = new Date().toUTCString();
+ var result = wrapped.chain().push(data).last().value();
+
+ if (result) {
+ return resolve(cloneDeep(result));
} else {
- var data = this._db(collection).find(query);
- return this._resolve(data);
+ return resolve(null, new Error('Problem inserting document'));
}
};
-FSDB.prototype.put = function(collection, query, data) {
- data = data || {};
- if (!query) {
- return this._resolve(null, new Error('No query given'));
- } else if (this._db(collection).find(query)) {
- var updated = this._db(collection)
- .chain()
- .find(query)
- .assign(data)
- .value();
- return this._resolve(updated);
+/**
+ * Update an existing document in the DB
+ */
+FSDB.prototype.update = function(collection, id, data) {
+ var schema = this._schemas[collection];
+ if (!schema)
+ return resolve(null, new Error('No such collection type'));
+
+ var query = getQuery(schema, id);
+ var wrapped = this._db(collection);
+ var chain = wrapped.chain().find(query);
+
+ if (!chain.value()) {
+ return resolve(null, new Error('Cannot update nonexistent entry'));
+ }
+
+ var result = chain.assign(cloneDeep(data)).value();
+ if (result) {
+ return resolve(cloneDeep(result));
} else {
- return this._resolve(null, new Error('Cannot update nonexistent entry'));
+ return resolve(null, new Error('Problem updating document'));
}
};
-FSDB.prototype.create = function(collection, data) {
- var created = this._db(collection)
- .chain()
- .push(data)
- .last()
- .value();
- return this._resolve(created);
-};
+/**
+ * TODO Should be atomic ¯\_(ツ)_/¯
+ */
+FSDB.prototype.updateBatch = function(collection, ids, datas) {
+ var schema = this._schemas[collection];
+ if (!schema)
+ return resolve(null, new Error('No such collection type'));
+ if (!ids || !ids.length || !datas || !datas.length ||
+ ids.length !== datas.length) {
+ return resolve(null, new Error('Bad input'));
+ }
-FSDB.prototype.remove = function(collection, id, data) {
- throw new Error('Not implemented!');
+ var doc = this._db(collection);
+ var results = ids.map(function(id, i) {
+ return doc.chain().find(getQuery(schema, id)).assign(datas[i]).value();
+ });
+
+ return resolve(results);
};
-FSDB.prototype.find = function(collection, query) {
- if (!collection || !query) {
- return this._resolve(null, new Error('Bad input'));
+FSDB.prototype.remove = function(collection, id) {
+ var schema = this._schemas[collection];
+ if (!schema)
+ return resolve(null, new Error('No such collection type'));
+
+ var query = getQuery(schema, id);
+ var result = this._db(collection).removeWhere(query);
+
+ if (result) {
+ // Don't need to clone this ref, since it's removed anyway
+ return resolve(result);
} else {
- var data = this._db(collection).find(query);
- return this._resolve(data);
+ return resolve(null, new Error('Cannot remove nonexistent entry'));
}
};
+FSDB.prototype.sortBy = function(collection, prop, descend) {
+ var schema = this._schemas[collection];
+ if (!schema)
+ return resolve(null, new Error('No such collection type'));
+ if (!prop)
+ return resolve(null, new Error('Bad input'));
+
+ var result = this._db(collection).sortByOrder([prop], [!descend]);
+ return resolve(result);
+};
+
+FSDB.prototype.find = function(collection, query) {
+ var schema = this._schemas[collection];
+ if (!schema)
+ return resolve(null, new Error('No such collection type'));
+ if (!query)
+ return resolve(null, new Error('Bad input'));
+
+ var result = this._db(collection).find(query);
+
+ return resolve(cloneDeep(result));
+};
+
+FSDB.prototype.all = function(collection) {
+ var schema = this._schemas[collection];
+ if (!schema)
+ return resolve(null, new Error('No such collection type'));
+
+ var data = this._db(collection).value();
+ return resolve(cloneDeep(data || []));
+};
+
FSDB.prototype.getMeta = function() {
var data = this._db('meta').first();
- return this._resolve(data || {});
+ return resolve(data || {});
};
-FSDB.prototype.getAll = function(collection) {
- var data = this._db(collection).toArray();
- return this._resolve(data || []);
+/**
+ * Function implementing DB auto increment support
+ * Naive implementation, assumes DB is relatively small.
+ */
+function autoincrement(wrapper, schema, data) {
+ return schema.autoIncrementFields.reduce(function(data, field) {
+ var last = wrapper.chain().sortByOrder([field], [true]).last().value();
+ var index = last ? last[field] : -1;
+ var incremented = {};
+ incremented[field] = (parseInt(index) + 1);
+ return assign(data, incremented);
+ }, data);
+}
+
+function getQuery(schema, id) {
+ if (schema && id) {
+ var query = {};
+ query[schema.idField] = id;
+ return query;
+ }
+}
+
+/**
+ * Helper function to create promises for DB data
+ */
+function resolve(data, error) {
+ return Q.promise(function resolvePromise(resolve, reject) {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(data);
+ }
+ });
};
+
module.exports = OKDB;
diff --git a/app/node_modules/okdb/package.json b/app/node_modules/okdb/package.json
index 659422e..5184cb6 100644
--- a/app/node_modules/okdb/package.json
+++ b/app/node_modules/okdb/package.json
@@ -9,7 +9,10 @@
"author": "OKFocus",
"license": "None",
"dependencies": {
+ "lodash.clonedeep": "^3.0.0",
+ "lodash.isobject": "^3.0.1",
"lowdb": "^0.7.2",
+ "object-assign": "^2.0.0",
"q": "^1.2.0",
"underscore-db": "^0.8.1"
}
diff --git a/app/node_modules/okquery/index.js b/app/node_modules/okquery/index.js
index 33a49c4..d4cb905 100644
--- a/app/node_modules/okquery/index.js
+++ b/app/node_modules/okquery/index.js
@@ -1,3 +1,4 @@
+var cloneDeep = require('lodash.clonedeep');
var assign = require('object-assign');
var isobject = require('lodash.isobject');
var Q = require('q');
@@ -16,6 +17,14 @@ function OKQuery(options) {
var resource = options.resource;
var type = resource.type;
var query = options.query || '*';
+ // Ensure immutability
+ if (isobject(query))
+ query = cloneDeep(query);
+
+ // Queries are ordered by index by default
+ var sortField = options.sortBy || '__index';
+ // TODO Make descending by default
+ var descending = options.descending || false;
Object.defineProperty(this, 'resource', {
value: resource,
@@ -29,8 +38,23 @@ function OKQuery(options) {
enumerable: true
});
+ Object.defineProperty(this, 'as', {
+ value: options.as,
+ writable: false,
+ enumerable: true
+ });
+
+ Object.defineProperty(this, 'groupBy', {
+ value: options.groupBy,
+ writable: false,
+ enumerable: true
+ })
+
this.get = createQuery(resource, query, {
- default: options.default
+ default: options.default,
+ sortField: sortField,
+ descending : descending,
+ groupBy: options.groupBy
});
}
@@ -43,18 +67,24 @@ function createQuery(resource, query, options) {
} else if (isDynamic(query)) {
query = queryDynamic(resource);
} else if (isSet(query)) {
- query = queryAll(resource);
+ query = queryAll(resource, options.sortField, options.descending);
} else {
query = querySingle(resource, query);
}
if (options.default) {
query = withDefault(query, options.default);
}
+ if (options.groupBy) {
+ query = withGrouping(query, options.groupBy)
+ }
return query;
}
function queryComplex(resource, query) {
var dynamicProp;
+ // Query is an object specifying key value pairs against which
+ // to match DB entries. Iterate through and check if any of the values
+ // is unbound e.g. :id
var notDynamic = Object.keys(query).every(function(prop) {
var matcher = query[prop];
if (isDynamic(matcher)) {
@@ -67,15 +97,15 @@ function queryComplex(resource, query) {
if (notDynamic) {
return function() {
- console.log('get it!', query)
return resource.find(query);
}
} else {
return function(id) {
+ // Bind the dynamic property to its value
+ // and add the pair to the query
var dynamicQuery = {};
dynamicQuery[dynamicProp] = id;
var query = assign({}, query, dynamicQuery);
- console.log('get it!', query)
return resource.find(query);
}
}
@@ -87,9 +117,9 @@ function queryDynamic(resource) {
};
}
-function queryAll(resource) {
+function queryAll(resource, sortField, descending) {
return function() {
- return resource.all();
+ return resource.sortBy(sortField, descending);
};
}
@@ -105,6 +135,37 @@ function queryBound(resource) {
};
}
+/**
+ * Transform the query such that the results are grouped by the
+ * given field
+ */
+function withGrouping(queryFn, groupField) {
+ return function() {
+ return Q.Promise(function(resolve, reject) {
+ queryFn().then(function(data) {
+ data = data || []
+ if (typeof data.length === 'undefined') {
+ data = [data]
+ }
+ var result = {}
+ result[groupField] = data.reduce(reduceToGroups, {})
+ resolve(result)
+ }, reject)
+ })
+ }
+
+ function reduceToGroups(acc, data) {
+ var groupName = data[groupField]
+ if (groupName) {
+ if (!acc[groupName]) {
+ acc[groupName] = []
+ }
+ acc[groupName].push(data)
+ }
+ return acc
+ }
+}
+
function withDefault(queryFn, resultDefault) {
return function() {
return Q.Promise(function(resolve, reject) {
diff --git a/app/node_modules/okquery/package.json b/app/node_modules/okquery/package.json
index 606d45b..5ba9dd5 100644
--- a/app/node_modules/okquery/package.json
+++ b/app/node_modules/okquery/package.json
@@ -9,6 +9,7 @@
"author": "OKFocus",
"license": "None",
"dependencies": {
+ "lodash.clonedeep": "^3.0.0",
"lodash.isobject": "^3.0.1",
"object-assign": "^2.0.0",
"q": "^1.2.0"
diff --git a/app/node_modules/okresource/index.js b/app/node_modules/okresource/index.js
index 94d8cfb..c1f2509 100644
--- a/app/node_modules/okresource/index.js
+++ b/app/node_modules/okresource/index.js
@@ -1,4 +1,6 @@
+var format = require('util').format
var assign = require('object-assign');
+var cloneDeep = require('lodash.clonedeep');
var Q = require('q');
/**
@@ -18,27 +20,44 @@ function OKResource(options) {
throw new Error('No DB provided to OKResource');
var schema = options.schema;
- // Iterate through spec to find field which will act as the
- // resource id in da db.
- var idField = Object.keys(schema.spec).reduce(function(idField, prop) {
- var spec = schema.spec[prop];
- if (spec.id)
- idField = prop;
- return idField;
- // If schema has a prop called 'id', default to that one
- }, schema.spec.id && 'id');
- if (!idField)
- throw new Error('Bad schema: no ID field');
+ var spec = schema.spec;
var type = options.type;
+ var hasForeignKey = false
this._db = options.db;
this._schema = schema;
+ var foreignKeys = Object.keys(spec).reduce(function(acc, field) {
+ var fieldSpec = spec[field]
+ if (fieldSpec.type === 'foreign-key') {
+ hasForeignKey = true
+ acc[field] = fieldSpec.key
+ }
+ return acc
+ }, {})
+
+ // Will store references to other resources referenced via foreign keys
+ this._foreignKeyedResources = {}
+
// Define properties which are part of the API
+
+ // Should be treated as read-only
+ Object.defineProperty(this, 'foreignKeys', {
+ get: function() {
+ return foreignKeys
+ }
+ })
+
+ Object.defineProperty(this, 'hasForeignKey', {
+ get: function() {
+ return hasForeignKey
+ }
+ })
Object.defineProperty(this, 'spec', {
- value: schema.spec,
- writable: false,
+ get: function() {
+ return schema.spec;
+ },
enumerable: true
});
@@ -48,12 +67,6 @@ function OKResource(options) {
enumerable: true
});
- Object.defineProperty(this, 'idField', {
- value: idField,
- writable: false,
- enumerable: true
- });
-
// Whether this resource represents a specific data point
// or a whole class of data
Object.defineProperty(this, 'bound', {
@@ -63,6 +76,30 @@ function OKResource(options) {
});
}
+OKResource.prototype._linkForeignKey = function(field, resource) {
+ this._foreignKeyedResources[field] = resource
+}
+
+/**
+ * Fetch all related resources for the given field
+ */
+OKResource.prototype.related = function(field) {
+ var resource = this._foreignKeyedResources[field]
+ return Q.promise(function(resolve, reject) {
+ if (!resource) {
+ return error(reject, new Error(format(
+ "No related resource for field '%s'", field)))
+ }
+ resource.all().then(resolve).fail(reject)
+ })
+
+ function error(reject, err) {
+ setTimeout(function() {
+ reject(err)
+ }, 0)
+ }
+}
+
/**
* Throws an error if data does not conform to schema
*/
@@ -71,31 +108,34 @@ OKResource.prototype.assertValid = function(data) {
};
OKResource.prototype.all = function() {
- return this._db.getAll(this.type);
+ return this._db.all(this.type);
};
-OKResource.prototype.create = function(data) {
+OKResource.prototype.getID = function(data) {
data = data || {};
+ return data[this._schema.idField];
+};
+
+OKResource.prototype.create = function(data) {
var type = this.type;
var db = this._db;
- var id = data[this.idField];
return Q.promise(function(resolve, reject) {
- if (!id) {
- reject(new Error('Data does not contain ID property'));
+ if (!data) {
+ reject(new Error('No data provided'));
} else {
- db.create(type, data).then(resolve).fail(reject);
+ db.insert(type, data).then(resolve).fail(reject);
}
});
};
-OKResource.prototype.destroy = function(data) {
- data = data || {};
- var id = data[this.idField];
+OKResource.prototype.destroy = function(id) {
+ var db = this._db;
+ var type = this.type;
return Q.promise(function(resolve, reject) {
if (!id) {
- reject(new Error('Data does not contain ID property'));
+ reject(new Error('No ID provided'));
} else {
- this._db.remove(this.type, data.id, data).then(resolve).fail(reject);
+ db.remove(type, id).then(resolve).fail(reject);
}
});
};
@@ -106,7 +146,7 @@ OKResource.prototype.find = function(query) {
var type = this.type;
return Q.promise(function(resolve, reject) {
if (!query) {
- throw new Error('No query given');
+ throw new Error('No query provided');
} else {
db.find(type, query).then(resolve).fail(reject);
}
@@ -116,53 +156,65 @@ OKResource.prototype.find = function(query) {
OKResource.prototype.get = function(id) {
var db = this._db;
var type = this.type;
- var idField = this.idField;
return Q.promise(function(resolve, reject) {
if (!id) {
- throw new Error('No ID given');
+ throw new Error('No ID provided');
} else {
- // We have the id, but we still need
- // to resolve which field is the id field
- // to match
- var query = {};
- query[idField] = id;
- db.get(type, query).then(resolve).fail(reject);
+ db.get(type, id).then(resolve).fail(reject);
}
});
};
OKResource.prototype.update = function(id, data) {
- data = data || {};
var db = this._db;
var type = this.type;
- var idField = this.idField;
return Q.promise(function(resolve, reject) {
if (!id) {
reject(new Error('No resource ID provided'));
+ } else if (!data) {
+ reject(new Error('No data provided'));
} else {
- var query = {};
- query[idField] = id;
- db.put(type, query, data).then(resolve).fail(reject);;
+ db.update(type, id, data).then(resolve).fail(reject);;
}
});
};
+OKResource.prototype.updateBatch = function(ids, datas) {
+ var self = this;
+ var db = this._db;
+ var type = this.type;
+ return Q.promise(function(resolve, reject) {
+ if (!ids || !ids.length || !datas || !datas.length ||
+ ids.length !== datas.length) {
+ reject(new Error('Bad input'));
+ } else {
+ db.updateBatch(type, ids, datas).then(resolve).fail(reject);
+ }
+ });
+}
+
+
+/**
+ * Get all documents in collection sorted by property,
+ * optionally in descending order
+ */
+OKResource.prototype.sortBy = function(prop, descend) {
+ return this._db.sortBy(this.type, prop, descend);
+};
+
OKResource.prototype.updateOrCreate = function(id, data) {
data = data || {};
var type = this.type;
var db = this._db;
- var idField = this.idField;
- var query = {};
- query[idField] = id;
return Q.promise(function(resolve, reject) {
if (!id) {
- reject(new Error('No resource ID provided'));
+ reject(new Error('No ID provided'));
} else {
- db.get(type, query).then(function(persisted) {
+ db.get(type, id).then(function(persisted) {
if (persisted) {
- db.put(type, query, data).then(resolve).fail(reject);
+ db.update(type, id, data).then(resolve).fail(reject);
} else {
- db.create(type, data).then(resolve).fail(reject);
+ db.insert(type, data).then(resolve).fail(reject);
}
}).fail(reject);
}
@@ -179,6 +231,9 @@ OKResource.prototype.instance = function(options) {
});
};
+/**
+ * TODO This class is such bullshit. Refactor out
+ */
function OKResourceInstance(resource, options) {
if (!(this instanceof OKResourceInstance)) return new OKResourceInstance(options);
// Only support static data instances for now
@@ -190,12 +245,16 @@ function OKResourceInstance(resource, options) {
// conceptually at all times since they are derived from app
// configuration, but may not actually be present
// in the database and need custom logic to handle this.
- var staticData = assign({}, options.static);
- var id = staticData[resource.idField];
+ var staticData = cloneDeep(options.static);
+ var id = resource.getID(staticData);
if (!id)
throw new Error(
'Cannot create static OKResourceInstance without an ID field');
+ this.getID = function() {
+ return id;
+ };
+
/**
* Ensure that static data is provided on get
*/
@@ -204,9 +263,9 @@ function OKResourceInstance(resource, options) {
resource.get(id).then(function(data) {
// Note the assign call. Don't expose private references!
if (data) {
- resolve(assign({}, data, staticData));
+ resolve(assign(data, cloneDeep(staticData)));
} else {
- resolve(assign({}, staticData));
+ resolve(cloneDeep(staticData));
}
}).fail(reject);
});
@@ -274,6 +333,18 @@ function OKResourceInstance(resource, options) {
resource.assertValid(data);
};
+ Object.defineProperty(this, 'foreignKeys', {
+ get: function() {
+ return []
+ }
+ })
+
+ Object.defineProperty(this, 'hasForeignKey', {
+ get: function() {
+ return false
+ }
+ })
+
Object.defineProperty(this, 'parent', {
value: resource,
writable: false,
@@ -281,14 +352,9 @@ function OKResourceInstance(resource, options) {
});
Object.defineProperty(this, 'spec', {
- value: resource.spec,
- writable: false,
- enumerable: true
- });
-
- Object.defineProperty(this, 'id', {
- value: id,
- writable: false,
+ get: function() {
+ return resource.spec
+ },
enumerable: true
});
@@ -298,23 +364,11 @@ function OKResourceInstance(resource, options) {
enumerable: true
});
- Object.defineProperty(this, 'idField', {
- value: resource.idField,
- writable: false,
- enumerable: true
- });
-
Object.defineProperty(this, 'bound', {
value: true,
writable: false,
enumerable: true
});
-
- Object.defineProperty(this, 'class', {
- value: resource,
- writable: false,
- enumerable: true
- });
}
module.exports = OKResource;
diff --git a/app/node_modules/okresource/package.json b/app/node_modules/okresource/package.json
index 7f19c9b..7b1dfbb 100644
--- a/app/node_modules/okresource/package.json
+++ b/app/node_modules/okresource/package.json
@@ -9,6 +9,7 @@
"author": "OKFocus",
"license": "None",
"dependencies": {
+ "lodash.clonedeep": "^3.0.0",
"object-assign": "^2.0.0",
"q": "^1.2.0"
}
diff --git a/app/node_modules/okschema/index.js b/app/node_modules/okschema/index.js
index 8871a99..0048fc5 100644
--- a/app/node_modules/okschema/index.js
+++ b/app/node_modules/okschema/index.js
@@ -1,4 +1,4 @@
-var assign = require('object-assign');
+var cloneDeep = require('lodash.clonedeep');
var mschema = require('mschema');
var v = require('validator');
@@ -41,36 +41,99 @@ var types = {
}
},
'captioned-image-list': {
+ isArray: true,
parent: [{
uri: { type: 'string' }, // TODO Implement URI type
caption: { type: 'string' }
}],
- assertValid: function(spec, value) {
- var message;
- var actual;
- if (!value || !value.length) {
- throw [{
- message: 'Not an array',
- expected: JSON.stringify(this.parent),
- actual: value
- }];
- }
- }
+ assertValid: function(spec, value) {}
+ },
+ 'gallery': {
+ isArray: true,
+ parent: [{
+ uri: { type: 'string' }, // TODO Implement URI type
+ caption: { type: 'string' }
+ }],
+ assertValid: function(spec, value) {}
+ },
+ // Special type for resource meta information
+ 'meta': {
+ parent: 'string',
+ assertValid: function(spec, value) {}
+ },
+ 'link-list': {
+ isArray: true,
+ parent: [{
+ uri: { type: 'string' },
+ text: { type: 'string' }
+ }],
+ assertValid: function(spec, value) {}
+ },
+ 'date': {
+ parent: 'string',
+ assertValid: function(spec, value) {}
+ },
+ 'flag': {
+ parent: 'boolean',
+ assertValid: function(spec, value) {}
+ },
+ 'foreign-key': {
+ parent: 'enum',
+ assertValid: function(spec, value) {}
+ },
+ 'media-list': {
+ isArray: true,
+ parent: [],
+ assertValid: function(spec, value) {}
+ },
+ 'media': {
+ isArray: true,
+ parent: [],
+ assertValid: function(spec, value) {}
+ },
+ 'double-captioned-image-list': {
+ isArray: true,
+ parent: [],
+ assertValid: function(spec, value) {}
+ },
+ 'triple-captioned-image-list': {
+ isArray: true,
+ parent: [],
+ assertValid: function(spec, value) {}
+ },
+}
+
+/*
+function checkArrayLength (spec, value) {
+ var message;
+ var actual;
+ if (!value || !value.length) {
+ throw [{
+ message: 'Not an array',
+ expected: JSON.stringify(this.parent),
+ actual: value
+ }];
}
}
+*/
+
/**
* OKSchema!
* Meant as a thin wrapper around some existing schema validation
* module, mostly to allow for the extension of types.
+ *
+ * NOTE: Currently just assumes spec is valid. If you give a bad spec
+ * strange things may or may not happen
*/
function OKSchema(spec) {
if (!(this instanceof OKSchema)) return new OKSchema(spec);
if (!spec)
throw new Error('No spec provided to OKSchema');
- spec = assign({}, spec);
+ spec = cloneDeep(spec);
+ var specKeys = Object.keys(spec);
// Cache the mschema version of our spec
- this._mschemaSpec = Object.keys(spec).reduce(function(cache, prop) {
+ this._mschemaSpec = specKeys.reduce(function(cache, prop) {
// If custom type, return its parent spec
var type = spec[prop].type;
if (types[type]) {
@@ -82,25 +145,104 @@ function OKSchema(spec) {
return cache;
}, {});
+ // Find ID field
+ var idField;
+ specKeys.every(function(prop) {
+ if (prop === 'id' || spec[prop].id) {
+ idField = prop;
+ return false;
+ } else {
+ return true;
+ }
+ });
+
+ // Register autoincrement fields
+ // NOTE Does not work for nested fields
+ var autoIncrementFields = specKeys.reduce(function(arr, prop) {
+ var specProp = spec[prop];
+ if (specProp.autoincrement) {
+ arr.push(prop);
+ }
+ return arr;
+ }, []);
+
Object.defineProperty(this, 'spec', {
- value: spec,
- writable: false
+ get: function() {
+ return cloneDeep(spec);
+ },
+ enumerable: true
+ });
+
+ Object.defineProperty(this, 'idField', {
+ value: idField,
+ writable: true,
+ enumerable: true
});
+
+ Object.defineProperty(this, 'autoIncrementFields',{
+ get: function() {
+ return cloneDeep(autoIncrementFields);
+ },
+ enumerable: true
+ });
+}
+
+OKSchema.prototype.fixMissingLists = function(data) {
+ var spec = this.spec;
+
+ // The qs body-parser module does not have a way to represent
+ // empty lists. If you delete all elements from a list,
+ // check against the spec so we know to replace with an empty list.
+ Object.keys(spec).forEach(function(prop){
+ var type = spec[prop].type;
+ if (types[type] && types[type].isArray && ! data[prop]) {
+ data[prop] = []
+ }
+ })
+}
+
+OKSchema.prototype.fixIndexField = function(data) {
+ // Likewise numbers always come in as strings. The field used to sort
+ // records, __index, is of type "meta", so the parseFloat in
+ // assertValid (below) never fires and we end up with sorting issues.
+ if (data.__index && typeof data.__index == "string") {
+ var __index = parseInt(data.__index)
+ if (! isNaN(__index)) {
+ data.__index = __index
+ }
+ }
}
OKSchema.prototype.assertValid = function(data) {
data = data || {};
var spec = this.spec;
+
// Run through custom validators, they'll throw if invalid
Object.keys(data).forEach(function(prop) {
var type = spec[prop].type;
- if (types[type]) {
+
+ // Check if it's a number/boolean and try to cast it
+ // otherwise pass and let mschema handle
+ if (type === 'number') {
+ try {
+ data[prop] = parseFloat(data[prop]);
+ } catch (err) {}
+ } else if (type === 'flag') {
+ data[prop] = data[prop] == "true" ? true : false
+ } else if (types[type]) {
types[type].assertValid(spec[prop], data[prop]);
}
});
var result = mschema.validate(data, this.toMschema());
- if (!result.valid)
+ if (!result.valid) {
throw result.errors;
+ }
+
+ // Fix various issues with our data, having to do
+ // with use of the "qs" body-parser module.
+ // TODO: just send JSON?
+ this.fixMissingLists(data)
+ this.fixIndexField(data)
};
/**
diff --git a/app/node_modules/okschema/package.json b/app/node_modules/okschema/package.json
index 21214fa..21a7c67 100644
--- a/app/node_modules/okschema/package.json
+++ b/app/node_modules/okschema/package.json
@@ -9,8 +9,8 @@
"author": "OKFocus",
"license": "None",
"dependencies": {
+ "lodash.clonedeep": "^3.0.0",
"mschema": "^0.5.5",
- "object-assign": "^2.0.0",
"validator": "^3.37.0"
}
}
diff --git a/app/node_modules/okserver/index.js b/app/node_modules/okserver/index.js
index 1645eaa..a89676f 100644
--- a/app/node_modules/okserver/index.js
+++ b/app/node_modules/okserver/index.js
@@ -16,6 +16,8 @@ function OKServer(options) {
throw new Error('No admin root directory provided to OKServer');
if (!options.adminPath)
throw new Error('No admin path provided to OKServer');
+ if (!options.errorHandler)
+ throw new Error('No error handler provided to OKServer');
var root = options.root;
var adminRoot = options.adminRoot;
var adminPath = options.adminPath;
@@ -25,6 +27,7 @@ function OKServer(options) {
var router = express.Router({
strict: app.get('strict routing')
});
+ var error = options.errorHandler;
var services = options.services || {};
Object.keys(views)
// Sort such that more general routes are matched last
@@ -49,11 +52,8 @@ function OKServer(options) {
* other middleware.
*/
- // Intercept favicon requests and 404 for now
- app.use('/favicon.ico', function(req, res) {
- res.status(404)
- return res.send('');
- });
+ // Disable x-powered-by express header
+ app.disable('x-powered-by')
// Serve user static files
app.use(express.static(root));
// Serve admin interface static files
@@ -61,22 +61,16 @@ function OKServer(options) {
// Application router
app.use(router);
// Add services
- if (services.image) {
- app.use('/_services/image', services.image.middleware());
- }
+ Object.keys(services).forEach(function(key){
+ app.use('/_services/' + key, services[key].middleware());
+ })
// Make sure this lady is last. Checks whether the desired
// route has a trailing-slash counterpart and redirects there
app.use(slash());
-
- /**
- * Create a handler which redirect all requests to
- * the same route with a trailing slash appended
- */
- function redirect(routeNoSlash) {
- return function(req, res) {
- res.redirect(301, routeNoSlash + '/');
- }
- }
+ // Otherwise it's a 404
+ app.use(function(req, res) {
+ error(req, res, 404)(new Error('No matching route'));
+ });
}
OKServer.prototype.listen = function listen(port) {
diff --git a/app/node_modules/okservices/index.js b/app/node_modules/okservices/index.js
deleted file mode 100644
index 46f7ffd..0000000
--- a/app/node_modules/okservices/index.js
+++ /dev/null
@@ -1,41 +0,0 @@
-var skipper = require('skipper');
-
-function OKImageService(options) {
- if (!(this instanceof OKImageService)) return new OKImageService(options);
- options = options || {};
- if (!options.express)
- throw new Error('Express not provided to OKImageService');
- if (!options.s3)
- throw new Error('S3 configuration not provided to OKImageService');
- var express = options.express;
-
- var router = express.Router();
-
- router.use(skipper());
-
- router.post('/', function(req, res) {
- // req should have a method `file` on it which is
- // provided by skipper. Use that to do AWS stuff
- req.file('image').upload({
- adapter: require('skipper-s3'),
- key: options.s3.key,
- secret: options.s3.secret,
- bucket: options.s3.bucket,
- headers: {
- 'x-amz-acl': 'public-read'
- }
- }, function (err, uploadedFiles) {
- res.json(uploadedFiles);
- });
- });
-
- this._middleware = router;
-}
-
-OKImageService.prototype.middleware = function() {
- return this._middleware;
-};
-
-module.exports = {
- OKImageService: OKImageService
-};
diff --git a/app/node_modules/okservices/install.sh b/app/node_modules/okservices/install.sh
new file mode 100755
index 0000000..5ffd898
--- /dev/null
+++ b/app/node_modules/okservices/install.sh
@@ -0,0 +1 @@
+for i in ok* ; do cd $i ; npm install; cd .. ; done ; cd ../..
diff --git a/app/node_modules/okservices/oks3/index.js b/app/node_modules/okservices/oks3/index.js
new file mode 100644
index 0000000..cc40b71
--- /dev/null
+++ b/app/node_modules/okservices/oks3/index.js
@@ -0,0 +1,145 @@
+var upload = require("./upload")
+var multer = require('multer')
+
+// Hack to prevent this god-forsaken module from crashing our shit
+var d = require('domain').create()
+d.on('error', function (err) {
+ console.log(err)
+ console.error('Stupid error in S3 upload. Upload probably prematurely canceled')
+})
+
+function OKS3(options) {
+ if (!(this instanceof OKS3)) return new OKS3(options);
+ options = options || {};
+ if (!options.express)
+ throw new Error('Express not provided to OKS3');
+ if (!options.s3)
+ throw new Error('S3 configuration not provided to OKS3');
+
+ if (!options.s3.image) options.s3.image = {}
+ if (!options.s3.audio) options.s3.audio = {}
+ if (!options.s3.video) options.s3.video = {}
+
+ // Make sure maxbytes property is there - it can be a number,
+ // or zero/undefined (for no maximum upload size)
+ if (options.s3.maxbytes) {
+ if (! ('maxbytes' in options.s3.image))
+ options.s3.image.maxbytes = options.s3.maxbytes
+ if (! ('maxbytes' in options.s3.video))
+ options.s3.video.maxbytes = options.s3.maxbytes
+ if (! ('maxbytes' in options.s3.audio))
+ options.s3.audio.maxbytes = options.s3.maxbytes
+ }
+ if (typeof options.s3.image.allowed !== "boolean")
+ options.s3.image.allowed = true
+ if (typeof options.s3.video.allowed !== "boolean")
+ options.s3.video.allowed = false
+ if (typeof options.s3.audio.allowed !== "boolean")
+ options.s3.audio.allowed = false
+
+ upload.init({
+ key: options.s3.key,
+ secret: options.s3.secret,
+ bucket: options.s3.bucket,
+ })
+
+ var express = options.express;
+
+ var router = express.Router();
+
+ var mult = multer()
+
+ router.post('/image', mult.single('image'), function(req, res) {
+ d.run(function () {
+
+ if (! options.s3.image.allowed) {
+ return res.status(500).json({ error: "Image uploading not permitted" })
+ }
+
+ upload.put({
+ file: req.file,
+ preserveFilename: options.s3.image.preserveFilename,
+ dirname: options.s3.dirname,
+ types: {
+ 'image/gif': 'gif',
+ 'image/jpeg': 'jpg',
+ 'image/jpg': 'jpg',
+ 'image/png': 'png',
+ },
+ unacceptable: function(err){
+ res.json({ error: err })
+ },
+ success: function(url){
+ res.json({ url: url })
+ }
+ })
+
+ });
+ });
+
+ router.post('/audio', mult.single('audio'), function(req, res) {
+ d.run(function () {
+
+ if (! options.s3.image.allowed) {
+ return res.status(500).json({ error: "Audio uploading not permitted" })
+ }
+
+ upload.put({
+ file: req.file,
+ preserveFilename: options.s3.audio.preserveFilename,
+ dirname: options.s3.dirname,
+ types: {
+ 'audio/mp3': 'mp3',
+ 'audio/mpeg': 'mp3',
+ 'audio/wav': 'wav',
+ 'audio/flac': 'flac',
+ },
+ unacceptable: function(err){
+ res.json({ error: err })
+ },
+ success: function(url){
+ res.json({ url: url })
+ }
+ })
+
+ });
+ });
+
+ router.post('/video', mult.single('video'), function(req, res) {
+ d.run(function () {
+
+ if (! options.s3.image.allowed) {
+ return res.status(500).json({ error: "Video uploading not permitted" })
+ }
+
+ upload.put({
+ file: req.file,
+ preserveFilename: options.s3.video.preserveFilename,
+ dirname: options.s3.dirname,
+ types: {
+ 'video/mp4': 'mp4',
+ 'video/webm': 'webm',
+ },
+ unacceptable: function(err){
+ res.json({ error: err })
+ },
+ success: function(url){
+ res.json({ url: url })
+ }
+ })
+
+ });
+ });
+
+ function preserveFilename (stream, cb){
+ cb(null, stream.filename)
+ }
+
+ this._middleware = router;
+}
+
+OKS3.prototype.middleware = function() {
+ return this._middleware;
+};
+
+module.exports = OKS3
diff --git a/app/node_modules/okservices/oks3/package.json b/app/node_modules/okservices/oks3/package.json
new file mode 100644
index 0000000..61da414
--- /dev/null
+++ b/app/node_modules/okservices/oks3/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "oks3",
+ "version": "1.0.0",
+ "description": "s3 wassup",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "OKFocus",
+ "license": "None",
+ "dependencies": {
+ "knox": "^0.9.2",
+ "multer": "^1.1.0",
+ "node-uuid": "^1.4.7"
+ }
+}
diff --git a/app/node_modules/okservices/oks3/upload.js b/app/node_modules/okservices/oks3/upload.js
new file mode 100644
index 0000000..517d5f7
--- /dev/null
+++ b/app/node_modules/okservices/oks3/upload.js
@@ -0,0 +1,79 @@
+
+var knox = require('knox')
+var uuid = require('node-uuid')
+
+var s3
+
+var acceptableuploadTypes = {
+ 'image/gif': 'gif',
+ 'image/jpeg': 'jpg',
+ 'image/jpg': 'jpg',
+ 'image/png': 'png',
+}
+
+module.exports = {}
+
+module.exports.init = function (opt){
+ s3 = knox.createClient({
+ key: opt.key,
+ secret: opt.secret,
+ bucket: opt.bucket,
+ })
+}
+
+module.exports.put = function (opt) {
+ var filename
+ var err
+ var now = new Date()
+
+ var file = opt.file
+
+ var types = opt.types || acceptableuploadTypes
+ var extension = types[file.mimetype]
+
+ if (opt.preserveFilename) {
+ filename = file.originalname
+ }
+ else {
+ filename = uuid.v1() + "." + extension;
+ }
+
+ var remote_path = "/" + opt.dirname + "/" + filename
+
+ if (! extension) {
+ err = "Unacceptable filetype."
+ }
+ else if (opt.maxSize && file.size > opt.maxSize) {
+ err = "File too large. Uploads can be a maximum of " + opt.maxSize + " bytes."
+ }
+
+ if (err) {
+ console.error(">>>", err)
+ opt.unacceptable && opt.unacceptable(err)
+ return
+ }
+
+ opt.acceptable && opt.acceptable(err)
+
+ // console.log("upload >", remote_path)
+ s3.putBuffer(file.buffer, remote_path, {
+ 'Content-Length': file.size,
+ 'Content-Type': file.mimetype,
+ 'x-amz-acl': 'public-read'
+ }, function(err, s3res) {
+ if (err || s3res.statusCode !== 200) {
+ console.error(err);
+ if (s3res && s3res.resume) {
+ s3res.resume()
+ }
+ return;
+ }
+
+ var file_url = s3res.url || s3res.req.url
+
+ opt.success && opt.success(file_url)
+ }).on('error', function(err, s3res){
+ console.error(err)
+ s3res && s3res.resume && s3res.resume()
+ })
+}
diff --git a/app/node_modules/okservices/oktwitter/Readme.md b/app/node_modules/okservices/oktwitter/Readme.md
new file mode 100644
index 0000000..def73db
--- /dev/null
+++ b/app/node_modules/okservices/oktwitter/Readme.md
@@ -0,0 +1,6 @@
+# oktwitter
+
+## Service to allow auth with Twitter API
+
+Requests to this service proxy to the twitter API, adding proper auth
+credentials along the way
diff --git a/app/node_modules/okservices/oktwitter/index.js b/app/node_modules/okservices/oktwitter/index.js
new file mode 100644
index 0000000..ec4945d
--- /dev/null
+++ b/app/node_modules/okservices/oktwitter/index.js
@@ -0,0 +1,48 @@
+var Twit = require('twit')
+
+/**
+ * Proxy to Twitter API adding auth creds
+ * TODO Technically can be abused by anyone right now.
+ * Should add some sort of same origin policy.
+ */
+function OKTwitter (options) {
+ if (!(this instanceof OKTwitter)) return new OKTwitter(options)
+ options = options || {}
+ if (!options.express)
+ throw new Error('Express not provided to OKTwitter');
+ if (!options.credentials)
+ throw new Error('Twitter credentials not provided to OKTwitter');
+
+ var express = options.express
+ var router = express.Router()
+ var creds = options.credentials
+ var twitter = new Twit({
+ consumer_key: creds.consumerKey,
+ consumer_secret: creds.consumerSecret,
+ access_token: creds.accessToken,
+ access_token_secret: creds.accessTokenSecret,
+ })
+
+ router.get('*', function (req, res) {
+ twitter.get(req.path.slice(1), req.query, function (err, data) {
+ if (err) {
+ res.status(err.statusCode)
+ res.send(err.twitterReply)
+ } else {
+ res.json(data)
+ }
+ })
+ })
+
+ router.post('*', function (req, res) {
+ throw new Error('Twitter POST requests not implemented')
+ })
+
+ this._router = router
+}
+
+OKTwitter.prototype.middleware = function () {
+ return this._router
+}
+
+module.exports = OKTwitter
diff --git a/app/node_modules/okservices/oktwitter/package.json b/app/node_modules/okservices/oktwitter/package.json
new file mode 100644
index 0000000..ddee2f9
--- /dev/null
+++ b/app/node_modules/okservices/oktwitter/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "oktwitter",
+ "version": "1.0.0",
+ "description": "Allows auth to Twitter API",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "OKFocus",
+ "dependencies": {
+ "twit": "^2.1.1"
+ }
+}
diff --git a/app/node_modules/okservices/okwebhook/index.js b/app/node_modules/okservices/okwebhook/index.js
new file mode 100644
index 0000000..d04e662
--- /dev/null
+++ b/app/node_modules/okservices/okwebhook/index.js
@@ -0,0 +1,83 @@
+
+/**
+ * Service which will listen for a Github webhook, fired on push.
+ * This service can be used to rebuild / restart the app automatically
+ * when new code is pushed.
+ */
+
+var crypto = require('crypto')
+var exec = require('child_process').exec
+var path = require('path')
+
+function OKWebhook (options) {
+ if (!(this instanceof OKWebhook)) return new OKWebhook(options)
+ options = options || {}
+ if (!options.express)
+ throw new Error('Express not provided to OKWebhook');
+ if (!options.config)
+ throw new Error('Configuration not provided to OKWebhook');
+ if (options.config.active && !options.config.secret)
+ throw new Error('Github secret not provided to OKWebhook');
+ if (options.config.active && !options.config.command)
+ throw new Error('Build command not provided to OKWebhook');
+
+ var express = options.express
+ var router = express.Router()
+ var config = options.config
+
+ var secret = config.secret
+ var command = config.command
+
+ router.get('/', function (req, res) {
+ res.send('GET not supported')
+ })
+
+ router.post('/', getBody, function (req, res) {
+ if (!config.active)
+ return
+ console.log("OKWebhook received push")
+ var event = req.headers['x-github-event']
+ if (event !== "push") {
+ return res.sendStatus(500)
+ }
+ var sig = req.headers['x-hub-signature'].split('=')[1]
+ var text = req.rawBody
+ var hash = crypto.createHmac('sha1', secret).update(text).digest('hex')
+ if (hash !== sig) {
+ return res.sendStatus(500)
+ }
+ res.sendStatus(200)
+ var cwd = path.dirname(command)
+ exec(command, { cwd: cwd }, function(err, stdout, stderr){
+ // may not fire if process was restarted..
+ console.log(process.env)
+ console.log(stdout)
+ })
+ })
+
+ function getBody (req, res, next) {
+ req.rawBody = ''
+ // req.setEncoding('utf8')
+
+ req.on('data', function(chunk) {
+ req.rawBody += chunk
+ })
+
+ req.on('end', function() {
+ try {
+ req.body = JSON.parse(req.rawBody)
+ } catch (e) {
+ return res.sendStatus(500)
+ }
+ next()
+ })
+ }
+
+ this._router = router
+}
+
+OKWebhook.prototype.middleware = function () {
+ return this._router
+}
+
+module.exports = OKWebhook
diff --git a/app/node_modules/okservices/okwebhook/package.json b/app/node_modules/okservices/okwebhook/package.json
new file mode 100644
index 0000000..0436f01
--- /dev/null
+++ b/app/node_modules/okservices/okwebhook/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "okwebhook",
+ "version": "1.0.0",
+ "description": "webhook to receive pushes from github",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "okfocus <frontdesk@okfoc.us>",
+ "license": "LNT"
+}
diff --git a/app/node_modules/okservices/package.json b/app/node_modules/okservices/package.json
index f27247b..6231499 100644
--- a/app/node_modules/okservices/package.json
+++ b/app/node_modules/okservices/package.json
@@ -4,12 +4,8 @@
"description": "providing very good services",
"main": "index.js",
"scripts": {
+ "postinstall": "./install.sh",
"test": "echo \"Error: no test specified\" && exit 1"
},
- "author": "OKFocus",
- "license": "None",
- "dependencies": {
- "skipper": "^0.5.5",
- "skipper-s3": "^0.5.5"
- }
+ "author": "OKFocus"
}
diff --git a/app/node_modules/oktemplate/index.js b/app/node_modules/oktemplate/index.js
index a37f78e..ae9d1b1 100644
--- a/app/node_modules/oktemplate/index.js
+++ b/app/node_modules/oktemplate/index.js
@@ -4,6 +4,7 @@ var path = require('path');
var glob = require('glob');
var stringify = require('json-to-html');
var Liquid = require('liquid-node');
+var chokidar = require('chokidar');
/**
* Define any custom liquid filters here.
@@ -14,13 +15,24 @@ var filters = {
* Return a string formatted version of a JSON object.
* Useful for quick debugging of template data.
*/
- stringify: function(obj) {
+ prettify: function(obj) {
try {
return '<pre>' + stringify(obj) + '</pre>';
} catch (e) {
+ return 'Error prettifying';
+ }
+ },
+
+ /**
+ * Serialize Javascript objects into a JSON string
+ */
+ stringify: function(obj) {
+ try {
+ return JSON.stringify(obj);
+ } catch (e) {
return 'Error stringifying';
}
- }
+ },
};
@@ -29,19 +41,56 @@ var filters = {
*/
function OKTemplateRepo(options) {
options = options || {};
+ var self = this;
var root = this._root = options.root || 'templates';
- var ext = 'liquid';
+ // TODO Support more templates?
+ var ext = this._ext = 'liquid';
var cache = this._cache = {};
var engine = this._engine = new Liquid.Engine;
+ var debug = options.debug;
+ var globString = this._globString = root + '/*.' + ext
engine.registerFilters(filters);
engine.fileSystem = new Liquid.LocalFileSystem(root, ext);
+
this._populateCache(engine, cache, ext);
+
+ if (debug) {
+ var watcher = chokidar.watch(globString);
+ watcher.on('change', reloadTemplate);
+ watcher.on('add', reloadTemplate);
+ }
+
+ function reloadTemplate(path) {
+ self._loadTemplate(path);
+ }
}
OKTemplateRepo.prototype.getTemplate = function getTemplate(name) {
return this._cache[name];
}
+OKTemplateRepo.prototype._loadTemplate = function loadTemplate(filePath) {
+ var engine = this._engine
+ var name = path.basename(filePath, '.' + this._ext);
+ var templateString = fs.readFileSync(filePath, {encoding: 'UTF8'});
+ if (!this._cache[name])
+ this._cache[name] = {};
+
+ var template = this._cache[name]
+ template.name = name;
+ template.templateString = templateString;
+ template.render = render
+
+ function render(data) {
+ return Q.promise(function(resolve, reject) {
+ // TODO Not sure if this caches parsed templates behind the scenes?
+ engine.parseAndRender(templateString, data)
+ .then(resolve)
+ .catch(reject);
+ });
+ }
+}
+
/**
* Go through our template dir and read the template files
* into memory as strings.
@@ -49,22 +98,9 @@ OKTemplateRepo.prototype.getTemplate = function getTemplate(name) {
*/
OKTemplateRepo.prototype._populateCache = function _populateCache(engine, cache, ext) {
var self = this;
- var files = glob.sync(this._root + '/*.' + ext);
- files.forEach(function eachFile(file) {
- var name = path.basename(file, '.' + ext);
- var templateString = fs.readFileSync(file, {encoding: 'UTF8'});
- cache[name] = {
- name: name,
- templateString: templateString,
- render: function(data) {
- return Q.promise(function(resolve, reject) {
- // TODO Not sure if this caches parsed templates behind the scenes?
- engine.parseAndRender(templateString, data)
- .then(resolve)
- .catch(reject);
- });
- }
- }
+ var files = glob.sync(this._globString);
+ files.forEach(function eachFile(path) {
+ self._loadTemplate(path);
});
}
diff --git a/app/node_modules/oktemplate/package.json b/app/node_modules/oktemplate/package.json
index 70e94e3..61d90e8 100644
--- a/app/node_modules/oktemplate/package.json
+++ b/app/node_modules/oktemplate/package.json
@@ -10,6 +10,7 @@
"license": "None",
"dependencies": {
"bluebird": "^2.9.21",
+ "chokidar": "^1.0.3",
"glob": "^5.0.3",
"json-to-html": "^0.1.2",
"liquid-node": "^2.5.0",
diff --git a/app/node_modules/okview/index.js b/app/node_modules/okview/index.js
index 6eebe6e..5f99d59 100644
--- a/app/node_modules/okview/index.js
+++ b/app/node_modules/okview/index.js
@@ -21,11 +21,14 @@ function OKView(options) {
if (!options.template)
throw new Error('No template provided to OKView.');
if (!options.meta)
- throw new Error('No meta resource provided to OKView');
+ 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 || [];
@@ -47,16 +50,9 @@ function OKView(options) {
enumerable: true
});
- this._middleware = createMiddleware(this);
- this._fetchTemplateData = unbound ? fetchUnbound : fetchBound;
-
- function fetchUnbound(id) {
- return fetchTemplateData(meta, queries, id)
- }
-
- function fetchBound() {
- return fetchTemplateData(meta, queries);
- }
+ this._middleware = unbound
+ ? unboundMiddleware(this, meta, queries, error)
+ : boundMiddleware(this, meta, queries, error);
}
OKView.prototype.middleware = function() {
@@ -66,39 +62,20 @@ OKView.prototype.middleware = function() {
OKView.prototype.render = function(req, res, data) {
this._template.render(data).then(function(html) {
res.send(html);
- }).fail(errorHandler(req, res, data));
-};
-
-OKView.prototype.fetchTemplateData = function() {
- return this._fetchTemplateData.apply(this, arguments);
+ }).fail(this._error(req, res, 500));
};
/**
- * Unbound views need different middleware to resolve requests
- */
-function createMiddleware(view) {
- if (view.unbound) {
- return unboundMiddleware(view);
- } else {
- return boundMiddleware(view);
- }
-}
-
-// Note that these middleware do not call next
-// and should thus always be added at the end of the
-// middleware chain.
-
-/**
* Creates middleware for a view which does not
* yet have a resource id associated with it
*/
-function unboundMiddleware(view) {
+function unboundMiddleware(view, meta, queries, error) {
var paramName = getParamName(view.route);
return function(req, res, next) {
var id = req.params[paramName];
- view.fetchTemplateData(id).then(function(data) {
+ fetchTemplateData(meta, queries, id).then(function(data) {
view.render(req, res, data);
- }).fail(errorHandler(req, res, next));
+ }).fail(failHandler(req, res, error));
};
}
@@ -106,20 +83,22 @@ function unboundMiddleware(view) {
* Creates middleware for a view which already
* has a resource id associated with it
*/
-function boundMiddleware(view) {
+function boundMiddleware(view, meta, queries, error) {
return function(req, res, next) {
- view.fetchTemplateData().then(function(data) {
+ fetchTemplateData(meta, queries).then(function(data) {
view.render(req, res, data);
- }).fail(errorHandler(req, res, next));
+ }).fail(failHandler(req, res, error));
};
}
-/**
- * TODO BS error handling for now
- */
-function errorHandler(req, res, next) {
- return function(err) {
- res.send(err.stack);
+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);
+ }
}
}
@@ -138,49 +117,56 @@ function getParamName(route) {
* and returns a promise for an object merging all queried
* data, pluralizing keys where necessary.
*
- * Lil bit convoluted, sorry.
+ * 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(
- [meta.get()].concat(queries.map(function(query) {
- return query.get(id);
- })))
- .then(function(results) {
- var metadata = results.shift();
- var normalized = results.reduce(function(cache, result, i) {
- // Could be just some rogue request
- if (!result) {
- return cache;
- }
- var resource = queries[i].resource;
- var type = queries[i].type;
- var manyResult = isarray(result);
- // Inform template of ID in generic field
- if (manyResult) {
- result = result.map(function(data) {
- return assign({}, data, {id: data[resource.idField]})
- });
- } else {
- result = assign({}, result, {id: result[resource.idField]});
- }
- // 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) {
- var plural = pluralize(type);
- delete cache[type];
- cache[plural] = [];
- if (manyResult) {
- cache[plural] = cache[plural].concat(result);
+ 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[plural].push(result);
+ cache[type] = result;
}
- } else {
- cache[type] = result;
- }
- return cache;
- }, {meta: metadata});
- resolve(normalized);
+ return cache;
+ }, {meta: meta});
+ resolve(normalized);
+ }
}).fail(reject);
});
}