From 98c97473e414183046d4ce09a079d00ee77f5496 Mon Sep 17 00:00:00 2001 From: David Burles Date: Wed, 1 Oct 2014 10:38:21 +1000 Subject: [PATCH] work in progress with chris' hooks --- counter-cache.js | 46 ++++++++++------- counter-cache_tests.js | 4 +- hooks.js | 109 +++++++++++++++++++++++++++++++++++++++ hooks_tests.js | 113 +++++++++++++++++++++++++++++++++++++++++ package.js | 15 +++--- versions.json | 87 +++++++++++++++++++++++++++++++ 6 files changed, 348 insertions(+), 26 deletions(-) create mode 100644 hooks.js create mode 100644 hooks_tests.js create mode 100644 versions.json diff --git a/counter-cache.js b/counter-cache.js index d8d039b..8348254 100644 --- a/counter-cache.js +++ b/counter-cache.js @@ -12,6 +12,7 @@ _.mixin({ }); var resolveForeignKey = function(doc, foreignKey) { + // console.log(doc, foreignKey); return _.isFunction(foreignKey) ? foreignKey(doc) : _.dottedProperty(doc, foreignKey); }; @@ -22,7 +23,7 @@ var applyFilter = function(doc, filter) { return true; }; -Meteor.Collection.prototype.maintainCountOf = function(collection, foreignKey, counterField, filter) { +Mongo.Collection.prototype.maintainCountOf = function(collection, foreignKey, counterField, filter) { var self = this; // what is Meteor.users an instanceof ? @@ -54,15 +55,17 @@ Meteor.Collection.prototype.maintainCountOf = function(collection, foreignKey, c self.update(_id, modifier); }; - collection.after.insert(function(userId, doc) { + collection.afterInsert(function(doc) { var foreignKeyValue = resolveForeignKey(doc, foreignKey); if (foreignKeyValue && applyFilter(doc, filter)) increment(foreignKeyValue); }); - collection.after.update(function(userId, doc, fieldNames, modifier, options) { + collection.afterUpdate({ needsPrevious: true }, function(_id, modifier) { var self = this; - var oldDoc = self.previous; + var oldDocs = this.previous; + + // console.log(oldDoc.fetch()[0]); // console.log(modifier); // console.log(fieldNames); @@ -71,25 +74,32 @@ Meteor.Collection.prototype.maintainCountOf = function(collection, foreignKey, c // LocalCollection._modify(doc, modifier); - var oldDocForeignKeyValue = resolveForeignKey(oldDoc, foreignKey); - var newDocForeignKeyValue = resolveForeignKey(doc, foreignKey); + _.each(oldDocs, function(oldDoc) { + var doc = collection.findOne(_id); + var oldDocForeignKeyValue = resolveForeignKey(oldDoc, foreignKey); + var newDocForeignKeyValue = resolveForeignKey(doc, foreignKey); - var filterApplyOldValue = applyFilter(oldDoc, filter); - var filterApplyNewValue = applyFilter(doc, filter); + var filterApplyOldValue = applyFilter(oldDoc, filter); + var filterApplyNewValue = applyFilter(doc, filter); - if (oldDocForeignKeyValue === newDocForeignKeyValue && filterApplyOldValue === filterApplyNewValue) - return; + if (oldDocForeignKeyValue === newDocForeignKeyValue && filterApplyOldValue === filterApplyNewValue) + return; - if (oldDocForeignKeyValue && filterApplyOldValue) - decrement(oldDocForeignKeyValue); + if (oldDocForeignKeyValue && filterApplyOldValue) + decrement(oldDocForeignKeyValue); - if (newDocForeignKeyValue && filterApplyNewValue) - increment(newDocForeignKeyValue); + if (newDocForeignKeyValue && filterApplyNewValue) + increment(newDocForeignKeyValue); + }); }); - collection.after.remove(function(userId, doc) { - var foreignKeyValue = resolveForeignKey(doc, foreignKey); - if (foreignKeyValue && applyFilter(doc, filter)) - decrement(foreignKeyValue); + collection.afterRemove({ needsPrevious: true }, function(_id) { + var docs = this.previous; + + _.each(docs, function(doc) { + var foreignKeyValue = resolveForeignKey(doc, foreignKey); + if (foreignKeyValue && applyFilter(doc, filter)) + decrement(foreignKeyValue); + }); }); }; diff --git a/counter-cache_tests.js b/counter-cache_tests.js index ca3b1bc..9706900 100644 --- a/counter-cache_tests.js +++ b/counter-cache_tests.js @@ -1,6 +1,6 @@ Tinytest.add('Counter cache - foreignKey works', function(test) { - Authors = new Meteor.Collection('authors' + test.id); - Books = new Meteor.Collection('books' + test.id); + Authors = new Mongo.Collection('authors' + test.id); + Books = new Mongo.Collection('books' + test.id); Authors.maintainCountOf(Books, 'authorId', 'booksCount'); diff --git a/hooks.js b/hooks.js new file mode 100644 index 0000000..b8634d9 --- /dev/null +++ b/hooks.js @@ -0,0 +1,109 @@ +var Collection = Mongo.Collection; + +var capitalize = function (string) { +  return string.charAt(0).toUpperCase() + string.substring(1).toLowerCase(); +}; + +var needsPrevious = function(klass, collectionName, method) { + // grab the hooks + + // console.log(collectionName); + var hooks = Meteor._ensure(klass, '_hooks', collectionName); + // console.log(hooks); + // console.log(method); + if (method === 'update') + hooks = hooks.afterUpdate; + if (method === 'remove') + hooks = hooks.afterRemove; + // if (hooks) console.log(hooks); + // if (method === 'remove') console.log(hooks); + return _.any(hooks, function(hook) { return hook.needsPrevious; }); +}; + +var runHooks = function (klass, hook, collection, args) { +  var self = this +    , hooks +    , callbacks; + +  hooks = Meteor._ensure(klass, '_hooks', collection); +  callbacks = hooks[hook]; + + // args.push() + +  // first arg might be the collection name + +//   if (typeof args[0] === 'string') +//     args = args.slice(1); + +  if (_.isArray(callbacks)) { +    _.each(callbacks, function (hook) { +      hook.callback.apply(self, args); +    }); +  } +}; + +var defineHookMethod = function (klass, name) { +  Collection.prototype[name] = function (options, fn) { + // if options is a function, fn = options; + fn = fn || _.isFunction(options) && options; + var hook = _.extend({callback: fn}, options); + +    var collectionName = this._name +      , hooks = Meteor._ensure(klass, '_hooks', collectionName) +      , callbacks; + +    callbacks = hooks[name] = hooks[name] || []; +    callbacks.push(hook); +  }; +}; + +var wrapMethod = function (klass, method) { +  var wrapped = klass.prototype[method]; +  klass.prototype[method] = function (/* args */) { +    var ret +      , args = _.toArray(arguments) +      , collectionName = this._name || args[0]; + + + // if (Meteor.isServer) + // args.shift(); // drop the first argument + + if (needsPrevious(klass, collectionName, method)) + this.previous = this.find(args[0]).fetch(); + + // console.log(args); +    runHooks.call(this, +      klass, +      'before' + capitalize(method), +      collectionName, +      args +    ); + +    ret = wrapped.apply(this, arguments); + +    runHooks.call(this, +      klass, +      'after' + capitalize(method), +      collectionName, +      args +    ); + +    return ret; +  }; +}; + +var wrapMutatorMethods = function (klass) { +  _.each(['insert', 'update', 'remove'], function (method) { +    defineHookMethod(klass, 'before' + capitalize(method)); +    defineHookMethod(klass, 'after' + capitalize(method)); +    wrapMethod(klass, method); +  }); +}; + +// if (Meteor.isServer) { +//   wrapMutatorMethods(MongoInternals.Connection); +// } + +// if (Meteor.isClient) { +wrapMutatorMethods(Collection); +// } \ No newline at end of file diff --git a/hooks_tests.js b/hooks_tests.js new file mode 100644 index 0000000..b10c0e7 --- /dev/null +++ b/hooks_tests.js @@ -0,0 +1,113 @@ +var capitalize = function (string) { +  return string.charAt(0).toUpperCase() + string.substring(1).toLowerCase(); +}; + +Tinytest.add('Collection Hooks', function (test) { + +  //XXX on client +  //  - insert/update/remove invokes callbacks +  //  - callbacks not invoked if inserted directly into the collection +  // +  //XXX on server +  //  - insert/update/remove invokes callbacks +  //  - callbacks ALSO invoked if you insert directly into the collection + + +  if (Meteor.isClient) { +    ClientItems = new Mongo.Collection('client_items', {connection: null}); + +    var hookCalls = { +      total: 0 +    }; + +    _.each(['insert', 'update', 'remove'], function (method) { +      var beforeHook = 'before' + capitalize(method); +      var afterHook = 'after' + capitalize(method); + +      hookCalls[beforeHook] = 0; +      hookCalls[afterHook] = 0; + +      ClientItems[beforeHook](function () { +        hookCalls[beforeHook]++; +        hookCalls.total++; +      }); + +      ClientItems[afterHook](function () { +        hookCalls[afterHook]++; +        hookCalls.total++; +      }); +    }); + +    var id = ClientItems._collection.insert({title: '1'}); +    test.equal(hookCalls.total, 0, 'Hooks should not be called for LocalCollection.prototype.insert'); + +    ClientItems._collection.update({_id: id}, {$set: {title: 'updated'}}); +    test.equal(hookCalls.total, 0, 'Hooks should not be called for LocalCollection.prototype.update'); + +    ClientItems._collection.remove({_id: id}); +    test.equal(hookCalls.total, 0, 'Hooks should not be called for LocalCollection.prototype.remove'); + +    var id = ClientItems.insert({title: '2'}); +    test.equal(hookCalls.total, 2, 'Hooks should be called for Collection.prototype.insert'); +    test.equal(hookCalls.beforeInsert, 1); +    test.equal(hookCalls.afterInsert, 1); + +    ClientItems.update({_id: id}, {$set: {title: 'updated'}}); +    test.equal(hookCalls.total, 4, 'Hooks should be called for Collection.prototype.update'); +    test.equal(hookCalls.beforeUpdate, 1); +    test.equal(hookCalls.afterUpdate, 1); + +    ClientItems.remove({_id: id}); +    test.equal(hookCalls.total, 6, 'Hooks should be called for Collection.prototype.remove'); +    test.equal(hookCalls.beforeRemove, 1); +    test.equal(hookCalls.afterRemove, 1); +  } + +  if (Meteor.isServer) { +    ServerItems = new Mongo.Collection('server_items'); + +    ServerItems.allow({ +      insert: function () { return true; }, +      update: function () { return true; }, +      remove: function () { return true; } +    }); + +    var hookCalls = { +      total: 0 +    }; + +    _.each(['insert', 'update', 'remove'], function (method) { +      var beforeHook = 'before' + capitalize(method); +      var afterHook = 'after' + capitalize(method); + +      hookCalls[beforeHook] = 0; +      hookCalls[afterHook] = 0; + +      ServerItems[beforeHook](function () { +        hookCalls[beforeHook]++; +        hookCalls.total++; +      }); + +      ServerItems[afterHook](function () { +        hookCalls[afterHook]++; +        hookCalls.total++; +      }); +    }); + +    var id = ServerItems._collection.insert({title: '1'}); +    test.equal(hookCalls.total, 2, 'Hooks should be called for Mongo insert'); +    test.equal(hookCalls.beforeInsert, 1); +    test.equal(hookCalls.afterInsert, 1); + +    ServerItems._collection.update({_id: id}, {$set: {title: 'updated'}}); +    test.equal(hookCalls.total, 4, 'Hooks should be called for Mongo update'); +    test.equal(hookCalls.beforeUpdate, 1); +    test.equal(hookCalls.afterUpdate, 1); + +    ServerItems._collection.remove({_id: id}); +    test.equal(hookCalls.total, 6, 'Hooks should be called for Mongo remove'); +    test.equal(hookCalls.beforeRemove, 1); +    test.equal(hookCalls.afterRemove, 1); + +  } +}); \ No newline at end of file diff --git a/package.js b/package.js index 095bbcf..143adb2 100644 --- a/package.js +++ b/package.js @@ -1,13 +1,16 @@ Package.describe({ - summary: 'Cache the counts of an associated collection.' + summary: "Cache the counts of an associated collection", + version: "1.0.0", + git: "https://github.com/percolatestudio/meteor-counter-cache.git" }); -Package.on_use(function(api) { - api.use(['collection-hooks', 'underscore']); - api.add_files('counter-cache.js', ['client', 'server']); +Package.onUse(function(api) { + api.use(['underscore', 'mongo']); + api.add_files('hooks.js'); + api.add_files('counter-cache.js'); }); -Package.on_test(function(api) { +Package.onTest(function(api) { api.use(['tinytest', 'counter-cache']); - api.add_files('counter-cache_tests.js', 'server'); + api.add_files('counter-cache_tests.js'); }); diff --git a/versions.json b/versions.json new file mode 100644 index 0000000..489cf44 --- /dev/null +++ b/versions.json @@ -0,0 +1,87 @@ +{ + "dependencies": [ + [ + "application-configuration", + "1.0.2" + ], + [ + "base64", + "1.0.0" + ], + [ + "binary-heap", + "1.0.0" + ], + [ + "callback-hook", + "1.0.0" + ], + [ + "check", + "1.0.1" + ], + [ + "ddp", + "1.0.9" + ], + [ + "ejson", + "1.0.3" + ], + [ + "follower-livedata", + "1.0.1" + ], + [ + "geojson-utils", + "1.0.0" + ], + [ + "id-map", + "1.0.0" + ], + [ + "json", + "1.0.0" + ], + [ + "logging", + "1.0.3" + ], + [ + "meteor", + "1.1.1" + ], + [ + "minimongo", + "1.0.3" + ], + [ + "mongo", + "1.0.6" + ], + [ + "ordered-dict", + "1.0.0" + ], + [ + "random", + "1.0.0" + ], + [ + "retry", + "1.0.0" + ], + [ + "tracker", + "1.0.2" + ], + [ + "underscore", + "1.0.0" + ] + ], + "pluginDependencies": [], + "toolVersion": "meteor-tool@1.0.32", + "format": "1.0" +} \ No newline at end of file