Check if other node has same child node value - firebase

I'm trying to implement validation rule that will check when new $resvs is created if any other $resvs contains the same eventKey (which is a child node of that object) as the already existing reservations nodes.
The following code is not working, but can't come up with anything better now:
"reservations": {
"$resvs": {
".write": "!data.exists() || newData.child('eventKey').val() == root.child('reservations/$resvs/eventKey').val()"
}
}
edit:
firebase.database().ref('/reservations').push({
name: name,
number: number,
email: email,
start: start,
end: end,
scheduleId: scheduleId,
scheduleName: scheduleName,
eventKey: eventKey,
placeId: placeId
}).then(function(){
$('#calendar').fullCalendar('refetchEvents'); //confirmaton first? TODO
}).catch(function(error){
alert('err');
});

To solve this problem, i recomand you change the database structure a little bit. Instead of using the event key as a field, try to store the reservation data directly under the event key. In this way you can double check, once in code and once using security rules. If you want to check in your code, just add a listener on the reservations node and use exists() method on the dataSnapshot object. In the same manner use the security rules, because with your actual database structure it's impossible to use wildcards in the way you want.
Your database new structure should look like this:
Firebase-root
|
--- reservations
|
--- eventKey1
| |
| --- //event1 details
|
--- eventKey2
|
--- //event2 details
But remember, this will work only and only if the the event key are unique.
Hope it helps.

Related

See Array Changes in Firestore Security

I have a collection reviews where each review contains a list of uids of users who have liked it called likes. The schema looks like:
review (collection)
title string
author uid
likes [uid]
posted timestamp
user (collection) - uid
created timestamp
email string
Currently, I'm handling a user liking a review with:
firebase.firestore().doc(rid).update({
likes: firebase.firestore.FieldValue.arrayUnion(this.fetchCurrentUID())
});
And unliking with:
firebase.firestore().doc(rid).update({
likes: firebase.firestore.FieldValue.arrayRemove(this.fetchCurrentUID())
});
I only want to let a user add or remove their own uid from likes.
How can I write a security rule to ensure this? Specifically, I need to see how the list is being updated, for instance something like:
let newVals = request.resource.data.new_values // or something
return (newVals.length == 1 && newVals[0] == request.auth.uid)
Instead of checking the value of the arrayUnion we are updating with, we can check the the final result.
request.resource.data.likes Is the array AFTER the arrayUnion has been applied.
Therefore liking should result in request.auth.uid in request.resource.data.likes == true
For unliking we negate it !(request.auth.uid in request.resource.data.likes) == true
This is easy because we have access to request.auth.uid but in cases where we don't have the supposed value accessible this way. The following answer has a solution:
Firebase security rules, ensure one "array remove" only, and only to userId
It uses the following trick - create an array intersection with removeAll:
resource.data.likes.removeAll(request.resource.data.likes)[0] == request.auth.uid

Firestore security rules with reference fields

