diff options
| author | Jules Laplace <jules@okfoc.us> | 2015-04-08 19:56:21 -0400 |
|---|---|---|
| committer | Jules Laplace <jules@okfoc.us> | 2015-04-08 19:56:21 -0400 |
| commit | fe1ea91f73059c146369ccb2404fcaf6d03d2f4d (patch) | |
| tree | 9b8c5e37821e147048e5d1ddbe5435af47716949 /app | |
| parent | 0c6d7edc0042464c8f05d38e6933186719dd259e (diff) | |
| parent | 32f87fcb58466e0e311349f96751c18e2a2cd7ea (diff) | |
k
Diffstat (limited to 'app')
| -rw-r--r-- | app/index.js | 139 | ||||
| -rw-r--r-- | app/node_modules/okadminview/index.js | 320 | ||||
| -rw-r--r-- | app/node_modules/okadminview/package.json | 17 | ||||
| -rw-r--r-- | app/node_modules/okdb/index.js | 9 | ||||
| -rw-r--r-- | app/node_modules/okquery/index.js | 26 | ||||
| -rw-r--r-- | app/node_modules/okresource/index.js | 209 | ||||
| -rw-r--r-- | app/node_modules/okresource/package.json | 1 | ||||
| -rw-r--r-- | app/node_modules/okrest/index.js | 20 | ||||
| -rw-r--r-- | app/node_modules/okrest/package.json | 11 | ||||
| -rw-r--r-- | app/node_modules/okserver/index.js | 7 | ||||
| -rw-r--r-- | app/node_modules/oktemplate/index.js | 7 | ||||
| -rw-r--r-- | app/node_modules/oktemplate/package.json | 3 | ||||
| -rw-r--r-- | app/node_modules/okutil/index.js | 29 | ||||
| -rw-r--r-- | app/node_modules/okview/index.js | 90 | ||||
| -rw-r--r-- | app/node_modules/okview/package.json | 7 |
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" + } } |
