diff --git a/lib/model.js b/lib/model.js index aaf5e29f..51a59397 100644 --- a/lib/model.js +++ b/lib/model.js @@ -158,10 +158,18 @@ ModelBaseClass.prototype._initProperties = function (data, options) { var def = properties[p]['default']; if (def !== undefined) { if (typeof def === 'function') { - self.__data[p] = def(); - } else { - self.__data[p] = def; + if (def === Date) { + // FIXME: We should coerce the value in general + // This is a work around to {default: Date} + // Date() will return a string instead of Date + def = new Date(); + } else { + def = def(); + } } + // FIXME: We should coerce the value + // will implement it after we refactor the PropertyDefinition + self.__data[p] = def; } } diff --git a/lib/relation-definition.js b/lib/relation-definition.js index 55add0b9..bf32e5a3 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -60,10 +60,10 @@ function RelationDefinition(definition) { this.type = normalizeType(definition.type); assert(this.type, 'Invalid relation type: ' + definition.type); this.modelFrom = definition.modelFrom; - assert(this.modelFrom); + assert(this.modelFrom, 'Source model is required'); this.keyFrom = definition.keyFrom; this.modelTo = definition.modelTo; - assert(this.modelTo); + assert(this.modelTo, 'Target model is required'); this.keyTo = definition.keyTo; this.modelThrough = definition.modelThrough; this.keyThrough = definition.keyThrough; @@ -106,6 +106,15 @@ function Relation(definition, modelInstance) { this.modelInstance = modelInstance; } +Relation.prototype.resetCache = function (cache) { + cache = cache || undefined; + this.modelInstance.__cachedRelations[this.definition.name] = cache; +}; + +Relation.prototype.getCache = function () { + return this.modelInstance.__cachedRelations[this.definition.name]; +}; + /** * HasMany subclass * @param {RelationDefinition|Object} definition @@ -124,6 +133,39 @@ function HasMany(definition, modelInstance) { util.inherits(HasMany, Relation); +HasMany.prototype.removeFromCache = function (id) { + var cache = this.modelInstance.__cachedRelations[this.definition.name]; + var idName = this.definition.modelTo.definition.idName(); + if (Array.isArray(cache)) { + for (var i = 0, n = cache.length; i < n; i++) { + if (cache[i][idName] === id) { + return cache.splice(i, 1); + } + } + } + return null; +}; + +HasMany.prototype.addToCache = function (inst) { + if (!inst) { + return; + } + var cache = this.modelInstance.__cachedRelations[this.definition.name]; + if (cache === undefined) { + cache = this.modelInstance.__cachedRelations[this.definition.name] = []; + } + var idName = this.definition.modelTo.definition.idName(); + if (Array.isArray(cache)) { + for (var i = 0, n = cache.length; i < n; i++) { + if (cache[i][idName] === inst[idName]) { + cache[i] = inst; + return; + } + } + cache.push(inst); + } +}; + /** * HasManyThrough subclass * @param {RelationDefinition|Object} definition @@ -210,7 +252,7 @@ function findBelongsTo(modelFrom, modelTo, keyTo) { var rel = relations[keys[k]]; if (rel.type === RelationTypes.belongsTo && rel.modelTo === modelTo && - rel.keyTo === keyTo) { + (keyTo === undefined || rel.keyTo === keyTo)) { return rel.keyFrom; } } @@ -356,6 +398,7 @@ HasMany.prototype.findById = function (id, cb) { }; HasMany.prototype.destroyById = function (id, cb) { + var self = this; var modelTo = this.definition.modelTo; var fk = this.definition.keyTo; var pk = this.definition.keyFrom; @@ -369,6 +412,7 @@ HasMany.prototype.destroyById = function (id, cb) { } // Check if the foreign key matches the primary key if (inst[fk] && inst[fk].toString() === modelInstance[pk].toString()) { + self.removeFromCache(inst[fk]); inst.destroy(cb); } else { cb(new Error('Permission denied: foreign key does not match the primary key')); @@ -379,6 +423,7 @@ HasMany.prototype.destroyById = function (id, cb) { // Create an instance of the target model and connect it to the instance of // the source model by creating an instance of the through model HasManyThrough.prototype.create = function create(data, done) { + var self = this; var definition = this.definition; var modelTo = definition.modelTo; var modelThrough = definition.modelThrough; @@ -391,27 +436,28 @@ HasManyThrough.prototype.create = function create(data, done) { var modelInstance = this.modelInstance; // First create the target model - modelTo.create(data, function (err, ac) { + modelTo.create(data, function (err, to) { if (err) { - return done && done(err, ac); + return done && done(err, to); } // The primary key for the target model - var pk2 = modelTo.dataSource.idName(modelTo.modelName) || 'id'; + var pk2 = definition.modelTo.definition.idName(); var fk1 = findBelongsTo(modelThrough, definition.modelFrom, definition.keyFrom); var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2); var d = {}; d[fk1] = modelInstance[definition.keyFrom]; - d[fk2] = ac[pk2]; + d[fk2] = to[pk2]; // Then create the through model - modelThrough.create(d, function (e) { + modelThrough.create(d, function (e, through) { if (e) { // Undo creation of the target model - ac.destroy(function () { + to.destroy(function () { done && done(e); }); } else { - done && done(err, ac); + self.addToCache(to); + done && done(err, to); } }); }); @@ -422,9 +468,9 @@ HasManyThrough.prototype.create = function create(data, done) { * @param {Object|ID} acInst The actual instance or id value */ HasManyThrough.prototype.add = function (acInst, done) { + var self = this; var definition = this.definition; var modelThrough = definition.modelThrough; - var modelTo = definition.modelTo; var pk1 = definition.keyFrom; var data = {}; @@ -434,7 +480,7 @@ HasManyThrough.prototype.add = function (acInst, done) { definition.keyFrom); // The primary key for the target model - var pk2 = modelTo.dataSource.idName(modelTo.modelName) || 'id'; + var pk2 = definition.modelTo.definition.idName(); var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2); @@ -445,7 +491,14 @@ HasManyThrough.prototype.add = function (acInst, done) { data[fk2] = acInst[pk2] || acInst; // Create an instance of the through model - modelThrough.findOrCreate({where: query}, data, done); + modelThrough.findOrCreate({where: query}, data, function(err, ac) { + if(!err) { + if (acInst instanceof definition.modelTo) { + self.addToCache(acInst); + } + } + done(err, ac); + }); }; /** @@ -453,13 +506,30 @@ HasManyThrough.prototype.add = function (acInst, done) { * @param {Object|ID) acInst The actual instance or id value */ HasManyThrough.prototype.remove = function (acInst, done) { - var modelThrough = this.definition.modelThrough; - var fk2 = this.definition.keyThrough; - var pk = this.definition.keyFrom; + var self = this; + var definition = this.definition; + var modelThrough = definition.modelThrough; + var pk1 = definition.keyFrom; - var q = {}; - q[fk2] = acInst[pk] || acInst; - modelThrough.deleteAll(q, done ); + var query = {}; + + var fk1 = findBelongsTo(modelThrough, definition.modelFrom, + definition.keyFrom); + + // The primary key for the target model + var pk2 = definition.modelTo.definition.idName(); + + var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2); + + query[fk1] = this.modelInstance[pk1]; + query[fk2] = acInst[pk2] || acInst; + + modelThrough.deleteAll(query, function (err) { + if (!err) { + self.removeFromCache(query[fk2]); + } + done(err); + }); }; @@ -539,6 +609,7 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { }; BelongsTo.prototype.create = function(targetModelData, cb) { + var self = this; var modelTo = this.definition.modelTo; var fk = this.definition.keyTo; var pk = this.definition.keyFrom; @@ -547,6 +618,7 @@ BelongsTo.prototype.create = function(targetModelData, cb) { modelTo.create(targetModelData, function(err, targetModel) { if(!err) { modelInstance[fk] = targetModel[pk]; + self.resetCache(targetModel); cb && cb(err, targetModel); } else { cb && cb(err); @@ -571,11 +643,11 @@ BelongsTo.prototype.build = function(targetModelData) { * @returns {*} */ BelongsTo.prototype.related = function (refresh, params) { + var self = this; var modelTo = this.definition.modelTo; var pk = this.definition.keyTo; var fk = this.definition.keyFrom; var modelInstance = this.modelInstance; - var relationName = this.definition.name; if (arguments.length === 1) { params = refresh; @@ -585,13 +657,12 @@ BelongsTo.prototype.related = function (refresh, params) { } var cachedValue; - if (!refresh && modelInstance.__cachedRelations - && (modelInstance.__cachedRelations[relationName] !== undefined)) { - cachedValue = modelInstance.__cachedRelations[relationName]; + if (!refresh) { + cachedValue = self.getCache(); } if (params instanceof ModelBaseClass) { // acts as setter modelInstance[fk] = params[pk]; - modelInstance.__cachedRelations[relationName] = params; + self.resetCache(params); } else if (typeof params === 'function') { // acts as async getter var cb = params; if (cachedValue === undefined) { @@ -604,9 +675,10 @@ BelongsTo.prototype.related = function (refresh, params) { } // Check if the foreign key matches the primary key if (inst[pk] === modelInstance[fk]) { + self.resetCache(inst); cb(null, inst); } else { - cb(new Error('Permission denied')); + cb(new Error('Permission denied: foreign key does not match the primary key')); } }); return modelInstance[fk]; @@ -618,7 +690,7 @@ BelongsTo.prototype.related = function (refresh, params) { return modelInstance[fk]; } else { // setter modelInstance[fk] = params; - delete modelInstance.__cachedRelations[relationName]; + self.resetCache(); } }; @@ -734,15 +806,19 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) { * @param {Object} The newly created target model instance */ HasOne.prototype.create = function(targetModelData, cb) { + var self = this; var modelTo = this.definition.modelTo; var fk = this.definition.keyTo; var pk = this.definition.keyFrom; var modelInstance = this.modelInstance; + var relationName = this.definition.name targetModelData = targetModelData || {}; targetModelData[fk] = modelInstance[pk]; modelTo.create(targetModelData, function(err, targetModel) { if(!err) { + // Refresh the cache + self.resetCache(targetModel); cb && cb(err, targetModel); } else { cb && cb(err); @@ -776,6 +852,7 @@ HasOne.prototype.build = function(targetModelData) { * @returns {Object} */ HasOne.prototype.related = function (refresh, params) { + var self = this; var modelTo = this.definition.modelTo; var fk = this.definition.keyTo; var pk = this.definition.keyFrom; @@ -790,13 +867,12 @@ HasOne.prototype.related = function (refresh, params) { } var cachedValue; - if (!refresh && modelInstance.__cachedRelations - && (modelInstance.__cachedRelations[relationName] !== undefined)) { - cachedValue = modelInstance.__cachedRelations[relationName]; + if (!refresh) { + cachedValue = self.getCache(); } if (params instanceof ModelBaseClass) { // acts as setter params[fk] = modelInstance[pk]; - modelInstance.__cachedRelations[relationName] = params; + self.resetCache(params); } else if (typeof params === 'function') { // acts as async getter var cb = params; if (cachedValue === undefined) { @@ -811,6 +887,7 @@ HasOne.prototype.related = function (refresh, params) { } // Check if the foreign key matches the primary key if (inst[fk] === modelInstance[pk]) { + self.resetCache(inst); cb(null, inst); } else { cb(new Error('Permission denied')); @@ -825,6 +902,6 @@ HasOne.prototype.related = function (refresh, params) { return modelInstance[pk]; } else { // setter params[fk] = modelInstance[pk]; - delete modelInstance.__cachedRelations[relationName]; + self.resetCache(); } }; diff --git a/test/loopback-dl.test.js b/test/loopback-dl.test.js index 095afb6c..aeda8992 100644 --- a/test/loopback-dl.test.js +++ b/test/loopback-dl.test.js @@ -264,7 +264,7 @@ describe('DataSource define model', function () { name: String, bio: ModelBuilder.Text, approved: Boolean, - joinedAt: Date, + joinedAt: {type: Date, default: Date}, age: Number }); @@ -280,6 +280,8 @@ describe('DataSource define model', function () { assert.equal(user.name, 'Joe'); assert.equal(user.group, 'G1'); + assert(user.joinedAt instanceof Date); + // setup relationships User.hasMany(Post, {as: 'posts', foreignKey: 'userId'});