Merge pull request #586 from PradnyaBaviskar/issue-559

Add new hook `persist`
This commit is contained in:
Miroslav Bajtoš 2015-06-11 18:44:12 +03:00
commit ec597ef207
2 changed files with 465 additions and 39 deletions

View File

@ -284,6 +284,7 @@ DataAccessObject.create = function (data, options, cb) {
return cb(err, obj); return cb(err, obj);
} }
obj.__persisted = true; obj.__persisted = true;
saveDone.call(obj, function () { saveDone.call(obj, function () {
createDone.call(obj, function () { createDone.call(obj, function () {
if (err) { if (err) {
@ -304,11 +305,23 @@ DataAccessObject.create = function (data, options, cb) {
}); });
} }
if (connector.create.length === 4) { context = {
connector.create(modelName, this.constructor._forDB(val), options, createCallback); Model: Model,
} else { data: val,
connector.create(modelName, this.constructor._forDB(val), createCallback); isNewInstance: true,
} currentInstance: obj,
hookState: hookState,
options: options
};
Model.notifyObserversOf('persist', context, function(err) {
if (err) return cb(err);
if (connector.create.length === 4) {
connector.create(modelName, obj.constructor._forDB(context.data), options, createCallback);
} else {
connector.create(modelName, obj.constructor._forDB(context.data), createCallback);
}
});
}, obj, cb); }, obj, cb);
}, obj, cb); }, obj, cb);
} }
@ -425,12 +438,7 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
var connector = self.getConnector(); var connector = self.getConnector();
if (Model.settings.validateUpsert === false) { if (Model.settings.validateUpsert === false) {
update = removeUndefined(update); callConnector();
if (connector.updateOrCreate.length === 4) {
connector.updateOrCreate(Model.modelName, update, options, done);
} else {
connector.updateOrCreate(Model.modelName, update, done);
}
} else { } else {
inst.isValid(function(valid) { inst.isValid(function(valid) {
if (!valid) { if (!valid) {
@ -443,16 +451,29 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
// continue with updateOrCreate // continue with updateOrCreate
} }
} }
callConnector();
}, update);
}
update = removeUndefined(update); function callConnector() {
update = removeUndefined(update);
context = {
Model: Model,
where: ctx.where,
data: update,
currentInstance: inst,
hookState: ctx.hookState,
options: options
};
Model.notifyObserversOf('persist', context, function(err) {
if (err) return done(err);
if (connector.updateOrCreate.length === 4) { if (connector.updateOrCreate.length === 4) {
connector.updateOrCreate(Model.modelName, update, options, done); connector.updateOrCreate(Model.modelName, update, options, done);
} else { } else {
connector.updateOrCreate(Model.modelName, update, done); connector.updateOrCreate(Model.modelName, update, done);
} }
}, update); });
} }
function done(err, data, info) { function done(err, data, info) {
var obj; var obj;
if (data && !(data instanceof Model)) { if (data && !(data instanceof Model)) {
@ -562,9 +583,8 @@ DataAccessObject.findOrCreate = function findOrCreate(query, data, options, cb)
var self = this; var self = this;
var connector = Model.getConnector(); var connector = Model.getConnector();
function _findOrCreate(query, data) { function _findOrCreate(query, data, currentInstance) {
var modelName = self.modelName; var modelName = self.modelName;
data = removeUndefined(data);
function findOrCreateCallback(err, data, created) { function findOrCreateCallback(err, data, created) {
var obj, Model = self.lookupModel(data); var obj, Model = self.lookupModel(data);
@ -598,11 +618,26 @@ DataAccessObject.findOrCreate = function findOrCreate(query, data, options, cb)
} }
} }
if (connector.findOrCreate.length === 5) { data = removeUndefined(data);
connector.findOrCreate(modelName, query, self._forDB(data), options, findOrCreateCallback); var context = {
} else { Model: Model,
connector.findOrCreate(modelName, query, self._forDB(data), findOrCreateCallback); where: query.where,
} data: data,
isNewInstance: true,
currentInstance : currentInstance,
hookState: hookState,
options: options
};
Model.notifyObserversOf('persist', context, function(err) {
if (err) return cb(err);
if (connector.findOrCreate.length === 5) {
connector.findOrCreate(modelName, query, self._forDB(context.data), options, findOrCreateCallback);
} else {
connector.findOrCreate(modelName, query, self._forDB(context.data), findOrCreateCallback);
}
});
} }
if (connector.findOrCreate) { if (connector.findOrCreate) {
@ -653,7 +688,7 @@ DataAccessObject.findOrCreate = function findOrCreate(query, data, options, cb)
// validation required // validation required
obj.isValid(function (valid) { obj.isValid(function (valid) {
if (valid) { if (valid) {
_findOrCreate(query, data); _findOrCreate(query, data, obj);
} else { } else {
cb(new ValidationError(obj), obj); cb(new ValidationError(obj), obj);
} }
@ -1789,11 +1824,25 @@ DataAccessObject.prototype.save = function (options, cb) {
}); });
} }
if (connector.save.length === 4) { context = {
connector.save(modelName, inst.constructor._forDB(data), options, saveCallback); Model: Model,
} else { data: data,
connector.save(modelName, inst.constructor._forDB(data), saveCallback); where: byIdQuery(Model, getIdValue(Model, inst)).where,
} currentInstance: inst,
hookState: hookState,
options: options
};
Model.notifyObserversOf('persist', context, function(err) {
if (err) return cb(err);
if (connector.save.length === 4) {
connector.save(modelName, inst.constructor._forDB(data), options, saveCallback);
} else {
connector.save(modelName, inst.constructor._forDB(data), saveCallback);
}
});
}, data, cb); }, data, cb);
}, data, cb); }, data, cb);
} }
@ -1919,11 +1968,22 @@ DataAccessObject.updateAll = function (where, data, options, cb) {
}); });
} }
if (connector.update.length === 5) { var context = {
connector.update(Model.modelName, where, data, options, updateCallback); Model: Model,
} else { where: where,
connector.update(Model.modelName, where, data, updateCallback); data: data,
} hookState: hookState,
options: options
};
Model.notifyObserversOf('persist', context, function(err, ctx) {
if (err) return cb (err);
if (connector.update.length === 5) {
connector.update(Model.modelName, where, data, options, updateCallback);
} else {
connector.update(Model.modelName, where, data, updateCallback);
}
});
} }
return cb.promise; return cb.promise;
}; };
@ -2249,13 +2309,23 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, op
}); });
} }
if (connector.updateAttributes.length === 5) { context = {
connector.updateAttributes(model, getIdValue(inst.constructor, inst), Model: Model,
inst.constructor._forDB(typedData), options, updateAttributesCallback); where: byIdQuery(Model, getIdValue(Model, inst)).where,
} else { data: data,
connector.updateAttributes(model, getIdValue(inst.constructor, inst), currentInstance: inst,
inst.constructor._forDB(typedData), updateAttributesCallback); hookState: hookState,
} options: options
};
Model.notifyObserversOf('persist', context, function(err) {
if (connector.updateAttributes.length === 5) {
connector.updateAttributes(model, getIdValue(inst.constructor, inst),
inst.constructor._forDB(typedData), options, updateAttributesCallback);
} else {
connector.updateAttributes(model, getIdValue(inst.constructor, inst),
inst.constructor._forDB(typedData), updateAttributesCallback);
}
});
}, data, cb); }, data, cb);
}, data, cb); }, data, cb);
}, data); }, data);

