From ce294f2591279e1fc342b6c3da4a2e0c22c805a2 Mon Sep 17 00:00:00 2001 From: Sean Fridman Date: Thu, 9 Apr 2015 14:59:30 -0400 Subject: Don't expose object refs in public APIs ya dummy! Make sure to deep clone them input/output objects to maintain immutability y'hear --- app/node_modules/okresource/index.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) (limited to 'app/node_modules/okresource/index.js') diff --git a/app/node_modules/okresource/index.js b/app/node_modules/okresource/index.js index 94d8cfb..5a643de 100644 --- a/app/node_modules/okresource/index.js +++ b/app/node_modules/okresource/index.js @@ -1,4 +1,5 @@ var assign = require('object-assign'); +var cloneDeep = require('cloneDeep'); var Q = require('q'); /** @@ -18,15 +19,16 @@ function OKResource(options) { throw new Error('No DB provided to OKResource'); var schema = options.schema; + var spec = schema.spec; // Iterate through spec to find field which will act as the // resource id in da db. - var idField = Object.keys(schema.spec).reduce(function(idField, prop) { - var spec = schema.spec[prop]; - if (spec.id) + var idField = Object.keys(spec).reduce(function(idField, prop) { + var propSpec = spec[prop]; + if (propSpec.id) idField = prop; return idField; // If schema has a prop called 'id', default to that one - }, schema.spec.id && 'id'); + }, spec.id && 'id'); if (!idField) throw new Error('Bad schema: no ID field'); @@ -37,8 +39,9 @@ function OKResource(options) { // Define properties which are part of the API Object.defineProperty(this, 'spec', { - value: schema.spec, - writable: false, + get: function() { + return schema.spec; + }, enumerable: true }); @@ -190,7 +193,7 @@ function OKResourceInstance(resource, options) { // conceptually at all times since they are derived from app // configuration, but may not actually be present // in the database and need custom logic to handle this. - var staticData = assign({}, options.static); + var staticData = cloneDeep(options.static); var id = staticData[resource.idField]; if (!id) throw new Error( @@ -204,9 +207,9 @@ function OKResourceInstance(resource, options) { resource.get(id).then(function(data) { // Note the assign call. Don't expose private references! if (data) { - resolve(assign({}, data, staticData)); + resolve(assign({}, data, cloneDeep(staticData))); } else { - resolve(assign({}, staticData)); + resolve(assign({}, cloneDeep(staticData))); } }).fail(reject); }); -- cgit v1.2.3-70-g09d2 From d51924d7296af861ee2d9d95b8ded12708679a66 Mon Sep 17 00:00:00 2001 From: Sean Fridman Date: Thu, 9 Apr 2015 15:02:48 -0400 Subject: Oops. Resolve lodash.clonedeep correctly --- app/node_modules/okresource/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/node_modules/okresource/index.js') diff --git a/app/node_modules/okresource/index.js b/app/node_modules/okresource/index.js index 5a643de..febd1d5 100644 --- a/app/node_modules/okresource/index.js +++ b/app/node_modules/okresource/index.js @@ -1,5 +1,5 @@ var assign = require('object-assign'); -var cloneDeep = require('cloneDeep'); +var cloneDeep = require('lodash.clonedeep'); var Q = require('q'); /** -- cgit v1.2.3-70-g09d2 From c4d8ee7c431b3511bf26da68e952808b51d663c7 Mon Sep 17 00:00:00 2001 From: Sean Fridman Date: Thu, 9 Apr 2015 16:00:21 -0400 Subject: Add resource delete functionality to admin --- app/node_modules/okadminview/index.js | 15 +++++++++++++++ app/node_modules/okresource/index.js | 12 +++++++----- themes/okadmin/templates/resource.liquid | 4 ++++ 3 files changed, 26 insertions(+), 5 deletions(-) (limited to 'app/node_modules/okresource/index.js') diff --git a/app/node_modules/okadminview/index.js b/app/node_modules/okadminview/index.js index 987fe51..a376df5 100644 --- a/app/node_modules/okadminview/index.js +++ b/app/node_modules/okadminview/index.js @@ -175,6 +175,21 @@ function OKAdminView(options) { } }); + router.delete('/:type/:id/', function deleteResource(req, res, next) { + var type = req.params.type; + var id = req.params.id; + var resource = resourceCache.get(type, id); + if (!resource) { + errorHandler(req, res)(new Error('No such resource ' + type)); + } else { + meta.get().then(function(metadata) { + resource.destroy(id).then(function() { + res.redirect(303, '../..'); + }).fail(errorHandler(req, res)); + }).fail(errorHandler(req, res)); + } + }); + return router; } } diff --git a/app/node_modules/okresource/index.js b/app/node_modules/okresource/index.js index febd1d5..0e8498f 100644 --- a/app/node_modules/okresource/index.js +++ b/app/node_modules/okresource/index.js @@ -91,14 +91,16 @@ OKResource.prototype.create = function(data) { }); }; -OKResource.prototype.destroy = function(data) { - data = data || {}; - var id = data[this.idField]; +OKResource.prototype.destroy = function(id) { + var db = this._db; + var type = this.type; + var query = {}; + query[this.idField] = id; return Q.promise(function(resolve, reject) { if (!id) { - reject(new Error('Data does not contain ID property')); + reject(new Error('No ID given')); } else { - this._db.remove(this.type, data.id, data).then(resolve).fail(reject); + db.remove(type, query).then(resolve).fail(reject); } }); }; diff --git a/themes/okadmin/templates/resource.liquid b/themes/okadmin/templates/resource.liquid index c0d348d..48e3ef2 100644 --- a/themes/okadmin/templates/resource.liquid +++ b/themes/okadmin/templates/resource.liquid @@ -14,6 +14,10 @@
+
+ + +
{% include 'partials/tail' %} -- cgit v1.2.3-70-g09d2 From 6e1f689cfd8f820090c4ab15519114f4d3bf929f Mon Sep 17 00:00:00 2001 From: Sean Fridman Date: Fri, 10 Apr 2015 21:38:31 -0400 Subject: Overhaul DB impl Make DB schema aware Add autoincrement support Add custom ID field support --- app/index.js | 7 +- app/node_modules/okadminview/index.js | 19 ++-- app/node_modules/okdb/index.js | 181 +++++++++++++++++++++++----------- app/node_modules/okdb/package.json | 2 + app/node_modules/okresource/index.js | 126 ++++++++++------------- 5 files changed, 194 insertions(+), 141 deletions(-) (limited to 'app/node_modules/okresource/index.js') diff --git a/app/index.js b/app/index.js index b312eb1..fc38b0a 100644 --- a/app/index.js +++ b/app/index.js @@ -77,8 +77,11 @@ function OKCMS(options) { var adminTemplateProvider = this._adminTemplateProvider = new OKTemplate({root: adminTemplateRoot}); - var db = new OKDB(options.db || 'fs'); var schemas = this._schemas = this._createSchemas(schemaConfig); + var db = new OKDB({ + db: options.db || 'fs', + schemas: schemas + }); var resourceCache = this._resourceCache = this._createResources(resourceConfig, db, schemas); @@ -248,7 +251,7 @@ function ResourceCache(resources) { throw new Error('Undefined resource given to ResourceCache'); if (resource.bound) { cache[resource.type] = resource.parent; - cache[resource.type + ':' + resource.id] = resource; + cache[resource.type + ':' + resource.getID()] = resource; } else { cache[resource.type] = resource; } diff --git a/app/node_modules/okadminview/index.js b/app/node_modules/okadminview/index.js index b8ade49..1668900 100644 --- a/app/node_modules/okadminview/index.js +++ b/app/node_modules/okadminview/index.js @@ -57,13 +57,14 @@ function OKAdminView(options) { var config = resourceConfig[key]; var type = config.type; var staticData = config.static || {}; + // Get parent level resource var resource = resourceCache.get(config.type); if (!resource) throw new Error('Something weird is going on'); - var id = staticData[resource.idField]; + var id = resource.getID(staticData); + // Check to see if there's a more specific instance resource = resourceCache.get(type, id) || resource; if (resource.bound) { - // Resource instances implement the query API return OKQuery({resource: resource});; } else { return OKQuery({resource: resource, query: config.query}) @@ -166,7 +167,7 @@ function OKAdminView(options) { resource.assertValid(data); resource.create(data).then(function(created) { req.flash('success', {action: 'create'}); - res.redirect(303, data[resource.idField]); + res.redirect(303, resource.getID(data)); }).fail(errorHandler(req, res)); } catch (errors) { var templateData = getResourceTemplateData(metadata, resource, data); @@ -190,7 +191,7 @@ function OKAdminView(options) { resource.assertValid(data); resource.update(id, data).then(function(updated) { req.flash('success', {action: 'update'}); - res.redirect(303, '../' + updated[resource.idField]); + res.redirect(303, '../' + resource.getID(updated)); }).fail(errorHandler(req, res)); } catch (errors) { var templateData = getResourceTemplateData(metadata, resource, data); @@ -236,7 +237,7 @@ function getResourceTemplateData(meta, resource, data) { return { meta: meta, resource: { - id: data[resource.idField], + id: resource.getID(data), type: resource.type, spec: spec } @@ -293,14 +294,14 @@ function fetchIndexTemplateData(meta, queries) { } if (result.length) { - result.forEach(addToCache) + result.forEach(addData) } else { - addToCache(result); + addData(result); } - function addToCache(data) { + function addData(data) { // Report id to template under standard name - data.id = data[resource.idField]; + data.id = resource.getID(data); cache[key].data.push(data); } diff --git a/app/node_modules/okdb/index.js b/app/node_modules/okdb/index.js index 4be646d..c9dca99 100644 --- a/app/node_modules/okdb/index.js +++ b/app/node_modules/okdb/index.js @@ -1,4 +1,6 @@ var Q = require('q'); +var cloneDeep = require('lodash.clonedeep'); +var isobject = require('lodash.isobject'); var format = require('util').format; var low = require('lowdb'); low.mixin(low.mixin(require('underscore-db'))); @@ -10,14 +12,14 @@ low.mixin(low.mixin(require('underscore-db'))); function OKDB(options) { if (!(this instanceof OKDB)) return new OKDB(options); options = options || {}; - var type; + var db; if (typeof options === 'string') - type = options; + db = options; else - type = options.type; - if (!type) - throw new Error('No DB type provided'); - switch (type) { + db = options.db; + if (!db) + throw new Error('No DB db provided to OKDB'); + switch (db) { case 'fs': return FSDB(options); default: @@ -27,88 +29,151 @@ function OKDB(options) { /** * DB implementation backed by a JSON file. - * TODO Incomplete */ function FSDB(options) { if (!(this instanceof FSDB)) return new FSDB(options); options = options || {}; + if (!options.schemas) + throw new Error('No schemas provided to FSDB') + this._schemas = options.schemas; var name = options.name || 'db'; var filename = name + '.json'; this._db = low(filename); } -FSDB.prototype._resolve = function(data, error) { - return Q.Promise(function resolvePromise(resolve, reject) { - if (error) { - reject(error); - } else { - resolve(data); - } - }); +FSDB.prototype.get = function(collection, id) { + var schema = this._schemas[collection]; + if (!schema) + return resolve(null, new Error('No such collection type')); + + var query = getQuery(schema, id); + if (!query) + return resolve(null, new Error('Bad query')); + + var result = this._db(collection).find(query); + return resolve(result ? cloneDeep(result) : result); }; -FSDB.prototype.get = function(collection, query) { - if (!query) { - return this._resolve(null, new Error('No query given')); +/** + * Add a new document to the DB + */ +FSDB.prototype.insert = function(collection, data) { + var schema = this._schemas[collection]; + var wrapped = this._db(collection); + if (!schema) + return resolve(null, new Error('No such collection type')); + // Get detached, clone deep, data sleep, beep beep + data = cloneDeep(data); + // Auto-increment fields + data = autoincrement(wrapped, schema, data); + var result = wrapped.chain().push(data).last().value(); + + if (result) { + return resolve(cloneDeep(result)); } else { - var data = this._db(collection).find(query); - return this._resolve(data); + return resolve(null, new Error('Problem inserting document')); } }; -FSDB.prototype.put = function(collection, query, data) { - data = data || {}; - if (!query) { - return this._resolve(null, new Error('No query given')); - } else if (this._db(collection).find(query)) { - var updated = this._db(collection) - .chain() - .find(query) - .assign(data) - .value(); - return this._resolve(updated); +/** + * Update an existing document in the DB + */ +FSDB.prototype.update = function(collection, id, data) { + var schema = this._schemas[collection]; + if (!schema) + return resolve(null, new Error('No such collection type')); + + var query = getQuery(schema, id); + var wrapped = this._db(collection); + var chain = wrapped.chain().find(query); + + if (!chain.value()) { + return resolve(null, new Error('Cannot update nonexistent entry')); + } + + var result = chain.assign(cloneDeep(data)).value(); + if (result ) { + return resolve(cloneDeep(result)); } else { - return this._resolve(null, new Error('Cannot update nonexistent entry')); + return resolve(null, new Error('Problem updating document')); } }; -FSDB.prototype.create = function(collection, data) { - var created = this._db(collection) - .chain() - .push(data) - .last() - .value(); - return this._resolve(created); -}; +FSDB.prototype.remove = function(collection, id) { + var schema = this._schemas[collection]; + if (!schema) + return resolve(null, new Error('No such collection type')); + + var query = getQuery(schema, id); + var result = this._db(collection).removeWhere(query); -FSDB.prototype.remove = function(collection, query) { - if (!collection || !query) { - return this._resolve(null, new Error('Bad input')); + if (result) { + // Don't need to clone this ref, since it's removed anyway + return resolve(result); } else { - var data = this._db(collection).removeWhere(query); - if (!data) - var error = new Error('Cannot remove nonexistent entry'); - return this._resolve(data, error); + return resolve(null, new Error('Cannot remove nonexistent entry')); } }; -FSDB.prototype.find = function(collection, query) { - if (!collection || !query) { - return this._resolve(null, new Error('Bad input')); - } else { - var data = this._db(collection).find(query); - return this._resolve(data); - } +FSDB.prototype.find = function(collection, id) { + var schema = this._schemas[collection]; + if (!schema) + return resolve(null, new Error('No such collection type')); + + var query = getQuery(schema, id); + var result = this._db(collection).find(query); + + return resolve(cloneDeep(result)); +}; + +FSDB.prototype.all = function(collection) { + var schema = this._schemas[collection]; + if (!schema) + return resolve(null, new Error('No such collection type')); + + var data = this._db(collection).value(); + return resolve(cloneDeep(data || [])); }; FSDB.prototype.getMeta = function() { var data = this._db('meta').first(); - return this._resolve(data || {}); + return resolve(data || {}); }; -FSDB.prototype.getAll = function(collection) { - var data = this._db(collection).toArray(); - return this._resolve(data || []); +/** + * Function implementing DB auto increment support + * Naive implementation, assumes DB is relatively small. + */ +function autoincrement(wrapper, schema, data) { + return schema.autoIncrementFields.reduce(function(data, field) { + var last = wrapper.chain().sort(field).last().value(); + var index = last ? last[field] : -1; + var incremented = {}; + incremented[field] = index + 1; + return assign(data, incremented); + }, data); +} + +function getQuery(schema, id) { + if (schema && id) { + var query = {}; + query[schema.idField] = id; + return query; + } +} + +/** + * Helper function to create promises for DB data + */ +function resolve(data, error) { + return Q.promise(function resolvePromise(resolve, reject) { + if (error) { + reject(error); + } else { + resolve(data); + } + }); }; + module.exports = OKDB; diff --git a/app/node_modules/okdb/package.json b/app/node_modules/okdb/package.json index 659422e..a6d13ff 100644 --- a/app/node_modules/okdb/package.json +++ b/app/node_modules/okdb/package.json @@ -9,6 +9,8 @@ "author": "OKFocus", "license": "None", "dependencies": { + "lodash.clonedeep": "^3.0.0", + "lodash.isobject": "^3.0.1", "lowdb": "^0.7.2", "q": "^1.2.0", "underscore-db": "^0.8.1" diff --git a/app/node_modules/okresource/index.js b/app/node_modules/okresource/index.js index 0e8498f..df89617 100644 --- a/app/node_modules/okresource/index.js +++ b/app/node_modules/okresource/index.js @@ -20,17 +20,6 @@ function OKResource(options) { var schema = options.schema; var spec = schema.spec; - // Iterate through spec to find field which will act as the - // resource id in da db. - var idField = Object.keys(spec).reduce(function(idField, prop) { - var propSpec = spec[prop]; - if (propSpec.id) - idField = prop; - return idField; - // If schema has a prop called 'id', default to that one - }, spec.id && 'id'); - if (!idField) - throw new Error('Bad schema: no ID field'); var type = options.type; this._db = options.db; @@ -51,12 +40,6 @@ function OKResource(options) { enumerable: true }); - Object.defineProperty(this, 'idField', { - value: idField, - writable: false, - enumerable: true - }); - // Whether this resource represents a specific data point // or a whole class of data Object.defineProperty(this, 'bound', { @@ -74,19 +57,22 @@ OKResource.prototype.assertValid = function(data) { }; OKResource.prototype.all = function() { - return this._db.getAll(this.type); + return this._db.all(this.type); }; -OKResource.prototype.create = function(data) { +OKResource.prototype.getID = function(data) { data = data || {}; + return data[this._schema.idField]; +}; + +OKResource.prototype.create = function(data) { var type = this.type; var db = this._db; - var id = data[this.idField]; return Q.promise(function(resolve, reject) { - if (!id) { - reject(new Error('Data does not contain ID property')); + if (!data) { + reject(new Error('No data provided')); } else { - db.create(type, data).then(resolve).fail(reject); + db.insert(type, data).then(resolve).fail(reject); } }); }; @@ -94,13 +80,11 @@ OKResource.prototype.create = function(data) { OKResource.prototype.destroy = function(id) { var db = this._db; var type = this.type; - var query = {}; - query[this.idField] = id; return Q.promise(function(resolve, reject) { if (!id) { - reject(new Error('No ID given')); + reject(new Error('No ID provided')); } else { - db.remove(type, query).then(resolve).fail(reject); + db.remove(type, id).then(resolve).fail(reject); } }); }; @@ -111,7 +95,7 @@ OKResource.prototype.find = function(query) { var type = this.type; return Q.promise(function(resolve, reject) { if (!query) { - throw new Error('No query given'); + throw new Error('No query provided'); } else { db.find(type, query).then(resolve).fail(reject); } @@ -121,53 +105,64 @@ OKResource.prototype.find = function(query) { OKResource.prototype.get = function(id) { var db = this._db; var type = this.type; - var idField = this.idField; return Q.promise(function(resolve, reject) { if (!id) { - throw new Error('No ID given'); + throw new Error('No ID provided'); } else { - // We have the id, but we still need - // to resolve which field is the id field - // to match - var query = {}; - query[idField] = id; - db.get(type, query).then(resolve).fail(reject); + db.get(type, id).then(resolve).fail(reject); } }); }; OKResource.prototype.update = function(id, data) { - data = data || {}; var db = this._db; var type = this.type; - var idField = this.idField; return Q.promise(function(resolve, reject) { if (!id) { reject(new Error('No resource ID provided')); - } else { - var query = {}; - query[idField] = id; - db.put(type, query, data).then(resolve).fail(reject);; + } else if (!data) { + reject(new Error('No data provided')); + }else { + db.update(type, id, data).then(resolve).fail(reject);; } }); }; +/** + * Update all resources with the given ids with the given data + */ +OKResource.prototype.updateBatch = function(ids, datas) { + // var type = this.type; + // var db = this._db; + // var idField = this.idField; + // return Q.promise(function(resolve, reject) { + // if (!ids || !ids.length || !datas || !datas.length || + // ids.length !== datas.length) { + // reject(new Error('Bad input')); + // } else { + // var queries = ids.map(function(id, i) { + // var query = {}; + // query[idField] = datas[i][idField]; + // return query; + // }); + // db.putAll(type, queries, datas).then(resolve).fail(reject); + // } + // }); +}; + OKResource.prototype.updateOrCreate = function(id, data) { data = data || {}; var type = this.type; var db = this._db; - var idField = this.idField; - var query = {}; - query[idField] = id; return Q.promise(function(resolve, reject) { if (!id) { - reject(new Error('No resource ID provided')); + reject(new Error('No ID provided')); } else { - db.get(type, query).then(function(persisted) { + db.get(type, id).then(function(persisted) { if (persisted) { - db.put(type, query, data).then(resolve).fail(reject); + db.update(type, id, data).then(resolve).fail(reject); } else { - db.create(type, data).then(resolve).fail(reject); + db.insert(type, data).then(resolve).fail(reject); } }).fail(reject); } @@ -196,11 +191,15 @@ function OKResourceInstance(resource, options) { // configuration, but may not actually be present // in the database and need custom logic to handle this. var staticData = cloneDeep(options.static); - var id = staticData[resource.idField]; + var id = resource.getID(staticData); if (!id) throw new Error( 'Cannot create static OKResourceInstance without an ID field'); + this.getID = function() { + return id; + }; + /** * Ensure that static data is provided on get */ @@ -209,9 +208,9 @@ function OKResourceInstance(resource, options) { resource.get(id).then(function(data) { // Note the assign call. Don't expose private references! if (data) { - resolve(assign({}, data, cloneDeep(staticData))); + resolve(assign(data, cloneDeep(staticData))); } else { - resolve(assign({}, cloneDeep(staticData))); + resolve(cloneDeep(staticData)); } }).fail(reject); }); @@ -286,14 +285,9 @@ function OKResourceInstance(resource, options) { }); Object.defineProperty(this, 'spec', { - value: resource.spec, - writable: false, - enumerable: true - }); - - Object.defineProperty(this, 'id', { - value: id, - writable: false, + get: function() { + return resource.spec + }, enumerable: true }); @@ -303,23 +297,11 @@ function OKResourceInstance(resource, options) { enumerable: true }); - Object.defineProperty(this, 'idField', { - value: resource.idField, - writable: false, - enumerable: true - }); - Object.defineProperty(this, 'bound', { value: true, writable: false, enumerable: true }); - - Object.defineProperty(this, 'class', { - value: resource, - writable: false, - enumerable: true - }); } module.exports = OKResource; -- cgit v1.2.3-70-g09d2 From 4021d7846ce164f3f0c3cb37d3a1d82d2de489d9 Mon Sep 17 00:00:00 2001 From: Sean Fridman Date: Fri, 10 Apr 2015 23:09:05 -0400 Subject: Give views sorted resources --- app/node_modules/okdb/index.js | 11 +++++++++++ app/node_modules/okquery/index.js | 3 ++- app/node_modules/okresource/index.js | 11 ++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) (limited to 'app/node_modules/okresource/index.js') diff --git a/app/node_modules/okdb/index.js b/app/node_modules/okdb/index.js index 7639ec6..c00087c 100644 --- a/app/node_modules/okdb/index.js +++ b/app/node_modules/okdb/index.js @@ -117,6 +117,17 @@ FSDB.prototype.remove = function(collection, id) { } }; +FSDB.prototype.sortBy = function(collection, prop, descend) { + var schema = this._schemas[collection]; + if (!schema) + return resolve(null, new Error('No such collection type')); + if (!prop) + return resolve(null, new Error('Bad input')); + + var result = this._db(collection).sortByOrder([prop], [!descend]); + return resolve(result); +}; + FSDB.prototype.find = function(collection, id) { var schema = this._schemas[collection]; if (!schema) diff --git a/app/node_modules/okquery/index.js b/app/node_modules/okquery/index.js index 89c8b73..9cc8b78 100644 --- a/app/node_modules/okquery/index.js +++ b/app/node_modules/okquery/index.js @@ -96,7 +96,8 @@ function queryDynamic(resource) { function queryAll(resource) { return function() { - return resource.all(); + // Always return sorted results + return resource.sortBy('__index'); }; } diff --git a/app/node_modules/okresource/index.js b/app/node_modules/okresource/index.js index df89617..8c0bb16 100644 --- a/app/node_modules/okresource/index.js +++ b/app/node_modules/okresource/index.js @@ -122,12 +122,21 @@ OKResource.prototype.update = function(id, data) { reject(new Error('No resource ID provided')); } else if (!data) { reject(new Error('No data provided')); - }else { + } else { db.update(type, id, data).then(resolve).fail(reject);; } }); }; + +/** + * Get all documents in collection sorted by property, + * optionally in descending order + */ +OKResource.prototype.sortBy = function(prop, descend) { + return this._db.sortBy(this.type, prop, descend); +}; + /** * Update all resources with the given ids with the given data */ -- cgit v1.2.3-70-g09d2 From a20297451b88c604b16a35223be4b25528713c6d Mon Sep 17 00:00:00 2001 From: Sean Fridman Date: Sat, 11 Apr 2015 01:36:09 -0400 Subject: Implement FSDB.updateBatch and OKResource.updateBatch --- app/node_modules/okdb/index.js | 20 ++++++++++++++++++++ app/node_modules/okresource/index.js | 36 ++++++++++++++---------------------- 2 files changed, 34 insertions(+), 22 deletions(-) (limited to 'app/node_modules/okresource/index.js') diff --git a/app/node_modules/okdb/index.js b/app/node_modules/okdb/index.js index c00087c..ffe3ff1 100644 --- a/app/node_modules/okdb/index.js +++ b/app/node_modules/okdb/index.js @@ -101,6 +101,26 @@ FSDB.prototype.update = function(collection, id, data) { } }; +/** + * TODO Should be atomic ¯\_(ツ)_/¯ + */ +FSDB.prototype.updateBatch = function(collection, ids, datas) { + var schema = this._schemas[collection]; + if (!schema) + return resolve(null, new Error('No such collection type')); + if (!ids || !ids.length || !datas || !datas.length || + ids.length !== datas.length) { + return resolve(null, new Error('Bad input')); + } + + var doc = this._db(collection); + var results = ids.map(function(id, i) { + return doc.chain().find(getQuery(schema, id)).assign(datas[i]).value(); + }); + + return resolve(results); +}; + FSDB.prototype.remove = function(collection, id) { var schema = this._schemas[collection]; if (!schema) diff --git a/app/node_modules/okresource/index.js b/app/node_modules/okresource/index.js index 8c0bb16..c3f9adb 100644 --- a/app/node_modules/okresource/index.js +++ b/app/node_modules/okresource/index.js @@ -128,6 +128,20 @@ OKResource.prototype.update = function(id, data) { }); }; +OKResource.prototype.updateBatch = function(ids, datas) { + var self = this; + var db = this._db; + var type = this.type; + return Q.promise(function(resolve, reject) { + if (!ids || !ids.length || !datas || !datas.length || + ids.length !== datas.length) { + reject(new Error('Bad input')); + } else { + db.updateBatch(type, ids, datas).then(resolve).fail(reject); + } + }); +} + /** * Get all documents in collection sorted by property, @@ -137,28 +151,6 @@ OKResource.prototype.sortBy = function(prop, descend) { return this._db.sortBy(this.type, prop, descend); }; -/** - * Update all resources with the given ids with the given data - */ -OKResource.prototype.updateBatch = function(ids, datas) { - // var type = this.type; - // var db = this._db; - // var idField = this.idField; - // return Q.promise(function(resolve, reject) { - // if (!ids || !ids.length || !datas || !datas.length || - // ids.length !== datas.length) { - // reject(new Error('Bad input')); - // } else { - // var queries = ids.map(function(id, i) { - // var query = {}; - // query[idField] = datas[i][idField]; - // return query; - // }); - // db.putAll(type, queries, datas).then(resolve).fail(reject); - // } - // }); -}; - OKResource.prototype.updateOrCreate = function(id, data) { data = data || {}; var type = this.type; -- cgit v1.2.3-70-g09d2 From 7249dc10568091194a77990513fa2b77fdbd088a Mon Sep 17 00:00:00 2001 From: Sean Fridman Date: Fri, 10 Jul 2015 17:31:40 -0400 Subject: Implement basic foreign key support --- app/index.js | 23 ++++++ app/node_modules/okadminview/index.js | 100 ++++++++++++++++++------ app/node_modules/okquery/index.js | 1 + app/node_modules/okresource/index.js | 66 ++++++++++++++++ app/node_modules/okschema/index.js | 4 + themes/okadmin/templates/partials/inputs.liquid | 2 +- 6 files changed, 169 insertions(+), 27 deletions(-) (limited to 'app/node_modules/okresource/index.js') diff --git a/app/index.js b/app/index.js index d514f10..2507dd2 100644 --- a/app/index.js +++ b/app/index.js @@ -78,6 +78,7 @@ function OKCMS(options) { }); var resourceCache = this._resourceCache = this._createResources(resourceConfig, db, schemas); + this._resolveForeignKeys(resourceCache) var errorHandler = createErrorHandlerProducer( templateProvider, adminTemplateProvider, debug); // Create view instances from config @@ -154,6 +155,21 @@ OKCMS.prototype._createResources = function(resourceConfig, db, schemaCache) { return ResourceCache(resources); }; +OKCMS.prototype._resolveForeignKeys = function(resourceCache) { + resourceCache.forEach(function(resource) { + Object.keys(resource.foreignKeys).forEach(function(field) { + var foreignKeyType = resource.foreignKeys[field] + var keyedResource = resourceCache.get(foreignKeyType) + if (!keyedResource) { + throw new Error(format( + "Foreign key field '%s' in '%s' resource references unknown" + + "resource of type '%s'", field, resource.type, foreignKeyType)) + } + resource._linkForeignKey(field, resourceCache.get(foreignKeyType)) + }) + }) +} + OKCMS.prototype._createViews = function(viewConfig, db, meta, resourceCache, templateProvider, errorHandler) { viewConfig = viewConfig || {}; @@ -279,6 +295,13 @@ ResourceCache.prototype.get = function(type, id) { } }; +ResourceCache.prototype.forEach = function(cb) { + cb = cb || function() {} + Object.keys(this._cache).forEach(function(key) { + cb(this._cache[key]) + }.bind(this)) +} + /** * Higher order function implementing customizable error handling */ diff --git a/app/node_modules/okadminview/index.js b/app/node_modules/okadminview/index.js index 273d541..06656dc 100644 --- a/app/node_modules/okadminview/index.js +++ b/app/node_modules/okadminview/index.js @@ -154,11 +154,12 @@ function OKAdminView(options) { if (!resource) { error(req, res, 404)(new Error('No such resource ' + type)); } else { - var templateData = transformData(meta, resource, {}); - view.renderResourceNew(req, res, assign(templateData, { - success: req.flash('success'), - errors: req.flash('errors'), - })); + fetchNewTemplateData(meta, resource, transformData).then(function(data) { + view.renderResourceNew(req, res, assign(data, { + success: req.flash('success'), + errors: req.flash('errors'), + })) + }).fail(error(req, res, 500)) } }); @@ -280,11 +281,11 @@ function OKAdminView(options) { /** * Yields formatted template data for a single resource */ -function transformData(meta, resource, data) { +function transformData(meta, spec, resource, data) { meta = meta || {}; resource = resource || {}; data = data || {}; - var spec = Object.keys(resource.spec).reduce(function(cache, prop) { + var spec = Object.keys(spec).reduce(function(cache, prop) { var value = data[prop]; var propSpec = cache[prop]; // Decorate spec with actual resource values @@ -294,7 +295,7 @@ function transformData(meta, resource, data) { propSpec.hidden = true; } return cache; - }, resource.spec); + }, spec); return { meta: meta, resource: { @@ -340,47 +341,94 @@ function fetchIndexTemplateData(meta, queries, dashboardConfig) { Q.all(queries.map(function(query) { return query.get(); })).then(function(results) { - var resources = results.reduce(function(cache, result, i) { - if (!result) - return cache; + var templateData = results.reduce(function(acc, result, i) { var resource = queries[i].resource; // We want the raw object spec var spec = resource.spec; var dashConf = resourceConfig[resource.type] || {} var groupBy = dashConf.groupBy var key = pluralize(resource.type) - cache[key] = { + acc[key] = { type: resource.type, spec: spec, data: result, groupBy: groupBy } - return cache; - }, {}); - + return acc + }, {}) resolve({ meta: meta, - resources: resources - }); - }).fail(reject); + resources: templateData + }) + }).fail(reject) }); } +function fetchNewTemplateData(meta, resource, transformFn) { + return Q.promise(function(resolve, reject) { + if (!resource.hasForeignKey) { + done({spec: resource.spec, resource: resource}) + } else { + fetchForeignKeyOptions(resource).then(done).fail(reject) + } + + function done(results) { + resolve(transformFn(meta, results.spec, results.resource, {})) + } + }) +} + /** * Annotate template data with schema info */ -function fetchResourceTemplateData(meta, query, fn) { - fn = fn || function(m, r, d) { return {meta: m, resource: d}; }; +function fetchResourceTemplateData(meta, query, transformFn) { return Q.promise(function(resolve, reject) { query.get().then(function(data) { - if (!data) { - reject(new Error('No resource data')); + if (!data) + return reject(new Error('No resource data')) + + var resource = query.resource + + if (resource.hasForeignKey) { + fetchForeignKeyOptions(resource).then(done).fail(reject) } else { - var resource = query.resource; - resolve(fn(meta, resource, data)); + done({spec: resource.spec, resource: resource}) } - }).fail(reject); - }); + + function done(results) { + resolve(transformFn(meta, results.spec, results.resource, data)) + } + }).fail(reject) + }) +} + +function fetchForeignKeyOptions(resource) { + var promises = Object.keys(resource.foreignKeys) + .map(fetchOptionsForKey) + var spec = resource.spec + + return Q.all(promises).then(done) + + function done() { + return Q.promise(function(resolve, reject) { + resolve({spec: spec, resource: resource}) + }) + } + + function fetchOptionsForKey(field) { + var relatedResourceType = resource.foreignKeys[field] + return resource.related(relatedResourceType).then(fillOptions) + + function fillOptions(results) { + return Q.promise(function(resolve, reject) { + spec[field].options = results.map(function(result) { + return result.id + }) + resolve() + }) + } + } } + module.exports = OKAdminView; diff --git a/app/node_modules/okquery/index.js b/app/node_modules/okquery/index.js index 09d867d..d4cb905 100644 --- a/app/node_modules/okquery/index.js +++ b/app/node_modules/okquery/index.js @@ -23,6 +23,7 @@ function OKQuery(options) { // Queries are ordered by index by default var sortField = options.sortBy || '__index'; + // TODO Make descending by default var descending = options.descending || false; Object.defineProperty(this, 'resource', { diff --git a/app/node_modules/okresource/index.js b/app/node_modules/okresource/index.js index c3f9adb..c1f2509 100644 --- a/app/node_modules/okresource/index.js +++ b/app/node_modules/okresource/index.js @@ -1,3 +1,4 @@ +var format = require('util').format var assign = require('object-assign'); var cloneDeep = require('lodash.clonedeep'); var Q = require('q'); @@ -22,10 +23,36 @@ function OKResource(options) { var spec = schema.spec; var type = options.type; + var hasForeignKey = false this._db = options.db; this._schema = schema; + var foreignKeys = Object.keys(spec).reduce(function(acc, field) { + var fieldSpec = spec[field] + if (fieldSpec.type === 'foreign-key') { + hasForeignKey = true + acc[field] = fieldSpec.key + } + return acc + }, {}) + + // Will store references to other resources referenced via foreign keys + this._foreignKeyedResources = {} + // Define properties which are part of the API + + // Should be treated as read-only + Object.defineProperty(this, 'foreignKeys', { + get: function() { + return foreignKeys + } + }) + + Object.defineProperty(this, 'hasForeignKey', { + get: function() { + return hasForeignKey + } + }) Object.defineProperty(this, 'spec', { get: function() { @@ -49,6 +76,30 @@ function OKResource(options) { }); } +OKResource.prototype._linkForeignKey = function(field, resource) { + this._foreignKeyedResources[field] = resource +} + +/** + * Fetch all related resources for the given field + */ +OKResource.prototype.related = function(field) { + var resource = this._foreignKeyedResources[field] + return Q.promise(function(resolve, reject) { + if (!resource) { + return error(reject, new Error(format( + "No related resource for field '%s'", field))) + } + resource.all().then(resolve).fail(reject) + }) + + function error(reject, err) { + setTimeout(function() { + reject(err) + }, 0) + } +} + /** * Throws an error if data does not conform to schema */ @@ -180,6 +231,9 @@ OKResource.prototype.instance = function(options) { }); }; +/** + * TODO This class is such bullshit. Refactor out + */ function OKResourceInstance(resource, options) { if (!(this instanceof OKResourceInstance)) return new OKResourceInstance(options); // Only support static data instances for now @@ -279,6 +333,18 @@ function OKResourceInstance(resource, options) { resource.assertValid(data); }; + Object.defineProperty(this, 'foreignKeys', { + get: function() { + return [] + } + }) + + Object.defineProperty(this, 'hasForeignKey', { + get: function() { + return false + } + }) + Object.defineProperty(this, 'parent', { value: resource, writable: false, diff --git a/app/node_modules/okschema/index.js b/app/node_modules/okschema/index.js index 89b59cc..330ad6b 100644 --- a/app/node_modules/okschema/index.js +++ b/app/node_modules/okschema/index.js @@ -73,6 +73,10 @@ var types = { 'flag': { parent: 'string', assertValid: function(spec, value) {} + }, + 'foreign-key': { + parent: 'enum', + assertValid: function(spec, value) {} } } diff --git a/themes/okadmin/templates/partials/inputs.liquid b/themes/okadmin/templates/partials/inputs.liquid index 9c1a26b..c6efc68 100644 --- a/themes/okadmin/templates/partials/inputs.liquid +++ b/themes/okadmin/templates/partials/inputs.liquid @@ -17,7 +17,7 @@ - {% elsif type == 'enum' %} + {% elsif type == 'enum' or type == 'foreign-key' %}