qualia:try-modify

v0.0.3Published 6 years ago

This package has not had recent updates. Please investigate it's current state before committing to using it in your project.

Qualia tryModify

tryModify is a helper function for building Mongo modifiers for multiple documents.

Motivation

We commonly need to modify multiple documents in a collection, for example in a loop:

1let ids = ['idA', 'idB', 'idC'];
2ids.forEach(id => {
3  MyCollection.update(id, {
4    $set: {
5      my_field: true,
6    }
7  });
8});

In addition, we often require that all of the updates should succeed, or else none of them should be applied at all.

In this simple example, we can reasonably expect all of the updates to succeed.

However, if we need more complex logic to determine each update modifier, it's possible that we will get most of the way through this forEach iteration and then fail to update an item:

1let ids = ['idA', 'idB', 'idC'];
2ids.forEach(id => {
3  if (Math.random() > 0.5) {
4    throw new Error('which updates have run? :)');
5  }
6  MyCollection.update(id, {
7    $set: {
8      my_field: true,
9    }
10  });
11});

When an iterating update fails, we potentially will have modified many documents in the collection, so it might be hard to undo the changes.

tryModify provides a way to compute the full list of modifiers before applying any of them to your collections, reducing the risk of failing in the middle of a multi-document update.

Usage

1tryModify(modifierBuilder);

tryModify allows you to write code describing the updates you want to perform, but it only applies them after you fully construct your modifier.

It takes a single argument, your modifierBuilder function, described below. The modifier builder calls stubbed collection operation functions. If it completes without throwing exceptions, all of the operations are applied to the collection.

tryModify returns different things depending on where it runs:

  • on the server, collection updates are synchronous, and tryModify

synchronously returns an array of the result of each collection operation, in order.

  • on the client, collection updates are asynchronous, and tryModify returns

an array of promises, each of which resolves to the result of the corresponding collection operation.

Here's the above simple example, updated to use tryModify:

1tryModify(({update}) => {
2  let ids = ['idA', 'idB', 'idC'];
3  ids.forEach(id => {
4    if (Math.random() > 0.5) {
5      throw new Error('At least no updates have run yet! :)');
6    }
7    update(MyCollection, id, {
8      $set: {
9        my_field: true,
10      }
11    });
12  });
13});

Pass tryModify a callback with code to modify your collections. Your callback will be invoked with an object containing the operator functions insert, update, and remove, which are API-compatible with the Mongo versions except that they require the collection as the first argument. Above, we only use update, so it's the only argument we destructure.

As you invoke the operator functions, tryModify appends the modifiers to an internal list. When your function completes, tryModify replays the modifiers on the collections you've specified.

If your function throws an exception, tryModify will allow it to bubble up, and it won't apply any of the modifiers to any collection.

If we end up throwing an exception above, then we can rest assured that MyCollection has not been modified at all. If we happen to make it through the forEach iteration without throwing, then our three updates will be applied to MyCollection in the order we called update.

Here's a more complete example, showing all of the available operations. Let's assume we have a collection called Fruits:

1try {
2
3  let results = tryModify(({insert, update, remove}) => {
4
5    insert(Fruits, {
6      name: 'Apple',
7      shape: 'round',
8    });
9
10    let purpleIDs = ['grape_id', 'plum_id', 'acai_id'];
11    purpleIDs.forEach(id => {
12      update(Fruits, id, {
13        $set: {
14          color: 'purple'
15        }
16      });
17    });
18
19    // Compute something hard, maybe throwing an exception
20    removeID = myFunctionThatMightThrowException();
21
22    remove(Fruits, removeID);
23
24  });
25
26  if (Meteor.isServer) {
27    // Results will be an array of the return values from Mongo for each operation
28    console.log(results) // e.g. "inserted-apple-id", 1, 1, 1, 1
29  } else {
30    // Results will be an array of promises for each Mongo operation
31    values = await Promise.all(results);
32    console.log(values); // e.g. "inserted-apple-id", 1, 1, 1, 1
33  }
34
35} catch (e) {
36  // Handle any exceptions thrown above if needed
37}