tl;dr: I think Set needs a way to get an element (set.toList()[0]), but maybe I'm missing something!
Hello! I'm working on a budgeting app using Firestore with a large number of small objects (credit card transactions). To limit the number of reads, it doesn't make sense to store each transaction as a separate document since a user is likely to want ~hundreds of transactions at a time.
Instead, I have a container to hold many transactions that looks like this:
/user/{user_id}/transactions/{container_id}
container: {
transactions: {
transaction_id_1: {
amount: 8.25,
note: 'chipotle lunch'
},
transaction_id_2: {
amount: 12.01
}
}
}
This works great, but I don't think the security rules can work for the write. I'd like to allow users to modify some fields (note) but not other fields (amount). If each transaction was a document, we could do this with MapDiff, but the nesting makes it harder.
Since we can't write for loops, if we constrain ourselves to one updated transaction per write, this should be completely possible with nested MapDiffs like this:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents/{document=**} {
function allowTransactionUpdate() {
let transactionId = <transaction ID of the single transaction being updated>;
// Limit fields updated for container.
return request.resource.data.diff(resource.data).changedKeys()
.hasOnly(['transactions']) &&
// Make sure only one transaction changed.
request.resource.data.transactions.diff(resource.data.transactions)
.changedKeys().hasOnly([transactionId]) &&
// Verify the transaction already exists.
transactionId in resource.data.transactions &&
// Only allow certain fields to be updated on that transaction.
request.resource.data.transactions[transactionId]
.diff(resource.data.transactions[transactionId]).affectedKeys()
.hasOnly(['note']);
}
match /transactions/{transMonthId} {
allow update: if allowTransactionWrite();
}
allow read, write: if false;
}
}
This would work great... if we could use MapDiff to get the transaction that changed in the container.transactions Map:
let transactionId = request.resource.data.transactions
.diff(resource.data.transactions).changedKeys()[0];
The key missing part is the last bit: [0]. Currently, Sets offer no way to get an element, which means that converting something to a Set (and thus anything using MapDiff) is a dead end: You can't ever actually know what the value is in a Set. It can only be compared to other Sets.
Otherwise... am I missing something? Is there another way to be limiting fields on the nested update?
Other options would be:
- Using a custom backend to do this write, which is doable but sad since a big advantage of Firestore is minimal backend + enforcement in security rules.
- Put user-editable properties in one container document + non-editable properties in another, but that doubles reads and adds annoying complexity to client subscriptions.
- Accepting that this is not possible and using a document per-transaction, which will result in 100x more reads. ;)