summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorJules Laplace <jules@okfoc.us>2015-04-08 19:56:21 -0400
committerJules Laplace <jules@okfoc.us>2015-04-08 19:56:21 -0400
commitfe1ea91f73059c146369ccb2404fcaf6d03d2f4d (patch)
tree9b8c5e37821e147048e5d1ddbe5435af47716949 /app
parent0c6d7edc0042464c8f05d38e6933186719dd259e (diff)
parent32f87fcb58466e0e311349f96751c18e2a2cd7ea (diff)
k
Diffstat (limited to 'app')
-rw-r--r--app/index.js139
-rw-r--r--app/node_modules/okadminview/index.js320
-rw-r--r--app/node_modules/okadminview/package.json17
-rw-r--r--app/node_modules/okdb/index.js9
-rw-r--r--app/node_modules/okquery/index.js26
-rw-r--r--app/node_modules/okresource/index.js209
-rw-r--r--app/node_modules/okresource/package.json1
-rw-r--r--app/node_modules/okrest/index.js20
-rw-r--r--app/node_modules/okrest/package.json11
-rw-r--r--app/node_modules/okserver/index.js7
-rw-r--r--app/node_modules/oktemplate/index.js7
-rw-r--r--app/node_modules/oktemplate/package.json3
-rw-r--r--app/node_modules/okutil/index.js29
-rw-r--r--app/node_modules/okview/index.js90
-rw-r--r--app/node_modules/okview/package.json7
15 files changed, 753 insertions, 142 deletions
diff --git a/app/index.js b/app/index.js
index c8dceb5..4ec8730 100644
--- a/app/index.js
+++ b/app/index.js
@@ -4,13 +4,14 @@ var withTrailingSlash = require('okutil').withTrailingSlash;
var withoutTrailingSlash = require('okutil').withoutTrailingSlash;
var assign = require('object-assign');
var express = require('express');
+var Q = require('q');
var OKQuery = require('okquery');
var OKView = require('okview');
+var OKAdminView = require('okadminview');
var OKDB = require('okdb');
var OKResource = require('okresource')
var OKTemplate = require('oktemplate');
var OKServer = require('okserver');
-var OKRestEndpoint = require('okrest');
var OKSchema = require('okschema');
/**
@@ -33,6 +34,34 @@ function OKCMS(options) {
var adminTemplateRoot = options.templateRoot ||
path.join(__dirname, '../themes/okadmin/templates');
+ // Set metadata defaults
+ // TODO Abstract this out somewhere else
+ var meta = {
+ type: 'meta',
+ get: function() {
+ return Q.promise(function(resolve, reject) {
+ db.getMeta().then(function(metadata) {
+ resolve(assign({}, {
+ static: ''
+ }, metadata));
+ }).fail(reject);
+ });
+ }
+ };
+
+ var adminMeta ={
+ type: 'meta',
+ get: function() {
+ return Q.promise(function(resolve, reject) {
+ db.getMeta().then(function(metadata) {
+ resolve(assign({}, {
+ static: withoutTrailingSlash(adminPath)
+ }, metadata));
+ }).fail(reject);
+ });
+ }
+ };
+
var schemaConfig = options.schemas || {};
var resourceConfig = options.resources || [];
var viewConfig = options.views || {
@@ -45,24 +74,23 @@ function OKCMS(options) {
new OKTemplate({root: adminTemplateRoot});
var db = new OKDB(options.db || 'fs');
- // Special query to get project wide meta data
- var meta = this._meta = {
- type: 'meta',
- get: function() {
- return db.getMeta();
- }
- };
var schemas = this._schemas = this._createSchemas(schemaConfig);
- var resources = this._resources =
+ var resourceCache = this._resourceCache =
this._createResources(resourceConfig, db, schemas);
// Create view instances from config
var views = this._views =
- this._createViews(viewConfig, db, meta, resources, templateProvider);
+ this._createViews(viewConfig, db, meta, resourceCache, templateProvider);
+ var adminViews = this._adminViews =
+ this._createAdminViews(adminPath, app, express, resourceConfig,
+ resourceCache, adminTemplateProvider, adminMeta);
+
var server = this._server = new OKServer({
express: express,
app: app,
- views: views,
+ // Merge admin views with normal views
+ views: assign(views, adminViews),
+ // Specify root folders and paths for serving static assets
root: root,
adminRoot: adminRoot,
adminPath: adminPath
@@ -85,22 +113,24 @@ OKCMS.prototype._createSchemas = function(schemaConfig) {
OKCMS.prototype._createResources = function(resourceConfig, db, schemaCache) {
resourceConfig = resourceConfig || {};
- var typeCache = {};
- return resourceConfig.reduce(function(cache, config) {
+ var resources = resourceConfig.map(function(config) {
var type = config.type;
var schema = schemaCache[type];
if (!schema)
throw new Error('Resource config references nonexistent schema');
- // If we already created resource class, just skip
- if (cache[type])
- return cache;
- cache[type] = OKResource({
+ var resource = OKResource({
type: type,
db: db,
schema: schema
- })
- return cache;
- }, {});
+ });
+ // Static resources have some data defined by configuration and
+ // are a special case
+ if (config.static) {
+ resource = resource.instance({static: config.static});
+ }
+ return resource;
+ });
+ return ResourceCache(resources);
};
OKCMS.prototype._createViews = function(viewConfig, db,
@@ -142,7 +172,37 @@ OKCMS.prototype._createViews = function(viewConfig, db,
else
return '404';
}
-}
+};
+
+OKCMS.prototype._createAdminViews = function(path, app, express,
+ resourceConfig, resourceCache, templateProvider, meta) {
+ var views = {};
+ var withTrail = withTrailingSlash(path);
+ var withoutTrail = withoutTrailingSlash(path);
+ // Stoopid fix for a bug in Express. Need to do this
+ // to ensure strict routing is not broken for the nested
+ // admin router.
+ // See: https://github.com/strongloop/express/issues/2281
+ // TODO Get rid of this crap
+ views[withoutTrail] = {
+ mount: 'get',
+ middleware: function() {
+ return function(req, res) {
+ res.redirect(301, withTrail);
+ }
+ }
+ };
+ // Add real view at trailing slash route
+ views[withTrail] = OKAdminView({
+ app: app,
+ express: express,
+ resourceConfig: resourceConfig,
+ resourceCache: resourceCache,
+ templateProvider: templateProvider,
+ meta: meta
+ });
+ return views;
+};
OKCMS.prototype._createQueries = function(queryConfig, resourceCache) {
queryConfig = queryConfig || {};
@@ -150,7 +210,7 @@ OKCMS.prototype._createQueries = function(queryConfig, resourceCache) {
queryConfig = [queryConfig];
return queryConfig.map(function(config) {
var type = config.type;
- var resource = resourceCache[type];
+ var resource = resourceCache.get(type, config.query);
if (!resource)
throw new Error('Query configured with nonexistent resource');
// Default to "select all" query
@@ -162,16 +222,43 @@ OKCMS.prototype._createQueries = function(queryConfig, resourceCache) {
});
};
+/**
+ * Stupid lil cache to help deal with the fact that
+ * resources can be indexed by either type or a type + id combo.
+ */
+function ResourceCache(resources) {
+ if (!(this instanceof ResourceCache)) return new ResourceCache(resources);
+ resources = resources || [];
+ var cache = this._cache = {};
+ resources.forEach(function(resource) {
+ if (!resource)
+ throw new Error('Undefined resource given to ResourceCache');
+ if (resource.bound) {
+ cache[resource.type] = resource.parent;
+ cache[resource.type + ':' + resource.id] = resource;
+ } else {
+ cache[resource.type] = resource;
+ }
+ });
+}
+
+ResourceCache.prototype.get = function(type, id) {
+ if (!type) return;
+ if (id && this._cache[type + ':' + id]) {
+ return this._cache[type + ':' + id];
+ } else {
+ return this._cache[type];
+ }
+};
+
module.exports = {
createApp: function(options) {
- return new OKCMS(options);
+ return OKCMS(options);
},
OKResource: OKResource,
- OKView: OKView,
-
- OKRestEndpoint: OKRestEndpoint
+ OKView: OKView
};
diff --git a/app/node_modules/okadminview/index.js b/app/node_modules/okadminview/index.js
new file mode 100644
index 0000000..42e7f36
--- /dev/null
+++ b/app/node_modules/okadminview/index.js
@@ -0,0 +1,320 @@
+var bodyParser = require('body-parser');
+var methodOverride = require('method-override');
+var Q = require('q');
+var pluralize = require('pluralize');
+var OKQuery = require('okquery');
+
+/**
+ * OKAdminView!
+ */
+function OKAdminView(options) {
+ if (!(this instanceof OKAdminView)) return new OKAdminView(options);
+ if (!options.app)
+ throw new Error('No Express app provided to OKAdminView');
+ if (!options.express)
+ throw new Error('No Express provided to OKAdminView');
+ if (!options.resourceConfig)
+ throw new Error('No resourceConfig provided to OKAdminView');
+ if (!options.resourceCache)
+ throw new Error('No resourceCache provided to OKAdminView');
+ if (!options.templateProvider)
+ throw new Error('No templateProvider provided to OKAdminView');
+ if (!options.meta)
+ throw new Error('No meta query provided to OKAdminView');
+ var app = options.app;
+ var express = options.express;
+ var meta = options.meta;
+ var resourceCache = this._resourceCache = options.resourceCache;
+ var resourceConfig = this._resourceConfig = options.resourceConfig;
+ var provider = options.templateProvider;
+ // Load templates
+ var templates = this._templates =
+ ['index', 'resource', 'resource_new'].reduce(function(cache, name) {
+ var template = provider.getTemplate(name);
+ if (!template)
+ throw new Error('Admin theme does not have needed template: "' + name + '"');
+ cache[name] = template;
+ return cache;
+ }, {});
+ // OKAdmin middleware is a router, so mounts on 'use'
+ Object.defineProperty(this, 'mount', {
+ value: 'use',
+ writable: false,
+ enumerable: true
+ });
+
+ // Resources which apper on the index are a function of
+ // the resource configuration.
+ var indexQueries = this._indexQueries = Object.keys(resourceConfig)
+ .map(function(key) {
+ var config = resourceConfig[key];
+ var type = config.type;
+ var staticData = config.static || {};
+ var resource = resourceCache.get(config.type);
+ if (!resource)
+ throw new Error('Something weird is going on');
+ var id = staticData[resource.idField];
+ resource = resourceCache.get(type, id) || resource;
+ if (resource.bound) {
+ // Resource instances implement the query API
+ return OKQuery({resource: resource});;
+ } else {
+ return OKQuery({resource: resource, query: config.query})
+ }
+ });
+
+ this._middleware = createMiddleware(this);
+
+ function createMiddleware(view) {
+ var router = express.Router({
+ strict: app.get('strict routing')
+ });
+
+ // Parse form data
+ router.use(bodyParser.urlencoded({extended: true}));
+ // HTML forms only support POST and GET methods
+ // We extend this by adding hidden input fields to the forms which
+ // specify the actual method desired.
+ router.use(methodOverride(function(req, res) {
+ // Parse out the hidden field
+ if (req.body && typeof req.body === 'object' && '_method' in req.body) {
+ var method = req.body._method;
+ delete req.body._method
+ return method
+ }
+ }));
+
+ router.get('/', function readIndex(req, res, next) {
+ fetchIndexTemplateData(meta, indexQueries).then(function(data) {
+ view.renderIndex(req, res, data);
+ }).fail(errorHandler(req, res));
+ });
+
+ router.get('/:type/new/', function createResourceView(req, res, next) {
+ var type = req.params.type || '';
+ var resource = resourceCache.get(type);
+ if (!resource) {
+ errorHandler(req, res)(new Error('No such resource ' + type));
+ } else {
+ meta.get().then(function(metadata) {
+ view.renderResourceNew(req, res, {
+ meta: metadata,
+ resource: {
+ type: resource.type,
+ spec: resource.spec
+ }
+ });
+ }).fail(errorHandler(req, res));
+ }
+ });
+
+
+ router.get('/:type/:id/', function readResource(req, res, next) {
+ var type = req.params.type || '';
+ var id = req.params.id || '';
+ var resource = resourceCache.get(type, id);
+ if (!resource) {
+ errorHandler(req, res)(new Error('No such resource'));
+ } else {
+ var query = OKQuery({
+ resource: resource,
+ query: id
+ });
+ fetchResourceTemplateData(meta, query, transform).then(function(data) {
+ if (!data) {
+ resourceMissingHandler(req, res)()
+ } else {
+ view.renderResource(req, res, data);
+ }
+ }).fail(errorHandler(req, res));
+ }
+
+ function transform(meta, resource, data) {
+ meta = meta || {};
+ resource = resource || {};
+ data = data || {};
+ var spec = Object.keys(resource.spec).reduce(function(cache, prop) {
+ var propSpec = resource.spec[prop];
+ var value = data[prop];
+ cache[prop].id = data[resource.idField];
+ cache[prop].value = value;
+ return cache;
+ }, resource.spec);
+ return {
+ meta: meta,
+ resource: {
+ type: resource.type,
+ spec: spec
+ }
+ };
+ }
+ });
+
+ router.post('/:type/', function createResource(req, res, next) {
+ var type = req.params.type;
+ var resource = resourceCache.get(type);
+ var data = req.body;
+ if (!resource) {
+ errorHandler(req, res)(new Error('No such resource ' + type));
+ } else {
+ meta.get().then(function(metadata) {
+ var templateData = {
+ meta: metadata,
+ resource: {
+ type: resource.type,
+ spec: resource.spec,
+ data: data
+ }
+ };
+ try {
+ resource.assertValid(data);
+ resource.create(data).then(function(created) {
+ res.redirect(303, data[resource.idField]);
+ }).fail(errorHandler(req, res));
+ } catch (errors) {
+ view.renderResource(req, res, templateData);
+ }
+ }).fail(errorHandler(req, res));;
+ }
+ });
+
+ router.put('/:type/:id/', function updateResource(req, res, next) {
+ var type = req.params.type;
+ var id = req.params.id;
+ var data = req.body;
+ var resource = resourceCache.get(type, id);
+ if (!resource) {
+ errorHandler(req, res)(new Error('No such resource ' + type));
+ } else {
+ // TODO Maybe should make metadata synchronous...
+ meta.get().then(function(metadata) {
+ var templateData = {
+ meta: metadata,
+ resource: {
+ spec: resource.spec,
+ data: data
+ }
+ };
+ try {
+ resource.assertValid(data);
+ resource.update(id, data).then(function(updated) {
+ res.redirect(303, '../' + updated[resource.idField]);
+ }).fail(errorHandler(req, res));
+ } catch (errors) {
+ view.renderResource(req, res, templateData);
+ }
+ }).fail(errorHandler(req, res));
+ }
+ });
+
+ return router;
+ }
+}
+
+OKAdminView.prototype.middleware = function() {
+ return this._middleware;
+};
+
+OKAdminView.prototype.renderIndex = function(req, res, data) {
+ data = data || {};
+ this._templates['index'].render(data).then(function(rendered) {
+ res.send(rendered);
+ }).fail(errorHandler(req, res));
+};
+
+OKAdminView.prototype.renderResource = function(req, res, data) {
+ data = data || {};
+ this._templates['resource'].render(data).then(function(rendered) {
+ res.send(rendered);
+ }).fail(errorHandler(req, res));
+};
+
+OKAdminView.prototype.renderResourceNew = function(req, res, data) {
+ data = data || {meta: {}, resource: {}};
+ this._templates['resource_new'].render(data).then(function(rendered) {
+ res.send(rendered);
+ }).fail(errorHandler(req, res));
+};
+
+/**
+ * Annotate template data with schema info
+ */
+function fetchIndexTemplateData(meta, queries) {
+ return Q.Promise(function(resolve, reject) {
+ Q.all([meta.get()].concat(queries.map(function(query) {
+ return query.get();
+ }))).then(function(results) {
+ var meta = results.shift();
+ var resources = results.reduce(function(cache, result, i) {
+ if (!result)
+ return cache;
+ var resource = queries[i].resource;
+ // We want the raw object spec
+ var spec = resource.spec;
+ var key = pluralize(resource.type);
+ if (!cache[key]) {
+ cache[key] = {
+ type: resource.type,
+ spec: spec,
+ data: []
+ };
+ }
+
+ if (result.length) {
+ result.forEach(addToCache)
+ } else {
+ addToCache(result);
+ }
+
+ function addToCache(data) {
+ // Report id to template under standard name
+ data.id = data[resource.idField];
+ cache[key].data.push(data);
+ }
+
+ return cache;
+ }, {});
+
+ resolve({
+ meta: meta,
+ resources: resources
+ });
+ }).fail(reject);
+ });
+}
+
+/**
+ * Annotate template data with schema info
+ */
+function fetchResourceTemplateData(meta, query, fn) {
+ fn = fn || function(m, r, d) { return {meta: m, resource: d}; };
+ return Q.Promise(function(resolve, reject) {
+ meta.get().then(function(metadata) {
+ query.get().then(function(data) {
+ var resource = query.resource;
+ resolve(fn(metadata, resource, data));
+ }).fail(reject);
+ }).fail(reject)
+ });
+}
+
+/**
+ * TODO Real error handling
+ */
+function errorHandler(req, res) {
+ return function(err) {
+ res.send(err.stack);
+ };
+}
+
+/**
+ * TODO Real 404 handling
+ */
+function resourceMissingHandler(req, res) {
+ return function() {
+ res.status(404);
+ res.send('404');
+ }
+}
+
+module.exports = OKAdminView;
diff --git a/app/node_modules/okadminview/package.json b/app/node_modules/okadminview/package.json
new file mode 100644
index 0000000..b07cb1a
--- /dev/null
+++ b/app/node_modules/okadminview/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "okadminview",
+ "version": "1.0.0",
+ "description": "administrate!",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "OKFocus",
+ "license": "None",
+ "dependencies": {
+ "body-parser": "^1.12.2",
+ "method-override": "^2.3.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 3089411..5368e4a 100644
--- a/app/node_modules/okdb/index.js
+++ b/app/node_modules/okdb/index.js
@@ -72,8 +72,13 @@ FSDB.prototype.put = function(collection, query, data) {
}
};
-FSDB.prototype.create = function(collection, id, data) {
- throw new Error('Not implemented!');
+FSDB.prototype.create = function(collection, data) {
+ var created = this._db(collection)
+ .chain()
+ .push(data)
+ .last()
+ .value();
+ return this._resolve(created);
};
FSDB.prototype.remove = function(collection, id, data) {
diff --git a/app/node_modules/okquery/index.js b/app/node_modules/okquery/index.js
index a64f0f2..9748067 100644
--- a/app/node_modules/okquery/index.js
+++ b/app/node_modules/okquery/index.js
@@ -14,14 +14,19 @@ function OKQuery(options) {
var resource = options.resource;
var type = resource.type;
var query = options.query || '*';
+
Object.defineProperty(this, 'resource', {
value: resource,
- writable: false
+ writable: false,
+ enumerable: true
});
+
Object.defineProperty(this, 'type', {
value: resource.type,
- writable: false
+ writable: false,
+ enumerable: true
});
+
this.get = createQuery(resource, query, {
default: options.default
});
@@ -29,8 +34,9 @@ function OKQuery(options) {
function createQuery(resource, query, options) {
options = options || {};
- var query;
- if (isDynamic(query)) {
+ if (resource.bound) {
+ query = queryBound(resource);
+ } else if (isDynamic(query)) {
query = queryDynamic(resource);
} else if (isSet(query)) {
query = queryAll(resource);
@@ -46,19 +52,25 @@ function createQuery(resource, query, options) {
function queryDynamic(resource) {
return function(id) {
return resource.get(id);
- }
+ };
}
function queryAll(resource) {
return function() {
return resource.all();
- }
+ };
}
function querySingle(resource, id) {
return function() {
return resource.get(id);
- }
+ };
+}
+
+function queryBound(resource) {
+ return function() {
+ return resource.get();
+ };
}
function withDefault(queryFn, resultDefault) {
diff --git a/app/node_modules/okresource/index.js b/app/node_modules/okresource/index.js
index 7cd51c7..80279dc 100644
--- a/app/node_modules/okresource/index.js
+++ b/app/node_modules/okresource/index.js
@@ -1,3 +1,4 @@
+var assign = require('object-assign');
var Q = require('q');
/**
@@ -25,21 +26,16 @@ function OKResource(options) {
idField = prop;
return idField;
// If schema has a prop called 'id', default to that one
- }, schema.id && 'id');
+ }, schema.spec.id && 'id');
if (!idField)
throw new Error('Bad schema: no ID field');
var type = options.type;
this._db = options.db;
+ this._schema = schema;
// Define properties which are part of the API
- Object.defineProperty(this, 'schema', {
- value: schema,
- writable: false,
- enumerable: true
- });
-
Object.defineProperty(this, 'spec', {
value: schema.spec,
writable: false,
@@ -57,13 +53,21 @@ function OKResource(options) {
writable: false,
enumerable: true
});
+
+ // Whether this resource represents a specific data point
+ // or a whole class of data
+ Object.defineProperty(this, 'bound', {
+ value: false,
+ writable: false,
+ enumerable: true
+ });
}
/**
* Throws an error if data does not conform to schema
*/
OKResource.prototype.assertValid = function(data) {
- this.schema.assertValid(data);
+ this._schema.assertValid(data);
};
OKResource.prototype.all = function() {
@@ -72,12 +76,14 @@ OKResource.prototype.all = function() {
OKResource.prototype.create = function(data) {
data = 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'));
} else {
- this._db.create(this.type, data).then(resolve, reject);
+ db.create(type, data).then(resolve).fail(reject);
}
});
};
@@ -89,7 +95,7 @@ OKResource.prototype.destroy = function(data) {
if (!id) {
reject(new Error('Data does not contain ID property'));
} else {
- this._db.remove(this.type, data.id, data).then(resolve, reject);
+ this._db.remove(this.type, data.id, data).then(resolve).fail(reject);
}
});
};
@@ -99,7 +105,7 @@ OKResource.prototype.find = function(query) {
if (!query) {
throw new Error('No query given');
} else {
- this._db.find(this.type, query).then(resolve, reject);
+ this._db.find(this.type, query).then(resolve).fail(reject);
}
});
};
@@ -117,48 +123,195 @@ OKResource.prototype.get = function(id) {
// to match
var query = {};
query[idField] = id;
- db.get(type, query).then(resolve, reject);
+ db.get(type, query).then(resolve).fail(reject);
}
});
};
-OKResource.prototype.update = function(data) {
+OKResource.prototype.update = function(id, data) {
data = data || {};
- var id = data[this.idField];
var db = this._db;
var type = this.type;
var idField = this.idField;
return Q.promise(function(resolve, reject) {
if (!id) {
- reject(new Error('Data does not contain ID property'));
+ reject(new Error('No resource ID provided'));
} else {
var query = {};
- query[idField] = data[idField];
- db.put(type, query, data).then(resolve, reject);;
+ query[idField] = id;
+ db.put(type, query, data).then(resolve).fail(reject);;
}
});
};
-OKResource.prototype.updateOrCreate = function(data) {
+OKResource.prototype.updateOrCreate = function(id, data) {
data = data || {};
- var id = data[this.idField];
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('Cannot updateOrCreate without ID'));
+ reject(new Error('No resource ID provided'));
} else {
- db.get(type, data.id).then(function(cached) {
- var query = {};
- query[idField] = id;
- if (cached)
- db.put(type, query, data).then(resolve, reject);
- else
- db.create(type, data).then(resolve, reject);
- }, reject);
+ db.get(type, query).then(function(persisted) {
+ if (persisted) {
+ db.put(type, query, data).then(resolve).fail(reject);
+ } else {
+ db.create(type, data).then(resolve).fail(reject);
+ }
+ }).fail(reject);
}
});
};
+/**
+ * Create special resource which is bound to particular data point
+ * and has custom validation and query properties.
+ */
+OKResource.prototype.instance = function(options) {
+ return new OKResourceInstance(this, {
+ static: options.static
+ });
+};
+
+function OKResourceInstance(resource, options) {
+ if (!(this instanceof OKResourceInstance)) return new OKResourceInstance(options);
+ // Only support static data instances for now
+ if (!options.static)
+ throw new Error(
+ 'Cannot create OKResourceInstance without static data');
+
+ // Resources with static data are a special case. They exist
+ // 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];
+ if (!id)
+ throw new Error(
+ 'Cannot create static OKResourceInstance without an ID field');
+
+ /**
+ * Ensure that static data is provided on get
+ */
+ this.get = function() {
+ return Q.promise(function(resolve, reject) {
+ resource.get(id).then(function(data) {
+ // Note the assign call. Don't expose private references!
+ if (data) {
+ resolve(assign({}, data, staticData));
+ } else {
+ resolve(assign({}, staticData));
+ }
+ }).fail(reject);
+ });
+ };
+
+ this.update = function(data) {
+ return Q.promise(function(resolve, reject) {
+ var valid = Object.keys(staticData).every(function(prop) {
+ return staticData[prop] === data[prop];
+ });
+ if (!valid) {
+ reject(new Error('Cannot update resource\'s static data'));
+ } else {
+ // When updating static resources, we create them if
+ // they don't actually exist in the DB
+ resource.updateOrCreate(id, data).then(resolve).fail(reject);
+ }
+ });
+ };
+
+ this.updateOrCreate = function(data) {
+ return Q.promise(function(resolve, reject) {
+ reject(new Error('Cannot updateOrCreate static resource'));
+ });
+ };
+
+ this.destroy = function(id) {
+ return Q.promise(function(resolve, reject) {
+ reject(new Error('Cannot destroy static resource'));
+ });
+ };
+
+ this.all = function(id) {
+ return Q.promise(function(resolve, reject) {
+ reject(new Error('Cannot get all for static resource'));
+ });
+ };
+
+ this.create = function(id) {
+ return Q.promise(function(resolve, reject) {
+ reject(new Error('Cannot create static resource'));
+ });
+ };
+
+ this.find = function(id) {
+ return Q.promise(function(resolve, reject) {
+ reject(new Error('Cannot perform find on static resource'));
+ });
+ };
+
+ this.assertValid = function(data) {
+ data = data || {};
+ Object.keys(staticData).forEach(function(prop) {
+ if (staticData[prop] !== data[prop]) {
+ // Validation error is in mschema error format
+ throw [{
+ property: prop,
+ constraint: 'static',
+ expected: staticData[prop],
+ actual: data[prop],
+ message: 'Data does not match static data'
+ }];
+ }
+ });
+ resource.assertValid(data);
+ };
+
+ Object.defineProperty(this, 'parent', {
+ value: resource,
+ writable: false,
+ enumerable: true
+ });
+
+ Object.defineProperty(this, 'spec', {
+ value: resource.spec,
+ writable: false,
+ enumerable: true
+ });
+
+ Object.defineProperty(this, 'id', {
+ value: id,
+ writable: false,
+ enumerable: true
+ });
+
+ Object.defineProperty(this, 'type', {
+ value: resource.type,
+ writable: false,
+ 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 da9dfe0..7f19c9b 100644
--- a/app/node_modules/okresource/package.json
+++ b/app/node_modules/okresource/package.json
@@ -9,6 +9,7 @@
"author": "OKFocus",
"license": "None",
"dependencies": {
+ "object-assign": "^2.0.0",
"q": "^1.2.0"
}
}
diff --git a/app/node_modules/okrest/index.js b/app/node_modules/okrest/index.js
deleted file mode 100644
index 169626d..0000000
--- a/app/node_modules/okrest/index.js
+++ /dev/null
@@ -1,20 +0,0 @@
-var OKView = require('okview');
-
-/**
- * OKRestEndpoint!
- * Takes a resources and creates a CRUD endpoint.
- */
-function OKRestEndpoint(resource, options) {
- if (!(this instanceof OKRestEndpoint)) return new OKRestEndpoint(resource, options);
- options = options || {};
- this._resource = resource;
-}
-
-OKRestEndpoint.prototype.middleware = function() {
- var self = this;
- return function handleREST(req, res, next) {
- res.send(self._resource.name);
- };
-}
-
-module.exports = OKRestEndpoint;
diff --git a/app/node_modules/okrest/package.json b/app/node_modules/okrest/package.json
deleted file mode 100644
index 462c890..0000000
--- a/app/node_modules/okrest/package.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "name": "okrest",
- "version": "1.0.0",
- "description": "nice",
- "main": "index.js",
- "scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
- },
- "author": "OKFocus",
- "license": "None"
-}
diff --git a/app/node_modules/okserver/index.js b/app/node_modules/okserver/index.js
index 952602d..e6145ee 100644
--- a/app/node_modules/okserver/index.js
+++ b/app/node_modules/okserver/index.js
@@ -45,9 +45,14 @@ function OKServer(options) {
* Order is important here! Requests go down the middleware
* chain until they are handled with a response, which could
* happen anywhere in the chain. Watch out for middleware shadowing
- * ither middleware.
+ * other middleware.
*/
+ // Intercept favicon requests and 404 for now
+ app.use('/favicon.ico', function(req, res) {
+ res.status(404)
+ return res.send('');
+ });
// Serve user static files
app.use(express.static(root));
// Serve admin interface static files
diff --git a/app/node_modules/oktemplate/index.js b/app/node_modules/oktemplate/index.js
index 700020c..dafe5e6 100644
--- a/app/node_modules/oktemplate/index.js
+++ b/app/node_modules/oktemplate/index.js
@@ -1,3 +1,4 @@
+var Q = require('q');
var fs = require('fs');
var path = require('path');
var glob = require('glob');
@@ -56,8 +57,12 @@ OKTemplateRepo.prototype._populateCache = function _populateCache(engine, cache,
name: name,
templateString: templateString,
render: function(data) {
+ return Q.promise(function(resolve, reject) {
// TODO Not sure if this caches parsed templates behind the scenes?
- return engine.parseAndRender(templateString, data);
+ engine.parseAndRender(templateString, data)
+ .then(resolve)
+ .catch(reject);
+ });
}
}
});
diff --git a/app/node_modules/oktemplate/package.json b/app/node_modules/oktemplate/package.json
index 3e92ccb..70e94e3 100644
--- a/app/node_modules/oktemplate/package.json
+++ b/app/node_modules/oktemplate/package.json
@@ -12,6 +12,7 @@
"bluebird": "^2.9.21",
"glob": "^5.0.3",
"json-to-html": "^0.1.2",
- "liquid-node": "^2.5.0"
+ "liquid-node": "^2.5.0",
+ "q": "^1.2.0"
}
}
diff --git a/app/node_modules/okutil/index.js b/app/node_modules/okutil/index.js
index 738c6a4..3142ae1 100644
--- a/app/node_modules/okutil/index.js
+++ b/app/node_modules/okutil/index.js
@@ -10,35 +10,6 @@ var Q = require('q');
module.exports = {
/**
- * Takes a meta data query and an array of resource queries
- * and returns a promise for an object merging all queried
- * data, pluralizing keys where necessary.
- *
- * Lil bit convoluted, sorry.
- */
- fetchTemplateData: function fetchTemplateData(meta, queries, options) {
- return Q.promise(function(resolve, reject) {
- return Q.all(
- [meta.get()].concat(queries.map(function(query) {
- return query.get(options);
- })))
- .then(function(results) {
- var metadata = results.shift();
- var normalized = results.reduce(function(data, result, i) {
- var type = queries[i].type;
- if (isarray(result)) {
- data[pluralize(type)] = result;
- } else {
- data[type] = result;
- }
- return data;
- }, {meta: metadata});
- resolve(normalized);
- }, reject);
- });
- },
-
- /**
* Return a copy of the route with a trailing slash
*/
withTrailingSlash: function withTrailingSlash(route) {
diff --git a/app/node_modules/okview/index.js b/app/node_modules/okview/index.js
index 1ceac03..c245b0c 100644
--- a/app/node_modules/okview/index.js
+++ b/app/node_modules/okview/index.js
@@ -1,4 +1,8 @@
-var fetchTemplateData = require('okutil').fetchTemplateData;
+var assign = require('object-assign');
+var pluralize = require('pluralize');
+var isarray = require('lodash.isarray');
+var Q = require('q');
+var OKQuery = require('okquery');
var OKResource = require('okresource');
// Routes for views over collections have a special pattern
@@ -15,11 +19,11 @@ function OKView(options) {
if (!(this instanceof OKView)) return new OKView(options);
options = options || {};
if (!options.template)
- throw new Error('No template provided to view.');
+ throw new Error('No template provided to OKView.');
if (!options.meta)
- throw new Error('No meta resource provided to view');
+ throw new Error('No meta resource provided to OKView');
if (!options.route)
- throw new Error('No route provided to view');
+ throw new Error('No route provided to OKView');
var route = options.route;
var mount = options.mount || 'get';
this._template = options.template;
@@ -29,28 +33,32 @@ function OKView(options) {
// resource will be resolved later
// TODO This bound / unbound thing can probably be expressed in a
// less convoluted way.
- var unbound = this.unbound = !!UNBOUND_ROUTE_PATTERN.exec(this.route);
+ var unbound = this.unbound = !!UNBOUND_ROUTE_PATTERN.exec(route);
+
Object.defineProperty(this, 'mount', {
value: mount,
writable: false,
enumerable: true
});
+
Object.defineProperty(this, 'route', {
value: route,
writable: false,
enumerable: true
});
+
this._middleware = createMiddleware(this);
- this._fetchTemplateData = unbound ?
- fetchResourceTemplateData : fetchCollectionTemplateData;
+ this._fetchTemplateData = unbound ? fetchUnbound : fetchBound;
- function fetchResourceTemplateData(id) {
- // Bound views only have a single query
- // TODO This is super convoluted
- return fetchTemplateData(meta, [queries[0].get(id)]);
+ function fetchUnbound(id) {
+ var resource = queries[0].resource;
+ return fetchTemplateData(meta, [OKQuery({
+ resource: resource,
+ query: id
+ })]);
}
- function fetchCollectionTemplateData() {
+ function fetchBound() {
return fetchTemplateData(meta, queries);
}
}
@@ -62,7 +70,7 @@ OKView.prototype.middleware = function() {
OKView.prototype.render = function(req, res, data) {
this._template.render(data).then(function(html) {
res.send(html);
- }, errorHandler(req, res, data));
+ }).fail(errorHandler(req, res, data));
};
OKView.prototype.fetchTemplateData = function() {
@@ -94,7 +102,7 @@ function unboundMiddleware(view) {
var id = req.params[paramName];
view.fetchTemplateData(id).then(function(data) {
view.render(req, res, data);
- }, errorHandler(req, res, next));
+ }).fail(errorHandler(req, res, next));
};
}
@@ -106,7 +114,7 @@ function boundMiddleware(view) {
return function(req, res, next) {
view.fetchTemplateData().then(function(data) {
view.render(req, res, data);
- }, errorHandler(req, res, next));
+ }).fail(errorHandler(req, res, next));
};
}
@@ -129,4 +137,56 @@ function getParamName(route) {
return matches[1];
}
+/**
+ * Takes a meta data query and an array of resource queries
+ * and returns a promise for an object merging all queried
+ * data, pluralizing keys where necessary.
+ *
+ * Lil bit convoluted, sorry.
+ */
+function fetchTemplateData(meta, queries) {
+ return Q.promise(function(resolve, reject) {
+ return Q.all(
+ [meta.get()].concat(queries.map(function(query) {
+ return query.get();
+ })))
+ .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);
+ } else {
+ cache[plural].push(result);
+ }
+ } else {
+ cache[type] = result;
+ }
+ return cache;
+ }, {meta: metadata});
+ resolve(normalized);
+ }).fail(reject);
+ });
+}
+
module.exports = OKView;
diff --git a/app/node_modules/okview/package.json b/app/node_modules/okview/package.json
index 8d95b40..bbf4e40 100644
--- a/app/node_modules/okview/package.json
+++ b/app/node_modules/okview/package.json
@@ -8,5 +8,10 @@
},
"author": "OKFocus",
"license": "None",
- "dependencies": {}
+ "dependencies": {
+ "lodash.isarray": "^3.0.1",
+ "object-assign": "^2.0.0",
+ "pluralize": "^1.1.2",
+ "q": "^1.2.0"
+ }
}