Angular - Combine two observables results into one - javascript

I have two nodes in my database - one which is allUsers and one which is usersChildren.
For example:
allUsers: { user1: {...}, user2: {...}}
usersChildren: { user1: {...} }
In this case user1 has children data and user2 does not.
I want to retrieve a list of all user objects, and inside each user's object I wish to add the children data from the usersChildren node(if there is one).
However, I am not really familiar with how I can do that. I have tried the following but this results in obtaining only the children information and not
a combined object with both the children information and the user meta data.
this.af.getObservable(`allUsers`).pipe(map(allUsers =>
allUsers.map(user => this.af.getObservable(`usersChildren/${user.id}`)))
.subscribe(allUsersData => this.userList = allUsersData);
What is the best way to achieve what I desire?

Try this
this.af.getObservable(`allUsers`)
.pipe(
mergeMap(users => forkJoin(
users.map(user => this.af.getObservable(`usersChildren/${user.id}`))
))
);
You make an HTTP call to get all the users, then you use forkJoin to make 1 call per user. The call is made thank to Array.map, which transforms your user into an HTTP call.
Now you can subscribe to it like this
this.myService.getAllUsers().subscribe(users => { console.log(users); });

I would use concat operator in your case.
Concat will combine two observables into a combined sequence, but the second observable will not start emitting until the first one has completed.
Example:
let first = Observable.timer(10,500).map(r => {
return {source:1,value:r};
}).take(4);
let second = Observable.timer(10,500).map(r => {
return {source:2,value:r};
}).take(4);
first.concat(second).subscribe(res => this.concatStream.push(res));

Related

Firestore: Array of references within a document into an object with array of objects (AngularFire)

I have a collection, where each single document have an array of references.
The structure in Firestore is like this:
collection1
doc1
doc2
doc3
name: string
...
refs: Array
collection2/x786123nd...
collection2/z1237683s...
...
I want to convert that into:
[
{<doc1 as object>},
{<doc2 as object},
{ name: "Document 3", refsResult:
[
{<object here mapped from reference>},
{<object here mapped from reference>},
]
}
]
Long story short, from firebase I need to get an output of a list of object where each object has a list of objects instead of references.
THE FOLLOWING SNIPPET IS NOT WORKING
I am trying to do it at service level in Angular using RxJS to transform the output, something like this:
return this.afs.collection('collection1')
.valueChanges()
.pipe(
switchMap(objects => from(objects).pipe(
switchMap(obj => from(obj.refs).pipe(
switchMap(ref => ref.get()),
map(r => ({ ...obj, refsResult: r.data() })),
tap(obj => console.log('OBJ', obj)),
)),
)),
tap(objects => console.log(objects))
);
But it seems that I only receive one object instead of a list of objects with 2. Also, it seems that the refsResult is also a single object instead of an array. I am sure I am using the switchMaps wrong, probably they are cancelling the others results or similar.
I would like to keep the solution within the chain of rxjs operators.
EDIT 1
I got it working in a hacky way, I am just sharing it here in order to give more context, and see if we can find a way to do it with RxJS.
return this.afs.collection('collection1')
.valueChanges()
.pipe(
tap(objects => {
objects.forEach(object => {
object.refsResult = [];
object.refs.forEach(ref => {
ref.get().then(d => object.refsResult.push(d.data()));
})
})
}),
tap(programs => console.log(programs))
);
The code above works but not very well, since first return the collection and then, mutates the objects to inject the items within the array. Hopefully it helps.
Help is very appreciated! :)
You can use combineLatest to create a single observable that emits an array of data.
In your case, we can use it twice in a nested way:
to handle each object in collection
to handle each ref in refs array for each object
The result is a single observable that emits the full collection of objects, each with their full collection of ref result data. You then only need a single
switchMap to handle subscribing to this single observable:
return this.afs.collection('collection1').valueChanges().pipe(
switchMap(objects => combineLatest(
objects.map(obj => combineLatest(
obj.refs.map(ref => from(ref.get()).pipe(map(r => r.data())))
).pipe(
map(refsResult => ({ ...obj, refsResult }))
)
))
))
);
For readability, I'd probably create a separate function:
function appendRefData(obj) {
return combineLatest(
obj.refs.map(ref => ref.get().pipe(map(r => r.data())))
).pipe(
map(refsResult => ({ ...obj, refsResult }))
);
}
return this.afs.collection('collection1').valueChanges().pipe(
switchMap(objects => combineLatest(objects.map(appendRefData)))
);

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 merge and observe two collections in Firestore based on reference ID in documents?

