Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
2275fe3
Rework test files into js from coffeescript
harryadel Jul 25, 2025
6224cf0
Convert package files to JS
harryadel Jul 25, 2025
42c71b1
Replace underscore with native utilities
harryadel Jul 25, 2025
0b66668
Get tests to boot
harryadel Jul 25, 2025
305050c
Drop const so ErrMsg and Helpers can be exported properly
harryadel Jul 25, 2025
21f1fed
Remove insecure login
harryadel Aug 1, 2025
eb5a7ea
Ensure proper context setup is required for user operations.
harryadel Aug 1, 2025
a9465a1
Await server operations in hook_tests
harryadel Aug 1, 2025
89d2e9a
Make findHook sync
harryadel Aug 1, 2025
ee0f6ff
update observeChanges to async and export Grouping in TestFuncs.
harryadel Aug 1, 2025
c7c876d
Split tests into server and client structure
harryadel Aug 2, 2025
932f26a
Remove debug log and improve error handling in user group binding
harryadel Aug 3, 2025
864a475
Fix selector assignment in userFindHook and enhance error handling in…
harryadel Aug 3, 2025
782e1a4
Freeze commit
harryadel Aug 3, 2025
53f2fd0
Add _isDirectGroupContext to maintain direct group context handling i…
harryadel Aug 7, 2025
473040b
Fix groupig_test_server except one
harryadel Aug 7, 2025
5c0fa50
Move grouping collections to utils.js
harryadel Aug 7, 2025
538dd69
Add hook_tests_client
harryadel Aug 7, 2025
504d437
Add grouping_test_clientjs
harryadel Aug 7, 2025
1a56d3c
Publish new beta 0.7.0-beta.1
harryadel Aug 7, 2025
a51bc28
Fix remaining test in hook_tests_server
harryadel Aug 22, 2025
fab1444
Add tests for admin visibility in user find hooks
harryadel Aug 22, 2025
855eddd
fix local empty find in grouping_test_server
harryadel Aug 22, 2025
2152e10
Replace grouping_test_client with grouping_integration_tests
harryadel Aug 22, 2025
52b2fde
Update README
harryadel Aug 22, 2025
5954cf9
Make Partitioner.directOperation return value
harryadel Aug 28, 2025
0c3b10b
Publish a new beta
harryadel Aug 28, 2025
a7573d0
Refactor error handling in hooks to use throwVerboseError helper
harryadel Sep 11, 2025
7e9de3c
Allow to configure whether to use Meteor.users or Grouping collection
harryadel Sep 11, 2025
fa892fb
Document new options
harryadel Sep 11, 2025
8423cac
Publish new version
harryadel Sep 11, 2025
54ef17f
Remove "Benefits" sections
harryadel Sep 11, 2025
3d1b856
Enhance configuration management in Partitioner.configure to auto-dis…
harryadel Sep 11, 2025
7379b42
Update version to 0.7.0-beta.5 in package.js
harryadel Sep 11, 2025
5cc1f7e
Remove extra logs
harryadel Sep 11, 2025
5bfc640
Update version to 0.7.0-beta.6 in package.js
harryadel Sep 11, 2025
a301310
Publish new beta 0.7.0-beta.7
harryadel Sep 18, 2025
fd9e104
Publish new beta 0.7.0-beta.8
harryadel Sep 25, 2025
848b245
save changes
harryadel Sep 25, 2025
997ade4
Publish new beta
harryadel Oct 2, 2025
0855bc9
Fix merge conflicts
harryadel Oct 2, 2025
7a725f9
Publish new beta
harryadel Oct 3, 2025
8057145
Add logging for direct access
harryadel Oct 3, 2025
782fdc9
0.7.0-beta.11
harryadel Oct 3, 2025
cf8cf53
Publish new beta
harryadel Oct 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 196 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,117 @@ Install with Meteor:
meteor add mizzao:partitioner
```

## Configuration

The partitioner package supports flexible configuration options to adapt to different storage strategies and performance requirements.

### Basic Configuration

```js
Partitioner.configure({
useMeteorUsers: false, // Use Meteor.users collection instead of separate grouping collection
groupingCollectionName: "ts.grouping", // Custom name for grouping collection
disableUserManagementHooks: false // Disable hooks on user management operations
});
```

**Note:** The package automatically manages conflicting configurations. When you set `useMeteorUsers: true`, it automatically disables separate collection features. When you set `useMeteorUsers: false`, it automatically disables Meteor.users specific features.

### Configuration Options

#### `useMeteorUsers` (Boolean, default: `false`)

When `true`, stores group data directly in the `Meteor.users` collection using a `groupId` field instead of a separate grouping collection.

**Example:**
```js
Partitioner.configure({ useMeteorUsers: true });
```

#### `groupingCollectionName` (String, default: `"ts.grouping"`)

Custom name for the grouping collection when not using `Meteor.users`. Useful for avoiding naming conflicts or organizing collections.

**Example:**
```js
Partitioner.configure({
useMeteorUsers: false,
groupingCollectionName: "myapp.user_groups"
});
```

#### `disableUserManagementHooks` (Boolean, default: `false`)

When `true` and `useMeteorUsers: true`, disables partitioning hooks on user management operations:
- `createUser`
- `findUserByEmail`
- `findUserByUsername`
- `_attemptLogin`

**Example:**
```js
Partitioner.configure({
useMeteorUsers: true,
disableUserManagementHooks: true
});
```

### Storage Strategies

#### Separate Collection Strategy (Default)
```js
Partitioner.configure({ useMeteorUsers: false });
```
- Uses dedicated `ts.grouping` collection
- Stores `{_id: userId, groupId: groupId}` documents
- Syncs to `Meteor.users.group` field for hooks
- Best for: Complex group management, multiple group types

#### Meteor.users Collection Strategy
```js
Partitioner.configure({
useMeteorUsers: true,
disableUserManagementHooks: true
});
```
- Stores group data in `Meteor.users.groupId` field
- No separate collection needed
- Optimized user management operations
- Best for: Simple group assignments, performance-critical applications

### Configuration Validation

The package includes built-in validation with helpful debug messages:

```js
// Debug messages show automatic configuration changes
Partitioner.configure({ useMeteorUsers: true });
// Output: "Configuration: Using Meteor.users collection for grouping. Separate grouping collection features disabled."

