Firebase real time database - subscription .on() does not trigger on client - javascript

I have a collection of notes in my Firebase realtime database.
My code is set to subscribe to modifications in the /notes path of the database.
When updating a note in the Firebase web console my client gets the updates, but when another client updates the note via ref.update() the data is not pushed to my client.
When updating in the Firebase web console, I am performing the update from one browser and watching the update in another browser.
How can this be? See code below.
export const startSubscribeNotes = () => {
return (dispatch, getState) => {
const uid = getState().auth.uid
const dbPath = 'notes'
database.ref(`${dbPath}`)
.orderByChild(`access/members/${uid}`)
.equalTo(true)
.on('value', (snapshot) => {
console.log('notes .on()')
let updatednotes = []
console.log('Existing notes', existingnotes)
snapshot.forEach( (data) => {
const note = {
id: data.key,
...data.val()
}
updatednotes.push(note)
})
dispatch(setnotes(notes))
})
}
}
What I have noticed is that if I change my access rules in the Firebase database I am able to get the updates, but can no longer restrict read access.
"notes": {
//entry-level access
".read": "
auth.uid !== null //&& query.equalTo === auth.uid
",
I can then loop all notes and collect the ones I should only be able to read.
const uid = getState().auth.uid
database.ref('notes')
.on('value', (snapshot) => {
const notes = []
snapshot.forEach((data) => {
const noteData = data.val()
if(noteData.access){
const authorArray = (noteData.access.author) ? [noteData.access.author] : []
const members = (noteData.access.members) ? noteData.access.members : {}
const membersArray = Object.keys(members)
const allUsers = authorArray.concat(membersArray)
const accessGranted = allUsers.includes(uid)
if(accessGranted){
const note = {
id: data.key,
...data.val()
}
notes.push(note)
}
}
})
if(notes.length > 0){ dispatch(setNotes(notes)) } //Dispatch if not empty
})
How can I apply the access rules and still get the updates pushed to my client?
I have checked that all clients can write to the database at /notes
Any help much appreciated!

Related

is it possible to call a firebase function onValue() in another onValue() if yes how to do

technologies used: React Native / Expo / Firebase
I explain to you, I recover a list of key with my first onValue, in this one I forEach for each key and in this one I make another onValue in order this time to recover the information of the keys, but I realize that it creates a problem, one event calls another and it creates duplicate calls. Moreover I could not intervene on the second listening to delete it since it is only initialized locally, so how can I call this in the right way? thank you in advance for your answers, I'm new to react native with firebase!
Here is a snippet of the code:
useFocusEffect( // first time the screen is loaded, the gardener list is empty, so we need to get the gardener list from firebase
React.useCallback(() => {
setGardeners([])
const gardenerRef = ref(db, 'users/' + auth.currentUser.uid + '/gardeners');
const gardenerListener = onValue(gardenerRef, (snapshot) => {
const data = snapshot.val();
if (data != null) {
setIsFirst(false)
Object.keys(data).forEach(e => {
const temp = ref(db, 'gardeners/' + e);
onValue(temp, (snapshot) => {
const data = snapshot.val();
if (data != null && data.metadata != undefined) {
setGardeners(gardeners => [...gardeners, { id: e, name: data.metadata.name, isIrrig: data.irrig }]);
}
})
gardeners.length > 0 ? setCurrentGardener(gardeners[0].id) : setCurrentGardener("")
setC(currentGardener != "" ? currentGardener : "")
})
} else {
setIsFirst(true)
}
})
and here is the way i turn off the event during disassembly, for me it looks correct, now if you think i did it wrong, don't hesitate
return () => {
setGardeners([])
off(gardenerRef, gardenerListener)
// console.log("unmounted")
}
}, []))

How to get data from 2 collection in firebase at a time?(similar to aggregate lookup in MongoDB) [duplicate]

I have a Cloud Firestore DB with the following structure:
users
[uid]
name: "Test User"
posts
[id]
content: "Just some test post."
timestamp: (Dec. 22, 2017)
uid: [uid]
There is more data present in the actual DB, the above just illustrates the collection/document/field structure.
I have a view in my web app where I'm displaying posts and would like to display the name of the user who posted. I'm using the below query to fetch the posts:
let loadedPosts = {};
posts = db.collection('posts')
.orderBy('timestamp', 'desc')
.limit(3);
posts.get()
.then((docSnaps) => {
const postDocs = docSnaps.docs;
for (let i in postDocs) {
loadedPosts[postDocs[i].id] = postDocs[i].data();
}
});
// Render loadedPosts later
What I want to do is query the user object by the uid stored in the post's uid field, and add the user's name field into the corresponding loadedPosts object. If I was only loading one post at a time this would be no problem, just wait for the query to come back with an object and in the .then() function make another query to the user document, and so on.
However because I'm getting multiple post documents at once, I'm having a hard time figuring out how to map the correct user to the correct post after calling .get() on each post's user/[uid] document due to the asynchronous way they return.
Can anyone think of an elegant solution to this issue?
It seems fairly simple to me:
let loadedPosts = {};
posts = db.collection('posts')
.orderBy('timestamp', 'desc')
.limit(3);
posts.get()
.then((docSnaps) => {
docSnaps.forEach((doc) => {
loadedPosts[doc.id] = doc.data();
db.collection('users').child(doc.data().uid).get().then((userDoc) => {
loadedPosts[doc.id].userName = userDoc.data().name;
});
})
});
If you want to prevent loading a user multiple times, you can cache the user data client side. In that case I'd recommend factoring the user-loading code into a helper function. But it'll be a variation of the above.
I would do 1 user doc call and the needed posts call.
let users = {} ;
let loadedPosts = {};
db.collection('users').get().then((results) => {
results.forEach((doc) => {
users[doc.id] = doc.data();
});
posts = db.collection('posts').orderBy('timestamp', 'desc').limit(3);
posts.get().then((docSnaps) => {
docSnaps.forEach((doc) => {
loadedPosts[doc.id] = doc.data();
loadedPosts[doc.id].userName = users[doc.data().uid].name;
});
});
After trying multiple solution I get it done with RXJS combineLatest, take operator. Using map function we can combine result.
Might not be an optimum solution but here its solve your problem.
combineLatest(
this.firestore.collection('Collection1').snapshotChanges(),
this.firestore.collection('Collection2').snapshotChanges(),
//In collection 2 we have document with reference id of collection 1
)
.pipe(
take(1),
).subscribe(
([dataFromCollection1, dataFromCollection2]) => {
this.dataofCollection1 = dataFromCollection1.map((data) => {
return {
id: data.payload.doc.id,
...data.payload.doc.data() as {},
}
as IdataFromCollection1;
});
this.dataofCollection2 = dataFromCollection2.map((data2) => {
return {
id: data2.payload.doc.id,
...data2.payload.doc.data() as {},
}
as IdataFromCollection2;
});
console.log(this.dataofCollection2, 'all feeess');
const mergeDataFromCollection =
this.dataofCollection1.map(itm => ({
payment: [this.dataofCollection2.find((item) => (item.RefId === itm.id))],
...itm
}))
console.log(mergeDataFromCollection, 'all data');
},
my solution as below.
Concept: You know user id you want to get information, in your posts list, you can request user document and save it as promise in your post item. after promise resolve then you get user information.
Note: i do not test below code, but it is simplify version of my code.
let posts: Observable<{}[]>; // you can display in HTML directly with | async tag
this.posts = this.listenPosts()
.map( posts => {
posts.forEach( post => {
post.promise = this.getUserDoc( post.uid )
.then( (doc: DocumentSnapshot) => {
post.userName = doc.data().name;
});
}); // end forEach
return posts;
});
// normally, i keep in provider
listenPosts(): Observable<any> {
let fsPath = 'posts';
return this.afDb.collection( fsPath ).valueChanges();
}
// to get the document according the user uid
getUserDoc( uid: string ): Promise<any> {
let fsPath = 'users/' + uid;
return this.afDb.doc( fsPath ).ref.get();
}
Note: afDb: AngularFirestore it is initialize in constructor (by angularFire lib)
If you want to join observables instead of promises, use combineLatest. Here is an example joining a user document to a post document:
getPosts(): Observable<Post[]> {
let data: any;
return this.afs.collection<Post>('posts').valueChanges().pipe(
switchMap((r: any[]) => {
data = r;
const docs = r.map(
(d: any) => this.afs.doc<any>(`users/${d.user}`).valueChanges()
);
return combineLatest(docs).pipe(
map((arr: any) => arr.reduce((acc: any, cur: any) => [acc].concat(cur)))
);
}),
map((d: any) => {
let i = 0;
return d.map(
(doc: any) => {
const t = { ...data[i], user: doc };
++i;
return t;
}
);
})
);
}
This example joins each document in a collection, but you could simplify this if you wanted to just join one single document to another.
This assumes your post document has a user variable with the userId of the document.
J

Firebase Multi path atomic update with child values?

I am succesfully updating my user's profile picture on their profile and on all of their reviews posted with this function:
export const storeUserProfileImage = (url) => {
const { currentUser } = firebase.auth();
firebase.database().ref(`/users/${currentUser.uid}/profilePic`)
.update({ url });
firebase.database().ref('reviews')
.orderByChild('username')
.equalTo('User3')
.once('value', (snapshot) => {
snapshot.forEach((child) => {
child.ref.update({ profilePic: url });
});
});
};
I am aware that I should be using an atomic update to do this so the data updates at the same time (in case a user leaves the app or something else goes wrong). I am confused on how I can accomplish this when querying over child values.
Any help or guidance would be greatly appreciated!
Declare a variable to store all the updates. Add the updates as you read them on your listener's loop. When the loop is finished, run the atomic update.
export const storeUserProfileImage = (url) => {
const { currentUser } = firebase.auth();
firebase.database().ref('reviews')
.orderByChild('username')
.equalTo('User3')
.once('value', (snapshot) => {
var updates = {};
updates[`/users/${currentUser.uid}/profilePic`] = url;
snapshot.forEach((child) => {
updates[`/reviews/${child.key}/profilePic`] = url;
});
firebase.database().ref().update(updates);
});
};

How to join multiple documents in a Cloud Firestore query?

I have a Cloud Firestore DB with the following structure:
users
[uid]
name: "Test User"
posts
[id]
content: "Just some test post."
timestamp: (Dec. 22, 2017)
uid: [uid]
There is more data present in the actual DB, the above just illustrates the collection/document/field structure.
I have a view in my web app where I'm displaying posts and would like to display the name of the user who posted. I'm using the below query to fetch the posts:
let loadedPosts = {};
posts = db.collection('posts')
.orderBy('timestamp', 'desc')
.limit(3);
posts.get()
.then((docSnaps) => {
const postDocs = docSnaps.docs;
for (let i in postDocs) {
loadedPosts[postDocs[i].id] = postDocs[i].data();
}
});
// Render loadedPosts later
What I want to do is query the user object by the uid stored in the post's uid field, and add the user's name field into the corresponding loadedPosts object. If I was only loading one post at a time this would be no problem, just wait for the query to come back with an object and in the .then() function make another query to the user document, and so on.
However because I'm getting multiple post documents at once, I'm having a hard time figuring out how to map the correct user to the correct post after calling .get() on each post's user/[uid] document due to the asynchronous way they return.
Can anyone think of an elegant solution to this issue?
It seems fairly simple to me:
let loadedPosts = {};
posts = db.collection('posts')
.orderBy('timestamp', 'desc')
.limit(3);
posts.get()
.then((docSnaps) => {
docSnaps.forEach((doc) => {
loadedPosts[doc.id] = doc.data();
db.collection('users').child(doc.data().uid).get().then((userDoc) => {
loadedPosts[doc.id].userName = userDoc.data().name;
});
})
});
If you want to prevent loading a user multiple times, you can cache the user data client side. In that case I'd recommend factoring the user-loading code into a helper function. But it'll be a variation of the above.
I would do 1 user doc call and the needed posts call.
let users = {} ;
let loadedPosts = {};
db.collection('users').get().then((results) => {
results.forEach((doc) => {
users[doc.id] = doc.data();
});
posts = db.collection('posts').orderBy('timestamp', 'desc').limit(3);
posts.get().then((docSnaps) => {
docSnaps.forEach((doc) => {
loadedPosts[doc.id] = doc.data();
loadedPosts[doc.id].userName = users[doc.data().uid].name;
});
});
After trying multiple solution I get it done with RXJS combineLatest, take operator. Using map function we can combine result.
Might not be an optimum solution but here its solve your problem.
combineLatest(
this.firestore.collection('Collection1').snapshotChanges(),
this.firestore.collection('Collection2').snapshotChanges(),
//In collection 2 we have document with reference id of collection 1
)
.pipe(
take(1),
).subscribe(
([dataFromCollection1, dataFromCollection2]) => {
this.dataofCollection1 = dataFromCollection1.map((data) => {
return {
id: data.payload.doc.id,
...data.payload.doc.data() as {},
}
as IdataFromCollection1;
});
this.dataofCollection2 = dataFromCollection2.map((data2) => {
return {
id: data2.payload.doc.id,
...data2.payload.doc.data() as {},
}
as IdataFromCollection2;
});
console.log(this.dataofCollection2, 'all feeess');
const mergeDataFromCollection =
this.dataofCollection1.map(itm => ({
payment: [this.dataofCollection2.find((item) => (item.RefId === itm.id))],
...itm
}))
console.log(mergeDataFromCollection, 'all data');
},
my solution as below.
Concept: You know user id you want to get information, in your posts list, you can request user document and save it as promise in your post item. after promise resolve then you get user information.
Note: i do not test below code, but it is simplify version of my code.
let posts: Observable<{}[]>; // you can display in HTML directly with | async tag
this.posts = this.listenPosts()
.map( posts => {
posts.forEach( post => {
post.promise = this.getUserDoc( post.uid )
.then( (doc: DocumentSnapshot) => {
post.userName = doc.data().name;
});
}); // end forEach
return posts;
});
// normally, i keep in provider
listenPosts(): Observable<any> {
let fsPath = 'posts';
return this.afDb.collection( fsPath ).valueChanges();
}
// to get the document according the user uid
getUserDoc( uid: string ): Promise<any> {
let fsPath = 'users/' + uid;
return this.afDb.doc( fsPath ).ref.get();
}
Note: afDb: AngularFirestore it is initialize in constructor (by angularFire lib)
If you want to join observables instead of promises, use combineLatest. Here is an example joining a user document to a post document:
getPosts(): Observable<Post[]> {
let data: any;
return this.afs.collection<Post>('posts').valueChanges().pipe(
switchMap((r: any[]) => {
data = r;
const docs = r.map(
(d: any) => this.afs.doc<any>(`users/${d.user}`).valueChanges()
);
return combineLatest(docs).pipe(
map((arr: any) => arr.reduce((acc: any, cur: any) => [acc].concat(cur)))
);
}),
map((d: any) => {
let i = 0;
return d.map(
(doc: any) => {
const t = { ...data[i], user: doc };
++i;
return t;
}
);
})
);
}
This example joins each document in a collection, but you could simplify this if you wanted to just join one single document to another.
This assumes your post document has a user variable with the userId of the document.
J

Firebase Functions: Cannot read property 'val' of undefined

I'm trying to add a Function in my Firebase Database, that creates/updates a combined property of two others, whenever they change.
The model is like this:
database/
sessions/
id/
day = "1"
room = "A100"
day_room = "1_A100"
And my function so far:
exports.combineOnDayChange = functions.database
.ref('/sessions/{sessionId}/day')
.onWrite(event => {
if (!event.data.exists()) {
return;
}
const day = event.data.val();
const room = event.data.ref.parent.child('room').data.val();
console.log(`Day: ${day}, Room: ${room}`)
return event.data.ref.parent.child("day_room").set(`${day}_${room}`)
});
exports.combineOnRoomChange = functions.database
.ref('/sessions/{sessionId}/room')
.onWrite(event => {
if (!event.data.exists()) {
return;
}
const room = event.data.val();
const day = event.data.ref.parent.child('day').data.val();
console.log(`Day: ${day}, Room: ${room}`)
return event.data.ref.parent.child("day_room").set(`${day}_${room}`)
});
But it's throwing this error:
TypeError: Cannot read property 'val' of undefined
I'm following the very first example in the Firebase Functions Get Started (Add the makeUppercase() function) and this is what it does in order to reach the entity reference:
event.data.ref.parent
Am I using the child() function wrongly? Any ideas?
When Cloud Functions triggers your code, it passes in a snapshot of the data that triggered the change.
In your code:
exports.combineOnDayChange = functions.database
.ref('/sessions/{sessionId}/day')
This means you event.data has the data for /sessions/{sessionId}/day. It does not contain any data from higher in the tree.
So when you call event.data.ref.parent, this points to a location in the database for which the data hasn't been loaded yet. If you want to load the additional data, you'll have to load it explicitly in your code:
exports.combineOnDayChange = functions.database
.ref('/sessions/{sessionId}/day')
.onWrite(event => {
if (!event.data.exists()) {
return;
}
const day = event.data.val();
const roomRef = event.data.ref.parent.child('room');
return roomRef.once("value").then(function(snapshot) {
const room = snapshot.val();
console.log(`Day: ${day}, Room: ${room}`)
return event.data.ref.parent.child("day_room").set(`${day}_${room}`)
});
});
Alternatively, consider triggering higher in your tree /sessions/{sessionId}. Doing so means that you get all the necessary data in event.data already, and also means you only need a single function:
exports.updateSyntheticSessionProperties = functions.database
.ref('/sessions/{sessionId}')
.onWrite(event => {
if (!event.data.exists()) {
return; // the session was deleted
}
const session = event.data.val();
const day_room = `${session.day}_${session.room}`;
if (day_room !== session.day_room) {
return event.data.ref.parent.child("day_room").set(`${day}_${room}`)
});
});

Categories

Resources