Firestore - Deep Queries - javascript

I have a Firestore that contains a collection (Items) that contains subcollections (called "things"). When I get my Items collection I get all of the child documents but none of the child collections. I would like to do a deep retrieval, one object called Items that contains the sub-collections.
I understand this is not possible out of the box, but I am struggling to write the code myself to do this.
Can anyone help?
constructor(public afs: AngularFirestore) {
//this.items = this.afs.collection('Items').valueChanges();
this.itemsCollection = this.afs.collection('Items', ref => ref.orderBy('year', 'asc'));
this.items = this.itemsCollection.snapshotChanges().map(changes => {
return changes.map(a => {
const data = a.payload.doc.data() as Item;
data.id = a.payload.doc.id;
return data;
});
});
}
getItems() {
return this.items;
}

Yes, you can execute the code below to get what you want to. Basically this code gets for each item its things subcollection:
getItems(): Promise<any> {
return this.db.collection('items').get().then(
items => {
return this.getThingsForItems(items);
});
}
getThingsForItems(items): Promise<any> {
return Promise.all(items.docs.map(async (element) => {
var things = []
const response = await this.db.collection('items')
.doc(element.id).collection('things').get();
response.forEach(subcollectionItem => {
things.push(subcollectionItem.data());
});
return { Item: element.data(), Things: things }
}));
}
The getItems method will return a promisse that contains your items with its things subcollection.
Something like that:
Keep in mind that this query can become a slow-running query since it is making a get for each document of the items collection.
So you might should consider denormalize your database or transform the things subcolletion in an array of the items documents (in this case you should be aware that a document has a max size of 1MB, so do not consider this option if you are array can become too big).

Related

Filter firestore request by separate steps

I am making a filter so that a user can search for exactly a few parameters, without having to put them all in the form. For example you could search for games of type X but of all genres (Leave the form empty and it is an empty array).
I had tried doing the whole query at the same time but since there are empty arrays an exception is thrown. Consequently, I have thought of going step by step checking the filter parameters and discarding the empty ones, but I don't know how to save each step in the reference.
Technologies:
Ionic
Angular
Firebase - Firestore
Service Code:
getFilteredResultsGames(filter: Filter) {
return this.firestore.collection<Game[]>(environment.db_tables.games,
(ref) => {
if (filter.types.length > 0){
ref.where('id_type', 'in', filter.types)
}
if (filter.genders.length > 0) {
ref.where('ids_genders', 'array-contains-any', filter.genders)
}
return ref
}).valueChanges({ idField: 'id' });
}
Model Code:
export interface Filter {
genders: string[];
types: string[];
}
Firestore queries objects are immutable. Calling where on a ref or query, doesn't modify the existing object, but rather returns a new query with the additional condition.
So to return the new query, you'd do something like:
getFilteredResultsGames(filter: Filter) {
return this.firestore.collection<Game[]>(environment.db_tables.games,
(ref) => {
if (filter.types.length > 0){
ref = ref.where('id_type', 'in', filter.types)
}
if (filter.genders.length > 0) {
ref = ref.where('ids_genders', 'array-contains-any', filter.genders)
}
return ref
}).valueChanges({ idField: 'id' });

How to map data correctly before subscription?

I have following function:
this.localStorage.getItem('user').subscribe(user => {
this.user = user;
this.authSrv.getOrders(this.user.einsender).pipe(map(orders => {
map(order => { order["etz"] = "23"; return order})
return orders;
})).subscribe(orders => {
this.orders = orders;
this.completeOrders = orders;
console.log(orders);
this.waitUntilContentLoaded = true;
})
})
The result without the map is:
[{id: 1, etz: "21"}]
With the map from above I try to enter the array, then the order and in the order I try to change the etz property but somehow nothing changes. Can someone look over?
I appreciate any help!
I see multiple issues here.
Try to avoid nested subscriptions. Instead you could use one of the RxJS higher order mapping operators like switchMap. You could find differences b/n different higher order mapping operators here and here.
To adjust each element of the array you need to use Array#map method in addition to the RxJS map operator.
You could use JS spread operator to adjust some of the properties of the object and retain other properties.
Try the following
this.localStorage.getItem('user').pipe(
switchMap(user => {
this.user = user;
return this.authSrv.getOrders(this.user.einsender).pipe(
map(orders => orders.map(order => ({...order, order['etz']: '23'})))
});
})
).subscribe(
orders => {
this.orders = orders;
this.completeOrders = orders;
console.log(orders);
this.waitUntilContentLoaded = true;
},
error => {
// good practice to handle HTTP errors
}
);
map is an operator that goes in a pipe like this:
someObs$.pipe(map(arg => { return 'something'}));
You've done this:
someObs$.pipe(map(arg => {
map(arg => { return 'something' }) // this line here does nothing
return arg;
}));
It doesn't make any sense to use map inside the function you've given to map

Return multiple collections from Firestore database with Angular