I'm creating a StencilJS app (no framework) with a Google Firestore backend, and I want to use the RxFire and RxJS libraries as much as possible to simplify data access code. How can I combine into a single observable stream data coming from two different collections that use a reference ID?
There are several examples online that I've read through and tried, each one using a different combination of operators with a different level of nested complexity. https://www.learnrxjs.io/ seems like a good resource, but it does not provide line-of-business examples that make sense to me. This question is very similar, and maybe the only difference is some translation into using RxFire? Still looking at that. Just for comparison, in SQL this would be a SELECT statement with an INNER JOIN on the reference ID.
Specifically, I have a collection for Games:
{ id: "abc000001", name: "Billiards" },
{ id: "abc000002", name: "Croquet" },
...
and a collection for Game Sessions:
{ id: "xyz000001", userId: "usr000001", gameId: "abc000001", duration: 30 },
{ id: "xyz000002", userId: "usr000001", gameId: "abc000001", duration: 45 },
{ id: "xyz000003", userId: "usr000001", gameId: "abc000002", duration: 55 },
...
And I want to observe a merged collection of Game Sessions where gameId is essentially replace with Game.name.
I current have a game-sessions-service.ts with a function to get sessions for a particular user:
import { collectionData } from 'rxfire/firestore';
import { Observable } from 'rxjs';
import { GameSession } from '../interfaces';
observeUserGameSesssions(userId: string): Observable<GameSession[]> {
let collectionRef = this.db.collection('game-sessions');
let query = collectionRef.where('userId', '==', userId);
return collectionData(query, 'id);
}
And I've tried variations of things with pipe and mergeMap, but I don't understand how to make them all fit together properly. I would like to establish an interface GameSessionView to represent the merged data:
export interface GameSessionView {
id: string,
userId: string,
gameName: string,
duration: number
}
observeUserGameSessionViews(userId: string): Observable<GameSessionView> {
this.observeUserGameSessions(userId)
.pipe(
mergeMap(sessions => {
// What do I do here? Iterate over sessions
// and embed other observables for each document?
}
)
}
Possibly, I'm just stuck in a normalized way of thinking, so I'm open to suggestions on better ways to manage the data. I just don't want too much duplication to keep synchronized.
You can use the following code (also available as Stackblitz):
const games: Game[] = [...];
const gameSessions: GameSession[] = [...];
combineLatest(
of(games),
of(gameSessions)
).pipe(
switchMap(results => {
const [gamesRes, gameSessionsRes] = results;
const gameSessionViews: GameSessionView[] = gameSessionsRes.map(gameSession => ({
id: gameSession.id,
userId: gameSession.userId,
gameName: gamesRes.find(game => game.id === gameSession.gameId).name,
duration: gameSession.duration
}));
return of(gameSessionViews);
})
).subscribe(mergedData => console.log(mergedData));
Explanation:
With combineLatest you can combine the latest values from a number of Obervables. It can be used if you have "multiple (..) observables that rely on eachother for some calculation or determination".
So assuming you lists of Games and GameSessions are Observables, you can combine the values of each list.
Within the switchMap you create new objects of type GameSessionView by iterating over your GameSessions, use the attributes id, userId and duration and find the value for gameName within the second list of Games by gameId. Mind that there is no error handling in this example.
As switchMap expects that you return another Observable, the merged list will be returned with of(gameSessionViews).
Finally, you can subscribe to this process and see the expected result.
For sure this is not the only way you can do it, but I find it the simplest one.

How to combine mergeMap observables calls and return just one value for the whole observable

I have this scenario in typescript/angular with rxjs 6.5:
main(){
const properties = ['session', 'user'];
const source: Observable<any> = from(properties);
source
.pipe(
mergeMap(key => this.getKey().map((value) => ({key: key, value: value}))),
tap((result) => {
// sending the result elsewhere;
}),
).subscribe(
(result) => {
console.log('Final result ->', result);
}
);
console.log('\n');
}
getKey(): Observable<any> {
// Mock function that returns an observable that emits 1 value
const observable = from(['test']);
return observable;
}
The output is:
Final result -> {key: "session", value: "test"}
Final result -> {key: "user", value: "test"}
1st question:
How do I return, in the most elegant way, upon subscription on source, just 1 value, with the combined results of the inner observables?
My wanted output, with the subscription in this way (because I want this combined operation to be in the pipe), would be:
(...).subscribe(
(result) => {console.log('Final Result:', result}
)
OUTPUT:
Final result -> [{key: "session", value: "test"}, {key: "user", value: "test"}]
2nd question
If I don't care about the result of the inner observables, how do I return just 1 value or how do I know when all the inner observables have been completed?
Thanks in advance.
To get combined result of all response of mergeMap, you can also try like this:
return this.request1().pipe(
mergeMap(res1=> this.request2(res1.id).pipe(
map(res2=> {
return {
res1: res1,
res2: res2
}
})
))
)
Q1: You need a toArray — it will combine all your stream values into one array:
Q2: To omit all values on the stream and emit a value upon completion
concat(
source$.pipe(ignoreElements()),
of(true)
)
See "Emit a value upon source completion" example in a playground
Here's an annotated example to help clarify your questions on the subscription process you ask about.
Q1:
As pointed out in another answer, the reduce operator is what you'll want to include in your source pipeline. A key detail with reduce is that it only emits upon completion of the corresponding source observable. If instead you want emission as those inner observables complete, then scan is appropriate. Another difference with the latter is that it doesn't require source completion.
Q2:
With this question, refer to my example below and think of each argument to the processing pipeline as the lifetime of a single request. Here, completion is implicit. It occurs after the last value of the inner observables is processed.
However, if there's no bound to inner observables, then knowing when all the inner observables are complete isn't possible. In such a case, you'll find that reduce() won't work.
const { from, of, Subject } = rxjs;
const { mergeMap, map, tap, reduce, scan } = rxjs.operators;
// Use a subject to simulate processing.
// Think of each argument as a request to the processing pipeline below.
const properties = new Subject();
// Establish processing pipeline
const source = properties.pipe(
// `mergeMap` here flattens the output value to the combined inner output values
mergeMap(props =>
// Each item inside the argument should be piped as separate values
from(props).pipe(
// `mergeMap` here flattens the output value to `{ key, value }`
mergeMap(key =>
of('test').pipe(
map(value => ({ key, value })),
),
),
// Unlike `scan`, `reduce` only emits upon "completion".
// Here, "completion" is implicit - it is after the last
// element of `from(props)` has been processed.
reduce((a, i) => [...a, i], []),
)
),
);
// Subscribe to the pipeline to observe processing.
source.subscribe(console.log);
// Trigger a processing request with an argument
properties.next(['session', 'user']);
// Trigger another processing request
properties.next(['session', 'user']);
<script src="https://unpkg.com/rxjs#6.5.1/bundles/rxjs.umd.min.js"></script>
Use reduce
.pipe(
reduce((results, result) => {
results.push(result);
return results;
}, [])
)
The resulting observable will only emit once all the others have emitted and the emitted value will be an array of all the results.
1st Question, you can use scan to handle and accumulate output
mergeMap(key => from(this.getKey())),
scan((acc,curr) =>acc.concat([{key: curr.key, value: curr.value}]),[])),
2n Question
use first() operator to take just one output from inner observable
attach finalize() to the inner observable which will be trigger when inner observable complete. or use last() to get the last accumulated result
mergeMap(key => from(this.getKey())),
scan((acc,curr) =>acc.concat([{key: curr.key, value: curr.value}]),[])),
first()

Both find() and findById() in one MongoDB call

I want to select data from 1 room, but I also want the ids of all other rooms.
I do it with
const roomId = req.params.roomId;
Room.findById(roomId).then(room => {
if (room) {
Room.find({}).sort({ createdAt: 1 }).then(rooms => {
if (rooms) {
res.render()
}
}).catch(next);
}
}).catch(next);
but this results in 2 database calls.
Is it possible to limit it to only 1 call?
The room I want has a lot of data which I don't need to extract for the other rooms since I only need their IDs.
Get all the rooms by .find() and then use underscore library's findWhere function to filter what you want out of complete dataset. The underscore library works very well for large datasets also.
Ideally the code should look like below:
Room.find({}).sort({ createdAt: 1 }).then(rooms => {
if (rooms) {
var filteredRoom = _.findWhere(rooms, {_id: roomId})
filteredRoom = filteredRoom.pop()
res.render()
}
}).catch(next);

Categories

Resources