I am a bit stuck here as there is no way to debug those rules. I'd appreciate help with below rules.
I want to access:
/modules/module-id/sessions/session-id/parts/
The comparison with null in the first part of hasCompletedPrerequisiteSession() works well, the second part doesn't!
The path /modules/moduleId/sessions/sessionId/prerequisite points to a reference field.
service cloud.firestore {
match /databases/{database}/documents {
function hasCompletedPrerequisiteSession(moduleId,sessionId) {
// this part works well
return getPrerequisiteSession(moduleId,sessionId) == null ||
// !!! this part does not work !!!
hasCompleted(getPrerequisiteSession(moduleId,sessionId).id);
}
function getPrerequisiteSession(moduleId,sessionId) {
return get(/databases/$(database)/documents/modules/$(moduleId)/sessions/$(sessionId)).data.prerequisite;
}
function hasCompleted(sessionId) {
return exists(/databases/$(database)/documents/progress/$(request.auth.uid)/sessions/$(sessionId));
}
match /modules/{moduleId}/sessions/{sessionId}/parts/{partId} {
allow read: if hasCompletedPrerequisiteSession(moduleId,sessionId);
}
}
}
(If I store the session ID as a string instead of a reference to the session, it works fine.)
Edit
Questions
Reference field in security rules. Assuming modules/moduleId/owner points to a field of the type reference. What is the proper way to get the id of the referenced document?get(../modules/moduleId).data.owner.data.id or get(../modules/moduleId).data.owner or something else?
From Firebase support:
It seems that in your use case, you want to get the document name (sessionId) from the value of your reference field (prerequisite), unfortunately, this is not currently supported by Firestore security rules. I would suggest that you store only the sessionId as String on your prerequisite field, or you can also add String field for the sessionId. Keep in mind that the exists() and get() functions only allow you to check if a document exists, or retrieve the document at the given path.
It might be that around getPrerequisiteSession, after using get to pull the object by ref path, you had to use .data first before referencing the id field. Of course, id field needs to be stored as an object field.
For example, in my case I needed to allow user to add a message into a chat only if they're the owner of that chat room. There are 2 "tables" - chats and chat_messages, and chat_messages relate to a specific chat through chatId field. chats objects have ownerId field.
The rule I've used goes like this:
match /chat_messages/{itemId} {
function isOwner() {
return get(/databases/$(database)/documents/chats/$(request.resource.data.chatId)).data.ownerId == request.auth.uid;
}
allow read: if true;
allow create: if isOwner();
}

Firestore data duplication