I have an Ionic app, on the service I declared a function which is supposed to obtain multiple collections from a Firestore Database and must return all of them.
I have managed to get one collection on the service section like this:
read_Divisions() {
return this.firestore.collection('divisions').snapshotChanges();
}
And here is the the page typescript
ngOnInit() {
this.crudService.read_Divisions().subscribe(data => {
this.Divisions = data.map(e => {
return {
Name: e.payload.doc.data()['name'],
};
})
});
}
This is my idea of the service function for multiple collections:
read_Divisions() {
let divisions = this.firestore.collection('divisions').snapshotChanges();
let teams = this.firestore.collection('teams').snapshotChanges();
return [divisions,teams];
}
the way I obtain one collection on the page doesn't seem to be easily applicable for an array. What's the best way to go about it?
You can use forkJoin to wait for multiple observables, in that case making two separate queries, so probably the code would look like this:
readDivisions() {
return forkJoin({
divisions: this.firestore.collection('divisions').snapshotChanges(),
teams: this.firestore.collection('teams').snapshotChanges()
});
}
in another place in your code you would subscribe to readDivisions:
readDivisions.subscribe(result => console.log(result));
it will be smth like:
{divisions: [], teams: []}

How to create multiple copies of a deeply nested immutable Object?

I'm trying to get data from firebase/firestore using javascript so i made a function where i get my products collection and passing this data to reactjs state.products by setState() method
My goal is to pass these products to my react state but also keeping the original data and not changing it by manipulating it. I understand that in JavaScript whenever we are assigning the objects to a variable we are actually passing them as a reference and not copying them, so that' why i used the 3 dots (spread syntax) to copy firestore data into tempProducts[] same way to copy it in virgins[]
getData = () => {
let tempProducts = [];
let virgins = [];
db.collection("products")
.get()
.then(querySnapshot => {
querySnapshot.forEach(item => {
const singleItem = { ...item.data() };
virgins = [...virgins, singleItem];
tempProducts = [...tempProducts, singleItem];
});
tempProducts[0].inCart = true;
this.setState(
() => {
return { products: tempProducts };
},
() => {
console.log(
"this.state.products[0].inCart: " + this.state.products[0].inCart
);
console.log("tempProducts[0].inCart: " + tempProducts[0].inCart);
console.log("virgins[0].inCart: " + virgins[0].inCart);
}
);
});
};
then calling this method in componentDidMount
componentDidMount() {
this.getData();
}
So I changed the first product inCart value in tempProducts to true when I console log tempProducts value and state.products value it gives me true all fine but I'm expecting to get false when i console log virgins value but i did not. I should also mention that all inCart values are false in firestore data.
I solved the issue by passing the original firestore data to virgins[] instead of the singleItem as it is an object referenced by tempProducts[] like so virgins = [...virgins, item.data()]; also it works if i copied the singleItem object instead of referencing it like so virgins = [...virgins, { ...singleItem }]; keep in mind that i have no idea if this solutions are in fact efficient (not "memory waste") or not.

Angular RxJs - fill array in the second stream (nested stream)

I have in my Angular application a list of a specific model. Each item of this list has a propery xyzList. This xyzList property should be filled from a request which depends on the id of the first request. Here an example:
Model:
{
id: number;
name: string;
xyzList: any[]
}
Now I have two requests:
Request 1: Fills the model, but not the xyzList
this.myVar$ = this.myService.getElements<Model[]>();
Request 2: Should fill xyzList
this.myService.getXyzElements<XyzModel[]>(idOfTheModelOfTheFirstRequest);
At first I thought something like that:
this.myService.getElements<Model[]>()
.pipe(
mergeMap(items => items)
mergeMap(item => {
return this.myService.getXyzElements<XyzModel[]>(item.id)
.pipe(
map(xyzList => {
item.xyzList = xyzList
return item;
})
)
})
)
This does not work as I have flattened my list and I need an Observable<Array[]>, but I think is more or less clear what I want to achieve. I assume I can achieve this with forkJoin for instance, but I don't know how.Or is there a way to convert the flattened list back to list?
You need to use toArray(), because mergeMap/flatMap is used to flatten the array of data stream. It will return an Array instead of data steam.
this.myService.getElements<Model[]>()
.pipe(
mergeMap(items => items)
mergeMap(item => {
return this.myService.getXyzElements<XyzModel[]>(item.id)
.pipe(
map(xyzList => {
item.xyzList = xyzList
return item;
})
)
}),
toArray()
)
Well, I understand that you have a service function that return a list of "items". EachItem you need call another service function that return the data "xyzList".
So, each "item" must be a call, then, you create an array of "calls"
myService.getElements().pipe(
switchMap(result=>{
//We can not return the "list"
let calls:Observable[]=[]
//Before, the first calls was the own result
calls.push(of(result));
//Each result push the calls
result.forEach(e=>{
calls.push(myService.getXyzElements(e.id));
})
return forkJoin(calls).pipe(map(result=>{
//in result[0] we have the list of items
//In result[1] we have the Xyz of items[0]
//In result[2] we have the Xyz of items[1]
int i:number=0;
result[0].map(x=>{
i++;
return {
...x,
xyzList:result[i]
}
})
return result[0];
}))
})
).subscribe(res=>console.log(res));

Categories

Resources