Reputation: 522
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:
Upvotes: 4
Views: 984
Reputation: 1001
for anyone else that is looking for an example on nested objects and MapDiff
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function affectedKeys(keys){
return request.resource.data.diff(resource.data).affectedKeys().hasOnly(keys)
}
function affectedKeysObj(obj1Key, obj2Key, keys){
return request.resource.data[obj1Key].diff(resource.data[obj2Key]).affectedKeys().hasOnly(keys)
}
match /{document=**} {
allow read, write: if false;
}
match /users/{uid}{
allow get: if request.auth.uid == uid;
allow update: if request.auth.uid == uid
&& ! affectedKeys(["meta"])
&& affectedKeys(["userData"])
&& affectedKeysObj("userData", "userData", ["bio", "displayName"]);
}
}
}
In this case I wanted the user to be able to edit ["bio", "displayName"]
within the userData
map, but I also wanted to disallow editing of the meta
map.
however pertaining to the question, Doug Stevensons is right, I'm just adding that this is how I use MapDiff with nested objects.
Upvotes: 6
Reputation: 317467
You are not missing something. What you're trying to do is not possible with security rules.
If you intend to collect items of data, and you want to reference those items of data and protect with with security rules, they should be individual documents in a collection or subcollection. Trying to jam them all in a single document is not advisable, nor is it scalable. If you are doing this to save on document reads, you're quickly finding out that this sort of "optimization" is not actually a very helpful one when it comes to security rules and managing those individual items. It's far easier and straightforward to protect items of data as individual documents than it is to manage them in a single document.
If you really must store everything together, I suggest limiting write access via some backend where you can write custom logic, and have your clients invoke the backend whenever they need to perform writes. Bear in mind that this is not scalable, and you can run into the max document size of 1MB, which is a more expensive problem to solve than the one you started out with.
Upvotes: 2