I am using this request to get my documents from Firestore asynchronously.
During an operation (delete for example) the list of my documents is not updated automatically. How can I transform my asynchronous function into an observable in order to take advantage of the real-time functionalities of Firestore and get the id of the document ?
import { Injectable } from '#angular/core';
import { SentencePair } from '../models/sentence-pair.model';
import { Firestore, collectionData, deleteDoc,limit, limitToLast,orderBy, increment,
addDoc, collection, doc, updateDoc, setDoc,query, where, getDocs } from
'#angular/fire/firestore';
import { Observable,combineLatest,map, defer } from 'rxjs';
#Injectable({ providedIn: 'root'})
export class SentencesPairsService {
constructor(private firestore: Firestore) { }
async FilterPairs(field: string, boolean: boolean, display:any) {
const sentencepairsRef = collection(this.firestore, 'SentencesPairs');
const q = query(sentencepairsRef,
where('validation', '==', boolean),
where('origin_trad', 'in', display),
orderBy(field),limitToLast(10));
const truc = await getDocs(q)
return truc.docs.map(docData=>({...docData.data(), id:docData.id}));
}
I use AngularFire 7.2. Thanks for your help.
Get an observable for data from Firestore query
If you want an observable from Firestore, you need to return the .valueChanges() on a AngularFirestoreCollection. Read this doc for reference: https://github.com/angular/angularfire2/blob/master/docs/firestore/querying-collections.md.
For example :
getUserByEmail(email: string): Observable<User> {
const collection = this.firestore.collection<User>('users', ref => ref.where('email', '==', email))
const user$ = collection
.valueChanges()
.pipe( map(users => {
const user = users[0];
console.log(user);
return user;
}));
return user$;
}
Get ID of the document
If you want the ID( which is not returned in valueChanges())on your document you need to use snapshotChanges(). Sometimes it's easier to maintain the id on the user data when saving to Firestore to avoid using snapshotChanges. Also SwitchMap is helpful in this case as when a new value comes from the source (userId in this case) it will cancel its previous firestore subscription and switch into a new one with the new user id. If for some reason you want to maintain firestore subscriptions for all userIds that come through at once, then use mergeMap instead. But typically you only want to be subscribed to one user's data at a time.
// Query the users by a specific email and return the first User with ID added using snapshotChanges()
return this.firestore.collection<User>('users', ref => ref.where('email', '==', email))
.snapshotChanges()
.pipe(map(users => {
const user = users[0];
if (user) {
const data = user.payload.doc.data() as User;
const id = user.payload.doc.id;
return { id, ...data };
}
else { return null;
}
}));
Note to subscribe to the changes :
The user will be null until you call subscribe on this.user$ :
const queryObservable = size$.pipe(
switchMap(size =>
afs.collection('items', ref => ref.where('size', '==', size)).snapshotChanges()
)
);
queryObservable.subscribe(queriedItems => {
console.log(queriedItems);
});
OR
In your html use the async pipe which handles subscribe and unsubscribe on an observable like below:
<div *ngIf="(user$ | async) as user">{{ user.email }}</div>
Try to avoid using the async pipe on the same observable multiple times in your html, and instead set it as a local html variable which i do above (as user) to avoid unnecessary data trips to the db.
Related
I would like to return the "User" object.
Got error message:
Variable 'user' is used before being assigned.ts(2454)
I tried to use async / await but I can't assign await to "return user" at the end of the code or user= await snapshot.val() because it is located on onValue() scope.
getLoggedInUser(id: string): User {
const db = getDatabase();
var user: User;
onValue(ref(db, '/users/' + id), (snapshot) => {
user = snapshot.val();
// ...
}, {
onlyOnce: true
});
return user;
}
When you call onValue you get the current value from that path in the database, and then also get all updates to that value. Since your callback may be called multiple times, there is no way to return a single value.
If you want to just get the value once and return it, you'll want to use get instead of onValue. Then you can also use async/await.
async getLoggedInUser(id: string): Promise<User> {
const db = getDatabase();
var user: User;
const snapshot = await get(ref(db, '/users/' + id))
user = snapshot.val();
return user;
}
I am actually having a similar issue, although I try to fetch data with paging logic.
We do have thousands of records and to render them nicely (10 - 25 per page) would be the best option anyhow.
const dbRef = query(ref(database, folder), orderByChild(field), startAt(start), limitToLast(limit))
return onValue(dbRef, (snapshot) => {
const values = Object.values(snapshot.val());
return {data: values, total: values.length, page: page}
})
I can see the values inside the onValue, but it seems not to return the value at all. I'm not sure where to go here, the documentation on that is not completely clear to me (a beginner as a developer).
hope you are coding well, I am using firebase and vue js to build a messenger application.
Each user has a document, and within each document there is a field named includeInMsg, which contains the Id of the message that the user included. I know how to read one document by its id, but I don't know how to read multiple documents by their ids.
here you can see my code:
import { ref } from "vue";
import { db } from "src/boot/firebase";
import { getAuth, onAuthStateChanged } from "firebase/auth";
import { updateDoc, arrayUnion, arrayRemove } from "firebase/firestore";
import {
addDoc,
collection,
query,
where,
getDocs,
getDoc
} from "firebase/firestore";
import { doc, onSnapshot } from "firebase/firestore";
export default {
setup() {
return {
userList: ref([]),
userMessageRefList: ref([]),
userMessageDocList: ref([]),
freelancerProfile: ref([]),
messages: ref([]),
userMessage: ref([]),
listofdata: ref([])
};
},
async created() {
// check if user loged in or not
const auth = getAuth();
onAuthStateChanged(auth, user => {
if (user) {
// store current user uid in userlist
this.userList.unshift(user.uid);
this.renderMessageList();
} else {
this.$router.push("/");
}
});
},
methods: {
// render user message list doc id
async renderMessageList() {
const currentUserProfile = query(
collection(db, "userProfile"),
where("uuid", "==", this.userList[0])
);
const userProfileQuerySnapshot = await getDocs(currentUserProfile);
userProfileQuerySnapshot.forEach(doc => {
let docData = doc.data();
this.userMessageRefList.unshift(docData.messageList);
this.listofdata.unshift(docData);
});
this.userMessageRefList.forEach(elem =>
// store doc id into userMessageDocList
elem.forEach(le => this.userMessageDocList.unshift(le))
);
console.log(this.userMessageDocList) //output: {0: {…}, 1: {…}}
// now I need to read each document by their doc Id and store it in an array to render
},
},
computed: {
checkMessages: function() {
if (this.messages.length > 1) {
return true;
} else {
return false;
}
}
}
};
I tried to use for loop to read each document and store the value in an array but I got SyntaxError: Unexpected reserved word 'await'. (71:26) error.
As confirmed in the comments, this.userMessageDocList contains a compounded object containing objects with the documents IDs.
In this case you should use Promise.all() to trigger a variable number of calls to Firestore in parallel.
As explained in the doc (see link above):
The Promise.all() method takes an iterable of promises as an
input, and returns a single Promise that resolves to an array of the
results of the input promises.
So you need to loop over the object, grab the Docs IDs and populate an Array of Promises that you pass to Promise.all(). In pseudo code (I let you use the method you want to loop over the object, there are a lot of examples on SO) it's something along the following lines:
const promisesArray = [];
<Loop For each element>
// Assumption: element.docId contains the Firestore doc ID
promisesArray.push(getDoc(doc(db, "yourCollection", element.docId)))
</End Loop>
const docSnapshotsArray = await Promise.all(promisesArray);
docSnapshotsArray.forEach(docSnap => {
console.log(docSnap.data());
// ...
});
docSnapshotsArrays is an Array of DocumentSnapshots. You can then loop over this array and do whatever you want.
I don't now if this is important for your specific case, but note that with Promise.all(), the returned values in the resulting Array will be in order of the Promises passed, regardless of completion order.
So i have 2 collections
1 collection is 'users'. There i have documents (objects) with property 'profile', that contains string. It's id of profile, that is stored in other collection 'roles' as document.
So i'm trying to get this data, but without success. Is there exist join method or something like that? Or i must use promise for getting data from collection roles, and then assign it with agent?
async componentDidMount() {
firebase
.firestore()
.collection('users')
.orderBy('lastName')
.onSnapshot(async snapshot => {
let changes = snapshot.docChanges()
const agents = this.state.agents
for (const change of changes) {
if (change.type === 'added') {
const agent = {
id: change.doc.id,
...change.doc.data()
}
await firebase
.firestore()
.collection('roles')
.doc(change.doc.data().profile).get().then( response => {
//when i get response i want to set for my object from above this val
agent['profile'] = response.data().profile
//after that i want to push my 'agent' object to array of 'agents'
agents.push(agent)
console.log(agent)
}
)
}
}
this.setState({
isLoading: false,
agents: agents
})
})
}
To do async operation on array of objects you can use promise.all, i have given a example below that is similar to your use case where multiple async operation has to be done
const all_past_deals = await Promise.all(past_deals.map(async (item, index) => {
const user = await Users.get_user_info(item.uid);
const dealDetails = await Deals.get_deal_by_ids(user.accepted_requests || []);
const test = await Users.get_user_info(dealDetails[0].uid);
return test
}))
}
This way you can get data from once api call and make other api call with the obtained data
I'm having some trouble that I'm not understanding very well playing with next.js and Firebase's Cloud Firestore, but basically this works:
export async function fetchBroadcasts() {
const db = await loadDB();
const firestore = db.firestore();
const settings = { timestampsInSnapshots: true };
firestore.settings(settings);
return await firestore.collection('broadcasts').doc('message').get().then(doc => ({ broadcast: doc.data() }));
}
and this doesn't:
export async function fetchBroadcasts() {
const db = await loadDB();
const firestore = db.firestore();
const settings = { timestampsInSnapshots: true };
firestore.settings(settings);
return await firestore.collection('broadcasts').doc('message').onSnapshot(doc => ({ broadcast: doc.data() }));
}
I can't figure out why the second option doesn't work since I'm basically following the documentation.
On my index.js page I have this:
static async getInitialProps() {
return fetchBroadcasts();
}
onSnapshot doesn't return a promise, so you can't await it. As you can see from the linked API docs, it returns a function that you call when you want to stop the listener that you just added.
You use onSnapshot when you want to set up a persistent listener on a document that constantly receives changes to that document. You use get when you want a single snapshot of that document.
Firestore's onSnapshot() always return documents array regardless of query you use:
firestore.collection('broadcasts').doc('message')
.onSnapshot(docs => {
return docs[0].data();
});
While in case of get you can get single document as well on the base of query:
firestore.collection('broadcasts').doc('message')
.get( doc => ({ broadcast: doc.data() }) );
Thanks for reading.
I have two Collections in Firestore and I'm using Angularfire2.
I have a collections of "Clients" and a collection of "Jobs". Clients can have a number of Jobs and each job has a linked client.
I've created a component to show a list of all the jobs, and I'm trying to pull in the associated client for each job using the Firestore key.
Here's my data:
I have been able to "hack" a solution but it is extremely buggy, and it looses all async - so I might as well write a MySQL backend and forget Firestore - the whole point of using Firestore is it's "live".
public jobs = {};
[...]
processData() {
const clients = [];
this.clientCollection.ref.get().then((results) => {
results.forEach((doc) => {
clients[doc.id] = doc.data();
});
const jobs = {};
this.jobsCollection.ref.get().then((docSnaps) => {
docSnaps.forEach((doc) => {
jobs[doc.id] = doc.data();
jobs[doc.id].id = doc.id;
jobs[doc.id].clientData = clients[doc.data().client];
});
this.jobs = jobs;
});
});
}
This works up to a point - but it strips out the async.
Key question: is there any way of doing a "join" as we would in an SQL database to pull these two data sets together? And is there a way of doing it in a way that will keep the async nature of the data?
You can try to use Promise.all() to fetch multiple promises into one object. Here is a revised version of your method that should give the desired result if I am understanding your question correctly:
async processData() {
const clientCollection = await firebase.firestore().collection(`clients`).get();
const jobsCollection = await firebase.firestore().collection(`jobs`).get();
const clients = [];
clients.push(clientCollection);
clients.push(jobsCollection);
const res = await Promise.all(clients);
// res should equal a combination of both querySnapshots
}
I just recreated the collection variables to show what to add to the array, but Promise.all() takes in an array of promises and resolves all of them into one array as the get() method in firestore is a Promise. This is also using async/await. Hope this can be of help!
EDIT:
Since you are using AngularFire2, you should use their method for this.
In you component you are going to want to import the angularfire2/firestore module and use the Observable methods they provide:
first import the module: import { AngularFireStore } from 'angularfire2/firestore';
then provide it to your constructor:
constructor(
private firestore: AngularFireStore
) { }
Then you could use Observable.combineLatest() to receive all the data at one time:
clients$: Observable<any[]>;
clients: any[];
jobs$: Observable<any[]>;
jobs: any;
joined: any;
ngOnInit() {
this.clients$ = this.getCollection('clients');
this.jobs$ = this.getCollection('jobs');
this.joined = Observable.combineLatest(this.clients$, this.jobs$);
this.joined.subscribe(([clients, jobs]) => {
this.clients = clients;
this.jobs = jobs;
});
}
getCollection(collectionName) {
return this.firestore.collection(`${collectionName}`).valueChanges();
}
In your markup you would just loop the data using *ngFor:
<div *ngFor="let client of clients">{{ client.name }}</div>
This way, your component will be listening for new data once firestore has the data and it will all come in at one time so you don't have nested subscriptions that are vulnerable to creating multiple subscriptions. Hope this can be of help.
late for solutions but after 4 hours searching I found this solution library its very useful for join collection and get data.
main resource : https://github.com/AngularFirebase/133-firestore-joins-custom-rx-operators
first create one type script file
file name : collectionjoin.ts
import { combineLatest, pipe, of, defer } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators';
export const leftJoinDocument = (afs: AngularFirestore, field, collection) => {
return source =>
defer(() => {
// Operator state
let collectionData;
const cache = new Map();
return source.pipe(
switchMap(data => {
// Clear mapping on each emitted val ;
cache.clear();
// Save the parent data state
collectionData = data as any[];
const reads$ = [];
let i = 0;
for (const doc of collectionData) {
// Skip if doc field does not exist or is already in cache
if (!doc[field] || cache.get(doc[field])) {
continue;
}
// Push doc read to Array
reads$.push(
afs
.collection(collection)
.doc(doc[field])
.valueChanges()
);
cache.set(doc[field], i);
i++;
}
return reads$.length ? combineLatest(reads$) : of([]);
}),
map(joins => {
return collectionData.map((v, i) => {
const joinIdx = cache.get(v[field]);
return { ...v, [field]: joins[joinIdx] || null };
});
}),
tap(final =>
console.log(
`Queried ${(final as any).length}, Joined ${cache.size} docs`
)
)
);
});
};
after that in your homepage.ts or any page typescript file.
import {
AngularFirestore,
AngularFirestoreCollection,
AngularFirestoreDocument,
} from "#angular/fire/firestore";
jobs: any
constructor( public afStore: AngularFirestore ) { }
this.afStore.collection('Jobs').valueChanges().pipe(
leftJoinDocument(this.afStore, 'client', 'Clients'),
shareReplay(1)
).subscribe((response) => {
this.products = response;
})
in this step we are passing three arguments
this.afStore = this is object of lib.
'client' = this is key/id of jobs collections
'Clients' = this is collection name which we are join.
now last step Display result
<ion-list lines="full" *ngFor="let job of Jobs">
<ion-item button detail>
<ion-label class="ion-text-wrap">{{ job.Name }}</ion-label>
<p>{{ job.Clients.companyName}}</p>
</ion-item>
</ion-list>
finally this code given two collection record.
thank you.
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');
},