55![ CodeQL Analysis] ( https://github.com/Meteor-Community-Packages/meteor-collection-hooks/workflows/CodeQL/badge.svg )
66
77
8- Extends Mongo.Collection with ` before ` /` after ` hooks for ` insert ` , ` update ` , ` remove ` , ` find ` , and ` findOne ` .
8+ Extends Mongo.Collection with ` before ` /` after ` hooks for ` insert ` , ` update ` , ` remove ` , ` upsert ` , ` find ` , and ` findOne ` . ** Meteor 2.16+ & 3.x compatible with async hook support. **
99
1010Works 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).
1111
@@ -19,6 +19,67 @@ Installation:
1919meteor add matb33:collection-hooks
2020```
2121
22+ ## Meteor 3 Compatibility & Async Hooks
23+
24+ ** Meteor Version Support:** 2.16+ through 3.1+
25+
26+ ** Important Behavioral Changes in Meteor 3:**
27+
28+ ### Async Hooks Support
29+ As of v2.0.0, most hooks support async functions:
30+
31+ ``` javascript
32+ // ✅ Async hooks work for these operations
33+ collection .before .insert (async function (userId , doc ) {
34+ await validateDoc (doc);
35+ });
36+
37+ collection .after .update (async function (userId , doc , fieldNames , modifier , options ) {
38+ await notifyExternalService (doc);
39+ });
40+ ```
41+
42+ ### Find Hooks Limitations
43+ Due to Meteor 3's synchronous ` find() ` method, find hooks have specific limitations:
44+
45+ ``` javascript
46+ // ✅ WORKS: Sync before.find hooks
47+ collection .before .find (function (userId , selector , options ) {
48+ selector .deletedAt = { $exists: false }; // Modify selector
49+ });
50+
51+ // ❌ THROWS ERROR: Async before.find hooks
52+ collection .before .find (async function (userId , selector , options ) {
53+ // This will throw: "Cannot use async function as before.find hook"
54+ });
55+
56+ // ✅ WORKS: after.find hooks (sync and async)
57+ collection .after .find (async function (userId , selector , options , cursor ) {
58+ await logFindOperation (selector);
59+ });
60+ ```
61+
62+ ### Hook Trigger Conditions
63+
64+ ** findOne Hooks:**
65+ ``` javascript
66+ await collection .findOneAsync ({}) // ✅ Triggers hooks
67+ collection .findOne ({}) // ❌ No hooks triggered
68+ ```
69+
70+ ** find Hooks:**
71+ ``` javascript
72+ // ✅ These trigger find hooks:
73+ const cursor = collection .find ({});
74+ await cursor .fetchAsync (); // ✅ Hooks fire
75+ await cursor .countAsync (); // ✅ Hooks fire
76+ await cursor .forEachAsync (); // ✅ Hooks fire
77+
78+ // ❌ These DON'T trigger find hooks:
79+ collection .find ({}).fetch (); // ❌ No hooks
80+ collection .find ({}).count (); // ❌ No hooks
81+ ```
82+
2283--------------------------------------------------------------------------------
2384
2485### .before.insert(userId, doc)
@@ -185,64 +246,100 @@ test.after.remove(function (userId, doc) {
185246
186247### .before.find(userId, selector, options)
187248
188- Fired before a find query.
249+ Fired before a find query. ** Meteor 3 Limitation: Cannot be async. **
189250
190251Allows you to adjust selector/options on-the-fly.
191252
192253``` javascript
193254test .before .find (function (userId , selector , options ) {
194- // ...
255+ // ✅ Sync operations only
256+ selector .deletedAt = { $exists: false };
257+ });
258+
259+ // ❌ This will throw an error:
260+ test .before .find (async function (userId , selector , options ) {
261+ // Error: "Cannot use async function as before.find hook"
195262});
196263```
197264
198265__ Important:__
199- - The function used as ` before.find ` hook cannot be async
200- - This hook does not get called for ` after.update ` hooks (see https://github.com/Meteor-Community-Packages/meteor-collection-hooks/pull/297 ).
266+ - The function used as ` before.find ` hook ** cannot be async** (throws error)
267+ - This hook does not get called for ` after.update ` hooks (see https://github.com/Meteor-Community-Packages/meteor-collection-hooks/pull/297 )
268+ - Only triggers when using cursor async methods (` fetchAsync() ` , ` countAsync() ` , etc.)
201269
202270--------------------------------------------------------------------------------
203271
204272### .after.find(userId, selector, options, cursor)
205273
206- Fired after a find query.
274+ Fired after a find query when using cursor async methods .
207275
208- Allows you to act on a given find query. The cursor resulting from
209- the query is provided as the last argument for convenience.
276+ Allows you to act on a given find query. Both sync and async functions are supported.
210277
211278``` javascript
279+ // ✅ Sync after.find
212280test .after .find (function (userId , selector , options , cursor ) {
213- // ...
281+ logOperation (selector);
282+ });
283+
284+ // ✅ Async after.find
285+ test .after .find (async function (userId , selector , options , cursor ) {
286+ await logToExternalService (selector);
214287});
215288```
216289
290+ ** Triggers only on cursor async methods:**
291+ ``` javascript
292+ const cursor = collection .find ({});
293+ await cursor .fetchAsync (); // ✅ Triggers after.find
294+ await cursor .countAsync (); // ✅ Triggers after.find
295+ cursor .fetch (); // ❌ No hooks triggered
296+ ```
297+
217298--------------------------------------------------------------------------------
218299
219300### .before.findOne(userId, selector, options)
220301
221- Fired before a findOne query.
222-
223- Allows you to adjust selector/options on-the-fly.
302+ Fired before a findOne query. ** Supports async functions.**
224303
225304``` javascript
305+ // ✅ Sync before.findOne
226306test .before .findOne (function (userId , selector , options ) {
227- // ...
307+ selector . status = ' active ' ;
228308});
309+
310+ // ✅ Async before.findOne
311+ test .before .findOne (async function (userId , selector , options ) {
312+ await enrichSelector (selector);
313+ // Return false to abort the operation
314+ return false ;
315+ });
316+ ```
317+
318+ ** Only triggers on async methods:**
319+ ``` javascript
320+ await collection .findOneAsync ({}) // ✅ Triggers hooks
321+ collection .findOne ({}) // ❌ No hooks triggered
229322```
230323
231324--------------------------------------------------------------------------------
232325
233326### .after.findOne(userId, selector, options, doc)
234327
235- Fired after a findOne query.
236-
237- Allows you to act on a given findOne query. The document resulting
238- from the query is provided as the last argument for convenience.
328+ Fired after a findOne query. ** Supports async functions.**
239329
240330``` javascript
241- test .after .findOne (function (userId , selector , options , doc ) {
242- // ...
331+ // ✅ Async after.findOne
332+ test .after .findOne (async function (userId , selector , options , doc ) {
333+ await processDocument (doc);
243334});
244335```
245336
337+ ** Only triggers on async methods:**
338+ ``` javascript
339+ await collection .findOneAsync ({}) // ✅ Triggers hooks
340+ collection .findOne ({}) // ❌ No hooks triggered
341+ ```
342+
246343--------------------------------------------------------------------------------
247344
248345## Direct access (circumventing hooks)
@@ -353,6 +450,12 @@ server.*
353450
354451- ` find ` hooks are also fired when fetching documents for ` update ` , ` upsert ` and ` remove ` hooks.
355452
453+ - ** Meteor 3 Behavior:** Find hooks only trigger on async cursor methods (` fetchAsync() ` , ` countAsync() ` , etc.). Sync methods (` fetch() ` , ` count() ` ) do not trigger hooks.
454+
455+ - ** findOne Behavior:** findOne hooks only trigger on ` findOneAsync() ` . The sync ` findOne() ` method does not trigger hooks in Meteor 3.
456+
457+ - ` before.find ` hooks cannot be async and will throw an error if an async function is provided.
458+
356459- If using the ` direct ` version to bypass a hook, any mongo operations done within nested
357460callbacks of the ` direct ` operation will also by default run as ` direct ` . You can use the following
358461line in a nested callback before the operation to unset the ` direct ` setting:
0 commit comments