diff --git a/support/describe-operation-hooks.js b/support/describe-operation-hooks.js new file mode 100644 index 00000000..d9a30391 --- /dev/null +++ b/support/describe-operation-hooks.js @@ -0,0 +1,220 @@ +/* + * Describe context objects of operation hooks in comprehensive HTML table. + * Usage: + * $ node support/describe-operation-hooks.js > hooks.html + * $ open hooks.hml + * + */ +var Promise = global.Promise = require('bluebird'); +var DataSource = require('../').DataSource; +var Memory = require('../lib/connectors/memory').Memory; + +var HOOK_NAMES = [ + 'access', + 'before save', 'persist', 'after save', + 'before delete', 'after delete' +]; + +var dataSources = [ + createOptimizedDataSource(), + createUnoptimizedDataSource() +]; + +var observedContexts = []; +var lastId = 0; + +Promise.onPossiblyUnhandledRejection(function(err) { + console.error('POSSIBLY UNHANDLED REJECTION', err.stack); +}); + +var operations = [ + function find(ds) { + return ds.TestModel.find({ where: { id: '1' } }); + }, + + function count(ds) { + return ds.TestModel.count({ id: ds.existingInstance.id }); + }, + + function create(ds) { + return ds.TestModel.create({ name: 'created' }); + }, + + function findOrCreate_found(ds) { + return ds.TestModel.findOrCreate( + { where: { name: ds.existingInstance.name } }, + { name: ds.existingInstance.name }); + }, + + function findOrCreate_create(ds) { + return ds.TestModel.findOrCreate( + { where: { name: 'new-record' } }, + { name: 'new-record' }); + }, + + function updateOrCreate_create(ds) { + return ds.TestModel.updateOrCreate({ id: 'not-found', name: 'not found' }); + }, + + function updateOrCreate_update(ds) { + return ds.TestModel.updateOrCreate( + { id: ds.existingInstance.id, name: 'new name' }); + }, + + function updateAll(ds) { + return ds.TestModel.updateAll({ name: 'searched' }, { name: 'updated' }); + }, + + function prototypeSave(ds) { + ds.existingInstance.name = 'changed'; + return ds.existingInstance.save(); + }, + + function prototypeUpdateAttributes(ds) { + return ds.existingInstance.updateAttributes({ name: 'changed' }); + }, + + function prototypeDelete(ds) { + return ds.existingInstance.delete(); + }, + + function deleteAll(ds) { + return ds.TestModel.deleteAll({ name: ds.existingInstance.name }); + }, +]; + +var p = setupTestModels(); +operations.forEach(function(op) { + p = p.then(runner(op)); +}); + +p.then(report, console.error); + + +function createOptimizedDataSource() { + var ds = new DataSource({ connector: Memory }); + ds.name = 'Optimized'; + + ds.connector.findOrCreate = function (model, query, data, callback) { + this.all(model, query, {}, function (err, list) { + if (err || (list && list[0])) return callback(err, list && list[0], false); + this.create(model, data, {}, function (err) { + callback(err, data, true); + }); + }.bind(this)); + }; + + return ds; +} + +function createUnoptimizedDataSource() { + var ds = new DataSource({ connector: Memory }); + ds.name = 'Unoptimized'; + + // disable optimized methods + ds.connector.updateOrCreate = false; + ds.connector.findOrCreate = false; + + return ds; +} + +function setupTestModels() { + dataSources.forEach(function setupOnDataSource(ds) { + var TestModel = ds.TestModel = ds.createModel('TestModel', { + id: { type: String, id: true, default: uid }, + name: { type: String, required: true }, + extra: { type: String, required: false } + }); + }); + return Promise.resolve(); +} + +function uid() { + lastId += 1; + return '' + lastId; +} + +function runner(fn) { + return function() { + var res = Promise.resolve(); + dataSources.forEach(function(ds) { + res = res.then(function() { + return resetStorage(ds); + }).then(function() { + observedContexts.push({ + operation: fn.name, + connector: ds.name, + hooks: {} + }); + return fn(ds); + }); + }); + return res; + }; +} + +function resetStorage(ds) { + var TestModel = ds.TestModel; + HOOK_NAMES.forEach(function(hook) { + TestModel.clearObservers(hook); + }); + return TestModel.deleteAll() + .then(function() { + return TestModel.create({ name: 'first' }) + }) + .then(function(instance) { + // Look it up from DB so that default values are retrieved + return TestModel.findById(instance.id) + }) + .then(function(instance) { + ds.existingInstance = instance; + return TestModel.create({ name: 'second' }); + }) + .then(function() { + HOOK_NAMES.forEach(function(hook) { + TestModel.observe(hook, function(ctx, next) { + var row = observedContexts[observedContexts.length-1]; + row.hooks[hook] = Object.keys(ctx); + next(); + }); + }); + }); +} + +function report() { + console.log(''); + + // merge rows where Optimized and Unoptimized produce the same context + observedContexts.forEach(function(row, ix) { + if (!ix) return; + var last = observedContexts[ix-1]; + if (row.operation != last.operation) return; + if (JSON.stringify(row.hooks) !== JSON.stringify(last.hooks)) return; + last.merge = true; + row.skip = true; + }); + + console.log('\n'); + console.log('\n '); + HOOK_NAMES.forEach(function(h) { console.log(' '); }); + console.log(''); + + observedContexts.forEach(function(row) { + if (row.skip) return; + var caption = row.operation; + if (!row.merge) caption += ' (' + row.connector + ')'; + console.log(''); + HOOK_NAMES.forEach(function(h) { + var text = row.hooks[h] ? row.hooks[h].join('
') : ''; + console.log(' '); + }); + console.log(''); + }); + console.log('
' + h + '
' + caption + '' + text + '
'); +}