var assign = require('object-assign'); var cloneDeep = require('cloneDeep'); var Q = require('q'); /** * OKResource! * TODO Would be nicer to compose this with an existing resource * module, but haven't found a good fit. Should keep an eye on * 'resource' by bigcompany for an update. */ function OKResource(options) { if (!(this instanceof OKResource)) return new OKResource(options); options = options || {}; if (!options.type) throw new Error('No resource type provided to OKResource') if (!options.schema) throw new Error('No schema provided to OKResource'); if (!options.db) 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(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; this._schema = schema; // Define properties which are part of the API Object.defineProperty(this, 'spec', { get: function() { return schema.spec; }, enumerable: true }); Object.defineProperty(this, 'type', { value: type, writable: false, 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', { 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); }; OKResource.prototype.all = function() { return this._db.getAll(this.type); }; 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 { db.create(type, data).then(resolve).fail(reject); } }); }; OKResource.prototype.destroy = function(data) { data = data || {}; 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.remove(this.type, data.id, data).then(resolve).fail(reject); } }); }; OKResource.prototype.find = function(query) { query = query || {}; var db = this._db; var type = this.type; return Q.promise(function(resolve, reject) { if (!query) { throw new Error('No query given'); } else { db.find(type, query).then(resolve).fail(reject); } }); }; 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'); } 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); } }); }; 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);; } }); }; 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')); } else { 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 = cloneDeep(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, cloneDeep(staticData))); } else { resolve(assign({}, cloneDeep(staticData))); } }).fail(reject); }); }; this.update = function(id, 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(id, 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;