Partitioner.configure({ useMeteorUsers: false });
// Output: "Configuration: Using separate grouping collection. Meteor.users specific features disabled."
```

## Compatibility

- Meteor: 3.0+
- Hooks: matb33:[email protected] (used internally)

## Usage

Partitioner uses the [collection-hooks](https://github.com/matb33/meteor-collection-hooks) package to transparently intercept collection operations on the client and server side so that writing code for each group of users is almost the same as writing for the whole app. Only minor modifications from a standalone app designed for a single group of users is necessary.

Partitioner operates at the collection level. On the server and client, call `Partition.partitionCollection` immediately after declaring a collection:
Partitioner operates at the collection level. On the server and client, call `Partitioner.partitionCollection` immediately after declaring a collection:

```
Foo = new Mongo.Collection("foo");
// Client: synchronous hook registration
Partitioner.partitionCollection(Foo, options);

// Server: recommended to await during startup
Meteor.startup(async () => {
await Partitioner.partitionCollection(Foo, options);
});
```

`options` determines how the partitioned collection will behave. The fields that are supported are
Expand All @@ -46,44 +148,94 @@ Collections that have been partitioned will behave as if there is a separate ins

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.

### Async database APIs in Meteor 3

Meteor 3 collection methods are async. Helpers used in this repo/tests include:

- `findOneAsync`, `fetchAsync`, `countAsync`, `insertAsync`, `updateAsync`, `removeAsync`, and `createIndex(...)`.

Use `await` for server code and tests where appropriate.

## Common (Client/Server) API

#### `Partitioner.partitionCollection(Mongo.Collection, options)`

Adds hooks to a particular collection so that it supports partition operations. This should be declared immediately after `new Mongo.Collection` on both the server and the client.

- Client: synchronous
- Server: async (await recommended during startup)

**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()`

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.
Gets the group of the current user. Returns `undefined` if the user is not logged in or not part of a group.

- Client: synchronous and reactive (depends on the logged-in user document)
- Server: async (returns a Promise)

## Server API

#### `Partitioner.setUserGroup(userId, groupId)`
#### `Partitioner.configure(options)` (sync)

Configures the partitioner package with the specified options. Should be called before any other partitioner operations.

**Parameters:**
- `options.useMeteorUsers` (Boolean, optional): Use Meteor.users collection instead of separate grouping collection
- `options.groupingCollectionName` (String, optional): Custom name for grouping collection
- `options.disableUserManagementHooks` (Boolean, optional): Disable hooks on user management operations

