Observable Confusion - firebase

I am using Ionic2 with AngularFire2.
I am also making use of a rxjs Observable. I have the following code:
findChatsForUid(uid: string): Observable<any[]> {
return this.af.database.list('/chat/', {
query: {
orderByChild: 'negativtimestamp'
}
}).map(items => {
const filtered = items.filter(
item => (item.memberId1 === uid || item.memberId2 === uid)
);
return filtered;
});
}
and
deleteChatsAndMessagesForUid(uid: string): Promise<any> {
return new Promise<any>((resolve) => {
let promiseArray: Promise<any>[] = [];
this.findChatsForUid(uid).map(items => {
return items;
}).forEach((chatItems) => {
for (let i: number = 0; i < chatItems.length; i++) {
promiseArray.push(this.deleteChat(chatItems[i], true));
}
Promise.all(promiseArray).then(() => {
resolve(true);
});
});
});
}
In the second function, you can see I retrieve the Observable from the first, and the loop through each item using the forEach function.
My problem is, because this is an Observable, there is always a handle to the object. So when I do the following:
deleteChatsAndMessagesForUid(uid).then(() => {
user.delete().then(() => {
...
}
}
It results in the following error because the deleted user is still trying to observe the Observable.
Error: Uncaught (in promise): Error: permission_denied at /chat:
Client doesn't have permission to access the desired data. Error:
permission_denied at /chat: Client doesn't have permission to access
the desired data.
Question
Is there a way to retrieve the data, without still being attached to the Observable? So that I am free to delete the associated user? Or is there a better way to handle this?
Thanks

It sounds like you want to unsubsribe from the list observable after the first emitted list.
You can use the first operator to complete the list observable after the first emitted list. This will result in automatic unsubscription and the listeners will be removed from the internal Firebase ref.
import 'rxjs/add/operator/first';
findChatsForUid(uid: string): Observable<any[]> {
return this.af.database
.list('/chat/', {
query: {
orderByChild: 'negativtimestamp'
}
})
.first()
.map(items => {
const filtered = items.filter(
item => (item.memberId1 === uid || item.memberId2 === uid)
);
return filtered;
});
}

Related

Updating displayed results after modifying Firestore doc React Native

I have a list of games that I'm able to add to without issue using UseEffect and onSnapshot. I can modify an item in the list without issue, and return one set of results (with the updated data properly displaying). When I try to modify another item (or the item same again), I get this error:
Could not update game: TypeError: undefined is not an object (evaluating '_doc.data().numPlayers') because the results/list of games are null. I'm sure I have something wrong with my code, but I can't figure it out.
Thanks in advance!
Here is my code:
useEffect(() => {
setIsLoading(true)
let results = [];
const unsubscribe = db
.collection('games')
.onSnapshot(
(querySnapshot) => {
querySnapshot.docChanges().forEach(change => {
const id = change.doc.id;
if (change.type === 'added') {
const gameData = change.doc.data();
gameData.id = id;
results.push(gameData);
}
if (change.type === 'modified') {
console.log('Modified game: ', id);
results = results.map(game => {
if (game.id === id) {
return change.doc.data()
}
return game
})
console.log(results)
}
if (change.type === 'removed') {
console.log('Removed game: ', id);
}
});
setIsLoading(false);
setGame(results);
return () => unsubscribe
},
(err) => {
setIsLoading(false);
console.log("Data could not be fetched", err);
}
);
}, []);
I forgot to add the doc ID to the gameData before adding it to the results. I did that in the "added" section, but not in the "modified" section (thinking that it was already included), forgetting that I hadn't added it as an actual field in the database (it just exists as the doc id).

How do I only get new documents from firebase using .onSnapshot when used with .limitToLast for pagination

I'm trying to implement a chat app with infinite scroll using Firebase. The problem is that if I don't empty my messages array when a new message is added then they're duplicated. If I empty the messages array then it doesn't keep the previous messages.
Here is the code:
getAllMessages(matchId: string) {
this.chatSvc.getAllMessages(matchId)
.orderBy('createdAt', 'asc')
.limitToLast(5)
.onSnapshot((doc) => {
if (!doc.empty) {
this.messages = [];
doc.forEach((snap) => {
this.messages.push({
content: snap.data().content,
createdAt: snap.data().createdAt,
sendingUserId: snap.data().sendingUserId,
receivingUserId: snap.data().receivingUserId
});
});
} else {
this.messages = [];
}
});
}
And the chat service that returns the reference:
getAllMessages(matchId: string): firebase.firestore.CollectionReference<firebase.firestore.DocumentData> {
return firebase
.firestore()
.collection(`matches`)
.doc(`${matchId}`)
.collection('messages');
}
I'm pushing the messages from the collection in to a messages array. If I don't add 'this.messages = []' then it will duplicate messages every time a new one is added to the collection.
How do I only get the new document from firebase with onSnapshot instead of it iterating through all of the collection again? I only want the last message because I'm going to implement infinite scroll with another query that retrieves previous messages.
Any help would be greatly appreciated.
the query will always return the last 5 result whenever a new entry that matches the condition occurs, which will create the duplicates. What you can do is to listen to changes between snapshots
getAllMessages(matchId: string) {
this.chatSvc.getAllMessages(matchId)
.orderBy('createdAt', 'asc')
.limitToLast(5)
.onSnapshot((snapshot) => {
snapshot.docChanges().forEach((change) => {
// push only new documents that were added
if(change.type === 'added'){
this.messages.push(change.doc.data());
}
});
});
}

Firebase Firestore returns a promise in Vue

I'm trying to use some data from from Firestore. before it used to work, now in Vuetify I keep getting 'PENDING' if I try to access the $data.users
export default {
data() {
return {
users: [],
};
},
created() {
db.collection('users').get().then((snapshot) => {
snapshot.forEach((doc) => {
const user = doc.data();
user.id = doc.id;
this.users = user;
console.log(user.documents.selfie.url); // Here the log return the value correctly
});
});
},
methods: {
imageUrl(user) {
console.log(user.documents.selfie.url); // Here the log return "Pending";
},
Inside the template I run a v-for (user, index) in users :key='index'
ERROR:
Uncaught (in promise) TypeError: Cannot read property 'selfie' of undefined
It's difficult to be 100% sure without reproducing your problem, but I think the problem comes from the fact that the Promise returned by the asynchronous get() method is not yet fulfilled when you call the imageUrl() method. This is why you get the pending value.
One possibility to solve that is to check as follows:
methods: {
imageUrl(user) {
if (user) {
console.log(user.documents.selfie.url);
} else {
//...
}
},
Also, is seems you want to populate the users Array with the docs from the users collection. You should do as follows:
created() {
db.collection('users').get().then((snapshot) => {
let usersArray = [];
snapshot.forEach((doc) => {
const user = doc.data();
user.id = doc.id;
usersArray.push(user);
console.log(user.documents.selfie.url); // Here the log return the value correctly
});
this.users = usersArray;
});
},
With your current code you assign the last user in the loop, not the list of users.

Can't access to a collection from a looped collection

I want to access to a collection inside another collection in a for loop.
Is it possible? I'm getting an error at the 4th line Error getting documents TypeError: cookUser is not a function
var mealsOnline = [];
return db.collection('users').get().then(function (snapshot) {
snapshot.forEach(cookUser => {
cookUser.collection('meals').get().then(function (snapshot2) {
snapshot2.forEach(meal => {
if (meal.data().portion > 0) {
var mealObject = meal.data();
mealObject.id = meal.id;
mealObject.address = cookUser.data().address;
mealObject.cookName = cookUser.data().displayName;
mealsOnline.push(mealObject);
}
});
});
});
return Promise.all(mealsOnline);
}).catch(err => {
console.log('Error getting documents', err);
});
With the forEach() method, your cookUser object is a QueryDocumentSnapshot.
As detailed in the documentation (link above), "QueryDocumentSnapshot offers the same API surface as a DocumentSnapshot". Therefore you should use the ref abstract type of a DocumentSnapshot, as follows:
snapshot.forEach(cookUser => {
cookUser.ref.collection('meals').get().then(snapshot2 => {
.....
})
})
https://firebase.google.com/docs/reference/js/firebase.firestore.QuerySnapshot#forEach

What's the best way to check if a Firestore record exists if its path is known?

Given a given Firestore path what's the easiest and most elegant way to check if that record exists or not short of creating a document observable and subscribing to it?
Taking a look at this question it looks like .exists can still be used just like with the standard Firebase database. Additionally, you can find some more people talking about this issue on github here
The documentation states
NEW EXAMPLE
var docRef = db.collection("cities").doc("SF");
docRef.get().then((doc) => {
if (doc.exists) {
console.log("Document data:", doc.data());
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
}).catch((error) => {
console.log("Error getting document:", error);
});
OLD EXAMPLE
const cityRef = db.collection('cities').doc('SF');
const doc = await cityRef.get();
if (!doc.exists) {
console.log('No such document!');
} else {
console.log('Document data:', doc.data());
}
Note: If there is no document at the location referenced by docRef, the resulting document will be empty and calling exists on it will return false.
OLD EXAMPLE 2
var cityRef = db.collection('cities').doc('SF');
var getDoc = cityRef.get()
.then(doc => {
if (!doc.exists) {
console.log('No such document!');
} else {
console.log('Document data:', doc.data());
}
})
.catch(err => {
console.log('Error getting document', err);
});
If the model contains too much fields, would be a better idea to apply a field mask on the CollectionReference::get() result (let's save more google cloud traffic plan, \o/). So would be a good idea choose to use the CollectionReference::select() + CollectionReference::where() to select only what we want to get from the firestore.
Supposing we have the same collection schema as firestore cities example, but with an id field in our doc with the same value of the doc::id. Then you can do:
var docRef = db.collection("cities").select("id").where("id", "==", "SF");
docRef.get().then(function(doc) {
if (!doc.empty) {
console.log("Document data:", doc[0].data());
} else {
console.log("No such document!");
}
}).catch(function(error) {
console.log("Error getting document:", error);
});
Now we download just the city::id instead of download entire doc just to check if it exists.
Check this :)
var doc = firestore.collection('some_collection').doc('some_doc');
doc.get().then((docData) => {
if (docData.exists) {
// document exists (online/offline)
} else {
// document does not exist (only on online)
}
}).catch((fail) => {
// Either
// 1. failed to read due to some reason such as permission denied ( online )
// 2. failed because document does not exists on local storage ( offline )
});
2022 answer: You can now use the count() aggregation to check if a document exists without downloading it.
Here is a TypeScript example:
import { getCountFromServer, query, collection, documentId } from '#firebase/firestore'
const db = // ...
async function userExists(id: string): Promise<boolean> {
const snap = await getCountFromServer(query(
collection(db, 'users'), where(documentId(), '==', id)
))
return !!snap.data().count
}
I Encountered Same Problem recently while using Firebase Firestore and i used following approach to overcome it.
mDb.collection("Users").document(mAuth.getUid()).collection("tasks").get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
if (task.getResult().isEmpty()){
Log.d("Test","Empty Data");
}else{
//Documents Found . add your Business logic here
}
}
}
});
task.getResult().isEmpty() provides solution that if documents against our query was found or not
Depending on which library you are using, it may be an observable instead of a promise. Only a promise will have the 'then' statement. You can use the 'doc' method instead of the collection.doc method, or toPromise() etc. Here is an example with the doc method:
let userRef = this.afs.firestore.doc(`users/${uid}`)
.get()
.then((doc) => {
if (!doc.exists) {
} else {
}
});
})
Hope this helps...
If for whatever reason you wanted to use an observable and rxjs in angular instead of a promise:
this.afs.doc('cities', "SF")
.valueChanges()
.pipe(
take(1),
tap((doc: any) => {
if (doc) {
console.log("exists");
return;
}
console.log("nope")
}));

Resources