I'm trying to setup a friend system in Firestore. My data model looks like this at the moment:
collection("users") ->
document("user1")
document("user2")
...
A document in the users collection contains data like the name, email... of the user. I'd like to enable a user to have friends now, but I'm unsure about the best way to model this.
So, I'd for sure add a friends field in the documents of the users, but what should this field contain? My first thought was a pointer to a new collection called friends in which the documents are users. Something like this:
collection("users") {
document("user1") {
name:user1,
friends: -> collection("friends") {
document("user2"),
...
}
}
}
This seems reasonable, but that'd mean that I'd have a lot of duplicate data in my database because each user that has friends will be duplicated in a friends collection. Should I worry about this or is this normal in a Firestore database structure?
Would it perhaps be possible to point to a document in the users collection from the friends collection? Something like:
collection("users") {
document("user1") {
name:user1,
friends: -> collection("friends") {
document, -----
... |
} |
}, |
document("user2")<-
}
Or should I throw away the thought of using a collection for friends and just keep a list with uids of all friends of the user?
Seems you are using two separate collections for users and friends first all you can do it by one collection. But I don't want to go there may be there was another scenario.
As your separate collection way, you can design your friends collection model to meet no duplication:
{
name : 'Name',
email : 'email#mail.com'
has_connected : {
'user1' : true // here you can use anyother unique key from user
}
}
The thing is that firestore recommend this types of design for query and for faster performance you can make that has_connected key as index.
In this approach, you have to check during adding new friend by email or any other unique key. if exists then just put another key into has_connected with the respective user. e.g user2 : true.
Finally, for fetching all friends for a user you have to do a query like this: e.g: in javascript
let ref = firebase.firestore().collection("friends");
ref
.where(`has_connected.${username}`, "==", true)
.get()
.then(//do your logic)
.catch()
Thanks

Grabbing Keys with Firebase

I'm currently creating an app using ionic2 and firebase for a class and I've run into an issue.
I"m not posting code because I don't have anything relevant enough to the question.
In my database I have a 'group' that people can create. I need the 'group' to be able to store a list of users. The way I'm currently doing it is that users will click an addGroup button, and the currentUser will be added to the list of users in that group simply by typing in the name of the group. (yeah i know that's going to need a password or something similar later, people obviously shouldn't just be able to join by group name)
*Names of groups will all be different so I should be able to query the database with that and get a single reference point.
The problem is that I don't know how to properly query the database and get the key value of the group object I want.
I keep looking things up and cant find a simple way to get the key of an object. I feel like this is a dumb question but, I'm unfamiliar with typescript, firebase, and ionic2 so I'm pretty lost in everything.
Answer for the question in comment.
you can query the database like this
this.group_val:any="group1"
let group=db.list('/groups', {
query: {
orderByChild: GroupName, //here specify the name of field in groups
equalTo: this.group_val //here specify the value u want to search
}
});
group.subscribe(grp=> {
//you can use the grup object here
})
First, define your database structure. The below is probably best:
groups
<first group> (Firebase-generated ID)
name: "Cat lovers"
users:
user1ID: true
user2ID: true
<second group>
...
I assume that when the user clicks on the group he wants to join, you know the group ID. For instance, it could be the value of an option in a select.
Get the list of groups:
groups$ = this.angularFireDatabase.list('groups');
To populate a list of groups into a select:
<select>
<option *ngFor="let group of groups$ | async" [value]="group.$key">
{{group.name}}
</option>
</select>
To create a new group:
groups$.push({name: "Dog lovers"})
The select (drop-down) will update itself.
To add a user to a group:
addUserToGroup(groupId, userId) {
this.angularFireDatabase.list(`groups/${groupID}/users`).update(userID, true);
}
To find a group based on its name:
// Define the shape of a group for better type checking.
interface Group { name: string; users: {[userId: string]: boolean;}; }
// Keep local list of groups.
groups$: FirebaseListObservable<Group>;
groups: Group[];
// Keep an updated local copy of groups.
ngOnInit() {
this.angularFireDatabase.list('groups').subscribe(groups => this.groups = groups);
}
function findGroupByName(name) {
return this.groups.find(group => group.name === name);
}

Firebase database: Referencing a dynamic value

Lets assume I'm trying to build a group messaging application, so I designed my database structure to look like so:
users: {
uid1: { //A user id using push()
username: "user1"
email: "aaa#bbb.ccc"
timestampJoined: 18594659346
groups: {
gid1: true,
gid3: true
}
}
uid2: {
username: "user2"
email: "ddd#eee.fff"
timestampJoined: 34598263402
groups: {
gid1: true,
gid5: true
}
}
....
}
groups: {
gid1: { //A group id using push()
name: "group1"
users: {
uid1: true,
uid2: true
}
}
gid2: {
name: "group2"
users: {
uid5: true,
uid7: true,
uid80: true
}
}
...
}
messages: {
gid1: {
mid1: { //A message id using push()
sender: uid1
message: "hello"
timestamp: 12839617675
}
mid2: {
sender: uid2
message: "welcome"
timestamp: 39653027465
}
...
}
...
}
According to Firebase's docs this would scale great.
Now lets assume that inside my application, I want to display the sender's username on every message.
Querying the username for every single message is obviously bad, so one of the solutions that I found was to duplicate the username in every message.
The messages node will now look like so:
messages: {
gid1: {
mid1: { //A message id using push()
sender: uid1
username: "user1"
message: "hello"
timestamp: 12839617675
}
mid2: {
sender: uid2
username: "user2"
message: "welcome"
timestamp: 39653027465
}
...
}
...
}
Now I want to add the option for the user to change his username.
So if a user decides to change his username, it has to be updated in the users node, and in every single message that he ever sent.
If I would have gone with the "listener for every message" approach, then changing the username would have been easy, because I would have needed to change the name in a single location.
Now, I have to also update the name in every message of every group that he sent.
I assume that querying the entire messages node for the user id is a bad design, so I thought about creating another node that stores the locations of all the messages that a user has sent.
It will look something like this:
userMessages: {
uid1: {
gid1: {
mid1: true
}
gid3: {
mid6: true,
mid12: true
}
...
}
uid2: {
gid1: {
mid2: true
}
gid5: {
mid13: true,
mid25: true
}
...
}
...
}
So now I could quickly fetch the locations of all the messages for a specific user, and update the username with a single updateChildren() call.
Is this really the best approach? Do I really have to duplicate so much data (millions of messages) only because I'm referencing a dynamic value (the username)?
Or is there a better approach when dealing with dynamic data?
This is a perfect example of why, in general, parent node names (keys) should be disassociated from the values they contain or represent.
So some big picture thinking may help and considering the user experience may provide the answer.
Now lets assume that inside my application, I want to display the
sender's username on every message.
But do you really want to do that? Does your user really want to scroll through a list of 10,000 messages? Probably not. Most likely, the app is going to display a subset of those messages and even at that probably 10 or 12 at a time.
Here's some thoughts:
Assume a users table:
users
uid_0
name: Charles
uid_1
name: Larry
uid_2:
name: Debbie
and a messages table
messages
msg_1
sender: uid_1
message: "hello"
timestamp: 12839617675
observers:
uid_0: true
uid_1: true
uid_2: true
Each user logs in and the app performs a query that observes the messages node they are part of - the app displays displays the message text of the message as well as each users name that's also observing that message (the 'group').
This could also be used to just display the user name of the user that posted it.
Solution 1: When the app starts, load in all of the users in the users node store them in dictionary with the uid_ as the key.
When the messages node is being observed, each message is loaded and you will have the uid's of the other users (or the poster) stored in the users_dict by key so just pick their name:
let name = users_dict["uid_2"]
Solution 2:
Suppose you have a LOT of data stored in your users node (which is typical) and a thousand users. There's no point in loading all of that data when all you are interested in is their name so your could either
a) Use solution #1 and just ignore all of the other data other than the uid and name or
b) Create a separate 'names' node in firebase which only keeps the user name so you don't need to store it in the users node.
names:
uid_0: Charles
uid_1: Larry
uid_2: Debbie
As you can see, even with a couple thousand users, that's a tiny bit of data to load in. And... the cool thing here is that if you add a listener to the names node, if a users changes their name the app will be notified and can update your UI accordingly.
Solution 3:
Load your names on an as needed basis. While technically you can do this, I don't recommend it:
Observe all of the messages nodes the user is part of. Those nodes will be read in and as they are read in, build a dictionary of uid's that you will need the names of. Then perform a query for each user name based on the uid. This can work but you have to take the asynchronous nature of Firebase into account and allow time for the names to be loaded in. Likewise, you could load in a message, then load in the user name for that message with the path: users/uid_x/user_name. Again though this get into an async timing issue where you are nesting async calls within async calls or a loop and that should probably be avoided.
The important point with any solution the user experience and keeping your Firebase structure as flat as possible.
For example, if you do in fact want to load 10,000 messages, consider breaking the message text or subject out into another node, and only load those nodes for your initial UI list. As the user drills down into the message, then load the rest of the data.
Steps to follow:
fetch username at every restart of app
cache them locally
show username from cache based on uid
done
Note: how you fetch username depends on your way of implementation
You only need this structure
mid1: { //A message id using push()
sender: uid1
message: "hello"
timestamp: 12839617675
}
The username can be read from the users directly "users/uid1/username" using a single value event listener after you read each child. Firebase is supposed to be used with sequential calls, since you cannot create complex queries like in SQL,
And just to keep it efficient you could:
1)Create a reference dictionary to use it as a cache handler in which after you read every message you verify if you have the value for each key:
[uid1:"John",uid2:"Peter",....etc...]
And if the key doesn't exist you add with the single value listener pointing to /users/$uid/username that handles the "add to cache" in its callback
2)Use the limitTo startAt and endAt queries to paginate the listener and avoid bringing data the user won't see
*There is no need to actually keep updating all the messages and all the nodes with every user change, imagine a chat group with 100 users in which every user have 20 messages ...2000 updates with your single updateChildren() call that would be extremely inefficient, since it is not scalable and you are updating data that surely no user will ever see again in a real life scenario (like the first message of the 2000 chat messages)

Resources