Add ctx.isNewInstance for "save" hooks

"before save" hooks provide "ctx.isNewInstance" whenever "ctx.instance"
is set. Possible values:
 - true for all CREATE operations
 - false for all UPDATE operations
 - undefined for "prototype.save"

"after save" hooks provide "ctx.isNewInstance" whenever "ctx.instance"
is set. Possible values:
 - true after all CREATE operations
 - false after all UPDATE operations
 - undefined after "updateOrCreate" and "save"

Note: both "updateOrCreate" and "prototype.updateAttributes"
don't provide `ctx.instance` to "before save" hooks, therefore
`ctx.isNewInstance` it not provided either.
This commit is contained in:
Miroslav Bajtoš 2015-03-19 13:25:03 +01:00
parent ae3dc3cec2
commit ffcaa4e76e
3 changed files with 159 additions and 69 deletions

View File

@ -225,11 +225,13 @@ Memory.prototype.updateOrCreate = function (model, data, callback) {
var self = this; var self = this;
this.exists(model, self.getIdValue(model, data), function (err, exists) { this.exists(model, self.getIdValue(model, data), function (err, exists) {
if (exists) { if (exists) {
self.save(model, data, callback); self.save(model, data, function(err, data) {
callback(err, data, { isNewInstance: false });
});
} else { } else {
self.create(model, data, function (err, id) { self.create(model, data, function (err, id) {
self.setIdValue(model, data, id); self.setIdValue(model, data, id);
callback(err, data); callback(err, data, { isNewInstance: true });
}); });
} }
}); });
@ -244,7 +246,9 @@ Memory.prototype.save = function save(model, data, callback) {
data = merge(modelData, data); data = merge(modelData, data);
} }
this.collection(model)[id] = serialize(data); this.collection(model)[id] = serialize(data);
this.saveToFile(data, callback); this.saveToFile(data, function(err) {
callback(err, data, { isNewInstance: !modelData });
});
}; };
Memory.prototype.exists = function exists(model, id, callback) { Memory.prototype.exists = function exists(model, id, callback) {
@ -462,12 +466,12 @@ function applyFilter(filter) {
if ('neq' in example) { if ('neq' in example) {
return compare(example.neq, value) !== 0; return compare(example.neq, value) !== 0;
} }
if ('between' in example ) { if ('between' in example ) {
return ( testInEquality({gte:example.between[0]}, value) && return ( testInEquality({gte:example.between[0]}, value) &&
testInEquality({lte:example.between[1]}, value) ); testInEquality({lte:example.between[1]}, value) );
} }
if (example.like || example.nlike) { if (example.like || example.nlike) {
var like = example.like || example.nlike; var like = example.like || example.nlike;

View File

@ -231,7 +231,12 @@ DataAccessObject.create = function (data, options, cb) {
Model = this.lookupModel(data); // data-specific Model = this.lookupModel(data); // data-specific
if (Model !== obj.constructor) obj = new Model(data); if (Model !== obj.constructor) obj = new Model(data);
var context = { Model: Model, instance: obj, hookState: hookState }; var context = {
Model: Model,
instance: obj,
isNewInstance: true,
hookState: hookState
};
Model.notifyObserversOf('before save', context, function(err) { Model.notifyObserversOf('before save', context, function(err) {
if (err) return cb(err); if (err) return cb(err);
@ -270,7 +275,12 @@ DataAccessObject.create = function (data, options, cb) {
if (err) { if (err) {
return cb(err, obj); return cb(err, obj);
} }
var context = { Model: Model, instance: obj, hookState: hookState }; var context = {
Model: Model,
instance: obj,
isNewInstance: true,
hookState: hookState
};
Model.notifyObserversOf('after save', context, function(err) { Model.notifyObserversOf('after save', context, function(err) {
cb(err, obj); cb(err, obj);
if(!err) Model.emit('changed', obj); if(!err) Model.emit('changed', obj);
@ -390,7 +400,7 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
self.getDataSource().connector self.getDataSource().connector
.updateOrCreate(Model.modelName, update, done); .updateOrCreate(Model.modelName, update, done);
function done(err, data) { function done(err, data, result) {
var obj; var obj;
if (data && !(data instanceof Model)) { if (data && !(data instanceof Model)) {
inst._initProperties(data, { persisted: true }); inst._initProperties(data, { persisted: true });
@ -404,7 +414,12 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
Model.emit('changed', inst); Model.emit('changed', inst);
} }
} else { } else {
var context = { Model: Model, instance: obj, hookState: hookState }; var context = {
Model: Model,
instance: obj,
isNewInstance: result ? result.isNewInstance : undefined,
hookState: hookState
};
Model.notifyObserversOf('after save', context, function(err) { Model.notifyObserversOf('after save', context, function(err) {
cb(err, obj); cb(err, obj);
if(!err) { if(!err) {
@ -505,7 +520,12 @@ DataAccessObject.findOrCreate = function findOrCreate(query, data, options, cb)
} }
if (created) { if (created) {
var context = { Model: Model, instance: obj, hookState: hookState }; var context = {
Model: Model,
instance: obj,
isNewInstance: true,
hookState: hookState
};
Model.notifyObserversOf('after save', context, function(err) { Model.notifyObserversOf('after save', context, function(err) {
if (cb.promise) { if (cb.promise) {
cb(err, [obj, created]); cb(err, [obj, created]);
@ -551,7 +571,12 @@ DataAccessObject.findOrCreate = function findOrCreate(query, data, options, cb)
Model.applyProperties(enforced, obj); Model.applyProperties(enforced, obj);
obj.setAttributes(enforced); obj.setAttributes(enforced);
var context = { Model: Model, instance: obj, hookState: hookState }; var context = {
Model: Model,
instance: obj,
isNewInstance: true,
hookState: hookState
};
Model.notifyObserversOf('before save', context, function(err, ctx) { Model.notifyObserversOf('before save', context, function(err, ctx) {
if (err) return cb(err); if (err) return cb(err);
@ -1562,12 +1587,17 @@ DataAccessObject.prototype.save = function (options, cb) {
inst.trigger('save', function (saveDone) { inst.trigger('save', function (saveDone) {
inst.trigger('update', function (updateDone) { inst.trigger('update', function (updateDone) {
data = removeUndefined(data); data = removeUndefined(data);
inst._adapter().save(modelName, inst.constructor._forDB(data), function (err) { inst._adapter().save(modelName, inst.constructor._forDB(data), function (err, data, result) {
if (err) { if (err) {
return cb(err, inst); return cb(err, inst);
} }
inst._initProperties(data, { persisted: true }); inst._initProperties(data, { persisted: true });
var context = { Model: Model, instance: inst, hookState: hookState }; var context = {
Model: Model,
instance: inst,
isNewInstance: result && result.isNewInstance,
hookState: hookState
};
Model.notifyObserversOf('after save', context, function(err) { Model.notifyObserversOf('after save', context, function(err) {
if (err) return cb(err, inst); if (err) return cb(err, inst);
updateDone.call(inst, function () { updateDone.call(inst, function () {
@ -1959,7 +1989,12 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, op
done.call(inst, function () { done.call(inst, function () {
saveDone.call(inst, function () { saveDone.call(inst, function () {
if (err) return cb(err, inst); if (err) return cb(err, inst);
var context = { Model: Model, instance: inst, hookState: hookState }; var context = {
Model: Model,
instance: inst,
isNewInstance: false,
hookState: hookState
};
Model.notifyObserversOf('after save', context, function(err) { Model.notifyObserversOf('after save', context, function(err) {
if(!err) Model.emit('changed', inst); if(!err) Model.emit('changed', inst);
cb(err, inst); cb(err, inst);

View File

@ -116,11 +116,14 @@ module.exports = function(dataSource, should) {
TestModel.create({ name: 'created' }, function(err, instance) { TestModel.create({ name: 'created' }, function(err, instance) {
if (err) return done(err); if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({ instance: { observedContexts.should.eql(aTestModelCtx({
id: instance.id, instance: {
name: 'created', id: instance.id,
extra: undefined name: 'created',
}})); extra: undefined
},
isNewInstance: true
}));
done(); done();
}); });
}); });
@ -161,10 +164,12 @@ module.exports = function(dataSource, should) {
}); });
observedContexts.should.eql([ observedContexts.should.eql([
aTestModelCtx({ aTestModelCtx({
instance: { id: list[0].id, name: '1', extra: undefined } instance: { id: list[0].id, name: '1', extra: undefined },
isNewInstance: true
}), }),
aTestModelCtx({ aTestModelCtx({
instance: { id: list[1].id, name: '2', extra: undefined } instance: { id: list[1].id, name: '2', extra: undefined },
isNewInstance: true
}), }),
]); ]);
done(); done();
@ -186,11 +191,14 @@ module.exports = function(dataSource, should) {
TestModel.create({ name: 'created' }, function(err, instance) { TestModel.create({ name: 'created' }, function(err, instance) {
if (err) return done(err); if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({ instance: { observedContexts.should.eql(aTestModelCtx({
id: instance.id, instance: {
name: 'created', id: instance.id,
extra: undefined name: 'created',
}})); extra: undefined
},
isNewInstance: true
}));
done(); done();
}); });
}); });
@ -231,10 +239,12 @@ module.exports = function(dataSource, should) {
}); });
observedContexts.should.eql([ observedContexts.should.eql([
aTestModelCtx({ aTestModelCtx({
instance: { id: list[0].id, name: '1', extra: undefined } instance: { id: list[0].id, name: '1', extra: undefined },
isNewInstance: true
}), }),
aTestModelCtx({ aTestModelCtx({
instance: { id: list[1].id, name: '2', extra: undefined } instance: { id: list[1].id, name: '2', extra: undefined },
isNewInstance: true
}), }),
]); ]);
done(); done();
@ -263,7 +273,8 @@ module.exports = function(dataSource, should) {
list.map(get('name')).should.eql(['ok', 'fail']); list.map(get('name')).should.eql(['ok', 'fail']);
observedContexts.should.eql(aTestModelCtx({ observedContexts.should.eql(aTestModelCtx({
instance: { id: list[0].id, name: 'ok', extra: undefined } instance: { id: list[0].id, name: 'ok', extra: undefined },
isNewInstance: true
})); }));
done(); done();
}); });
@ -299,11 +310,14 @@ module.exports = function(dataSource, should) {
function(err, record, created) { function(err, record, created) {
if (err) return done(err); if (err) return done(err);
record.id.should.eql(existingInstance.id); record.id.should.eql(existingInstance.id);
observedContexts.should.eql(aTestModelCtx({ instance: { observedContexts.should.eql(aTestModelCtx({
id: getLastGeneratedUid(), instance: {
name: existingInstance.name, id: getLastGeneratedUid(),
extra: undefined name: existingInstance.name,
}})); extra: undefined
},
isNewInstance: true
}));
done(); done();
}); });
}); });
@ -317,11 +331,14 @@ module.exports = function(dataSource, should) {
{ name: 'new-record' }, { name: 'new-record' },
function(err, record, created) { function(err, record, created) {
if (err) return done(err); if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({ instance: { observedContexts.should.eql(aTestModelCtx({
id: record.id, instance: {
name: 'new-record', id: record.id,
extra: undefined name: 'new-record',
}})); extra: undefined
},
isNewInstance: true
}));
done(); done();
}); });
}); });
@ -393,11 +410,14 @@ module.exports = function(dataSource, should) {
{ name: 'new name' }, { name: 'new name' },
function(err, instance) { function(err, instance) {
if (err) return done(err); if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({ instance: { observedContexts.should.eql(aTestModelCtx({
id: instance.id, instance: {
name: 'new name', id: instance.id,
extra: undefined name: 'new name',
}})); extra: undefined
},
isNewInstance: true
}));
done(); done();
}); });
}); });
@ -492,17 +512,38 @@ module.exports = function(dataSource, should) {
}); });
}); });
it('triggers `after save` hook', function(done) { it('triggers `after save` hook on update', function(done) {
TestModel.observe('after save', pushContextAndNext()); TestModel.observe('after save', pushContextAndNext());
existingInstance.name = 'changed'; existingInstance.name = 'changed';
existingInstance.save(function(err, instance) { existingInstance.save(function(err, instance) {
if (err) return done(err); if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({ instance: { observedContexts.should.eql(aTestModelCtx({
id: existingInstance.id, instance: {
name: 'changed', id: existingInstance.id,
extra: undefined name: 'changed',
}})); extra: undefined
},
isNewInstance: false
}));
done();
});
});
it('triggers `after save` hook on create', function(done) {
TestModel.observe('after save', pushContextAndNext());
var instance = new TestModel({ name: 'created' });
instance.save(function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
instance: {
id: instance.id,
name: 'created',
extra: undefined
},
isNewInstance: true
}));
done(); done();
}); });
}); });
@ -597,11 +638,14 @@ module.exports = function(dataSource, should) {
existingInstance.name = 'changed'; existingInstance.name = 'changed';
existingInstance.updateAttributes({ name: 'changed' }, function(err) { existingInstance.updateAttributes({ name: 'changed' }, function(err) {
if (err) return done(err); if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({ instance: { observedContexts.should.eql(aTestModelCtx({
id: existingInstance.id, instance: {
name: 'changed', id: existingInstance.id,
extra: undefined name: 'changed',
}})); extra: undefined
},
isNewInstance: false
}));
done(); done();
}); });
}); });
@ -781,7 +825,8 @@ module.exports = function(dataSource, should) {
// The default unoptimized implementation runs // The default unoptimized implementation runs
// `instance.save` and thus a full instance is availalbe // `instance.save` and thus a full instance is availalbe
observedContexts.should.eql(aTestModelCtx({ observedContexts.should.eql(aTestModelCtx({
instance: { id: 'new-id', name: 'a name', extra: undefined } instance: { id: 'new-id', name: 'a name', extra: undefined },
isNewInstance: true
})); }));
} }
@ -859,11 +904,14 @@ module.exports = function(dataSource, should) {
{ id: existingInstance.id, name: 'updated name' }, { id: existingInstance.id, name: 'updated name' },
function(err, instance) { function(err, instance) {
if (err) return done(err); if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({ instance: { observedContexts.should.eql(aTestModelCtx({
id: existingInstance.id, instance: {
name: 'updated name', id: existingInstance.id,
extra: undefined name: 'updated name',
}})); extra: undefined
},
isNewInstance: false
}));
done(); done();
}); });
}); });
@ -875,11 +923,14 @@ module.exports = function(dataSource, should) {
{ 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);
observedContexts.should.eql(aTestModelCtx({ instance: { observedContexts.should.eql(aTestModelCtx({
id: instance.id, instance: {
name: 'a name', id: instance.id,
extra: undefined name: 'a name',
}})); extra: undefined
},
isNewInstance: true
}));
done(); done();
}); });
}); });
@ -1126,12 +1177,12 @@ module.exports = function(dataSource, should) {
existingInstance.delete(function(err) { existingInstance.delete(function(err) {
if (err) return done(err); if (err) return done(err);
observedContexts.should.eql([ observedContexts.should.eql([
aTestModelCtx({ aTestModelCtx({
hookState: { foo: 'bar', test: true }, hookState: { foo: 'bar', test: true },
where: { id: '1' }, where: { id: '1' },
instance: existingInstance instance: existingInstance
}), }),
aTestModelCtx({ aTestModelCtx({
hookState: { foo: 'BAR', test: true }, hookState: { foo: 'BAR', test: true },
where: { id: '1' }, where: { id: '1' },
instance: existingInstance instance: existingInstance