**Example:**
```js
Meteor.startup(() => {
Partitioner.configure({
useMeteorUsers: true,
disableUserManagementHooks: true
});
});
```

#### `Partitioner.setUserGroup(userId, groupId)` (async)

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.getUserGroup(userId)` (async)

Gets the group of the current user.

#### `Partitioner.clearUserGroup(userId)`
#### `Partitioner.clearUserGroup(userId)` (async)

Removes the current group assignment of the user. The user will no longer be able to operate on any partitioned collections.

#### `Partitioner.bindGroup(groupId, func)`
#### `Partitioner.bindGroup(groupId, func)` (async)

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.bindUserGroup(userId, func)` (async)

A convenience function for running `Partitioner.bindGroup` as the group of a particular user.

#### `Partitioner.directOperation(func)`
#### `Partitioner.directOperation(func)` (sync wrapper)

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.

Notes:
- This is a synchronous wrapper around an environment flag. It does not return the value of `func`.
- If `func` is async, capture/return its Promise yourself from the calling site and `await` that, rather than awaiting `Partitioner.directOperation`.

Example:

```js
// GOOD
const result = await (async () => {
let value;
Partitioner.directOperation(async () => {
value = await SomeCollection.find(selector).fetchAsync();
});
return value;
})();
```

## Configuring Subscriptions

Suppose you have a publication on the server such as the following:
Expand Down Expand Up @@ -116,13 +268,20 @@ Deps.autorun(function() {

## Partitioning of `Meteor.users`

`Meteor.users` is partitioned by default. Users will only see other users in their group in default publications. However, unscoped operations do not throw an error, because server operations (login, etc) need to proceed as normal when groups are not specified. This generally causes everything to work as expected, but please report any unexpected behavior that you see.
`Meteor.users` is partitioned by default.

- Server: user finds must run inside a group context (`Partitioner.bindUserGroup` or `Partitioner.bindGroup`). Outside of a group context, user find operations throw `403` with reason `User find operation attempted outside group context`.
- Client: regular users are filtered server-side; for admin users, the client hook additionally merges `{ admin: { $exists: false } }` into global user finds so admins don’t see themselves in global lists.
- The package publishes `admin` and `group` fields of the current user so `Partitioner.group()` can be reactive on the client.

## Admin users

Partitioner treats users with `admin: true` as special. These users are able to see the entire contents of partitioned collections as well as all users when they are not assigned to a group, and operations will not result in errors.
Admin users are identified via `Meteor.user().admin === true`.

However, when admin users join a group, they will only see the data and users in that group (if you set up the subscriptions as noted above.) They will also, currently, be unable to do **any** operations on partitioned collections. The idea is to allow admin users to be able to join games, chatrooms, etc for observational purposes, but to prevent them from making unintended edits from the user interface.
- Admins can see all partitioned collections when not assigned to a group.
- When an admin joins a group, they only see that group's data (consistent with non-admin behavior).
- Admins are prevented from writes to partitioned collections via a deny rule.
- Client-only: global finds on `Meteor.users` for admins exclude admin users themselves.

If you would like to see other ways to define admin permissions, please open an issue.

Expand Down Expand Up @@ -155,13 +314,38 @@ ChatMessages.insert({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?

### After
### After (Meteor 3, async APIs)

With this package, you can create a partition of the `ChatMessages` collection:

```js
// Configure the partitioner (optional - defaults work fine)
Meteor.startup(() => {
Partitioner.configure({
useMeteorUsers: false, // Use separate grouping collection (default)
groupingCollectionName: "ts.grouping"
});
});

ChatMessages = new Mongo.Collection("messages");
// Client
Partitioner.partitionCollection(ChatMessages, {index: {timestamp: 1}});
// Server
Meteor.startup(async () => {
await Partitioner.partitionCollection(ChatMessages, {index: {timestamp: 1}});
});
```

**Alternative configuration using Meteor.users collection:**

```js
// Configure to use Meteor.users collection for better performance
Meteor.startup(() => {
Partitioner.configure({
useMeteorUsers: true,
disableUserManagementHooks: true // Optimize user management operations
});
});
```

The second argument tells the partitioner that you want an index of `timestamp` within each group. Partitioned lookups using `timestamp` will be done efficiently. Then, you can just write your publication as follows:
Expand Down
15 changes: 0 additions & 15 deletions common.coffee

This file was deleted.

Loading