Meteor Collection Hooks

repository·master·Indexed 20 days ago

https://github.com/meteor-community-packages/meteor-collection-hooks

Extends Mongo.Collection with before and after hooks for insert, update, remove, upsert, find, and findOne operations. Version 2.0.0 provides support for Meteor 3's asynchronous patterns, allowing async functions for most hooks, though before.find remains synchronous. Includes features for modifying documents via this.transform(), managing hook lifecycles with remove() and replace(), and bypassing hooks using .direct methods.

Tokens
6.1K
Snippets
24
Records
35
Agent score
71%

What's inside meteor-collection-hooks

  1. Meteor 3 compatibility and Find hook behavior

    master

    In Meteor 3, hooks behave differently regarding synchronous vs asynchronous methods:

    • Find hooks: Only trigger on async cursor methods (e.g., fetchAsync(), countAsync()). Synchronous methods like fetch() or count() do not trigger hooks.
    • findOne hooks: Only trigger on findOneAsync(). The synchronous findOne() method does not trigger hooks.
    • Constraint: before.find hooks cannot be async; providing an async function to a before.find hook will throw an error.
  2. Trigger conditions for find and findOne hooks

    master

    Hooks for find and findOne are only triggered when using the Async versions of the methods. Using synchronous methods will bypass the hooks.

    find hooks

    To trigger before.find or after.find, you must use cursor async methods:

    • await cursor.fetchAsync()
    • await cursor.countAsync()
    • await cursor.forEachAsync()

    Note: collection.find({}).fetch() will NOT trigger hooks.

    findOne hooks

    To trigger before.findOne or after.findOne, you must use:

    • await collection.findOneAsync({})

    Note: collection.findOne({}) will NOT trigger hooks.

    // find hooks trigger
    const cursor = collection.find({});
    await cursor.fetchAsync();    // ✅ Hooks fire
    
    // findOne hooks trigger
    await collection.findOneAsync({}); // ✅ Triggers hooks
  3. How async hooks work in Meteor 3

    master

    As of v2.0.0, most hooks support async functions. However, due to Meteor 3's synchronous find() method, there are specific limitations on where you can use async functions:

    • Supported Async Hooks: before.insert, after.insert, before.update, after.update, before.remove, after.remove, before.upsert, before.findOne, and after.findOne.
    • Unsupported Async Hooks: before.find cannot be an async function and will throw an error if it is.
    • Supported Sync/Async: after.find supports both sync and async functions.
    // ✅ Async hooks work for these operations
    collection.before.insert(async function(userId, doc) {
      await validateDoc(doc);
    });
    
    // ❌ THROWS ERROR: Async before.find hooks
    collection.before.find(async function(userId, selector, options) {
      // This will throw: "Cannot use async function as before.find hook"
    });
    
    // ✅ WORKS: after.find hooks (sync and async)
    collection.after.find(async function(userId, selector, options, cursor) {
      await logFindOperation(selector);
    });
  4. How hooks and direct operations work together

    master
    The library uses a directEnv (an environment variable) to manage the execution context. When collection.direct.method() is called, it sets a flag that tells the wrapped collection methods to bypass the hook execution logic and call the original underlying driver method instead. This prevents infinite recursion and allows for clean separation between 'hooked' and 'unhooked' operations.
  5. Configure default hook options

    master

    You can specify default options for hooks globally or on a per-collection basis. Options follow a specificity hierarchy: more specific definitions override more general ones.

    Specificity order (from least to most specific):

    1. CollectionHooks.defaults.all.all (Global, all hooks)
    2. CollectionHooks.defaults.before.all (Global, all 'before' hooks)
    3. CollectionHooks.defaults.after.all (Global, all 'after' hooks)
    4. CollectionHooks.defaults.all.update (Global, specific method)
    5. CollectionHooks.defaults.before.insert (Global, specific method and type)
    6. testCollection.hookOptions... (Collection-specific overrides)

    Note: As of version 0.7.0, the only implemented option is fetchPrevious, which is only relevant to after.update hooks.

    import { CollectionHooks } from 'meteor/matb33:collection-hooks';
    import { Mongo } from 'meteor/mongo';
    
    // Global defaults
    CollectionHooks.defaults.all.all = {exampleOption: 1};
    CollectionHooks.defaults.before.insert = {exampleOption: 6};
    
    // Collection-specific defaults (higher specificity)
    const testCollection = new Mongo.Collection("test");
    testCollection.hookOptions.all.all = {exampleOption: 1};
    testCollection.hookOptions.before.insert = {exampleOption: 6};
  6. Use .after.update(userId, doc, fieldNames, modifier, options) to compare documents

    master

    The after.update hook fires after a document is updated. This is useful for comparing the previous state of a document with its new state.

    • this.previous: Contains the document before it was updated.
    • fetchPrevious option: By default, the hook fetches the previous document. To avoid this performance hit, set { fetchPrevious: false } in the hook options.
    • Important: If any after.update hook for a collection does not set fetchPrevious: false, all hooks for that collection will fetch the document. It is recommended to set this globally via MyCollection.hookOptions.after.update = {fetchPrevious: false};.
    • this.transform(): Accepts an optional parameter to transform the previous document: this.transform(this.previous).
    test.after.update(function (userId, doc, fieldNames, modifier, options) {
      // ... post-update logic
    }, {fetchPrevious: true/false});
  7. Use .after.insert(userId, doc) for post-insertion tasks

    master

    The after.insert hook fires after a document has been successfully inserted. Use this for tasks like sending notifications or triggering external workflows.

    • this.transform(): Obtains the transformed version of the document.
    • this._id: Holds the newly inserted _id.
    test.after.insert(function (userId, doc) {
      // ... post-insert logic
    });
  8. Circumvent hooks using .direct methods

    master

    If you need to perform a collection operation without triggering any defined hooks, use the .direct property on the collection. This provides access to both synchronous and asynchronous versions of all standard Mongo methods.

    collection.direct.insert({_id: "test", test: 1});
    collection.direct.insertAsync({_id: "test", test: 1});
    collection.direct.update({_id: "test"}, {$set: {test: 1}});
    collection.direct.updateAsync({_id: "test"}, {$set: {test: 1}});
    collection.direct.find({test: 1});
    collection.direct.findOne({test: 1});
    collection.direct.findOneAsync({test: 1});
    collection.direct.remove({_id: "test"});
    collection.direct.removeAsync({_id: "test"});
  9. Bypass hooks using the direct environment

    master

    If you need to perform a Mongo operation without triggering hooks, use the direct version of the method.

    Note on nesting: Any Mongo operations performed within nested callbacks of a direct operation will also run as direct by default. To revert to normal hook behavior within a nested callback, unset the direct setting using: CollectionHooks.directEnv = new Meteor.EnvironmentVariable(false)

  10. Use .after.findOne(userId, selector, options, doc) for post-findOne tasks

    master

    The after.findOne hook fires after a findOne query. It supports async functions and provides the found doc. This hook only triggers when using findOneAsync().

    // ✅ Async after.findOne
    test.after.findOne(async function (userId, selector, options, doc) {
      await processDocument(doc);
    });