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}