summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSean Fridman <fridman@mail.sfsu.edu>2015-04-08 14:06:30 -0400
committerSean Fridman <fridman@mail.sfsu.edu>2015-04-08 14:06:39 -0400
commit0fc380788157779a39fe630b2464280eea9f5088 (patch)
treef4f2f88c3bd098dd8ec083e56ec64f5340e2c445
parentb91c88be52170a7c9a8b133ef72052bec8caccba (diff)
Add resource instance concept
Can now define resource classes bound to particular data points, in particular they can be static data defined by app config and not even in DB
-rw-r--r--app/index.js56
-rw-r--r--app/node_modules/okquery/index.js26
-rw-r--r--app/node_modules/okresource/index.js178
-rw-r--r--app/node_modules/okresource/package.json1
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"
}
}