From 9dd918955fbc9602e42c6ed1a177eae6ecee976d Mon Sep 17 00:00:00 2001 From: klaucode <103204675+klaucode@users.noreply.github.com> Date: Wed, 27 Nov 2024 07:37:23 +0100 Subject: [PATCH 1/4] update --- grouping.js | 196 ++++++++++++++++++++++++++++++++++++---------------- hooks.js | 4 +- package.js | 6 +- 3 files changed, 139 insertions(+), 67 deletions(-) diff --git a/grouping.js b/grouping.js index 8024b9f..a279e75 100644 --- a/grouping.js +++ b/grouping.js @@ -7,7 +7,7 @@ Meteor.publish(null, function () { }); // Special hook for Meteor.users to scope for each group -function userFindHook(userId, selector /*, options*/) { +async function userFindAsyncHook(userId, selector /*, options*/) { const isDirectSelector = Helpers.isDirectUserSelector(selector); if ( ((allowDirectIdSelectors || Partitioner._searchAllUsers.get()) && isDirectSelector) @@ -21,7 +21,7 @@ function userFindHook(userId, selector /*, options*/) { if (!userId && !groupId) return true; if (!groupId) { - const user = Meteor.users._partitionerDirect.findOne(userId, {fields: {group: 1}}); + const user = await Meteor.users._partitionerDirect.findOneAsync(userId, {fields: {group: 1}}); // user will be undefined inside reactive publish when user is deleted while subscribed if (!user) return false; @@ -30,12 +30,12 @@ function userFindHook(userId, selector /*, options*/) { // If user is admin and not in a group, proceed as normal (select all users) // do user2 findOne separately so that the findOne above can hit the cache - if (!groupId && Meteor.users._partitionerDirect.findOne(userId, {fields: {admin: 1}}).admin) return true; + if (!groupId && await Meteor.users._partitionerDirect.findOneAsync(userId, {fields: {admin: 1}}).admin) return true; // Normal users need to be in a group if (!groupId) throw new Meteor.Error(403, ErrMsg.groupErr); - Partitioner._currentGroup.set(groupId); + Partitioner._currentGroup._set(groupId); } filter = { @@ -56,6 +56,25 @@ function userFindHook(userId, selector /*, options*/) { return true; } +function userFindHook(userId, selector /*, options*/) { + return Meteor.wrapAsync(userFindAsyncHook)(userId, selector); +} + +async function hookGetSetGroupAsync(userId) { + let groupId = Partitioner._currentGroup.get(); + + if (!groupId) { + if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); + + groupId = await Partitioner.getUserGroupAsync(userId); + if (!groupId) throw new Meteor.Error(403, ErrMsg.groupErr); + + Partitioner._currentGroup._set(groupId); + } + + return groupId; +} + function hookGetSetGroup(userId) { let groupId = Partitioner._currentGroup.get(); @@ -65,8 +84,9 @@ function hookGetSetGroup(userId) { groupId = Partitioner.getUserGroup(userId); if (!groupId) throw new Meteor.Error(403, ErrMsg.groupErr); - Partitioner._currentGroup.set(groupId); + Partitioner._currentGroup._set(groupId); } + return groupId; } @@ -110,7 +130,7 @@ function findHook(userId, selector, options) { return true; }; -function insertHook(multipleGroups, userId, doc) { +async function insertAsyncHook(multipleGroups, userId, doc) { // Don't add group for direct inserts if (Partitioner._directOps.get() === true) return true; @@ -120,7 +140,11 @@ function insertHook(multipleGroups, userId, doc) { return true; }; -function userInsertHook(userId, doc) { +function insertHook(multipleGroups, userId, doc) { + return Meteor.wrapAsync(insertAsyncHook)(multipleGroups, userId, doc); +} + +function userInsertAsyncHook(userId, doc) { // Don't add group for direct inserts if (Partitioner._directOps.get() === true) return true; @@ -131,7 +155,7 @@ function userInsertHook(userId, doc) { return true; }; -function upsertHook(multipleGroups, userId, selector, doc) { +function upsertAsyncHook(multipleGroups, userId, selector, doc) { // Don't add group for direct inserts if (Partitioner._directOps.get() === true) return true; @@ -141,7 +165,11 @@ function upsertHook(multipleGroups, userId, selector, doc) { return true; }; -function userUpsertHook(userId, selector, doc) { +function upsertHook(multipleGroups, userId, selector, doc) { + return Meteor.wrapAsync(upsertAsyncHook)(multipleGroups, userId, selector, doc); +} + +function userUpsertAsyncHook(userId, selector, doc) { // Don't add group for direct inserts if (Partitioner._directOps.get() === true) return true; @@ -151,11 +179,15 @@ function userUpsertHook(userId, selector, doc) { return true; }; +function userUpsertHook(userId, selector, doc) { + return Meteor.wrapAsync(userUpsertAsyncHook)(userId, selector, doc); +} + // Attach the find/insert hooks to Meteor.users Meteor.users._partitionerBefore.find(userFindHook); -Meteor.users._partitionerBefore.findOne(userFindHook); -Meteor.users._partitionerBefore.insert(userInsertHook); -Meteor.users._partitionerBefore.upsert(userUpsertHook); +Meteor.users._partitionerBefore.findOneAsync(userFindAsyncHook); +Meteor.users._partitionerBefore.insertAsync(userInsertAsyncHook); +Meteor.users._partitionerBefore.upsertAsync(userUpsertAsyncHook); function getPartitionedIndex(index) { const defaultIndex = {_groupId: 1}; @@ -174,28 +206,43 @@ Partitioner = { _directOps: new Meteor.EnvironmentVariable(), _searchAllUsers: new Meteor.EnvironmentVariable(), - setUserGroup(userId, groupId) { + async setUserGroupAsync(userId, groupId) { check(userId, String); check(groupId, String); - if (Meteor.users._partitionerDirect.findOne(userId, {fields: {group: 1}}).group) { + if (await Meteor.users._partitionerDirect.findOneAsync(userId, {fields: {group: 1}}).group) { throw new Meteor.Error(403, "User is already in a group"); } return Meteor.users._partitionerDirect.update(userId, {$set: {group: groupId}}); }, + // Static method to backwards-compatibly + setUserGroup(userId, groupId) { + return Meteor.wrapAsync(this.setUserGroupAsync)(userId, groupId); + }, + + async getUserGroupAsync(userId) { + check(userId, String); + return (await Meteor.users._partitionerDirect.findOneAsync(userId, {fields: {group: 1}}) || {}).group; + }, + + // Static method to backwards-compatibly getUserGroup(userId) { + return Meteor.wrapAsync(this.getUserGroupAsync)(userId); + }, + + async clearUserGroupAsync(userId) { check(userId, String); - return (Meteor.users._partitionerDirect.findOne(userId, {fields: {group: 1}}) || {}).group; + return await Meteor.users._partitionerDirect.updateAsync(userId, {$unset: {group: 1}}); }, + // Static method to backwards-compatibly clearUserGroup(userId) { - check(userId, String); - return Meteor.users._partitionerDirect.update(userId, {$unset: {group: 1}}); + return Meteor.wrapAsync(this.clearUserGroupAsync)(userId); }, - group() { + async groupAsync() { const groupId = this._currentGroup.get(); if (groupId) return groupId; @@ -204,38 +251,53 @@ Partitioner = { userId = Meteor.userId(); } catch (error) {} - return userId && this.getUserGroup(userId); + return userId && await this.getUserGroupAsync(userId); + }, + + // Static method to backwards-compatibly + group() { + return Meteor.wrapAsync(this.groupAsync)(); }, bindGroup(groupId, func) { return this._currentGroup.withValue(groupId, func); }, - bindUserGroup(userId, func) { - const groupId = Partitioner.getUserGroup(userId); + async bindUserGroupAsync(userId, func) { + const groupId = Partitioner.getUserGroupAsync(userId); if (!groupId) { Meteor._debug(`Dropping operation because ${userId} is not in a group`); return; } - return Partitioner.bindGroup(groupId, func); + return await Partitioner.bindGroup(groupId, func); + }, + + // Static method to backwards-compatibly + bindUserGroup(userId, func) { + return Meteor.wrapAsync(this.bindUserGroupAsync)(userId, func); }, directOperation(func) { return this._directOps.withValue(true, func); }, + async _isAdminAsync(_id) { + return !!(await Meteor.users._partitionerDirect.findOneAsync({_id, admin: true}, {fields: {_id: 1}})); + }, + + // Static method to backwards-compatibly _isAdmin(_id) { - return !!Meteor.users._partitionerDirect.findOne({_id, admin: true}, {fields: {_id: 1}}); + return Meteor.wrapAsync(this._isAdminAsync)(_id); }, - addToGroup(collection, entityId, groupId) { + async addToGroupAsync(collection, entityId, groupId) { if (!multipleGroupCollections[collection._name]) { throw new Meteor.Error(403, ErrMsg.multiGroupErr); } - let currentGroupIds = collection._partitionerDirect.findOne(entityId, {fields: {_groupId: 1}})?._groupId; + let currentGroupIds = (await collection._partitionerDirect.findOneAsync(entityId, {fields: {_groupId: 1}}))?._groupId; if (!currentGroupIds) { currentGroupIds = [groupId]; } else if (typeof currentGroupIds == 'string') { @@ -244,17 +306,22 @@ Partitioner = { if (currentGroupIds.indexOf(groupId) == -1) { currentGroupIds.push(groupId); - collection._partitionerDirect.update(entityId, {$set: {_groupId: currentGroupIds}}); + await collection._partitionerDirect.updateAsync(entityId, {$set: {_groupId: currentGroupIds}}); } return currentGroupIds; }, - removeFromGroup(collection, entityId, groupId) { + // Static method to backwards-compatibly + addToGroup(collection, entityId, groupId) { + return Meteor.wrapAsync(this.addToGroupAsync)(collection, entityId, groupId); + }, + + async removeFromGroupAsync(collection, entityId, groupId) { if (!multipleGroupCollections[collection._name]) { throw new Meteor.Error(403, ErrMsg.multiGroupErr); } - let currentGroupIds = collection._partitionerDirect.findOne(entityId, {fields: {_groupId: 1}})?._groupId; + let currentGroupIds = await collection._partitionerDirect.findOneAsync(entityId, {fields: {_groupId: 1}})?._groupId; if (!currentGroupIds) { return []; } @@ -265,45 +332,52 @@ Partitioner = { const index = currentGroupIds.indexOf(groupId); if (index != -1) { currentGroupIds.splice(index, 1); - collection._partitionerDirect.update(entityId, {$set: {_groupId: currentGroupIds}}); + await collection._partitionerDirect.updateAsync(entityId, {$set: {_groupId: currentGroupIds}}); } return currentGroupIds; }, - partitionCollection(collection, options = {}) { - // Because of the deny below, need to create an allow validator - // on an insecure collection if there isn't one already - if (collection._isInsecure()) { - collection.allow({ - insert: () => true, - update: () => true, - remove: () => true, - }); - } - - // Idiot-proof the collection against admin users - collection.deny({ - insert: this._isAdmin, - update: this._isAdmin, - remove: this._isAdmin - }); - collection._partitionerBefore.find(findHook); - collection._partitionerBefore.findOne(findHook); - collection._partitionerBefore.upsert((...args) => upsertHook(options.multipleGroups, ...args)); - // These will hook the _validated methods as well - - collection._partitionerBefore.insert((...args) => insertHook(options.multipleGroups, ...args)); - // No update/remove hook necessary, findHook will be used automatically - - // store a hash of which collections allow multiple groups - if (options.multipleGroups) { - multipleGroupCollections[collection._name] = true; - } + // Static method to backwards-compatibly + removeFromGroup(collection, entityId, groupId) { + return Meteor.wrapAsync(this.removeFromGroupAsync)(collection, entityId, groupId); + }, - // Index the collections by groupId on the server for faster lookups across groups - return collection.createIndex ? collection.createIndex(getPartitionedIndex(options.index), options.indexOptions) - : collection._ensureIndex(getPartitionedIndex(options.index), options.indexOptions); + async partitionCollection(collection, options = {}) { + return Meteor.wrapAsync(async () => { + // Because of the deny below, need to create an allow validator + // on an insecure collection if there isn't one already + if (collection._isInsecure()) { + collection.allow({ + insert: () => true, + update: () => true, + remove: () => true, + }); + } + + // Idiot-proof the collection against admin users + collection.deny({ + insert: await this._isAdminAsync, + update: await this._isAdminAsync, + remove: await this._isAdminAsync + }); + collection._partitionerBefore.find(findHook); + collection._partitionerBefore.findOneAsync(findHook); + collection._partitionerBefore.upsertAsync((...args) => upsertHook(options.multipleGroups, ...args)); + // These will hook the _validated methods as well + + collection._partitionerBefore.insertAsync((...args) => insertHook(options.multipleGroups, ...args)); + // No update/remove hook necessary, findHook will be used automatically + + // store a hash of which collections allow multiple groups + if (options.multipleGroups) { + multipleGroupCollections[collection._name] = true; + } + + // Index the collections by groupId on the server for faster lookups across groups + return collection.createIndex ? collection.createIndex(getPartitionedIndex(options.index), options.indexOptions) + : collection._ensureIndex(getPartitionedIndex(options.index), options.indexOptions); + })(); }, get allowDirectIdSelectors() { diff --git a/hooks.js b/hooks.js index e99307f..d43137e 100644 --- a/hooks.js +++ b/hooks.js @@ -30,7 +30,7 @@ function getUserId () { const proto = Mongo.Collection.prototype; const directEnv = new Meteor.EnvironmentVariable(false); -const methods = ['find', 'findOne', 'insert', 'update', 'remove', 'upsert']; +const methods = ['find', 'findOne', 'findOneAsync', 'insert', 'insertAsync', 'updateAsync', 'removeAsync', 'upsertAsync']; // create the collection._partitionerBefore.* methods // have to create it initially using a getter so we can store self=this and create a new group of functions which have access to self @@ -77,7 +77,7 @@ methods.forEach(method => { global.hookLogging && typeof args[0]!='string' && console.log('hook', '\n\n'); // if the method is update or remove, automatically apply the find hooks to limit the update/remove to the user's group - if ((method=='update' || method=='remove') && this._groupingBefore_find) { + if ((method=='updateAsync' || method=='removeAsync') && this._groupingBefore_find) { global.hookLogging && typeof args[0]!='string' && console.log('hook', 'b4i', this._name+"."+method, JSON.stringify(args[0]), JSON.stringify(args[1])); // don't send args[1] for update or remove. // need to send empty object instead to prevent args[1] being modified diff --git a/package.js b/package.js index 6e5d10b..6a62495 100644 --- a/package.js +++ b/package.js @@ -1,12 +1,12 @@ Package.describe({ name: "wildhart:partitioner", summary: "Transparently divide a meteor app into different instances shared between groups of users.", - version: "3.0.2", + version: "3.0.3", git: "https://github.com/wildhart/meteor-partitioner" }); Package.onUse(function (api) { - api.versionsFrom(["1.6.1", "2.3"]); + api.versionsFrom(["3.0"]); // Client & Server deps api.use([ 'accounts-base', @@ -16,8 +16,6 @@ Package.onUse(function (api) { 'mongo' // Mongo.Collection available ]); - api.use("wildhart:env-var-set@0.0.1"); - api.addFiles('hooks.js'); api.addFiles('common.js'); api.addFiles('grouping.js', 'server'); From ecfd8d525ad5ac509d86b60b95cfaa4009fe4e17 Mon Sep 17 00:00:00 2001 From: klaucode <103204675+klaucode@users.noreply.github.com> Date: Wed, 11 Dec 2024 00:09:11 +0100 Subject: [PATCH 2/4] Meteor3 update --- README.md | 35 ++++++--- grouping.js | 191 +++++++++++++++--------------------------------- hooks.js | 183 ++++++++++++++++++++++++++++------------------ hooks_client.js | 98 +++++++++++++++++++++++++ package.js | 5 +- 5 files changed, 296 insertions(+), 216 deletions(-) create mode 100644 hooks_client.js diff --git a/README.md b/README.md index dfe53e1..97135c6 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,9 @@ Partitioner.partitionCollection(Foo, options); Collections that have been partitioned will behave as if there is a separate instance for each group. In particular, on the server and client, the user's current group is used to do the following: -- `find` and `findOne` operations will only return documents for the current group. -- `insert` will cause documents to appear only in the current group. -- `update` and `remove` operations will only affect documents for the current group. +- `find`, `findOne` and `findOneAsync` operations will only return documents for the current group. +- `insert` and `insertAsync` will cause documents to appear only in the current group. +- `update`, `updateAsync`, `remove` and `removeAsync` operations will only affect documents for the current group. - Attempting any operations on a partitioned collection for which a user has not been assigned to a group will result in an error. This is accomplished using selector rewriting based on the current `userId` both on the client and in server methods, and Meteor's environment variables. For more details see the source. @@ -57,21 +57,21 @@ Adds hooks to a particular collection so that it supports partition operations. **NOTE**: Any documents in the collection that were not created from a group will not be visible to any groups in the partition. You should think of creating a partitioned collection as an atomic operation consisting of declaring the collection and calling `partitionCollection`; we will consider rolling this into a single API call in the future. -#### `Partitioner.group()` +#### `Partitioner.groupAsync()` On the server and client, gets the group of the current user. Returns `undefined` if the user is not logged in or not part of a group. A reactive variable. ## Server API -#### `Partitioner.setUserGroup(userId, groupId)` +#### `Partitioner.setUserGroupAsync(userId, groupId)` Adds a particular user to the group identified by `groupId`. The user will now be able to operate on partitioned collections and will only be able to affect documents scoped to the group. An error will be thrown if the user is already in a group. -#### `Partitioner.getUserGroup(userId)` +#### `Partitioner.getUserGroupAsync(userId)` Gets the group of the current user. -#### `Partitioner.clearUserGroup(userId)` +#### `Partitioner.clearUserGroupAsync(userId)` Removes the current group assignment of the user. The user will no longer be able to operate on any partitioned collections. @@ -79,7 +79,7 @@ Removes the current group assignment of the user. The user will no longer be abl Run a function (presumably doing collection operations) masquerading as a particular group. This is necessary for server-originated code that isn't caused by one particular user. -#### `Partitioner.bindUserGroup(userId, func)` +#### `Partitioner.bindUserGroupAsync(userId, func)` A convenience function for running `Partitioner.bindGroup` as the group of a particular user. @@ -87,7 +87,7 @@ A convenience function for running `Partitioner.bindGroup` as the group of a par Sometimes we need to do operations over the entire underlying collection, including all groups. This provides a way to do that, and will not throw an error if the current user method invocation context is not part of a group. -#### `Partitioner.addToGroup(collection, entityId, groupId)` +#### `Partitioner.addToGroupAsync(collection, entityId, groupId)` Allows a document to be shared across multiple groups. Will throw an error if the collection wasn't partitioned with the `multipleGroups: true` option. E.g.: ``` @@ -99,7 +99,7 @@ const newFooId = Foo.insert({msg: 'Hi, I'm a shared Foo'}); Partitioner.addToGroup(Foo, newFooId, sharedGroupId) ``` -#### `Partitioner.removeFromGroup(collection, entityId, groupId)` +#### `Partitioner.removeFromGroupAsync(collection, entityId, groupId)` Remove a document from a shared group. @@ -136,7 +136,7 @@ Note that due to a current limitation of Meteor (see below), this subscription w ```js Deps.autorun(function() { - var group = Partitioner.group(); + var group = Partitioner.groupAsync(); Meteor.subscribe("fooPub", bar, group); }); ``` @@ -179,7 +179,10 @@ Deps.autorun(function() { To send a message, a client or server method might do something like ```js +// Client ChatMessages.insert({text: "hello world", room: currentRoom, timestamp: Date.now()}); +// Client/Server +ChatMessages.insertAsync({text: "hello world", room: currentRoom, timestamp: Date.now()}); ``` This looks simple enough, until you realize that you need to keep track of the `room` for each message that is entered in to the collection. Why not have some code do it for you automagically? @@ -205,7 +208,7 @@ The client's subscription would simply be the following: ```js Deps.autorun(function() { - var group = Partitioner.group(); + var group = Partitioner.groupAsync(); Meteor.subscribe("messages", group); }); ``` @@ -213,7 +216,10 @@ Deps.autorun(function() { Now, sending a chat message is as easy as this: ```js +// Client ChatMessages.insert({text: "hello world", timestamp: Date.now()}); +// Client/Server +ChatMessages.insertAsync({text: "hello world", timestamp: Date.now()}); ``` To change chat rooms, simply have server code call `Partitioner.setUserGroup` for a particular user. @@ -241,6 +247,11 @@ See [CrowdMapper](https://github.com/mizzao/CrowdMapper) for a highly concurrent ## Version History +### **3.1.0 (2024-12-10)** + +- Meteor 3.1 compatibility +- ref#ctored hooks to use async methods (check this README) + ### **3.0.2 (2022-03-22)** - Limit extent of allowing Accounts Core packages such as `CreateUser`, `_attemptLogin`, etc from searching all users so that publications for the same user which are already running cannot return all users. diff --git a/grouping.js b/grouping.js index a279e75..acbba57 100644 --- a/grouping.js +++ b/grouping.js @@ -7,7 +7,7 @@ Meteor.publish(null, function () { }); // Special hook for Meteor.users to scope for each group -async function userFindAsyncHook(userId, selector /*, options*/) { +async function userFindHookAsync(userId, selector /*, options*/) { const isDirectSelector = Helpers.isDirectUserSelector(selector); if ( ((allowDirectIdSelectors || Partitioner._searchAllUsers.get()) && isDirectSelector) @@ -30,7 +30,7 @@ async function userFindAsyncHook(userId, selector /*, options*/) { // If user is admin and not in a group, proceed as normal (select all users) // do user2 findOne separately so that the findOne above can hit the cache - if (!groupId && await Meteor.users._partitionerDirect.findOneAsync(userId, {fields: {admin: 1}}).admin) return true; + if (!groupId && (await Meteor.users._partitionerDirect.findOneAsync(userId, {fields: {admin: 1}})).admin) return true; // Normal users need to be in a group if (!groupId) throw new Meteor.Error(403, ErrMsg.groupErr); @@ -56,10 +56,6 @@ async function userFindAsyncHook(userId, selector /*, options*/) { return true; } -function userFindHook(userId, selector /*, options*/) { - return Meteor.wrapAsync(userFindAsyncHook)(userId, selector); -} - async function hookGetSetGroupAsync(userId) { let groupId = Partitioner._currentGroup.get(); @@ -71,27 +67,11 @@ async function hookGetSetGroupAsync(userId) { Partitioner._currentGroup._set(groupId); } - - return groupId; -} - -function hookGetSetGroup(userId) { - let groupId = Partitioner._currentGroup.get(); - - if (!groupId) { - if (!userId) throw new Meteor.Error(403, ErrMsg.userIdErr); - - groupId = Partitioner.getUserGroup(userId); - if (!groupId) throw new Meteor.Error(403, ErrMsg.groupErr); - - Partitioner._currentGroup._set(groupId); - } - return groupId; } // No allow/deny for find so we make our own checks -function findHook(userId, selector, options) { +async function findHookAsync(userId, selector, options) { if ( // Don't scope for direct operations Partitioner._directOps.get() === true @@ -102,7 +82,7 @@ function findHook(userId, selector, options) { ) return true; - const groupId = hookGetSetGroup(userId); + const groupId = await hookGetSetGroupAsync(userId); // force the selector to scope for the _groupId if (selector == null) { @@ -130,64 +110,52 @@ function findHook(userId, selector, options) { return true; }; -async function insertAsyncHook(multipleGroups, userId, doc) { +async function insertHookAsync(multipleGroups, userId, doc) { // Don't add group for direct inserts if (Partitioner._directOps.get() === true) return true; - const groupId = hookGetSetGroup(userId); + const groupId = await hookGetSetGroupAsync(userId); doc._groupId = multipleGroups ? [groupId] : groupId; return true; }; -function insertHook(multipleGroups, userId, doc) { - return Meteor.wrapAsync(insertAsyncHook)(multipleGroups, userId, doc); -} - -function userInsertAsyncHook(userId, doc) { +async function userInsertHookAsync(userId, doc) { // Don't add group for direct inserts if (Partitioner._directOps.get() === true) return true; - const groupId = hookGetSetGroup(userId); + const groupId = await hookGetSetGroupAsync(userId); doc.group = groupId; return true; }; -function upsertAsyncHook(multipleGroups, userId, selector, doc) { +async function upsertHookAsync(multipleGroups, userId, selector, doc) { // Don't add group for direct inserts if (Partitioner._directOps.get() === true) return true; - const groupId = hookGetSetGroup(userId); + const groupId = await hookGetSetGroupAsync(userId); doc._groupId = multipleGroups ? [groupId] : groupId; return true; }; -function upsertHook(multipleGroups, userId, selector, doc) { - return Meteor.wrapAsync(upsertAsyncHook)(multipleGroups, userId, selector, doc); -} - -function userUpsertAsyncHook(userId, selector, doc) { +async function userUpsertHookAsync(userId, selector, doc) { // Don't add group for direct inserts if (Partitioner._directOps.get() === true) return true; - const groupId = hookGetSetGroup(userId); + const groupId = await hookGetSetGroupAsync(userId); doc.group = groupId; return true; }; -function userUpsertHook(userId, selector, doc) { - return Meteor.wrapAsync(userUpsertAsyncHook)(userId, selector, doc); -} - // Attach the find/insert hooks to Meteor.users -Meteor.users._partitionerBefore.find(userFindHook); -Meteor.users._partitionerBefore.findOneAsync(userFindAsyncHook); -Meteor.users._partitionerBefore.insertAsync(userInsertAsyncHook); -Meteor.users._partitionerBefore.upsertAsync(userUpsertAsyncHook); +Meteor.users._partitionerBefore.find(userFindHookAsync); +Meteor.users._partitionerBefore.findOneAsync(userFindHookAsync); +Meteor.users._partitionerBefore.insertAsync(userInsertHookAsync); +Meteor.users._partitionerBefore.upsertAsync(userUpsertHookAsync); function getPartitionedIndex(index) { const defaultIndex = {_groupId: 1}; @@ -210,36 +178,21 @@ Partitioner = { check(userId, String); check(groupId, String); - if (await Meteor.users._partitionerDirect.findOneAsync(userId, {fields: {group: 1}}).group) { + if ((await Meteor.users._partitionerDirect.findOneAsync(userId, {fields: {group: 1}})).group) { throw new Meteor.Error(403, "User is already in a group"); } - return Meteor.users._partitionerDirect.update(userId, {$set: {group: groupId}}); - }, - - // Static method to backwards-compatibly - setUserGroup(userId, groupId) { - return Meteor.wrapAsync(this.setUserGroupAsync)(userId, groupId); + return Meteor.users._partitionerDirect.updateAsync(userId, {$set: {group: groupId}}); }, async getUserGroupAsync(userId) { check(userId, String); - return (await Meteor.users._partitionerDirect.findOneAsync(userId, {fields: {group: 1}}) || {}).group; - }, - - // Static method to backwards-compatibly - getUserGroup(userId) { - return Meteor.wrapAsync(this.getUserGroupAsync)(userId); + return ((await Meteor.users._partitionerDirect.findOneAsync(userId, {fields: {group: 1}})) || {}).group; }, async clearUserGroupAsync(userId) { check(userId, String); - return await Meteor.users._partitionerDirect.updateAsync(userId, {$unset: {group: 1}}); - }, - - // Static method to backwards-compatibly - clearUserGroup(userId) { - return Meteor.wrapAsync(this.clearUserGroupAsync)(userId); + return Meteor.users._partitionerDirect.updateAsync(userId, {$unset: {group: 1}}); }, async groupAsync() { @@ -254,44 +207,29 @@ Partitioner = { return userId && await this.getUserGroupAsync(userId); }, - // Static method to backwards-compatibly - group() { - return Meteor.wrapAsync(this.groupAsync)(); - }, - bindGroup(groupId, func) { return this._currentGroup.withValue(groupId, func); }, async bindUserGroupAsync(userId, func) { - const groupId = Partitioner.getUserGroupAsync(userId); + const groupId = await Partitioner.getUserGroupAsync(userId); if (!groupId) { Meteor._debug(`Dropping operation because ${userId} is not in a group`); return; } - return await Partitioner.bindGroup(groupId, func); - }, - - // Static method to backwards-compatibly - bindUserGroup(userId, func) { - return Meteor.wrapAsync(this.bindUserGroupAsync)(userId, func); + return Partitioner.bindGroup(groupId, func); }, directOperation(func) { return this._directOps.withValue(true, func); }, - async _isAdminAsync(_id) { + async _isAdmin(_id) { return !!(await Meteor.users._partitionerDirect.findOneAsync({_id, admin: true}, {fields: {_id: 1}})); }, - // Static method to backwards-compatibly - _isAdmin(_id) { - return Meteor.wrapAsync(this._isAdminAsync)(_id); - }, - async addToGroupAsync(collection, entityId, groupId) { if (!multipleGroupCollections[collection._name]) { throw new Meteor.Error(403, ErrMsg.multiGroupErr); @@ -311,10 +249,6 @@ Partitioner = { return currentGroupIds; }, - // Static method to backwards-compatibly - addToGroup(collection, entityId, groupId) { - return Meteor.wrapAsync(this.addToGroupAsync)(collection, entityId, groupId); - }, async removeFromGroupAsync(collection, entityId, groupId) { if (!multipleGroupCollections[collection._name]) { @@ -338,46 +272,41 @@ Partitioner = { return currentGroupIds; }, - // Static method to backwards-compatibly - removeFromGroup(collection, entityId, groupId) { - return Meteor.wrapAsync(this.removeFromGroupAsync)(collection, entityId, groupId); - }, + partitionCollection(collection, options = {}) { + // Because of the deny below, need to create an allow validator + // on an insecure collection if there isn't one already + // TODO toto treba zobrat a dat do hookov, lebo deny nepodporuje async + // if (collection._isInsecure()) { + // collection.allow({ + // insert: () => true, + // update: () => true, + // remove: () => true, + // }); + // } + // + // // Idiot-proof the collection against admin users + // collection.deny({ + // insert: this._isAdmin, + // update: this._isAdmin, + // remove: this._isAdmin + // }); + + collection._partitionerBefore.find(findHookAsync); // used in findOneAsync, fetchAsync, observeChangesAsync, countAsync + collection._partitionerBefore.findOneAsync(findHookAsync); + collection._partitionerBefore.upsertAsync((...args) => upsertHookAsync(options.multipleGroups, ...args)); + // These will hook the _validated methods as well + + collection._partitionerBefore.insertAsync((...args) => insertHookAsync(options.multipleGroups, ...args)); + // No update/remove hook necessary, findHook will be used automatically + + // store a hash of which collections allow multiple groups + if (options.multipleGroups) { + multipleGroupCollections[collection._name] = true; + } - async partitionCollection(collection, options = {}) { - return Meteor.wrapAsync(async () => { - // Because of the deny below, need to create an allow validator - // on an insecure collection if there isn't one already - if (collection._isInsecure()) { - collection.allow({ - insert: () => true, - update: () => true, - remove: () => true, - }); - } - - // Idiot-proof the collection against admin users - collection.deny({ - insert: await this._isAdminAsync, - update: await this._isAdminAsync, - remove: await this._isAdminAsync - }); - collection._partitionerBefore.find(findHook); - collection._partitionerBefore.findOneAsync(findHook); - collection._partitionerBefore.upsertAsync((...args) => upsertHook(options.multipleGroups, ...args)); - // These will hook the _validated methods as well - - collection._partitionerBefore.insertAsync((...args) => insertHook(options.multipleGroups, ...args)); - // No update/remove hook necessary, findHook will be used automatically - - // store a hash of which collections allow multiple groups - if (options.multipleGroups) { - multipleGroupCollections[collection._name] = true; - } - - // Index the collections by groupId on the server for faster lookups across groups - return collection.createIndex ? collection.createIndex(getPartitionedIndex(options.index), options.indexOptions) - : collection._ensureIndex(getPartitionedIndex(options.index), options.indexOptions); - })(); + // Index the collections by groupId on the server for faster lookups across groups + return collection.createIndex ? collection.createIndex(getPartitionedIndex(options.index), options.indexOptions) + : collection._ensureIndex(getPartitionedIndex(options.index), options.indexOptions); }, get allowDirectIdSelectors() { @@ -401,11 +330,11 @@ Partitioner = { // Don't wrap createUser with Partitioner.directOperation because want inserted user doc to be // automatically assigned to the group -['createUser', 'findUserByEmail', 'findUserByUsername', '_attemptLogin'].forEach(fn => { +['createUserAsync', 'findUserByEmail', 'findUserByUsername', '_attemptLogin'].forEach(fn => { const orig = Accounts[fn]; if (orig) { - Accounts[fn] = function() { - return Partitioner._searchAllUsers.withValue(true, () => orig.apply(this, arguments)); + Accounts[fn] = async function() { + return Partitioner._searchAllUsers.withValue(true, async () => await orig.apply(this, arguments)); }; } }); diff --git a/hooks.js b/hooks.js index d43137e..55a760a 100644 --- a/hooks.js +++ b/hooks.js @@ -1,98 +1,139 @@ let publishUserId; if (Meteor.isServer) { - publishUserId = new Meteor.EnvironmentVariable(); - const _publish = Meteor.publish; - Meteor.publish = function(name, handler, options) { - return _publish.call(this, name, function(...args) { - // This function is called repeatedly in publications - return publishUserId.withValue(this && this.userId, () => handler.apply(this, args)); - }, options) - } + publishUserId = new Meteor.EnvironmentVariable(); + const _publish = Meteor.publish; + Meteor.publish = function (name, handler, options) { + return _publish.call( + this, + name, + function (...args) { + // This function is called repeatedly in publications + return publishUserId.withValue(this && this.userId, () => handler.apply(this, args)); + }, + options, + ); + }; } -function getUserId () { - let userId; +function getUserId() { + let userId; - try { - // Will throw an error unless within method call. - // Attempt to recover gracefully by catching: - userId = Meteor.userId && Meteor.userId(); - } catch (e) {} + try { + // Will throw an error unless within method call. + // Attempt to recover gracefully by catching: + userId = Meteor.userId && Meteor.userId(); + } catch (e) {} - if (userId == null && Meteor.isServer) { - // Get the userId if we are in a publish function. - userId = publishUserId.get(); - } + if (userId == null && Meteor.isServer) { + // Get the userId if we are in a publish function. + userId = publishUserId.get(); + } - return userId + return userId; } const proto = Mongo.Collection.prototype; const directEnv = new Meteor.EnvironmentVariable(false); -const methods = ['find', 'findOne', 'findOneAsync', 'insert', 'insertAsync', 'updateAsync', 'removeAsync', 'upsertAsync']; +const selectionMethods = ["find", "findOneAsync", "insertAsync", "updateAsync", "removeAsync", "upsertAsync"]; +const fetchMethods = ["fetchAsync", "observeChangesAsync", "countAsync"]; // create the collection._partitionerBefore.* methods // have to create it initially using a getter so we can store self=this and create a new group of functions which have access to self -Object.defineProperty(proto, '_partitionerBefore', { - get() { - // console.log('creating before functions', this._name); - const self = this; - const fns = {}; - methods.forEach(method => fns[method] = function(hookFn) { - self['_groupingBefore_'+method] = hookFn; - }); - // replace the .direct prototype with the created object, so we don't have to recreate it every time. - Object.defineProperty(this, '_partitionerBefore', {value: fns}); - return fns; - } +Object.defineProperty(proto, "_partitionerBefore", { + get() { + // console.log('creating before functions', this._name); + const self = this; + const fns = {}; + selectionMethods.forEach( + (method) => + (fns[method] = function (hookFn) { + self[`_groupingBefore_${method}`] = hookFn; + }), + ); + // replace the .direct prototype with the created object, so we don't have to recreate it every time. + Object.defineProperty(this, "_partitionerBefore", { value: fns }); + return fns; + }, }); // create the collection._partitionerDirect.* methods // have to create it initially using a getter so we can store self=this and create a new group of functions which have access to self -Object.defineProperty(proto, '_partitionerDirect', { - get() { - // console.log('creating direct functions', this._name); - const self = this; - const fns = {}; - methods.forEach(method => fns[method] = function(...args) { - return directEnv.withValue(true, () => proto[method].apply(self, args)); - }); - // replace the .direct prototype with the created object, so we don't have to recreate it every time. - Object.defineProperty(this, '_partitionerDirect', {value: fns}); - return fns; - } +Object.defineProperty(proto, "_partitionerDirect", { + get() { + // console.log('creating direct functions', this._name); + const self = this; + const fns = {}; + selectionMethods.forEach( + (method) => + (fns[method] = async function (...args) { + return directEnv.withValue(true, async () => { + return proto[method].apply(self, args); + }); + }), + ); + // replace the .direct prototype with the created object, so we don't have to recreate it every time. + Object.defineProperty(this, "_partitionerDirect", { value: fns }); + return fns; + }, }); global.hookLogging = false; // if (Meteor.isServer) global.hookLogging = true; -methods.forEach(method => { - const _orig = proto[method]; - proto[method] = function(...args) { - if (directEnv.get()) return _orig.apply(this, args); - // give the hooks a private context so that they can modify this.args without putting this.args onto the prototype - const context = {args}; - const userId = getUserId(); - global.hookLogging && typeof args[0]!='string' && console.log('hook', '\n\n'); +selectionMethods.forEach(method => { + const _orig = proto[method]; + // if the method is find, we do not replace the original method + // because it is sync and we replace the childfetch methods instead + if (method == 'find') { + proto[method] = function(...args) { + const self = this; + const cursor = _orig.apply(this, args); // we need to get cursor to get fetch methods + fetchMethods.forEach(fetchMethod => { + const _orig = cursor[fetchMethod]; + cursor[fetchMethod] = async function(...args) { + // modify the selector in the cursor before calling the fetch method + if(self._groupingBefore_find) { + const selector = cursor._cursorDescription.selector; + const userId = getUserId(); + await self._groupingBefore_find.call(args, userId, selector, {}); + cursor._cursorDescription.selector = selector; + } + // run the original fetch method + return _orig.apply(this, args); + }; + }); + return cursor; + }; + return; + } - // if the method is update or remove, automatically apply the find hooks to limit the update/remove to the user's group - if ((method=='updateAsync' || method=='removeAsync') && this._groupingBefore_find) { - global.hookLogging && typeof args[0]!='string' && console.log('hook', 'b4i', this._name+"."+method, JSON.stringify(args[0]), JSON.stringify(args[1])); - // don't send args[1] for update or remove. - // need to send empty object instead to prevent args[1] being modified - this._groupingBefore_find.call(context, userId, args[0], {}); - global.hookLogging && typeof args[0]!='string' && console.log('hook', 'afi', this._name+"."+method, JSON.stringify(args[0]), JSON.stringify(args[1])); - } + // this replaces all async original methods + // except find, which is sync and needs to be handled differently + proto[method] = async function(...args) { + if (directEnv.get()) return _orig.apply(this, args); + // give the hooks a private context so that they can modify this.args without putting this.args onto the prototype + const context = {args}; + const userId = getUserId(); + global.hookLogging && typeof args[0]!='string' && console.log('hook', '\n\n'); - // run the hook - if (this['_groupingBefore_'+method]) { - global.hookLogging && typeof args[0]!='string' && console.log('hook', 'b4', this._name+"."+method, JSON.stringify(args[0]), JSON.stringify(args[1])); - this['_groupingBefore_'+method].call(context, userId, args[0], args[1], args[2]); - global.hookLogging && typeof args[0]!='string' && console.log('hook', 'af', this._name+"."+method, JSON.stringify(args[0]), JSON.stringify(args[1])); - } + // if the method is update or remove, automatically apply the find hooks to limit the update/remove to the user's group + if ((method=='updateAsync' || method=='removeAsync') && this._groupingBefore_find) { + global.hookLogging && typeof args[0]!='string' && console.log('hook', 'b4i', this._name+"."+method, JSON.stringify(args[0]), JSON.stringify(args[1])); + // don't send args[1] for update or remove. + // need to send empty object instead to prevent args[1] being modified + await this._groupingBefore_find.call(context, userId, args[0], {}); + global.hookLogging && typeof args[0]!='string' && console.log('hook', 'afi', this._name+"."+method, JSON.stringify(args[0]), JSON.stringify(args[1])); + } - // run the original method - return _orig.apply(this, args); - } -}); + // run the hook + if (this['_groupingBefore_'+method]) { + global.hookLogging && typeof args[0]!='string' && console.log('hook', 'b4', this._name+"."+method, JSON.stringify(args[0]), JSON.stringify(args[1])); + await this['_groupingBefore_'+method].call(context, userId, args[0], args[1], args[2]); + global.hookLogging && typeof args[0]!='string' && console.log('hook', 'af', this._name+"."+method, JSON.stringify(args[0]), JSON.stringify(args[1])); + } + + // run the original method + return _orig.apply(this, args); + } +}); \ No newline at end of file diff --git a/hooks_client.js b/hooks_client.js new file mode 100644 index 0000000..e99307f --- /dev/null +++ b/hooks_client.js @@ -0,0 +1,98 @@ +let publishUserId; + +if (Meteor.isServer) { + publishUserId = new Meteor.EnvironmentVariable(); + const _publish = Meteor.publish; + Meteor.publish = function(name, handler, options) { + return _publish.call(this, name, function(...args) { + // This function is called repeatedly in publications + return publishUserId.withValue(this && this.userId, () => handler.apply(this, args)); + }, options) + } +} + +function getUserId () { + let userId; + + try { + // Will throw an error unless within method call. + // Attempt to recover gracefully by catching: + userId = Meteor.userId && Meteor.userId(); + } catch (e) {} + + if (userId == null && Meteor.isServer) { + // Get the userId if we are in a publish function. + userId = publishUserId.get(); + } + + return userId +} + +const proto = Mongo.Collection.prototype; +const directEnv = new Meteor.EnvironmentVariable(false); +const methods = ['find', 'findOne', 'insert', 'update', 'remove', 'upsert']; + +// create the collection._partitionerBefore.* methods +// have to create it initially using a getter so we can store self=this and create a new group of functions which have access to self +Object.defineProperty(proto, '_partitionerBefore', { + get() { + // console.log('creating before functions', this._name); + const self = this; + const fns = {}; + methods.forEach(method => fns[method] = function(hookFn) { + self['_groupingBefore_'+method] = hookFn; + }); + // replace the .direct prototype with the created object, so we don't have to recreate it every time. + Object.defineProperty(this, '_partitionerBefore', {value: fns}); + return fns; + } +}); + +// create the collection._partitionerDirect.* methods +// have to create it initially using a getter so we can store self=this and create a new group of functions which have access to self +Object.defineProperty(proto, '_partitionerDirect', { + get() { + // console.log('creating direct functions', this._name); + const self = this; + const fns = {}; + methods.forEach(method => fns[method] = function(...args) { + return directEnv.withValue(true, () => proto[method].apply(self, args)); + }); + // replace the .direct prototype with the created object, so we don't have to recreate it every time. + Object.defineProperty(this, '_partitionerDirect', {value: fns}); + return fns; + } +}); + +global.hookLogging = false; +// if (Meteor.isServer) global.hookLogging = true; + +methods.forEach(method => { + const _orig = proto[method]; + proto[method] = function(...args) { + if (directEnv.get()) return _orig.apply(this, args); + // give the hooks a private context so that they can modify this.args without putting this.args onto the prototype + const context = {args}; + const userId = getUserId(); + global.hookLogging && typeof args[0]!='string' && console.log('hook', '\n\n'); + + // if the method is update or remove, automatically apply the find hooks to limit the update/remove to the user's group + if ((method=='update' || method=='remove') && this._groupingBefore_find) { + global.hookLogging && typeof args[0]!='string' && console.log('hook', 'b4i', this._name+"."+method, JSON.stringify(args[0]), JSON.stringify(args[1])); + // don't send args[1] for update or remove. + // need to send empty object instead to prevent args[1] being modified + this._groupingBefore_find.call(context, userId, args[0], {}); + global.hookLogging && typeof args[0]!='string' && console.log('hook', 'afi', this._name+"."+method, JSON.stringify(args[0]), JSON.stringify(args[1])); + } + + // run the hook + if (this['_groupingBefore_'+method]) { + global.hookLogging && typeof args[0]!='string' && console.log('hook', 'b4', this._name+"."+method, JSON.stringify(args[0]), JSON.stringify(args[1])); + this['_groupingBefore_'+method].call(context, userId, args[0], args[1], args[2]); + global.hookLogging && typeof args[0]!='string' && console.log('hook', 'af', this._name+"."+method, JSON.stringify(args[0]), JSON.stringify(args[1])); + } + + // run the original method + return _orig.apply(this, args); + } +}); diff --git a/package.js b/package.js index 6a62495..ca2ef22 100644 --- a/package.js +++ b/package.js @@ -1,7 +1,7 @@ Package.describe({ name: "wildhart:partitioner", summary: "Transparently divide a meteor app into different instances shared between groups of users.", - version: "3.0.3", + version: "3.1.0", git: "https://github.com/wildhart/meteor-partitioner" }); @@ -16,8 +16,9 @@ Package.onUse(function (api) { 'mongo' // Mongo.Collection available ]); - api.addFiles('hooks.js'); api.addFiles('common.js'); + api.addFiles('hooks.js', 'server'); + api.addFiles('hooks_client.js', 'client'); api.addFiles('grouping.js', 'server'); api.addFiles('grouping_client.js', 'client'); From 7e0fed97ef9f6e6943bcb3b62520e579e41815c3 Mon Sep 17 00:00:00 2001 From: klaucode <103204675+klaucode@users.noreply.github.com> Date: Fri, 13 Dec 2024 23:11:35 +0100 Subject: [PATCH 3/4] groupping added back to publications --- hooks.js | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/hooks.js b/hooks.js index 55a760a..bb43051 100644 --- a/hooks.js +++ b/hooks.js @@ -36,7 +36,15 @@ function getUserId() { const proto = Mongo.Collection.prototype; const directEnv = new Meteor.EnvironmentVariable(false); const selectionMethods = ["find", "findOneAsync", "insertAsync", "updateAsync", "removeAsync", "upsertAsync"]; -const fetchMethods = ["fetchAsync", "observeChangesAsync", "countAsync"]; +const fetchMethods = [ + "fetchAsync", + "observeAsync", + "observeChangesAsync", + "countAsync", + "forEachAsync", + "mapAsync", + Symbol.asyncIterator, +]; // create the collection._partitionerBefore.* methods // have to create it initially using a getter so we can store self=this and create a new group of functions which have access to self @@ -81,6 +89,8 @@ Object.defineProperty(proto, "_partitionerDirect", { global.hookLogging = false; // if (Meteor.isServer) global.hookLogging = true; +global.uuu = false; + selectionMethods.forEach(method => { const _orig = proto[method]; // if the method is find, we do not replace the original method @@ -89,6 +99,24 @@ selectionMethods.forEach(method => { proto[method] = function(...args) { const self = this; const cursor = _orig.apply(this, args); // we need to get cursor to get fetch methods + const userId = getUserId(); + // Method _observeChanges is used by Meteor publications + // Store original method to prevent infinite loop + if(!cursor._mongo._observeChangesOrig) { + cursor._mongo._observeChangesOrig = cursor._mongo._observeChanges; + } + // Modify cursor and then call original method + cursor._mongo._observeChanges = async function(...args) { + if(self._groupingBefore_find) { + const selector = cursor._cursorDescription.selector; + await self._groupingBefore_find.call({args}, userId, selector, {}); + cursor._cursorDescription.selector = selector; + } + return cursor._mongo._observeChangesOrig.apply(this, args); + }; + + // Now modify all cursor methods + // ...methods after find (fetchAsync, countAsync...) fetchMethods.forEach(fetchMethod => { const _orig = cursor[fetchMethod]; cursor[fetchMethod] = async function(...args) { @@ -96,7 +124,7 @@ selectionMethods.forEach(method => { if(self._groupingBefore_find) { const selector = cursor._cursorDescription.selector; const userId = getUserId(); - await self._groupingBefore_find.call(args, userId, selector, {}); + await self._groupingBefore_find.call({args}, userId, selector, {}); cursor._cursorDescription.selector = selector; } // run the original fetch method @@ -108,6 +136,7 @@ selectionMethods.forEach(method => { return; } + // Now modify data manipulation async methods (insertAsync, updateAsync...) // this replaces all async original methods // except find, which is sync and needs to be handled differently proto[method] = async function(...args) { From 58efddb3605c9f7e6240216ecd2f6017ae518593 Mon Sep 17 00:00:00 2001 From: klaucode <103204675+klaucode@users.noreply.github.com> Date: Sun, 15 Dec 2024 23:09:52 +0100 Subject: [PATCH 4/4] Update hooks --- hooks.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/hooks.js b/hooks.js index bb43051..f679e4d 100644 --- a/hooks.js +++ b/hooks.js @@ -40,9 +40,9 @@ const fetchMethods = [ "fetchAsync", "observeAsync", "observeChangesAsync", + "mapAsync", "countAsync", "forEachAsync", - "mapAsync", Symbol.asyncIterator, ]; @@ -107,7 +107,7 @@ selectionMethods.forEach(method => { } // Modify cursor and then call original method cursor._mongo._observeChanges = async function(...args) { - if(self._groupingBefore_find) { + if(userId && self._groupingBefore_find) { const selector = cursor._cursorDescription.selector; await self._groupingBefore_find.call({args}, userId, selector, {}); cursor._cursorDescription.selector = selector; @@ -123,7 +123,6 @@ selectionMethods.forEach(method => { // modify the selector in the cursor before calling the fetch method if(self._groupingBefore_find) { const selector = cursor._cursorDescription.selector; - const userId = getUserId(); await self._groupingBefore_find.call({args}, userId, selector, {}); cursor._cursorDescription.selector = selector; }