diff options
| -rw-r--r-- | app/index.js | 56 | ||||
| -rw-r--r-- | app/node_modules/okquery/index.js | 26 | ||||
| -rw-r--r-- | app/node_modules/okresource/index.js | 178 | ||||
| -rw-r--r-- | app/node_modules/okresource/package.json | 1 |
4 files changed, 230 insertions, 31 deletions
diff --git a/app/index.js b/app/index.js index c8dceb5..ca09079 100644 --- a/app/index.js +++ b/app/index.js @@ -53,12 +53,13 @@ function OKCMS(options) { } }; 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 server = this._server = new OKServer({ express: express, app: app, @@ -85,22 +86,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, @@ -150,7 +153,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); if (!resource) throw new Error('Query configured with nonexistent resource'); // Default to "select all" query @@ -162,6 +165,35 @@ 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) { 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..0e99883 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'); /** @@ -31,15 +32,10 @@ function OKResource(options) { 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, reject); } }); }; @@ -145,14 +151,14 @@ OKResource.prototype.updateOrCreate = function(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('Cannot updateOrCreate without ID')); } else { - db.get(type, data.id).then(function(cached) { - var query = {}; - query[idField] = id; - if (cached) + db.get(type, query).then(function(persisted) { + if (persisted) db.put(type, query, data).then(resolve, reject); else db.create(type, data).then(resolve, reject); @@ -161,4 +167,152 @@ OKResource.prototype.updateOrCreate = function(data) { }); }; +/** + * 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(data).then(resolve).fail(reject); + } + }); + }; + + this.updateOrCreate = function(id) { + 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" } } |
