Meteor Collection Hooks
Extends Mongo.Collection with before
/after
hooks for insert
, update
, remove
, find
, and findOne
.
Works across client, server or a mix. Also works when a client initiates a collection method and the server runs the hook, all while respecting the collection validators (allow/deny).
Please refer to History.md for a summary of recent changes.
Getting Started
Installation:
meteor add matb33:collection-hooks
.before.insert(userId, doc)
Fired before the doc is inserted.
Allows you to modify doc as needed, or run additional functionality
this.transform()
obtains transformed version of document, if a transform was
defined.
1import { Mongo } from 'meteor/mongo'; 2const test = new Mongo.Collection("test"); 3 4test.before.insert(function (userId, doc) { 5 doc.createdAt = Date.now(); 6});
.before.update(userId, doc, fieldNames, modifier, options)
Fired before the doc is updated.
Allows you to to change the modifier
as needed, or run additional
functionality.
this.transform()
obtains transformed version of document, if a transform was
defined.
1test.before.update(function (userId, doc, fieldNames, modifier, options) { 2 modifier.$set = modifier.$set || {}; 3 modifier.$set.modifiedAt = Date.now(); 4});
Important:
- Note that we are changing
modifier
, and notdoc
.
Changing doc
won't have any effect as the document is a copy and is not what
ultimately gets sent down to the underlying update
method.
- When triggering a single update targeting multiple documents using the option
multi: true
(see Meteor documentation), thebefore.update
hook is called once per document about to be updated, but the collection update called afterwards remains a single update (targetting multiple documents) with a single modifier. Hence it is not possible at the time to usebefore.update
to create a specific modifier for each targeted document.
.before.remove(userId, doc)
Fired just before the doc is removed.
Allows you to to affect your system while the document is still in existence -- useful for maintaining system integrity, such as cascading deletes.
this.transform()
obtains transformed version of document, if a transform was
defined.
1test.before.remove(function (userId, doc) { 2 // ... 3});
.before.upsert(userId, selector, modifier, options)
Fired before the doc is upserted.
Allows you to to change the modifier
as needed, or run additional
functionality.
1test.before.upsert(function (userId, selector, modifier, options) { 2 modifier.$set = modifier.$set || {}; 3 modifier.$set.modifiedAt = Date.now(); 4});
Note that calling upsert
will always fire .before.upsert
hooks, but will
call either .after.insert
or .after.update
hooks depending on the outcome of
the upsert
operation. There is no such thing as a .after.upsert
hook at this
time.
.after.insert(userId, doc)
Fired after the doc was inserted.
Allows you to run post-insert tasks, such as sending notifications of new document insertions.
this.transform()
obtains transformed version of document, if a transform was
defined;
this._id
holds the newly inserted_id
if available.
1test.after.insert(function (userId, doc) { 2 // ... 3});
.after.update(userId, doc, fieldNames, modifier, options)
Fired after the doc was updated.
Allows you to to run post-update tasks, potentially comparing the previous and new documents to take further action.
this.previous
contains the document before it was updated.- The optional
fetchPrevious
option, when set to false, will not fetch documents before running the hooks.this.previous
will then not be available. The default behavior is to fetch the documents.
- The optional
this.transform()
obtains transformed version of document, if a transform was defined. Note that this function accepts an optional parameter to specify the document to transform — useful to transform previous:this.transform(this.previous)
.
1test.after.update(function (userId, doc, fieldNames, modifier, options) { 2 // ... 3}, {fetchPrevious: true/false});
Important: If you have multiple hooks defined, and at least one of them does
not specify fetchPrevious: false
, then the documents will be fetched
and provided as this.previous
to all hook callbacks. All after-update hooks
for the same collection must have fetchPrevious: false
set in order to
effectively disable the pre-fetching of documents.
It is instead recommended to use the collection-wide options (e.g.
MyCollection.hookOptions.after.update = {fetchPrevious: false};
).
This hook will always be called with the new documents; even if the updated document gets modified in a way were it would normally not be able to be found because of before.find
hooks (see https://github.com/Meteor-Community-Packages/meteor-collection-hooks/pull/297).
.after.remove(userId, doc)
Fired after the doc was removed.
doc
contains a copy of the document before it was removed.
Allows you to run post-removal tasks that don't necessarily depend on the document being found in the database (external service clean-up for instance).
this.transform()
obtains transformed version of document, if a transform was
defined.
1test.after.remove(function (userId, doc) { 2 // ... 3});
.before.find(userId, selector, options)
Fired before a find query.
Allows you to adjust selector/options on-the-fly.
1test.before.find(function (userId, selector, options) { 2 // ... 3});
Important:
- The function used as
before.find
hook cannot be async - This hook does not get called for
after.update
hooks (see https://github.com/Meteor-Community-Packages/meteor-collection-hooks/pull/297).
.after.find(userId, selector, options, cursor)
Fired after a find query.
Allows you to act on a given find query. The cursor resulting from the query is provided as the last argument for convenience.
1test.after.find(function (userId, selector, options, cursor) { 2 // ... 3});
.before.findOne(userId, selector, options)
Fired before a findOne query.
Allows you to adjust selector/options on-the-fly.
1test.before.findOne(function (userId, selector, options) { 2 // ... 3});
.after.findOne(userId, selector, options, doc)
Fired after a findOne query.
Allows you to act on a given findOne query. The document resulting from the query is provided as the last argument for convenience.
1test.after.findOne(function (userId, selector, options, doc) { 2 // ... 3});
Direct access (circumventing hooks)
All compatible methods have a direct
version that circumvent any defined hooks. For example:
1collection.direct.insert({_id: "test", test: 1}); 2collection.direct.insertAsync({_id: "test", test: 1}); 3collection.direct.upsert({_id: "test", test: 1}); 4collection.direct.upsertAsync({_id: "test", test: 1}); 5collection.direct.update({_id: "test"}, {$set: {test: 1}}); 6collection.direct.updateAsync({_id: "test"}, {$set: {test: 1}}); 7collection.direct.find({test: 1}); 8collection.direct.findOne({test: 1}); 9collection.direct.findOneAsync({test: 1}); 10collection.direct.remove({_id: "test"}); 11collection.direct.removeAsync({_id: "test"});
Default options
As of version 0.7.0, options can be passed to hook definitions. Default options can be specified globally and on a per-collection basis for all or some hooks, with more specific ones having higher specificity.
Examples (in order of least specific to most specific):
1import { CollectionHooks } from 'meteor/matb33:collection-hooks'; 2 3CollectionHooks.defaults.all.all = {exampleOption: 1}; 4 5CollectionHooks.defaults.before.all = {exampleOption: 2}; 6CollectionHooks.defaults.after.all = {exampleOption: 3}; 7 8CollectionHooks.defaults.all.update = {exampleOption: 4}; 9CollectionHooks.defaults.all.remove = {exampleOption: 5}; 10 11CollectionHooks.defaults.before.insert = {exampleOption: 6}; 12CollectionHooks.defaults.after.remove = {exampleOption: 7};
Similarly, collection-wide options can be defined (these have a higher specificity than the global defaults from above):
1import { Mongo } from 'meteor/mongo'; 2const testCollection = new Mongo.Collection("test"); 3 4testCollection.hookOptions.all.all = {exampleOption: 1}; 5 6testCollection.hookOptions.before.all = {exampleOption: 2}; 7testCollection.hookOptions.after.all = {exampleOption: 3}; 8 9testCollection.hookOptions.all.update = {exampleOption: 4}; 10testCollection.hookOptions.all.remove = {exampleOption: 5}; 11 12testCollection.hookOptions.before.insert = {exampleOption: 6}; 13testCollection.hookOptions.after.remove = {exampleOption: 7};
Currently (as of 0.7.0), only fetchPrevious
is implemented as an option, and
is only relevant to after-update hooks.
Additional notes
- Returning
false
in anybefore
hook will prevent the underlying method (and
subsequent after
hooks) from executing. Note that all before
hooks will
still continue to run even if the first hook returns false
.
- ~~If you wish to make
userId
available to afind
query in apublish
function, try the technique detailed in this comment~~ userId
is available to find
and findOne
queries that were invoked within a publish
function.
- All hook callbacks have
this._super
available to them (the underlying
method) as well as this.context
, the equivalent of this
to the underlying
method. Additionally, this.args
contain the original arguments passed to the
method and can be modified by reference (for example, modifying a selector in a
before
hook so that the underlying method uses this new selector).
- It is quite normal for
userId
to sometimes be unavailable to hook callbacks
in some circumstances. For example, if an update
is fired from the server
with no user context, the server certainly won't be able to provide any
particular userId.
-
You can define a
defaultUserId
in case you want to pass an userId to the hooks but there is no context. For instance if you are executing and API endpoint where theuserId
is derived from a token. Just assign the userId toCollectionHooks.defaultUserId
. It will be overriden by the userId of the context if it exists. -
If, like me, you transform
Meteor.users
through a round-about way involving
find
and findOne
, then you won't be able to use this.transform()
. Instead,
grab the transformed user with findOne
.
-
When adding a hook, a handler object is returned with these methods:
remove()
: will remove that particular hook;replace(callback, options)
: will replace the hook callback and options.
-
If your hook is defined in common code (both server and client), it will run
twice: once on the server and once on the client. If your intention is for the hook to run only once, make sure the hook is defined somewhere where only either the client or the server reads it. When in doubt, define your hooks on the server.
- Both
update
andremove
internally make use offind
, so be aware that
find
/findOne
hooks can fire for those methods.
-
find
hooks are also fired when fetching documents forupdate
,upsert
andremove
hooks. -
If using the
direct
version to bypass a hook, any mongo operations done within nested
callbacks of the direct
operation will also by default run as direct
. You can use the following
line in a nested callback before the operation to unset the direct
setting:
CollectionHooks.directEnv = new Meteor.EnvironmentVariable(false)
Maintainers
Maintained by Meteor Community Packages and in particular by:
- Mathieu Bouchard (matb33)
- Andrew Mao (mizzao)
- Simon Fridlund (zimme)
- Jan Dvorak (StorytellerCZ)
Contributors
- Eric Dobbertin (aldeed)
- Kevin Kaland (wizonesolutions)
- Jonathan James (jonjamz)
- Dave Workman (davidworkman9)
- Tarang Patel (Tarangp)
- Nathan Strauser (nate-strauser)
- Hubert OG (subhog)
- Richard Lai (rclai)
- Sahebjot Singh (raunaqrox)
- Aram Kocharyan (aramk)
- Pierre Ozoux (pierreozoux)
- Tom Coleman (tmeasday)
- Eric Jackson (repjackson)
- Koen Lav (KoenLav)
- Chris Pravetz (cpravetz)
- Jan Kuster (jankapunkt)