Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

With association autoFetch cache may return incomplete instance. #685

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
6 changes: 5 additions & 1 deletion lib/Associations/Extend.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,11 @@ function autoFetchInstance(Instance, association, opts, cb) {
opts.autoFetchLimit = association.autoFetchLimit;
}

if (opts.autoFetchLimit === 0 || (!opts.autoFetch && !association.autoFetch)) {
// autoFetchLimit has been reached -
// opts.autoFetch is present, so it overrides association.autoFetch
// opts.autoFetch not present, use value from original assn definition
if (opts.autoFetchLimit === 0 || (typeof opts.autoFetch !== "undefined" && ! opts.autoFetch)
|| (typeof opts.autoFetch === "undefined" && !association.autoFetch)) {
return cb();
}

Expand Down
6 changes: 5 additions & 1 deletion lib/Associations/Many.js
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,11 @@ function autoFetchInstance(Instance, association, opts, cb) {
opts.autoFetchLimit = association.autoFetchLimit;
}

if (opts.autoFetchLimit === 0 || (!opts.autoFetch && !association.autoFetch)) {
// autoFetchLimit has been reached -
// opts.autoFetch is present, so it overrides association.autoFetch
// opts.autoFetch not present, use value from original assn definition
if (opts.autoFetchLimit === 0 || (typeof opts.autoFetch !== "undefined" && ! opts.autoFetch)
|| (typeof opts.autoFetch === "undefined" && !association.autoFetch)) {
return cb();
}

Expand Down
6 changes: 5 additions & 1 deletion lib/Associations/One.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,11 @@ function autoFetchInstance(Instance, association, opts, cb) {
opts.autoFetchLimit = association.autoFetchLimit;
}

if (opts.autoFetchLimit === 0 || (!opts.autoFetch && !association.autoFetch)) {
// autoFetchLimit has been reached -
// opts.autoFetch is present, so it overrides association.autoFetch
// opts.autoFetch not present, use value from original assn definition
if (opts.autoFetchLimit === 0 || (typeof opts.autoFetch !== "undefined" && ! opts.autoFetch)
|| (typeof opts.autoFetch === "undefined" && !association.autoFetch)) {
return cb();
}

Expand Down
38 changes: 30 additions & 8 deletions lib/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function Model(opts) {
}

var assoc_opts = {
autoFetch : inst_opts.autoFetch || false,
autoFetch : inst_opts.autoFetch,
autoFetchLimit : inst_opts.autoFetchLimit,
cascadeRemove : inst_opts.cascadeRemove
};
Expand All @@ -92,6 +92,21 @@ function Model(opts) {
ExtendAssociation.extend(model, instance, opts.driver, extend_associations, assoc_opts);
};

// check all the auto-fetch associations have been populated for the given instance
var hasAutoFetchAssociationsLoaded = function(instance) {
var checkAssn = function(associations) {
for (var assn = 0; assn < associations.length; assn++) {
if(associations[assn].autoFetch && ! instance.hasOwnProperty(associations[assn].name)) {
return false;
}
};
return true;
};
return checkAssn(one_associations) &&
checkAssn(many_associations) &&
checkAssn(extend_associations);
};

var pending = 2, create_err = null;
var instance = new Instance(model, {
uid : inst_opts.uid, // singleton unique id
Expand All @@ -112,6 +127,7 @@ function Model(opts) {
extend_associations : extend_associations,
association_properties : association_properties,
setupAssociations : setupAssociations,
hasAutoFetchAssociationsLoaded: hasAutoFetchAssociationsLoaded,
fieldToPropertyMap : fieldToPropertyMap,
keyProperties : keyProperties
});
Expand Down Expand Up @@ -275,9 +291,6 @@ function Model(opts) {
conditions[prop.mapsTo] = ids[i];
}

if (!options.hasOwnProperty("autoFetch")) {
options.autoFetch = opts.autoFetch;
}
if (!options.hasOwnProperty("autoFetchLimit")) {
options.autoFetchLimit = opts.autoFetchLimit;
}
Expand All @@ -299,7 +312,8 @@ function Model(opts) {

Singleton.get(uid, {
cache : (options.hasOwnProperty("cache") ? options.cache : opts.cache),
save_check : opts.settings.get("instance.cacheSaveCheck")
save_check : opts.settings.get("instance.cacheSaveCheck"),
autoFetch : options.autoFetch
}, function (cb) {
return createInstance(data[0], {
uid : uid,
Expand Down Expand Up @@ -406,15 +420,23 @@ function Model(opts) {
uid += "/" + data[opts.keys[i]];
}

// Now we can do the cache lookup
// When there is extra_info we need a slightly longer uid for the Singleton lookup
if(merge && options.extra_info) {
for(var j=0; j<options.extra_info.id.length; j++) {
uid += "/" + options.extra_info.id[j];
}
}

// Now we can do the cache lookup
Singleton.get(uid, {
cache : options.cache,
save_check : opts.settings.get("instance.cacheSaveCheck")
save_check : opts.settings.get("instance.cacheSaveCheck"),
autoFetch : options.autoFetch
}, function (cb) {
return createInstance(data, {
uid : uid,
autoSave : opts.autoSave,
autoFetch : (options.autoFetchLimit === 0 ? false : (options.autoFetch || opts.autoFetch)),
autoFetch : (options.autoFetchLimit === 0 ? false : options.autoFetch),
autoFetchLimit : options.autoFetchLimit,
cascadeRemove : options.cascadeRemove,
extra : options.extra,
Expand Down
15 changes: 12 additions & 3 deletions lib/Singleton.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,26 @@ exports.get = function (key, opts, createCb, returnCb) {
if (opts && opts.cache === false) {
return createCb(returnCb);
}
// we've got a custom autoFetch setting for this get/find so don't use the cache
if(typeof opts.autoFetch !== "undefined") {
return createCb(returnCb);
}
if (map.hasOwnProperty(key)) {
if (opts && opts.save_check && typeof map[key].o.saved === "function" && !map[key].o.saved()) {
// if not saved, don't return it, fetch original from db
// if not saved, don't return it, fetch original from db, but don't refresh the cache
return createCb(returnCb);
} else if (map[key].t !== null && map[key].t <= Date.now()) {
delete map[key];
} else {
return returnCb(null, map[key].o);
} else {
var instance = map[key].o;
if(instance.__opts.hasAutoFetchAssociationsLoaded(instance)) {
// instance in cache has all auto-fetch associations populated - we can re-use it
return returnCb(null, instance);
}
}
}

// Can't re-use item in cache - or it's not in cache - re-fetch from db & load the cache
createCb(function (err, value) {
if (err) return returnCb(err);

Expand Down
55 changes: 54 additions & 1 deletion test/integration/association-extend.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ describe("Model.extendsTo()", function() {
var db = null;
var Person = null;
var PersonAddress = null;
var johnId = null;

var setup = function () {
return function (done) {
Expand All @@ -24,7 +25,7 @@ describe("Model.extendsTo()", function() {
name: "John Doe"
}, function (err, person) {
should.not.exist(err);

johnId = person[Person.id];
return person.setAddress(new PersonAddress({
street : "Liberty",
number : 123
Expand Down Expand Up @@ -97,6 +98,49 @@ describe("Model.extendsTo()", function() {
Person.find().first(function (err, John) {
should.equal(err, null);

John.should.not.have.property("address");

John.getAddress(function (err, Address) {
should.equal(err, null);
Address.should.be.a("object");
Address.should.have.property("street", "Liberty");

return done();
});
});
});

it("should honor autoFetch:TRUE on find", function (done) {
Person.find({name:"John Doe"}, {autoFetch:true}).first(function (err, John) {
should.equal(err, null);

John.should.have.property("address");

John.getAddress(function (err, Address) {
should.equal(err, null);
Address.should.be.a("object");
Address.should.have.property("street", "Liberty");

return done();
});
});
});

it("should honor autoFetch:FALSE on find", function (done) {
Person.find({name:"John Doe"}, {autoFetch:false}).first(function (err, John) {
should.equal(err, null);

John.should.not.have.property("address");
return done();
});
});

it("should honor autoFetch:TRUE on get", function (done) {
Person.get(johnId, {autoFetch:true}, function (err, John) {
should.equal(err, null);

John.should.have.property("address");

John.getAddress(function (err, Address) {
should.equal(err, null);
Address.should.be.a("object");
Expand All @@ -107,6 +151,15 @@ describe("Model.extendsTo()", function() {
});
});

it("should honor autoFetch:FALSE on get", function (done) {
Person.get(johnId, {autoFetch:false}, function (err, John) {
should.equal(err, null);

John.should.not.have.property("address");
return done();
});
});

it("should return error if not found", function (done) {
Person.find().first(function (err, John) {
should.equal(err, null);
Expand Down
93 changes: 55 additions & 38 deletions test/integration/association-hasmany-extra.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,43 @@ describe("hasMany extra properties", function() {
var Person = null;
var Pet = null;

var data = { adopted: true };
var since14 = new Date("2014-01-01");
var since15 = new Date("1915-04-25");

var setup = function (opts) {
opts = opts || {};
return function (done) {
db.settings.set('instance.cache', false);

Person = db.define('person', {
name : String,
name : String
}, opts);
Pet = db.define('pet', {
name : String
});
}, opts);
Person.hasMany('pets', Pet, {
since : Date,
data : Object
});

return helper.dropSync([ Person, Pet ], done);
};
};
return helper.dropSync([ Person, Pet ], function(err) {
if (err) return done(err);
Person.create([{name : "John"},
{name : "Jane"}], function (err, people) {

if (err) return done(err);
Pet.create([{name : "Deco"},
{name : "Mutt"}], function (err, pets) {

if (err) return done(err);
people[0].addPets(pets, { since : since14, data: data }, function (err) {
people[1].addPets(pets, { since : since15, data: data }, done);
});
});
});
});
};
};

before(function(done) {
this.timeout(4000);
Expand All @@ -36,45 +54,44 @@ describe("hasMany extra properties", function() {
});
});

[true, false].forEach(function(cache) {
describe("if passed to addAccessor", function () {
before(setup());
before(setup( {cache:cache}));

it("should be added to association", function (done) {
Person.create([{
name : "John"
}], function (err, people) {
Pet.create([{
name : "Deco"
}, {
name : "Mutt"
}], function (err, pets) {
var data = { adopted: true };
it("should be added to association and correctly retrieved " + (cache ? "with" : "without") + " the cache", function (done) {
Person.find({ name: "John" }, { autoFetch : true }).first(function (err, John) {
should.equal(err, null);

people[0].addPets(pets, { since : new Date(), data: data }, function (err) {
should.equal(err, null);
John.should.have.property("pets");
should(Array.isArray(John.pets));

Person.find({ name: "John" }, { autoFetch : true }).first(function (err, John) {
should.equal(err, null);
John.pets.length.should.equal(2);

John.should.have.property("pets");
should(Array.isArray(pets));
John.pets[0].should.have.property("name");
John.pets[0].should.have.property("extra");
John.pets[0].extra.should.be.a("object");
John.pets[0].extra.should.have.property("since");
should(John.pets[0].extra.since instanceof Date);
should.equal(John.pets[0].extra.since.getFullYear(), since14.getFullYear());

John.pets.length.should.equal(2);
should.equal(typeof John.pets[0].extra.data, 'object');
should.equal(JSON.stringify(data), JSON.stringify(John.pets[0].extra.data));

John.pets[0].should.have.property("name");
John.pets[0].should.have.property("extra");
John.pets[0].extra.should.be.a("object");
John.pets[0].extra.should.have.property("since");
should(John.pets[0].extra.since instanceof Date);
// Do a second retrieve (which will break the cache)
Person.find({name: "Jane"}, {autoFetch:true}).first(function(err, Jane) {
should.equal(err, null);

should.equal(typeof John.pets[0].extra.data, 'object');
should.equal(JSON.stringify(data), JSON.stringify(John.pets[0].extra.data));
Jane.pets[0].should.have.property("name");
Jane.pets[0].should.have.property("extra");
Jane.pets[0].extra.should.be.a("object");
Jane.pets[0].extra.should.have.property("since");
should(Jane.pets[0].extra.since instanceof Date);
should.equal(Jane.pets[0].extra.since.getFullYear(), since15.getFullYear());

return done();
});
});
});
});
});
});
return done();
})
});
});
});
});
});
Loading