summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSean Fridman <fridman@mail.sfsu.edu>2015-04-10 21:38:31 -0400
committerSean Fridman <fridman@mail.sfsu.edu>2015-04-10 21:41:01 -0400
commit6e1f689cfd8f820090c4ab15519114f4d3bf929f (patch)
tree3ce5f2515fb37777660bb36f635071afbb9bf224
parent5abbdf600396144fbbe32b97e83beaabf6ed5c39 (diff)
Overhaul DB impl
Make DB schema aware Add autoincrement support Add custom ID field support
-rw-r--r--app/index.js7
-rw-r--r--app/node_modules/okadminview/index.js19
-rw-r--r--app/node_modules/okdb/index.js181
-rw-r--r--app/node_modules/okdb/package.json2
-rw-r--r--app/node_modules/okresource/index.js126
5 files changed, 194 insertions, 141 deletions
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;