diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index ea9c9d3a..29f25166 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -70,6 +70,31 @@ function deserialize(dbObj) { } } +Memory.prototype.getCollection = function(model) { + var modelClass = this._models[model]; + if (modelClass.settings.memory) { + model = modelClass.settings.memory.collection || model; + } + return model; +} + +Memory.prototype.initCollection = function(model) { + this.collection(model, {}); + this.collectionSeq(model, 1); +} + +Memory.prototype.collection = function(model, val) { + model = this.getCollection(model); + if (arguments.length > 1) this.cache[model] = val; + return this.cache[model]; +}; + +Memory.prototype.collectionSeq = function(model, val) { + model = this.getCollection(model); + if (arguments.length > 1) this.ids[model] = val; + return this.ids[model]; +}; + Memory.prototype.loadFromFile = function(callback) { var self = this; var hasLocalStorage = typeof window !== 'undefined' && window.localStorage; @@ -161,36 +186,31 @@ Memory.prototype.saveToFile = function (result, callback) { Memory.prototype.define = function defineModel(definition) { this.constructor.super_.prototype.define.apply(this, [].slice.call(arguments)); var m = definition.model.modelName; - if(!this.cache[m]) { - this.cache[m] = {}; - this.ids[m] = 1; - } + if(!this.collection(m)) this.initCollection(m); }; Memory.prototype.create = function create(model, data, callback) { // FIXME: [rfeng] We need to generate unique ids based on the id type // FIXME: [rfeng] We don't support composite ids yet - var currentId = this.ids[model]; - if (currentId === undefined) { - // First time - this.ids[model] = 1; - currentId = 1; + var currentId = this.collectionSeq(model); + if (currentId === undefined) { // First time + currentId = this.collectionSeq(model, 1); } var id = this.getIdValue(model, data) || currentId; if (id > currentId) { // If the id is passed in and the value is greater than the current id currentId = id; } - this.ids[model] = Number(currentId) + 1; + this.collectionSeq(model, Number(currentId) + 1); var props = this._models[model].properties; var idName = this.idName(model); id = (props[idName] && props[idName].type && props[idName].type(id)) || id; this.setIdValue(model, data, id); - if(!this.cache[model]) { - this.cache[model] = {}; + if(!this.collection(model)) { + this.collection(model, {}); } - this.cache[model][id] = serialize(data); + this.collection(model)[id] = serialize(data); this.saveToFile(id, callback); }; @@ -210,30 +230,30 @@ Memory.prototype.updateOrCreate = function (model, data, callback) { Memory.prototype.save = function save(model, data, callback) { var id = this.getIdValue(model, data); - var cachedModels = this.cache[model]; - var modelData = cachedModels && this.cache[model][id]; + var cachedModels = this.collection(model); + var modelData = cachedModels && this.collection(model)[id]; modelData = modelData && deserialize(modelData); if (modelData) { data = merge(modelData, data); } - this.cache[model][id] = serialize(data); + this.collection(model)[id] = serialize(data); this.saveToFile(data, callback); }; Memory.prototype.exists = function exists(model, id, callback) { process.nextTick(function () { - callback(null, this.cache[model] && this.cache[model].hasOwnProperty(id)); + callback(null, this.collection(model) && this.collection(model).hasOwnProperty(id)); }.bind(this)); }; Memory.prototype.find = function find(model, id, callback) { process.nextTick(function () { - callback(null, id in this.cache[model] && this.fromDb(model, this.cache[model][id])); + callback(null, id in this.collection(model) && this.fromDb(model, this.collection(model)[id])); }.bind(this)); }; Memory.prototype.destroy = function destroy(model, id, callback) { - delete this.cache[model][id]; + delete this.collection(model)[id]; this.saveToFile(null, callback); }; @@ -266,8 +286,8 @@ Memory.prototype.fromDb = function (model, data) { Memory.prototype.all = function all(model, filter, callback) { var self = this; - var nodes = Object.keys(this.cache[model]).map(function (key) { - return this.fromDb(model, this.cache[model][key]); + var nodes = Object.keys(this.collection(model)).map(function (key) { + return this.fromDb(model, this.collection(model)[key]); }.bind(this)); if (filter) { @@ -505,24 +525,23 @@ Memory.prototype.destroyAll = function destroyAll(model, where, callback) { callback = where; where = undefined; } - var cache = this.cache[model]; + var cache = this.collection(model); var filter = null; if (where) { filter = applyFilter({where: where}); - } - Object.keys(cache).forEach(function (id) { - if (!filter || filter(this.fromDb(model, cache[id]))) { - delete cache[id]; - } - }.bind(this)); - if (!where) { - this.cache[model] = {}; + Object.keys(cache).forEach(function (id) { + if (!filter || filter(this.fromDb(model, cache[id]))) { + delete cache[id]; + } + }.bind(this)); + } else { + this.collection(model, {}); } this.saveToFile(null, callback); }; Memory.prototype.count = function count(model, callback, where) { - var cache = this.cache[model]; + var cache = this.collection(model); var data = Object.keys(cache); if (where) { var filter = {where: where}; @@ -539,7 +558,7 @@ Memory.prototype.count = function count(model, callback, where) { Memory.prototype.update = Memory.prototype.updateAll = function updateAll(model, where, data, cb) { var self = this; - var cache = this.cache[model]; + var cache = this.collection(model); var filter = null; where = where || {}; filter = applyFilter({where: where}); @@ -571,8 +590,8 @@ Memory.prototype.updateAttributes = function updateAttributes(model, id, data, c this.setIdValue(model, data, id); - var cachedModels = this.cache[model]; - var modelData = cachedModels && this.cache[model][id]; + var cachedModels = this.collection(model); + var modelData = cachedModels && this.collection(model)[id]; if (modelData) { this.save(model, data, cb); @@ -594,6 +613,16 @@ Memory.prototype.buildNearFilter = function (filter) { // noop } +Memory.prototype.automigrate = function (models, cb) { + if (typeof models === 'function') cb = models, models = []; + if (models.length === 0) models = Object.keys(this._models); + var self = this; + models.forEach(function(m) { + self.initCollection(m); + }); + if (cb) cb(); +} + function merge(base, update) { if (!base) { return update; diff --git a/lib/dao.js b/lib/dao.js index c4236a91..cff2e52c 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -13,11 +13,12 @@ var Relation = require('./relations.js'); var Inclusion = require('./include.js'); var List = require('./list.js'); var geo = require('./geo'); -var mergeQuery = require('./scope.js').mergeQuery; var Memory = require('./connectors/memory').Memory; var utils = require('./utils'); var fieldsToArray = utils.fieldsToArray; var removeUndefined = utils.removeUndefined; +var setScopeValuesFromWhere = utils.setScopeValuesFromWhere; +var mergeQuery = utils.mergeQuery; var util = require('util'); var assert = require('assert'); @@ -53,6 +54,14 @@ function setIdValue(m, data, value) { } } +function byIdQuery(m, id) { + var pk = idName(m); + var query = { where: {} }; + query.where[pk] = id; + m.applyScope(query); + return query; +} + DataAccessObject._forDB = function (data) { if (!(this.getDataSource().isRelational && this.getDataSource().isRelational())) { return data; @@ -69,6 +78,40 @@ DataAccessObject._forDB = function (data) { return res; }; +DataAccessObject.defaultScope = function(target, inst) { + var scope = this.definition.settings.scope; + if (typeof scope === 'function') { + scope = this.definition.settings.scope.call(this, target, inst); + } + return scope; +}; + +DataAccessObject.applyScope = function(query, inst) { + var scope = this.defaultScope(query, inst) || {}; + if (typeof scope === 'object') { + mergeQuery(query, scope || {}, this.definition.settings.scoping); + } +}; + +DataAccessObject.applyProperties = function(data, inst) { + var properties = this.definition.settings.properties; + properties = properties || this.definition.settings.attributes; + if (typeof properties === 'object') { + util._extend(data, properties); + } else if (typeof properties === 'function') { + util._extend(data, properties.call(this, data, inst) || {}); + } else if (properties !== false) { + var scope = this.defaultScope(data, inst) || {}; + if (typeof scope.where === 'object') { + setScopeValuesFromWhere(data, scope.where, this); + } + } +}; + +DataAccessObject.lookupModel = function(data) { + return this; +}; + /** * Create an instance of Model with given data and save to the attached data source. Callback is optional. * Example: @@ -89,8 +132,8 @@ DataAccessObject.create = function (data, callback) { if (stillConnecting(this.getDataSource(), this, arguments)) return; var Model = this; - var modelName = Model.modelName; - + var self = this; + if (typeof data === 'function') { callback = data; data = {}; @@ -116,6 +159,7 @@ DataAccessObject.create = function (data, callback) { for (var i = 0; i < data.length; i += 1) { (function (d, i) { + Model = self.lookupModel(d); // data-specific instances.push(Model.create(d, function (err, inst) { if (err) { errors[i] = err; @@ -131,11 +175,16 @@ DataAccessObject.create = function (data, callback) { function modelCreated() { if (--wait === 0) { callback(gotError ? errors : null, instances); - if(!gotError) instances.forEach(Model.emit.bind('changed')); + if(!gotError) { + instances.forEach(function(inst) { + inst.constructor.emit('changed'); + }); + } } } } - + + var enforced = {}; var obj; var idValue = getIdValue(this, data); @@ -145,6 +194,13 @@ DataAccessObject.create = function (data, callback) { } else { obj = new Model(data); } + + this.applyProperties(enforced, obj); + obj.setAttributes(enforced); + + Model = this.lookupModel(data); // data-specific + if (Model !== obj.constructor) obj = new Model(data); + data = obj.toObject(true); // validation required @@ -155,12 +211,13 @@ DataAccessObject.create = function (data, callback) { callback(new ValidationError(obj), obj); } }, data); - + function create() { obj.trigger('create', function (createDone) { obj.trigger('save', function (saveDone) { var _idName = idName(Model); + var modelName = Model.modelName; this._adapter().create(modelName, this.constructor._forDB(obj.toObject(true)), function (err, id, rev) { if (id) { obj.__data[_idName] = id; @@ -208,7 +265,7 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data if (stillConnecting(this.getDataSource(), this, arguments)) { return; } - + var self = this; var Model = this; if (!getIdValue(this, data)) { return this.create(data, callback); @@ -220,7 +277,9 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data inst = new Model(data); } update = inst.toObject(false); + this.applyProperties(update, inst); update = removeUndefined(update); + Model = this.lookupModel(update); this.getDataSource().connector.updateOrCreate(Model.modelName, update, function (err, data) { var obj; if (data && !(data instanceof Model)) { @@ -242,6 +301,7 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data if (inst) { inst.updateAttributes(data, callback); } else { + Model = self.lookupModel(data); var obj = new Model(data); obj.save(data, callback); } @@ -290,7 +350,9 @@ DataAccessObject.exists = function exists(id, cb) { if (stillConnecting(this.getDataSource(), this, arguments)) return; if (id !== undefined && id !== null && id !== '') { - this.dataSource.connector.exists(this.modelName, id, cb); + this.count(byIdQuery(this, id).where, function(err, count) { + cb(err, err ? false : count === 1); + }); } else { cb(new Error('Model::exists requires the id argument')); } @@ -311,17 +373,7 @@ DataAccessObject.exists = function exists(id, cb) { */ DataAccessObject.findById = function find(id, cb) { if (stillConnecting(this.getDataSource(), this, arguments)) return; - - this.getDataSource().connector.find(this.modelName, id, function (err, data) { - var obj = null; - if (data) { - if (!getIdValue(this, data)) { - setIdValue(this, data, id); - } - obj = new this(data, {applySetters: false, persisted: true}); - } - cb(err, obj); - }.bind(this)); + this.findOne(byIdQuery(this, id), cb); }; DataAccessObject.findByIds = function(ids, cond, cb) { @@ -683,7 +735,7 @@ DataAccessObject.find = function find(query, cb) { var self = this; query = query || {}; - + try { this._normalize(query); } catch (err) { @@ -692,6 +744,8 @@ DataAccessObject.find = function find(query, cb) { }); } + this.applyScope(query); + var near = query && geo.nearFilter(query.where); var supportsGeo = !!this.getDataSource().connector.buildNearFilter; @@ -702,6 +756,7 @@ DataAccessObject.find = function find(query, cb) { } else if (query.where) { // do in memory query // using all documents + // TODO [fabien] use default scope here? this.getDataSource().connector.all(this.modelName, {}, function (err, data) { var memory = new Memory(); var modelName = self.modelName; @@ -735,8 +790,9 @@ DataAccessObject.find = function find(query, cb) { this.getDataSource().connector.all(this.modelName, query, function (err, data) { if (data && data.forEach) { data.forEach(function (d, i) { - var obj = new self(d, {fields: query.fields, applySetters: false, persisted: true}); - + var Model = self.lookupModel(d); + var obj = new Model(d, {fields: query.fields, applySetters: false, persisted: true}); + if (query && query.include) { if (query.collect) { // The collect property indicates that the query is to return the @@ -817,34 +873,39 @@ DataAccessObject.findOne = function findOne(query, cb) { * @param {Function} [cb] Callback called with (err) */ DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyAll = function destroyAll(where, cb) { - if (stillConnecting(this.getDataSource(), this, arguments)) return; - var Model = this; + if (stillConnecting(this.getDataSource(), this, arguments)) return; + var Model = this; - if (!cb && 'function' === typeof where) { - cb = where; - where = undefined; - } - if (!where) { - this.getDataSource().connector.destroyAll(this.modelName, function (err, data) { - cb && cb(err, data); - if(!err) Model.emit('deletedAll'); - }.bind(this)); - } else { - try { - // Support an optional where object - where = removeUndefined(where); - where = this._coerce(where); - } catch (err) { - return process.nextTick(function() { - cb && cb(err); - }); - } - this.getDataSource().connector.destroyAll(this.modelName, where, function (err, data) { - cb && cb(err, data); - if(!err) Model.emit('deletedAll', where); - }.bind(this)); - } - }; + if (!cb && 'function' === typeof where) { + cb = where; + where = undefined; + } + + var query = { where: where }; + this.applyScope(query); + where = query.where; + + if (!where || (typeof where === 'object' && Object.keys(where).length === 0)) { + this.getDataSource().connector.destroyAll(this.modelName, function (err, data) { + cb && cb(err, data); + if(!err) Model.emit('deletedAll'); + }.bind(this)); + } else { + try { + // Support an optional where object + where = removeUndefined(where); + where = this._coerce(where); + } catch (err) { + return process.nextTick(function() { + cb && cb(err); + }); + } + this.getDataSource().connector.destroyAll(this.modelName, where, function (err, data) { + cb && cb(err, data); + if(!err) Model.emit('deletedAll', where); + }.bind(this)); + } +}; /** * Delete the record with the specified ID. @@ -857,16 +918,16 @@ DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyA // 'deleteById' will be used as the name for strong-remoting to keep it backward // compatible for angular SDK DataAccessObject.removeById = DataAccessObject.destroyById = DataAccessObject.deleteById = function deleteById(id, cb) { - if (stillConnecting(this.getDataSource(), this, arguments)) return; - var Model = this; - - this.getDataSource().connector.destroy(this.modelName, id, function (err) { - if ('function' === typeof cb) { - cb(err); - } - if(!err) Model.emit('deleted', id); - }.bind(this)); - }; + if (stillConnecting(this.getDataSource(), this, arguments)) return; + var Model = this; + + this.remove(byIdQuery(this, id).where, function(err) { + if ('function' === typeof cb) { + cb(err); + } + if(!err) Model.emit('deleted', id); + }); +}; /** * Return count of matched records. Optional query parameter allows you to count filtered set of model instances. @@ -888,6 +949,11 @@ DataAccessObject.count = function (where, cb) { cb = where; where = null; } + + var query = { where: where }; + this.applyScope(query); + where = query.where; + try { where = removeUndefined(where); where = this._coerce(where); @@ -896,6 +962,7 @@ DataAccessObject.count = function (where, cb) { cb && cb(err); }); } + this.getDataSource().connector.count(this.modelName, cb, where); }; @@ -926,13 +993,17 @@ DataAccessObject.prototype.save = function (options, callback) { if (!('throws' in options)) { options.throws = false; } - + var inst = this; var data = inst.toObject(true); var modelName = Model.modelName; - + + Model.applyProperties(data, this); + if (this.isNewRecord()) { return Model.create(this, callback); + } else { + inst.setAttributes(data); } // validate first @@ -1016,7 +1087,13 @@ DataAccessObject.updateAll = function (where, data, cb) { assert(typeof where === 'object', 'The where argument should be an object'); assert(typeof data === 'object', 'The data argument should be an object'); assert(cb === null || typeof cb === 'function', 'The cb argument should be a function'); - + + var query = { where: where }; + this.applyScope(query); + this.applyProperties(data); + + where = query.where; + try { where = removeUndefined(where); where = this._coerce(where); @@ -1025,6 +1102,7 @@ DataAccessObject.updateAll = function (where, data, cb) { cb && cb(err); }); } + var connector = this.getDataSource().connector; connector.update(this.modelName, where, data, cb); }; @@ -1075,7 +1153,7 @@ DataAccessObject.prototype.remove = * @param {Mixed} value Value of property */ DataAccessObject.prototype.setAttribute = function setAttribute(name, value) { - this[name] = value; + this[name] = value; // TODO [fabien] - currently not protected by applyProperties }; /** @@ -1101,6 +1179,8 @@ DataAccessObject.prototype.updateAttribute = function updateAttribute(name, valu DataAccessObject.prototype.setAttributes = function setAttributes(data) { if (typeof data !== 'object') return; + this.constructor.applyProperties(data, this); + var Model = this.constructor; var inst = this; diff --git a/lib/model.js b/lib/model.js index e12b9b4b..7e7a66f2 100644 --- a/lib/model.js +++ b/lib/model.js @@ -60,6 +60,10 @@ ModelBaseClass.prototype._initProperties = function (data, options) { } var properties = _extend({}, ctor.definition.properties); data = data || {}; + + if (typeof ctor.applyProperties === 'function') { + ctor.applyProperties(data); + } options = options || {}; var applySetters = options.applySetters; @@ -130,7 +134,7 @@ ModelBaseClass.prototype._initProperties = function (data, options) { } if (properties[p]) { // Managed property - if (applySetters) { + if (applySetters || properties[p].id) { self[p] = propVal; } else { self.__data[p] = propVal; diff --git a/lib/relation-definition.js b/lib/relation-definition.js index 2590a3a5..1fa299e4 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -3,9 +3,10 @@ */ var assert = require('assert'); var util = require('util'); +var utils = require('./utils'); var i8n = require('inflection'); var defineScope = require('./scope.js').defineScope; -var mergeQuery = require('./scope.js').mergeQuery; +var mergeQuery = utils.mergeQuery; var ModelBaseClass = require('./model.js'); var applyFilter = require('./connectors/memory').applyFilter; var ValidationError = require('./validations.js').ValidationError; diff --git a/lib/scope.js b/lib/scope.js index 6b4dafb1..dfdb364d 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -1,13 +1,14 @@ var i8n = require('inflection'); var utils = require('./utils'); var defineCachedRelations = utils.defineCachedRelations; +var setScopeValuesFromWhere = utils.setScopeValuesFromWhere; +var mergeQuery = utils.mergeQuery; var DefaultModelBaseClass = require('./model.js'); /** * Module exports */ exports.defineScope = defineScope; -exports.mergeQuery = mergeQuery; function ScopeDefinition(definition) { this.isStatic = definition.isStatic; @@ -229,35 +230,6 @@ function defineScope(cls, targetClass, name, params, methods, options) { cls['__count__' + name] = fn_count; - /* - * Extracting fixed property values for the scope from the where clause into - * the data object - * - * @param {Object} The data object - * @param {Object} The where clause - */ - function setScopeValuesFromWhere(data, where, targetModel) { - for (var i in where) { - if (i === 'and') { - // Find fixed property values from each subclauses - for (var w = 0, n = where[i].length; w < n; w++) { - setScopeValuesFromWhere(data, where[i][w], targetModel); - } - continue; - } - var prop = targetModel.definition.properties[i]; - if (prop) { - var val = where[i]; - if (typeof val !== 'object' || val instanceof prop.type - || prop.type.name === 'ObjectID') // MongoDB key - { - // Only pick the {propertyName: propertyValue} - data[i] = where[i]; - } - } - } - } - // and it should have create/build methods with binded thisModelNameId param function build(data) { data = data || {}; @@ -300,60 +272,3 @@ function defineScope(cls, targetClass, name, params, methods, options) { return definition; } - -/*! - * Merge query parameters - * @param {Object} base The base object to contain the merged results - * @param {Object} update The object containing updates to be merged - * @returns {*|Object} The base object - * @private - */ -function mergeQuery(base, update) { - if (!update) { - return; - } - base = base || {}; - if (update.where && Object.keys(update.where).length > 0) { - if (base.where && Object.keys(base.where).length > 0) { - base.where = {and: [base.where, update.where]}; - } else { - base.where = update.where; - } - } - - // Merge inclusion - if (update.include) { - if (!base.include) { - base.include = update.include; - } else { - var saved = base.include; - base.include = {}; - base.include[update.include] = saved; - } - } - if (update.collect) { - base.collect = update.collect; - } - - // set order - if (!base.order && update.order) { - base.order = update.order; - } - - // overwrite pagination - if (update.limit !== undefined) { - base.limit = update.limit; - } - if (update.skip !== undefined) { - base.skip = update.skip; - } - if (update.offset !== undefined) { - base.offset = update.offset; - } - - // Overwrite fields - if (update.fields !== undefined) { - base.fields = update.fields; - } - return base; -} diff --git a/lib/utils.js b/lib/utils.js index c774ecae..4e317058 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -7,6 +7,8 @@ exports.mergeSettings = mergeSettings; exports.isPlainObject = isPlainObject; exports.defineCachedRelations = defineCachedRelations; exports.sortObjectsByIds = sortObjectsByIds; +exports.setScopeValuesFromWhere = setScopeValuesFromWhere; +exports.mergeQuery = mergeQuery; var traverse = require('traverse'); @@ -21,6 +23,101 @@ function safeRequire(module) { } } +/* + * Extracting fixed property values for the scope from the where clause into + * the data object + * + * @param {Object} The data object + * @param {Object} The where clause + */ +function setScopeValuesFromWhere(data, where, targetModel) { + for (var i in where) { + if (i === 'and') { + // Find fixed property values from each subclauses + for (var w = 0, n = where[i].length; w < n; w++) { + setScopeValuesFromWhere(data, where[i][w], targetModel); + } + continue; + } + var prop = targetModel.definition.properties[i]; + if (prop) { + var val = where[i]; + if (typeof val !== 'object' || val instanceof prop.type + || prop.type.name === 'ObjectID') // MongoDB key + { + // Only pick the {propertyName: propertyValue} + data[i] = where[i]; + } + } + } +} + +/*! + * Merge query parameters + * @param {Object} base The base object to contain the merged results + * @param {Object} update The object containing updates to be merged + * @param {Object} spec Optionally specifies parameters to exclude (set to false) + * @returns {*|Object} The base object + * @private + */ +function mergeQuery(base, update, spec) { + if (!update) { + return; + } + spec = spec || {}; + base = base || {}; + + if (update.where && Object.keys(update.where).length > 0) { + if (base.where && Object.keys(base.where).length > 0) { + base.where = {and: [base.where, update.where]}; + } else { + base.where = update.where; + } + } + + // Merge inclusion + if (spec.include !== false && update.include) { + if (!base.include) { + base.include = update.include; + } else { + var saved = base.include; + base.include = {}; + base.include[update.include] = saved; + } + } + + if (spec.collect !== false && update.collect) { + base.collect = update.collect; + } + + // Overwrite fields + if (spec.fields !== false && update.fields !== undefined) { + base.fields = update.fields; + } + + // set order + if ((!base.order || spec.order === false) && update.order) { + base.order = update.order; + } + + // overwrite pagination + if (spec.limit !== false && update.limit !== undefined) { + base.limit = update.limit; + } + + var skip = spec.skip !== false && spec.offset !== false; + + if (skip && update.skip !== undefined) { + base.skip = update.skip; + } + + if (skip && update.offset !== undefined) { + base.offset = update.offset; + } + + return base; +} + function fieldsToArray(fields, properties) { if (!fields) return; diff --git a/test/default-scope.test.js b/test/default-scope.test.js new file mode 100644 index 00000000..0d510cff --- /dev/null +++ b/test/default-scope.test.js @@ -0,0 +1,808 @@ +// This test written in mocha+should.js +var should = require('./init.js'); +var async = require('async'); + +var db, Category, Product, Tool, Widget, Thing; + +// This test requires a connector that can +// handle a custom collection or table name + +// TODO [fabien] add table for pgsql/mysql +// TODO [fabien] change model definition - see #293 + +var setupProducts = function(ids, done) { + async.series([ + function(next) { + Tool.create({name: 'Tool Z'}, function(err, inst) { + ids.toolZ = inst.id; + next(); + }); + }, + function(next) { + Widget.create({name: 'Widget Z'}, function(err, inst) { + ids.widgetZ = inst.id; + next(); + }); + }, + function(next) { + Tool.create({name: 'Tool A', active: false}, function(err, inst) { + ids.toolA = inst.id; + next(); + }); + }, + function(next) { + Widget.create({name: 'Widget A'}, function(err, inst) { + ids.widgetA = inst.id; + next(); + }); + }, + function(next) { + Widget.create({name: 'Widget B', active: false}, function(err, inst) { + ids.widgetB = inst.id; + next(); + }); + } + ], done); +}; + +describe('default scope', function () { + + before(function (done) { + db = getSchema(); + + Category = db.define('Category', { + name: String + }); + + Product = db.define('Product', { + name: String, + kind: String, + description: String, + active: { type: Boolean, default: true } + }, { + scope: { order: 'name' }, + scopes: { active: { where: { active: true } } } + }); + + Product.lookupModel = function(data) { + var m = this.dataSource.models[data.kind]; + if (m.base === this) return m; + return this; + }; + + Tool = db.define('Tool', Product.definition.properties, { + base: 'Product', + scope: { where: { kind: 'Tool' }, order: 'name' }, + scopes: { active: { where: { active: true } } }, + mongodb: { collection: 'Product' }, + memory: { collection: 'Product' } + }); + + Widget = db.define('Widget', Product.definition.properties, { + base: 'Product', + properties: { kind: 'Widget' }, + scope: { where: { kind: 'Widget' }, order: 'name' }, + scopes: { active: { where: { active: true } } }, + mongodb: { collection: 'Product' }, + memory: { collection: 'Product' } + }); + + // inst is only valid for instance methods + // like save, updateAttributes + + var scopeFn = function(target, inst) { + return { where: { kind: this.modelName } }; + }; + + var propertiesFn = function(target, inst) { + return { kind: this.modelName }; + }; + + Thing = db.define('Thing', Product.definition.properties, { + base: 'Product', + attributes: propertiesFn, + scope: scopeFn, + mongodb: { collection: 'Product' }, + memory: { collection: 'Product' } + }); + + Category.hasMany(Product); + Category.hasMany(Tool, {scope: {order: 'name DESC'}}); + Category.hasMany(Widget); + Category.hasMany(Thing); + + Product.belongsTo(Category); + Tool.belongsTo(Category); + Widget.belongsTo(Category); + Thing.belongsTo(Category); + + db.automigrate(done); + }); + + describe('manipulation', function() { + + var ids = {}; + + before(function(done) { + db.automigrate(done); + }); + + it('should return a scoped instance', function() { + var p = new Tool({name: 'Product A', kind:'ignored'}); + p.name.should.equal('Product A'); + p.kind.should.equal('Tool'); + p.setAttributes({ kind: 'ignored' }); + p.kind.should.equal('Tool'); + + p.setAttribute('kind', 'other'); // currently not enforced + p.kind.should.equal('other'); + }); + + it('should create a scoped instance - tool', function(done) { + Tool.create({name: 'Product A', kind: 'ignored'}, function(err, p) { + should.not.exist(err); + p.name.should.equal('Product A'); + p.kind.should.equal('Tool'); + ids.productA = p.id; + done(); + }); + }); + + it('should create a scoped instance - widget', function(done) { + Widget.create({name: 'Product B', kind: 'ignored'}, function(err, p) { + should.not.exist(err); + p.name.should.equal('Product B'); + p.kind.should.equal('Widget'); + ids.productB = p.id; + done(); + }); + }); + + it('should update a scoped instance - updateAttributes', function(done) { + Tool.findById(ids.productA, function(err, p) { + p.updateAttributes({description: 'A thing...', kind: 'ingored'}, function(err, inst) { + should.not.exist(err); + p.name.should.equal('Product A'); + p.kind.should.equal('Tool'); + p.description.should.equal('A thing...'); + done(); + }); + }); + }); + + it('should update a scoped instance - save', function(done) { + Tool.findById(ids.productA, function(err, p) { + p.description = 'Something...'; + p.kind = 'ignored'; + p.save(function(err, inst) { + should.not.exist(err); + p.name.should.equal('Product A'); + p.kind.should.equal('Tool'); + p.description.should.equal('Something...'); + Tool.findById(ids.productA, function(err, p) { + p.kind.should.equal('Tool'); + done(); + }); + }); + }); + }); + + it('should update a scoped instance - updateOrCreate', function(done) { + var data = {id: ids.productA, description: 'Anything...', kind: 'ingored'}; + Tool.updateOrCreate(data, function(err, p) { + should.not.exist(err); + p.name.should.equal('Product A'); + p.kind.should.equal('Tool'); + p.description.should.equal('Anything...'); + done(); + }); + }); + + }); + + describe('findById', function() { + + var ids = {}; + + before(function (done) { + db.automigrate(setupProducts.bind(null, ids, done)); + }); + + it('should apply default scope', function(done) { + Product.findById(ids.toolA, function(err, inst) { + should.not.exist(err); + inst.name.should.equal('Tool A'); + inst.should.be.instanceof(Tool); + done(); + }); + }); + + it('should apply default scope - tool', function(done) { + Tool.findById(ids.toolA, function(err, inst) { + should.not.exist(err); + inst.name.should.equal('Tool A'); + done(); + }); + }); + + it('should apply default scope (no match)', function(done) { + Widget.findById(ids.toolA, function(err, inst) { + should.not.exist(err); + should.not.exist(inst); + done(); + }); + }); + + }); + + describe('find', function() { + + var ids = {}; + + before(function (done) { + db.automigrate(setupProducts.bind(null, ids, done)); + }); + + it('should apply default scope - order', function(done) { + Product.find(function(err, products) { + should.not.exist(err); + products.should.have.length(5); + products[0].name.should.equal('Tool A'); + products[1].name.should.equal('Tool Z'); + products[2].name.should.equal('Widget A'); + products[3].name.should.equal('Widget B'); + products[4].name.should.equal('Widget Z'); + + products[0].should.be.instanceof(Product); + products[0].should.be.instanceof(Tool); + + products[2].should.be.instanceof(Product); + products[2].should.be.instanceof(Widget); + + done(); + }); + }); + + it('should apply default scope - order override', function(done) { + Product.find({ order: 'name DESC' }, function(err, products) { + should.not.exist(err); + products.should.have.length(5); + products[0].name.should.equal('Widget Z'); + products[1].name.should.equal('Widget B'); + products[2].name.should.equal('Widget A'); + products[3].name.should.equal('Tool Z'); + products[4].name.should.equal('Tool A'); + done(); + }); + }); + + it('should apply default scope - tool', function(done) { + Tool.find(function(err, products) { + should.not.exist(err); + products.should.have.length(2); + products[0].name.should.equal('Tool A'); + products[1].name.should.equal('Tool Z'); + done(); + }); + }); + + it('should apply default scope - where (widget)', function(done) { + Widget.find({ where: { active: true } }, function(err, products) { + should.not.exist(err); + products.should.have.length(2); + products[0].name.should.equal('Widget A'); + products[1].name.should.equal('Widget Z'); + done(); + }); + }); + + it('should apply default scope - order (widget)', function(done) { + Widget.find({ order: 'name DESC' }, function(err, products) { + should.not.exist(err); + products.should.have.length(3); + products[0].name.should.equal('Widget Z'); + products[1].name.should.equal('Widget B'); + products[2].name.should.equal('Widget A'); + done(); + }); + }); + + }); + + describe('exists', function() { + + var ids = {}; + + before(function (done) { + db.automigrate(setupProducts.bind(null, ids, done)); + }); + + it('should apply default scope', function(done) { + Product.exists(ids.widgetA, function(err, exists) { + should.not.exist(err); + exists.should.be.true; + done(); + }); + }); + + it('should apply default scope - tool', function(done) { + Tool.exists(ids.toolZ, function(err, exists) { + should.not.exist(err); + exists.should.be.true; + done(); + }); + }); + + it('should apply default scope - widget', function(done) { + Widget.exists(ids.widgetA, function(err, exists) { + should.not.exist(err); + exists.should.be.true; + done(); + }); + }); + + it('should apply default scope - tool (no match)', function(done) { + Tool.exists(ids.widgetA, function(err, exists) { + should.not.exist(err); + exists.should.be.false; + done(); + }); + }); + + it('should apply default scope - widget (no match)', function(done) { + Widget.exists(ids.toolZ, function(err, exists) { + should.not.exist(err); + exists.should.be.false; + done(); + }); + }); + + }); + + describe('count', function() { + + var ids = {}; + + before(function (done) { + db.automigrate(setupProducts.bind(null, ids, done)); + }); + + it('should apply default scope - order', function(done) { + Product.count(function(err, count) { + should.not.exist(err); + count.should.equal(5); + done(); + }); + }); + + it('should apply default scope - tool', function(done) { + Tool.count(function(err, count) { + should.not.exist(err); + count.should.equal(2); + done(); + }); + }); + + it('should apply default scope - widget', function(done) { + Widget.count(function(err, count) { + should.not.exist(err); + count.should.equal(3); + done(); + }); + }); + + it('should apply default scope - where', function(done) { + Widget.count({name: 'Widget Z'}, function(err, count) { + should.not.exist(err); + count.should.equal(1); + done(); + }); + }); + + it('should apply default scope - no match', function(done) { + Tool.count({name: 'Widget Z'}, function(err, count) { + should.not.exist(err); + count.should.equal(0); + done(); + }); + }); + + }); + + describe('removeById', function() { + + var ids = {}; + + function isDeleted(id, done) { + Product.exists(id, function(err, exists) { + should.not.exist(err); + exists.should.be.false; + done(); + }); + }; + + before(function (done) { + db.automigrate(setupProducts.bind(null, ids, done)); + }); + + it('should apply default scope', function(done) { + Product.removeById(ids.widgetZ, function(err) { + should.not.exist(err); + isDeleted(ids.widgetZ, done); + }); + }); + + it('should apply default scope - tool', function(done) { + Tool.removeById(ids.toolA, function(err) { + should.not.exist(err); + isDeleted(ids.toolA, done); + }); + }); + + it('should apply default scope - no match', function(done) { + Tool.removeById(ids.widgetA, function(err) { + should.not.exist(err); + Product.exists(ids.widgetA, function(err, exists) { + should.not.exist(err); + exists.should.be.true; + done(); + }); + }); + }); + + it('should apply default scope - widget', function(done) { + Widget.removeById(ids.widgetA, function(err) { + should.not.exist(err); + isDeleted(ids.widgetA, done); + }); + }); + + it('should apply default scope - verify', function(done) { + Product.find(function(err, products) { + should.not.exist(err); + products.should.have.length(2); + products[0].name.should.equal('Tool Z'); + products[1].name.should.equal('Widget B'); + done(); + }); + }); + + }); + + describe('update', function() { + + var ids = {}; + + before(function (done) { + db.automigrate(setupProducts.bind(null, ids, done)); + }); + + it('should apply default scope', function(done) { + Widget.update({active: false},{active: true, kind: 'ignored'}, function(err) { + should.not.exist(err); + Widget.find({where: { active: true }}, function(err, products) { + should.not.exist(err); + products.should.have.length(3); + products[0].name.should.equal('Widget A'); + products[1].name.should.equal('Widget B'); + products[2].name.should.equal('Widget Z'); + done(); + }); + }); + }); + + it('should apply default scope - no match', function(done) { + Tool.update({name: 'Widget A'},{name: 'Ignored'}, function(err) { + should.not.exist(err); + Product.findById(ids.widgetA, function(err, product) { + should.not.exist(err); + product.name.should.equal('Widget A'); + done(); + }); + }); + }); + + it('should have updated within scope', function(done) { + Product.find({where: {active: true}}, function(err, products) { + should.not.exist(err); + products.should.have.length(4); + products[0].name.should.equal('Tool Z'); + products[1].name.should.equal('Widget A'); + products[2].name.should.equal('Widget B'); + products[3].name.should.equal('Widget Z'); + done(); + }); + }); + + }); + + describe('remove', function() { + + var ids = {}; + + before(function (done) { + db.automigrate(setupProducts.bind(null, ids, done)); + }); + + it('should apply default scope - custom where', function(done) { + Widget.remove({name: 'Widget A'}, function(err) { + should.not.exist(err); + Product.find(function(err, products) { + products.should.have.length(4); + products[0].name.should.equal('Tool A'); + products[1].name.should.equal('Tool Z'); + products[2].name.should.equal('Widget B'); + products[3].name.should.equal('Widget Z'); + done(); + }); + }); + }); + + it('should apply default scope - custom where (no match)', function(done) { + Tool.remove({name: 'Widget Z'}, function(err) { + should.not.exist(err); + Product.find(function(err, products) { + products.should.have.length(4); + products[0].name.should.equal('Tool A'); + products[1].name.should.equal('Tool Z'); + products[2].name.should.equal('Widget B'); + products[3].name.should.equal('Widget Z'); + done(); + }); + }); + }); + + it('should apply default scope - deleteAll', function(done) { + Tool.deleteAll(function(err) { + should.not.exist(err); + Product.find(function(err, products) { + products.should.have.length(2); + products[0].name.should.equal('Widget B'); + products[1].name.should.equal('Widget Z'); + done(); + }); + }); + }); + + it('should create a scoped instance - tool', function(done) { + Tool.create({name: 'Tool B'}, function(err, p) { + should.not.exist(err); + Product.find(function(err, products) { + products.should.have.length(3); + products[0].name.should.equal('Tool B'); + products[1].name.should.equal('Widget B'); + products[2].name.should.equal('Widget Z'); + done(); + }); + }); + }); + + it('should apply default scope - destroyAll', function(done) { + Widget.destroyAll(function(err) { + should.not.exist(err); + Product.find(function(err, products) { + products.should.have.length(1); + products[0].name.should.equal('Tool B'); + done(); + }); + }); + }); + + }); + + describe('scopes', function() { + + var ids = {}; + + before(function (done) { + db.automigrate(setupProducts.bind(null, ids, done)); + }); + + it('should merge with default scope', function(done) { + Product.active(function(err, products) { + should.not.exist(err); + products.should.have.length(3); + products[0].name.should.equal('Tool Z'); + products[1].name.should.equal('Widget A'); + products[2].name.should.equal('Widget Z'); + done(); + }); + }); + + it('should merge with default scope - tool', function(done) { + Tool.active(function(err, products) { + should.not.exist(err); + products.should.have.length(1); + products[0].name.should.equal('Tool Z'); + done(); + }); + }); + + it('should merge with default scope - widget', function(done) { + Widget.active(function(err, products) { + should.not.exist(err); + products.should.have.length(2); + products[0].name.should.equal('Widget A'); + products[1].name.should.equal('Widget Z'); + done(); + }); + }); + + }); + + describe('scope function', function() { + + before(function(done) { + db.automigrate(done); + }); + + it('should create a scoped instance - widget', function(done) { + Widget.create({name: 'Product', kind:'ignored'}, function(err, p) { + p.name.should.equal('Product'); + p.kind.should.equal('Widget'); + done(); + }); + }); + + it('should create a scoped instance - thing', function(done) { + Thing.create({name: 'Product', kind:'ignored'}, function(err, p) { + p.name.should.equal('Product'); + p.kind.should.equal('Thing'); + done(); + }); + }); + + it('should find a scoped instance - widget', function(done) { + Widget.findOne({where: {name: 'Product'}}, function(err, p) { + p.name.should.equal('Product'); + p.kind.should.equal('Widget'); + done(); + }); + }); + + it('should find a scoped instance - thing', function(done) { + Thing.findOne({where: {name: 'Product'}}, function(err, p) { + p.name.should.equal('Product'); + p.kind.should.equal('Thing'); + done(); + }); + }); + + it('should find a scoped instance - thing', function(done) { + Product.find({where: {name: 'Product'}}, function(err, products) { + products.should.have.length(2); + products[0].name.should.equal('Product'); + products[1].name.should.equal('Product'); + var kinds = products.map(function(p) { return p.kind; }) + kinds.sort(); + kinds.should.eql(['Thing', 'Widget']); + done(); + }); + }); + + }); + + describe('relations', function() { + + var ids = {}; + + before(function (done) { + db.automigrate(done); + }); + + before(function (done) { + Category.create({name: 'Category A'}, function(err, cat) { + ids.categoryA = cat.id; + async.series([ + function(next) { + cat.widgets.create({name: 'Widget B', kind: 'ignored'}, next); + }, + function(next) { + cat.widgets.create({name: 'Widget A'}, next); + }, + function(next) { + cat.tools.create({name: 'Tool A'}, next); + }, + function(next) { + cat.things.create({name: 'Thing A'}, next); + } + ], done); + }); + }); + + it('should apply default scope - products', function(done) { + Category.findById(ids.categoryA, function(err, cat) { + should.not.exist(err); + cat.products(function(err, products) { + should.not.exist(err); + products.should.have.length(4); + products[0].name.should.equal('Thing A'); + products[1].name.should.equal('Tool A'); + products[2].name.should.equal('Widget A'); + products[3].name.should.equal('Widget B'); + + products[0].should.be.instanceof(Product); + products[0].should.be.instanceof(Thing); + + products[1].should.be.instanceof(Product); + products[1].should.be.instanceof(Tool); + + products[2].should.be.instanceof(Product); + products[2].should.be.instanceof(Widget); + + done(); + }); + }); + }); + + it('should apply default scope - widgets', function(done) { + Category.findById(ids.categoryA, function(err, cat) { + should.not.exist(err); + cat.widgets(function(err, products) { + should.not.exist(err); + products.should.have.length(2); + products[0].should.be.instanceof(Widget); + products[0].name.should.equal('Widget A'); + products[1].name.should.equal('Widget B'); + products[0].category(function(err, inst) { + inst.name.should.equal('Category A'); + done(); + }); + }); + }); + }); + + it('should apply default scope - tools', function(done) { + Category.findById(ids.categoryA, function(err, cat) { + should.not.exist(err); + cat.tools(function(err, products) { + should.not.exist(err); + products.should.have.length(1); + products[0].should.be.instanceof(Tool); + products[0].name.should.equal('Tool A'); + products[0].category(function(err, inst) { + inst.name.should.equal('Category A'); + done(); + }); + }); + }); + }); + + it('should apply default scope - things', function(done) { + Category.findById(ids.categoryA, function(err, cat) { + should.not.exist(err); + cat.things(function(err, products) { + should.not.exist(err); + products.should.have.length(1); + products[0].should.be.instanceof(Thing); + products[0].name.should.equal('Thing A'); + products[0].category(function(err, inst) { + inst.name.should.equal('Category A'); + done(); + }); + }); + }); + }); + + it('should create related item with default scope', function(done) { + Category.findById(ids.categoryA, function(err, cat) { + cat.tools.create({name: 'Tool B'}, done); + }); + }); + + it('should use relation scope order', function(done) { + Category.findById(ids.categoryA, function(err, cat) { + should.not.exist(err); + cat.tools(function(err, products) { + should.not.exist(err); + products.should.have.length(2); + products[0].name.should.equal('Tool B'); + products[1].name.should.equal('Tool A'); + done(); + }); + }); + }); + + }); + +}); diff --git a/test/loopback-dl.test.js b/test/loopback-dl.test.js index 10c9c10e..41d600f6 100644 --- a/test/loopback-dl.test.js +++ b/test/loopback-dl.test.js @@ -639,7 +639,7 @@ describe('Load models with base', function () { assert(Customer.prototype.instanceMethod === User.prototype.instanceMethod); assert.equal(Customer.base, User); assert.equal(Customer.base, Customer.super_); - + try { var Customer1 = ds.define('Customer1', {vip: Boolean}, {base: 'User1'}); } catch (e) { diff --git a/test/memory.test.js b/test/memory.test.js index 453ac9e8..66ad2346 100644 --- a/test/memory.test.js +++ b/test/memory.test.js @@ -269,6 +269,48 @@ describe('Memory connector', function () { } }); + + it('should use collection setting', function (done) { + var ds = new DataSource({ + connector: 'memory' + }); + + var Product = ds.createModel('Product', { + name: String + }); + + var Tool = ds.createModel('Tool', { + name: String + }, {memory: {collection: 'Product'}}); + + var Widget = ds.createModel('Widget', { + name: String + }, {memory: {collection: 'Product'}}); + + ds.connector.getCollection('Tool').should.equal('Product'); + ds.connector.getCollection('Widget').should.equal('Product'); + + async.series([ + function(next) { + Tool.create({ name: 'Tool A' }, next); + }, + function(next) { + Tool.create({ name: 'Tool B' }, next); + }, + function(next) { + Widget.create({ name: 'Widget A' }, next); + } + ], function(err) { + Product.find(function(err, products) { + should.not.exist(err); + products.should.have.length(3); + products[0].toObject().should.eql({ name: 'Tool A', id: 1 }); + products[1].toObject().should.eql({ name: 'Tool B', id: 2 }); + products[2].toObject().should.eql({ name: 'Widget A', id: 3 }); + done(); + }); + }); + }); }); diff --git a/test/relations.test.js b/test/relations.test.js index 12e78d68..f6848d11 100644 --- a/test/relations.test.js +++ b/test/relations.test.js @@ -335,7 +335,7 @@ describe('relations', function () { physician.patients.findById(id, function (err, ch) { should.not.exist(err); should.exist(ch); - ch.id.should.equal(id); + ch.id.should.eql(id); done(); }); } @@ -387,7 +387,7 @@ describe('relations', function () { physician.patients.findById(id, function (err, ch) { should.not.exist(err); should.exist(ch); - ch.id.should.equal(id); + ch.id.should.eql(id); ch.name.should.equal('aa'); done(); }); @@ -1659,7 +1659,7 @@ describe('relations', function () { should.not.exist(e); should.exist(tags); - article.tags().should.eql(tags); + article.tagNames().should.eql(tags); done(); });