From 3b0e77cb0a69cdc70e50e7be5d30a6e5f78de474 Mon Sep 17 00:00:00 2001 From: Pradnya Baviskar Date: Wed, 13 May 2015 16:14:40 -0700 Subject: [PATCH] Add new hook 'persist' --- lib/dao.js | 148 +++++++++---- test/persistence-hooks.suite.js | 356 ++++++++++++++++++++++++++++++++ 2 files changed, 465 insertions(+), 39 deletions(-) diff --git a/lib/dao.js b/lib/dao.js index 6dd5594d..964c8be9 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -284,6 +284,7 @@ DataAccessObject.create = function (data, options, cb) { return cb(err, obj); } obj.__persisted = true; + saveDone.call(obj, function () { createDone.call(obj, function () { if (err) { @@ -304,11 +305,23 @@ DataAccessObject.create = function (data, options, cb) { }); } - if (connector.create.length === 4) { - connector.create(modelName, this.constructor._forDB(val), options, createCallback); - } else { - connector.create(modelName, this.constructor._forDB(val), createCallback); - } + context = { + Model: Model, + data: val, + 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); } @@ -425,12 +438,7 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data var connector = self.getConnector(); if (Model.settings.validateUpsert === false) { - update = removeUndefined(update); - if (connector.updateOrCreate.length === 4) { - connector.updateOrCreate(Model.modelName, update, options, done); - } else { - connector.updateOrCreate(Model.modelName, update, done); - } + callConnector(); } else { inst.isValid(function(valid) { if (!valid) { @@ -443,16 +451,29 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data // 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) { connector.updateOrCreate(Model.modelName, update, options, done); } else { connector.updateOrCreate(Model.modelName, update, done); } - }, update); + }); } - function done(err, data, info) { var obj; if (data && !(data instanceof Model)) { @@ -562,9 +583,8 @@ DataAccessObject.findOrCreate = function findOrCreate(query, data, options, cb) var self = this; var connector = Model.getConnector(); - function _findOrCreate(query, data) { + function _findOrCreate(query, data, currentInstance) { var modelName = self.modelName; - data = removeUndefined(data); function findOrCreateCallback(err, data, created) { var obj, Model = self.lookupModel(data); @@ -598,11 +618,26 @@ DataAccessObject.findOrCreate = function findOrCreate(query, data, options, cb) } } - if (connector.findOrCreate.length === 5) { - connector.findOrCreate(modelName, query, self._forDB(data), options, findOrCreateCallback); - } else { - connector.findOrCreate(modelName, query, self._forDB(data), findOrCreateCallback); - } + data = removeUndefined(data); + var context = { + Model: Model, + 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) { @@ -653,7 +688,7 @@ DataAccessObject.findOrCreate = function findOrCreate(query, data, options, cb) // validation required obj.isValid(function (valid) { if (valid) { - _findOrCreate(query, data); + _findOrCreate(query, data, obj); } else { cb(new ValidationError(obj), obj); } @@ -1789,11 +1824,25 @@ DataAccessObject.prototype.save = function (options, cb) { }); } - if (connector.save.length === 4) { - connector.save(modelName, inst.constructor._forDB(data), options, saveCallback); - } else { - connector.save(modelName, inst.constructor._forDB(data), saveCallback); - } + context = { + Model: Model, + data: data, + 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); } @@ -1919,11 +1968,22 @@ DataAccessObject.updateAll = function (where, data, options, cb) { }); } - if (connector.update.length === 5) { - connector.update(Model.modelName, where, data, options, updateCallback); - } else { - connector.update(Model.modelName, where, data, updateCallback); - } + var context = { + Model: Model, + where: where, + 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; }; @@ -2249,13 +2309,23 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, op }); } - 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); - } + context = { + Model: Model, + where: byIdQuery(Model, getIdValue(Model, inst)).where, + data: data, + currentInstance: inst, + 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); diff --git a/test/persistence-hooks.suite.js b/test/persistence-hooks.suite.js index ea4fd5d5..35c6b882 100644 --- a/test/persistence-hooks.suite.js +++ b/test/persistence-hooks.suite.js @@ -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) { TestModel.observe('after save', pushContextAndNext()); @@ -372,6 +419,7 @@ module.exports = function(dataSource, should) { triggered.should.eql([ 'access', 'before save', + 'persist', 'after save' ]); 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) { 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) { 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) { 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) { 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) { TestModel.observe('after save', pushContextAndNext());