View File

@ -186,6 +186,53 @@ module.exports = function(dataSource, should) {
}); });
}); });
it('triggers `persist` hook', function(done) {
TestModel.observe('persist', pushContextAndNext());
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,
currentInstance: { extra: null, id: 'new-id', name: 'a name' }
}));
done();
});
});
it('applies updates from `persist` hook', function(done) {
TestModel.observe('persist', pushContextAndNext(function(ctx){
ctx.data.extra = 'hook data';
}));
TestModel.create(
{ id: 'new-id', name: 'a name' },
function(err, instance) {
if (err) return done(err);
// the, instance returned by `create` context does not have the
// values updated from `persist` hook
instance.should.not.have.property('extra', 'hook data');
// So, we must query the database here because on `create`
// updates from `persist` hook are reflected into database
TestModel.findById('new-id', function(err, dbInstance) {
if (err) return done(err);
dbInstance.toObject(true).should.eql({
id: 'new-id',
name: 'a name',
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());
@ -372,6 +419,7 @@ module.exports = function(dataSource, should) {
triggered.should.eql([ triggered.should.eql([
'access', 'access',
'before save', 'before save',
'persist',
'after save' 'after save'
]); ]);
done(); done();
@ -402,6 +450,151 @@ module.exports = function(dataSource, should) {
}); });
}); });
if (dataSource.connector.findOrCreate) {
it('triggers `persist` hook when found', function(done) {
TestModel.observe('persist', 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);
// `findOrCreate` creates a new instance of the object everytime.
// So, `data.id` as well as `currentInstance.id` always matches
// the newly generated UID.
// Hence, the test below asserts both `data.id` and
// `currentInstance.id` to match getLastGeneratedUid().
// On same lines, it also asserts `isNewInstance` to be true.
observedContexts.should.eql(aTestModelCtx({
data: {
id: getLastGeneratedUid(),
name: existingInstance.name
},
isNewInstance: true,
currentInstance: {
id: getLastGeneratedUid(),
name: record.name,
extra: null
},
where: { name: existingInstance.name }
}));
done();
});
});
}
it('triggers `persist` hook when not found', function(done) {
TestModel.observe('persist', pushContextAndNext());
TestModel.findOrCreate(
{ where: { name: 'new-record' } },
{ name: 'new-record' },
function(err, record, created) {
if (err) return done(err);
// `context.where` is present in Optimized connector context,
// but, unoptimized connector does NOT have it.
if (dataSource.connector.findOrCreate) {
observedContexts.should.eql(aTestModelCtx({
data: {
id: record.id,
name: 'new-record'
},
isNewInstance: true,
currentInstance: {
id: record.id,
name: record.name,
extra: null
},
where: { name: 'new-record' }
}));
} else {
observedContexts.should.eql(aTestModelCtx({
data: {
id: record.id,
name: 'new-record'
},
isNewInstance: true,
currentInstance: { id: record.id, name: record.name, extra: null }
}));
}
done();
});
});
if (dataSource.connector.findOrCreate) {
it('applies updates from `persist` hook when found', function(done) {
TestModel.observe('persist', 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 returned by `findOrCreate` context does not
// have the values updated from `persist` hook
instance.should.not.have.property('extra', 'hook data');
// Query the database. Here, since record already exists
// `findOrCreate`, does not update database for
// updates from `persist` hook
TestModel.findById(existingInstance.id, function(err, dbInstance) {
if (err) return done(err);
dbInstance.toObject(true).should.eql({
id: existingInstance.id,
name: existingInstance.name,
extra: undefined
});
});
done();
});
});
}
it('applies updates from `persist` hook when not found', function(done) {
TestModel.observe('persist', pushContextAndNext(function(ctx){
ctx.data.extra = 'hook data';
}));
TestModel.findOrCreate(
{ where: { name: 'new-record' } },
{ name: 'new-record' },
function(err, instance) {
if (err) return done(err);
if (dataSource.connector.findOrCreate) {
instance.should.have.property('extra', 'hook data');
} else {
// Unoptimized connector gives a call to `create. And during
// create the updates applied through persist hook are
// reflected into the database, but the same updates are
// NOT reflected in the instance object obtained in callback
// of create.
// So, this test asserts unoptimized connector to
// NOT have `extra` property. And then verifes that the
// property `extra` is actually updated in DB
instance.should.not.have.property('extra', 'hook data');
TestModel.findById(instance.id, function(err, dbInstance) {
if (err) return done(err);
dbInstance.toObject(true).should.eql({
id: instance.id,
name: instance.name,
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());
@ -512,6 +705,43 @@ module.exports = function(dataSource, should) {
}); });
}); });
it('triggers `persist` hook', function(done) {
TestModel.observe('persist', pushContextAndNext());
existingInstance.name = 'changed';
existingInstance.save(function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
data: {
id: existingInstance.id,
name: 'changed'
},
currentInstance: {
id: existingInstance.id,
name: 'changed',
extra: undefined
},
where: { id: existingInstance.id },
options: { throws: false, validate: true }
}));
done();
});
});
it('applies updates from `persist` hook', function(done) {
TestModel.observe('persist', 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());
@ -637,6 +867,37 @@ module.exports = function(dataSource, should) {
}); });
}); });
it('triggers `persist` hook', function(done) {
TestModel.observe('persist', pushContextAndNext());
existingInstance.updateAttributes({ name: 'changed' }, function(err) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
where: { id: existingInstance.id },
data: { name: 'changed' },
currentInstance: {
id: existingInstance.id,
name: 'changed',
extra: null
}
}));
done();
});
});
it('applies updates from `persist` hook', function(done) {
TestModel.observe('persist', 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', function(done) { it('triggers `after save` hook', function(done) {
TestModel.observe('after save', pushContextAndNext()); TestModel.observe('after save', pushContextAndNext());
@ -901,6 +1162,66 @@ module.exports = function(dataSource, should) {
}); });
}); });
it('triggers `persist` hook on create', function(done) {
TestModel.observe('persist', 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({
where: { id: 'new-id' },
data: { id: 'new-id', name: 'a name' },
currentInstance: {
id: 'new-id',
name: 'a name',
extra: undefined
}
}));
} else {
observedContexts.should.eql(aTestModelCtx({
data: {
id: 'new-id',
name: 'a name'
},
isNewInstance: true,
currentInstance: {
id: 'new-id',
name: 'a name',
extra: undefined
}
}));
}
done();
});
});
it('triggers `persist` hook on update', function(done) {
TestModel.observe('persist', pushContextAndNext());
TestModel.updateOrCreate(
{ id: existingInstance.id, name: 'updated name' },
function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
where: { id: existingInstance.id },
data: {
id: existingInstance.id,
name: 'updated name'
},
currentInstance: {
id: existingInstance.id,
name: 'updated name',
extra: undefined
}
}));
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());
@ -1287,6 +1608,41 @@ module.exports = function(dataSource, should) {
}); });
}); });
it('triggers `persist` hook', function(done) {
TestModel.observe('persist', pushContextAndNext());
TestModel.updateAll(
{ where: { name: existingInstance.name } },
{ name: 'changed' },
function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
data: { name: 'changed' },
where: { where: { name: existingInstance.name } }
}));
done();
});
});
it('applies updates from `persist` hook', function(done) {
TestModel.observe('persist', pushContextAndNext(function(ctx){
ctx.data.extra = 'hook data';
}));
TestModel.updateAll(
{ id: existingInstance.id },
{ name: 'changed' },
function(err) {
if (err) return done(err);
loadTestModel(existingInstance.id, function(err, instance) {
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());