Merge pull request #599 from PradnyaBaviskar/issue-441

Add new hook 'loaded'
This commit is contained in:
Miroslav Bajtoš 2015-06-24 16:35:50 +02:00
commit 293240d752
2 changed files with 612 additions and 122 deletions

View File

@ -285,21 +285,39 @@ DataAccessObject.create = function (data, options, cb) {
} }
obj.__persisted = true; obj.__persisted = true;
saveDone.call(obj, function () { var context = {
createDone.call(obj, function () { Model: Model,
if (err) { data: val,
return cb(err, obj); isNewInstance: true,
} hookState: hookState,
var context = { options: options
Model: Model, };
instance: obj, Model.notifyObserversOf('loaded', context, function(err) {
isNewInstance: true,
hookState: hookState, // By default, the instance passed to create callback is NOT updated
options: options // with the changes made through persist/loaded hooks. To preserve
}; // backwards compatibility, we introduced a new setting updateOnLoad,
Model.notifyObserversOf('after save', context, function(err) { // which if set, will apply these changes to the model instance too.
cb(err, obj); if(Model.settings.updateOnLoad) {
if(!err) Model.emit('changed', obj); obj.setAttributes(context.data);
}
saveDone.call(obj, function () {
createDone.call(obj, function () {
if (err) {
return cb(err, obj);
}
var context = {
Model: Model,
instance: obj,
isNewInstance: true,
hookState: hookState,
options: options
};
Model.notifyObserversOf('after save', context, function(err) {
cb(err, obj);
if(!err) Model.emit('changed', obj);
});
}); });
}); });
}); });
@ -475,33 +493,42 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
}); });
} }
function done(err, data, info) { function done(err, data, info) {
var obj; var context = {
if (data && !(data instanceof Model)) { Model: Model,
inst._initProperties(data, { persisted: true }); data: data,
obj = inst; hookState: ctx.hookState,
} else { options: options
obj = data; };
} Model.notifyObserversOf('loaded', context, function(err) {
if (err) { var obj;
cb(err, obj); if (data && !(data instanceof Model)) {
if(!err) { inst._initProperties(data, { persisted: true });
Model.emit('changed', inst); obj = inst;
} else {
obj = data;
} }
} else { if (err) {
var context = {
Model: Model,
instance: obj,
isNewInstance: info ? info.isNewInstance : undefined,
hookState: hookState,
options: options
};
Model.notifyObserversOf('after save', context, function(err) {
cb(err, obj); cb(err, obj);
if(!err) { if(!err) {
Model.emit('changed', inst); Model.emit('changed', inst);
} }
}); } else {
} var context = {
Model: Model,
instance: obj,
isNewInstance: info ? info.isNewInstance : undefined,
hookState: hookState,
options: options
};
Model.notifyObserversOf('after save', context, function(err) {
cb(err, obj);
if(!err) {
Model.emit('changed', inst);
}
});
}
});
} }
}); });
} else { } else {
@ -586,36 +613,46 @@ DataAccessObject.findOrCreate = function findOrCreate(query, data, options, cb)
function _findOrCreate(query, data, currentInstance) { function _findOrCreate(query, data, currentInstance) {
var modelName = self.modelName; var modelName = self.modelName;
function findOrCreateCallback(err, data, created) { function findOrCreateCallback(err, data, created) {
var obj, Model = self.lookupModel(data); var context = {
Model: Model,
data: data,
isNewInstance: created,
hookState: hookState,
options: options
};
Model.notifyObserversOf('loaded', context, function(err) {
var obj, Model = self.lookupModel(data);
if (data) { if (data) {
obj = new Model(data, {fields: query.fields, applySetters: false, obj = new Model(data, {fields: query.fields, applySetters: false,
persisted: true}); persisted: true});
} }
if (created) { if (created) {
var context = { var context = {
Model: Model, Model: Model,
instance: obj, instance: obj,
isNewInstance: true, isNewInstance: true,
hookState: hookState, hookState: hookState,
options: options options: options
}; };
Model.notifyObserversOf('after save', context, function(err) { Model.notifyObserversOf('after save', context, function(err) {
if (cb.promise) {
cb(err, [obj, created]);
} else {
cb(err, obj, created);
}
if (!err) Model.emit('changed', obj);
});
} else {
if (cb.promise) { if (cb.promise) {
cb(err, [obj, created]); cb(err, [obj, created]);
} else { } else {
cb(err, obj, created); cb(err, obj, created);
} }
if (!err) Model.emit('changed', obj);
});
} else {
if (cb.promise) {
cb(err, [obj, created]);
} else {
cb(err, obj, created);
} }
} });
} }
data = removeUndefined(data); data = removeUndefined(data);
@ -1329,38 +1366,50 @@ DataAccessObject.find = function find(query, options, cb) {
var Model = self.lookupModel(d); var Model = self.lookupModel(d);
var obj = new Model(d, {fields: query.fields, applySetters: false, persisted: true}); var obj = new Model(d, {fields: query.fields, applySetters: false, persisted: true});
if (query && query.include) { context = {
if (query.collect) { Model: Model,
// The collect property indicates that the query is to return the instance: obj,
// standalone items for a related model, not as child of the parent object isNewInstance: false,
// For example, article.tags hookState: hookState,
obj = obj.__cachedRelations[query.collect]; options: options
if (obj === null) { };
obj = undefined;
}
} else {
// This handles the case to return parent items including the related
// models. For example, Article.find({include: 'tags'}, ...);
// Try to normalize the include
var includes = Inclusion.normalizeInclude(query.include || []);
includes.forEach(function(inc) {
var relationName = inc;
if (utils.isPlainObject(inc)) {
relationName = Object.keys(inc)[0];
}
// Promote the included model as a direct property if (query && query.include) {
var included = obj.__cachedRelations[relationName]; Model.notifyObserversOf('loaded', context, function(err) {
if (Array.isArray(included)) { if (query.collect) {
included = new List(included, null, obj); // The collect property indicates that the query is to return the
// standalone items for a related model, not as child of the parent object
// For example, article.tags
obj = obj.__cachedRelations[query.collect];
if (obj === null) {
obj = undefined;
} }
if (included) obj.__data[relationName] = included; } else {
}); // This handles the case to return parent items including the related
delete obj.__data.__cachedRelations; // models. For example, Article.find({include: 'tags'}, ...);
} // Try to normalize the include
var includes = Inclusion.normalizeInclude(query.include || []);
includes.forEach(function(inc) {
var relationName = inc;
if (utils.isPlainObject(inc)) {
relationName = Object.keys(inc)[0];
}
// Promote the included model as a direct property
var included = obj.__cachedRelations[relationName];
if (Array.isArray(included)) {
included = new List(included, null, obj);
}
if (included) obj.__data[relationName] = included;
});
delete obj.__data.__cachedRelations;
}
});
} }
if (obj !== undefined) { if (obj !== undefined) {
results.push(obj); Model.notifyObserversOf('loaded', context, function(err) {
results.push(obj);
});
} }
} }
@ -1803,22 +1852,33 @@ DataAccessObject.prototype.save = function (options, cb) {
if (err) { if (err) {
return cb(err, inst); return cb(err, inst);
} }
inst._initProperties(data, { persisted: true });
var context = { var context = {
Model: Model, Model: Model,
instance: inst, data: data,
isNewInstance: result && result.isNewInstance, isNewInstance: result && result.isNewInstance,
hookState: hookState, hookState: hookState,
options: options options: options
}; };
Model.notifyObserversOf('after save', context, function(err) { Model.notifyObserversOf('loaded', context, function(err) {
if (err) return cb(err, inst); inst._initProperties(data, { persisted: true });
updateDone.call(inst, function () {
saveDone.call(inst, function () { var context = {
cb(err, inst); Model: Model,
if(!err) { instance: inst,
Model.emit('changed', inst); isNewInstance: result && result.isNewInstance,
} hookState: hookState,
options: options
};
Model.notifyObserversOf('after save', context, function(err) {
if (err) return cb(err, inst);
updateDone.call(inst, function () {
saveDone.call(inst, function () {
cb(err, inst);
if(!err) {
Model.emit('changed', inst);
}
});
}); });
}); });
}); });
@ -1956,6 +2016,7 @@ DataAccessObject.updateAll = function (where, data, options, cb) {
function updateCallback(err, info) { function updateCallback(err, info) {
if (err) return cb (err); if (err) return cb (err);
var context = { var context = {
Model: Model, Model: Model,
where: where, where: where,
@ -2291,20 +2352,37 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, op
} }
function updateAttributesCallback(err) { function updateAttributesCallback(err) {
if (!err) inst.__persisted = true; var context = {
done.call(inst, function () { Model: Model,
saveDone.call(inst, function () { data: data,
if (err) return cb(err, inst); hookState: hookState,
var context = { options: options
Model: Model, };
instance: inst, Model.notifyObserversOf('loaded', context, function(err) {
isNewInstance: false, if (!err) inst.__persisted = true;
hookState: hookState,
options: options // By default, the instance passed to updateAttributes callback is NOT updated
}; // with the changes made through persist/loaded hooks. To preserve
Model.notifyObserversOf('after save', context, function(err) { // backwards compatibility, we introduced a new setting updateOnLoad,
if(!err) Model.emit('changed', inst); // which if set, will apply these changes to the model instance too.
cb(err, inst); if(Model.settings.updateOnLoad) {
inst.setAttributes(context.data);
}
done.call(inst, function () {
saveDone.call(inst, function () {
if (err) return cb(err, inst);
var context = {
Model: Model,
instance: inst,
isNewInstance: false,
hookState: hookState,
options: options
};
Model.notifyObserversOf('after save', context, function(err) {
if(!err) Model.emit('changed', inst);
cb(err, inst);
});
}); });
}); });
}); });

View File

@ -6,6 +6,7 @@ module.exports = function(dataSource, should) {
var observedContexts, expectedError, observersCalled; var observedContexts, expectedError, observersCalled;
var TestModel, existingInstance; var TestModel, existingInstance;
var migrated = false, lastId; var migrated = false, lastId;
var triggered;
var undefinedValue = undefined; var undefinedValue = undefined;
@ -112,6 +113,24 @@ module.exports = function(dataSource, should) {
}); });
describe('PersistedModel.create', function() { describe('PersistedModel.create', function() {
it('triggers hooks in the correct order', function(done) {
monitorHookExecution();
TestModel.create(
{ name: 'created' },
function(err, record, created) {
if (err) return done(err);
triggered.should.eql([
'before save',
'persist',
'loaded',
'after save'
]);
done();
});
});
it('triggers `before save` hook', function(done) { it('triggers `before save` hook', function(done) {
TestModel.observe('before save', pushContextAndNext()); TestModel.observe('before save', pushContextAndNext());
@ -210,16 +229,19 @@ module.exports = function(dataSource, should) {
ctx.data.extra = 'hook data'; ctx.data.extra = 'hook data';
})); }));
// By default, the instance passed to create callback is NOT updated
// with the changes made through persist/loaded hooks. To preserve
// backwards compatibility, we introduced a new setting updateOnLoad,
// which if set, will apply these changes to the model instance too.
TestModel.settings.updateOnLoad = true;
TestModel.create( TestModel.create(
{ id: 'new-id', name: 'a name' }, { id: 'new-id', name: 'a name' },
function(err, instance) { function(err, instance) {
if (err) return done(err); if (err) return done(err);
// the, instance returned by `create` context does not have the instance.should.have.property('extra', 'hook data');
// values updated from `persist` hook
instance.should.not.have.property('extra', 'hook data');
// So, we must query the database here because on `create` // Also query the database here to verify that, on `create`
// updates from `persist` hook are reflected into database // updates from `persist` hook are reflected into database
TestModel.findById('new-id', function(err, dbInstance) { TestModel.findById('new-id', function(err, dbInstance) {
if (err) return done(err); if (err) return done(err);
@ -234,6 +256,48 @@ module.exports = function(dataSource, should) {
}); });
}); });
it('triggers `loaded` hook', function(done) {
TestModel.observe('loaded', pushContextAndNext());
// By default, the instance passed to create callback is NOT updated
// with the changes made through persist/loaded hooks. To preserve
// backwards compatibility, we introduced a new setting updateOnLoad,
// which if set, will apply these changes to the model instance too.
TestModel.settings.updateOnLoad = true;
TestModel.create(
{ id: 'new-id', name: 'a name' },
function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
data: { id: 'new-id', name: 'a name' },
isNewInstance: true
}));
done();
});
});
it('applies updates from `loaded` hook', function(done) {
TestModel.observe('loaded', pushContextAndNext(function(ctx){
ctx.data.extra = 'hook data';
}));
// By default, the instance passed to create callback is NOT updated
// with the changes made through persist/loaded hooks. To preserve
// backwards compatibility, we introduced a new setting updateOnLoad,
// which if set, will apply these changes to the model instance too.
TestModel.settings.updateOnLoad = true;
TestModel.create(
{ id: 'new-id', name: 'a name' },
function(err, instance) {
if (err) return done(err);
instance.should.have.property('extra', 'hook data');
done();
});
});
it('triggers `after save` hook', function(done) { it('triggers `after save` hook', function(done) {
TestModel.observe('after save', pushContextAndNext()); TestModel.observe('after save', pushContextAndNext());
@ -405,12 +469,7 @@ module.exports = function(dataSource, should) {
}); });
it('triggers hooks in the correct order when not found', function(done) { it('triggers hooks in the correct order when not found', function(done) {
var triggered = []; monitorHookExecution();
TestModel._notify = TestModel.notifyObserversOf;
TestModel.notifyObserversOf = function(operation, context, callback) {
triggered.push(operation);
this._notify.apply(this, arguments);
};
TestModel.findOrCreate( TestModel.findOrCreate(
{ where: { name: 'new-record' } }, { where: { name: 'new-record' } },
@ -421,12 +480,39 @@ module.exports = function(dataSource, should) {
'access', 'access',
'before save', 'before save',
'persist', 'persist',
'loaded',
'after save' 'after save'
]); ]);
done(); done();
}); });
}); });
it('triggers hooks in the correct order when found', function(done) {
monitorHookExecution();
TestModel.findOrCreate(
{ where: { name: existingInstance.name } },
{ name: existingInstance.name },
function(err, record, created) {
if (err) return done(err);
if (dataSource.connector.findOrCreate) {
triggered.should.eql([
'access',
'before save',
'persist',
'loaded'
]);
} else {
triggered.should.eql([
'access',
'loaded'
]);
}
done();
});
});
it('aborts when `access` hook fails', function(done) { it('aborts when `access` hook fails', function(done) {
TestModel.observe('access', nextWithError(expectedError)); TestModel.observe('access', nextWithError(expectedError));
@ -596,6 +682,98 @@ module.exports = function(dataSource, should) {
}); });
}); });
if (dataSource.connector.findOrCreate) {
it('triggers `loaded` hook when found', function(done) {
TestModel.observe('loaded', pushContextAndNext());
TestModel.findOrCreate(
{ where: { name: existingInstance.name } },
{ name: existingInstance.name },
function(err, record, created) {
if (err) return done(err);
record.id.should.eql(existingInstance.id);
// After the call to `connector.findOrCreate`, since the record
// already exists, `data.id` matches `existingInstance.id`
// as against the behaviour noted for `persist` hook
observedContexts.should.eql(aTestModelCtx({
data: {
id: existingInstance.id,
name: existingInstance.name
},
isNewInstance: false
}));
done();
});
});
}
it('triggers `loaded` hook when not found', function(done) {
TestModel.observe('loaded', pushContextAndNext());
TestModel.findOrCreate(
{ where: { name: 'new-record' } },
{ name: 'new-record' },
function(err, record, created) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
data: {
id: record.id,
name: 'new-record'
},
isNewInstance: true
}));
done();
});
});
if (dataSource.connector.findOrCreate) {
it('applies updates from `loaded` hook when found', function(done) {
TestModel.observe('loaded', pushContextAndNext(function(ctx){
ctx.data.extra = 'hook data';
}));
TestModel.findOrCreate(
{ where: { name: existingInstance.name } },
{ name: existingInstance.name },
function(err, instance) {
if (err) return done(err);
instance.should.have.property('extra', 'hook data');
done();
});
});
}
it('applies updates from `loaded` hook when not found', function(done) {
TestModel.observe('loaded', pushContextAndNext(function(ctx){
ctx.data.extra = 'hook data';
}));
// Unoptimized connector gives a call to `create. But,
// by default, the instance passed to create callback is NOT updated
// with the changes made through persist/loaded hooks. To preserve
// backwards compatibility, we introduced a new setting updateOnLoad,
// which if set, will apply these changes to the model instance too.
// Note - in case of findOrCreate, this setting is needed ONLY for
// unoptimized connector.
TestModel.settings.updateOnLoad = true;
TestModel.findOrCreate(
{ where: { name: 'new-record' } },
{ name: 'new-record' },
function(err, instance) {
if (err) return done(err);
instance.should.have.property('extra', 'hook data');
done();
});
});
it('triggers `after save` hook when not found', function(done) { it('triggers `after save` hook when not found', function(done) {
TestModel.observe('after save', pushContextAndNext()); TestModel.observe('after save', pushContextAndNext());
@ -658,6 +836,22 @@ module.exports = function(dataSource, should) {
}); });
describe('PersistedModel.prototype.save', function() { describe('PersistedModel.prototype.save', function() {
it('triggers hooks in the correct order', function(done) {
monitorHookExecution();
existingInstance.save(
function(err, record, created) {
if (err) return done(err);
triggered.should.eql([
'before save',
'persist',
'loaded',
'after save'
]);
done();
});
});
it('triggers `before save` hook', function(done) { it('triggers `before save` hook', function(done) {
TestModel.observe('before save', pushContextAndNext()); TestModel.observe('before save', pushContextAndNext());
@ -745,6 +939,38 @@ module.exports = function(dataSource, should) {
}); });
}); });
it('triggers `loaded` hook', function(done) {
TestModel.observe('loaded', pushContextAndNext());
existingInstance.name = 'changed';
existingInstance.save(function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
data: {
id: existingInstance.id,
name: 'changed'
},
isNewInstance: false,
options: { throws: false, validate: true }
}));
done();
});
});
it('applies updates from `loaded` hook', function(done) {
TestModel.observe('loaded', pushContextAndNext(function(ctx){
ctx.data.extra = 'hook data';
}));
existingInstance.save(function(err, instance) {
if (err) return done(err);
instance.should.have.property('extra', 'hook data');
done();
});
});
it('triggers `after save` hook on update', function(done) { it('triggers `after save` hook on update', function(done) {
TestModel.observe('after save', pushContextAndNext()); TestModel.observe('after save', pushContextAndNext());
@ -811,6 +1037,23 @@ module.exports = function(dataSource, should) {
}); });
describe('PersistedModel.prototype.updateAttributes', function() { describe('PersistedModel.prototype.updateAttributes', function() {
it('triggers hooks in the correct order', function(done) {
monitorHookExecution();
existingInstance.updateAttributes(
{ name: 'changed' },
function(err, record, created) {
if (err) return done(err);
triggered.should.eql([
'before save',
'persist',
'loaded',
'after save'
]);
done();
});
});
it('triggers `before save` hook', function(done) { it('triggers `before save` hook', function(done) {
TestModel.observe('before save', pushContextAndNext()); TestModel.observe('before save', pushContextAndNext());
@ -894,7 +1137,42 @@ module.exports = function(dataSource, should) {
ctx.data.extra = 'hook data'; ctx.data.extra = 'hook data';
})); }));
existingInstance.save(function(err, instance) { // By default, the instance passed to updateAttributes callback is NOT updated
// with the changes made through persist/loaded hooks. To preserve
// backwards compatibility, we introduced a new setting updateOnLoad,
// which if set, will apply these changes to the model instance too.
TestModel.settings.updateOnLoad = true;
existingInstance.updateAttributes({ name: 'changed' }, function(err, instance) {
if (err) return done(err);
instance.should.have.property('extra', 'hook data');
done();
});
});
it('triggers `loaded` hook', function(done) {
TestModel.observe('loaded', pushContextAndNext());
existingInstance.updateAttributes({ name: 'changed' }, function(err) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
data: { name: 'changed' }
}));
done();
});
});
it('applies updates from `loaded` hook updateAttributes', function(done) {
TestModel.observe('loaded', pushContextAndNext(function(ctx){
ctx.data.extra = 'hook data';
}));
// By default, the instance passed to updateAttributes callback is NOT updated
// with the changes made through persist/loaded hooks. To preserve
// backwards compatibility, we introduced a new setting updateOnLoad,
// which if set, will apply these changes to the model instance too.
TestModel.settings.updateOnLoad = true;
existingInstance.updateAttributes({ name: 'changed' }, function(err, instance) {
if (err) return done(err); if (err) return done(err);
instance.should.have.property('extra', 'hook data'); instance.should.have.property('extra', 'hook data');
done(); done();
@ -944,6 +1222,53 @@ module.exports = function(dataSource, should) {
}); });
describe('PersistedModel.updateOrCreate', function() { describe('PersistedModel.updateOrCreate', function() {
it('triggers hooks in the correct order on create', function(done) {
monitorHookExecution();
TestModel.updateOrCreate(
{ id: 'not-found', name: 'not found' },
function(err, record, created) {
if (err) return done(err);
triggered.should.eql([
'access',
'before save',
'persist',
'loaded',
'after save'
]);
done();
});
});
it('triggers hooks in the correct order on update', function(done) {
monitorHookExecution();
TestModel.updateOrCreate(
{ id: existingInstance.id, name: 'new name' },
function(err, record, created) {
if (err) return done(err);
if (dataSource.connector.updateOrCreate) {
triggered.should.eql([
'access',
'before save',
'persist',
'loaded',
'after save'
]);
} else {
triggered.should.eql([
'access',
'loaded',
'before save',
'persist',
'loaded',
'after save'
]);
}
done();
});
});
it('triggers `access` hook on create', function(done) { it('triggers `access` hook on create', function(done) {
TestModel.observe('access', pushContextAndNext()); TestModel.observe('access', pushContextAndNext());
@ -1225,6 +1550,71 @@ module.exports = function(dataSource, should) {
}); });
}); });
it('triggers `loaded` hook on create', function(done) {
TestModel.observe('loaded', pushContextAndNext());
TestModel.updateOrCreate(
{ id: 'new-id', name: 'a name' },
function(err, instance) {
if (err) return done(err);
if (dataSource.connector.updateOrCreate) {
observedContexts.should.eql(aTestModelCtx({
data: { id: 'new-id', name: 'a name' }
}));
} else {
observedContexts.should.eql(aTestModelCtx({
data: {
id: 'new-id',
name: 'a name'
},
isNewInstance: true
}));
}
done();
});
});
it('triggers `loaded` hook on update', function(done) {
TestModel.observe('loaded', pushContextAndNext());
TestModel.updateOrCreate(
{ id: existingInstance.id, name: 'updated name' },
function(err, instance) {
if (err) return done(err);
if (dataSource.connector.updateOrCreate) {
observedContexts.should.eql(aTestModelCtx({
data: {
id: existingInstance.id,
name: 'updated name'
}
}));
} else {
// For Unoptimized connector, the callback function `pushContextAndNext`
// is called twice. As a result, observedContexts
// returns an array and NOT a single instance.
observedContexts.should.eql([
aTestModelCtx({
instance: {
id: existingInstance.id,
name: 'first',
extra: null
},
isNewInstance: false,
options: { notify: false }
}),
aTestModelCtx({
data: {
id: existingInstance.id,
name: 'updated name'
}
})
]);
}
done();
});
});
it('triggers `after save` hook on update', function(done) { it('triggers `after save` hook on update', function(done) {
TestModel.observe('after save', pushContextAndNext()); TestModel.observe('after save', pushContextAndNext());
@ -1646,6 +2036,19 @@ module.exports = function(dataSource, should) {
}); });
}); });
it('does not trigger `loaded`', function(done) {
TestModel.observe('loaded', pushContextAndNext());
TestModel.updateAll(
{ where: { id: existingInstance.id } },
{ name: 'changed' },
function(err, instance) {
if (err) return done(err);
observedContexts.should.eql("hook not called");
done();
});
});
it('triggers `after save` hook', function(done) { it('triggers `after save` hook', function(done) {
TestModel.observe('after save', pushContextAndNext()); TestModel.observe('after save', pushContextAndNext());
@ -1759,6 +2162,15 @@ module.exports = function(dataSource, should) {
function getLastGeneratedUid() { function getLastGeneratedUid() {
return '' + lastId; return '' + lastId;
} }
function monitorHookExecution() {
triggered = [];
TestModel._notify = TestModel.notifyObserversOf;
TestModel.notifyObserversOf = function(operation, context, callback) {
triggered.push(operation);
this._notify.apply(this, arguments);
};
}
}); });
function deepCloneToObject(obj) { function deepCloneToObject(obj) {