Chaining rxjs 6 observables with nested array - rxjs

I have to do 3 dependent request to an API.
The first retreive an array of user id's.
The second have to iterate over the user id's array and for each retreive an array of project id's related to the user.
The third have to iterate over the project id's array and retreive data related to projects.
I want this kind of result:
[{'username': 'richard', projects: [{"id": 1, "name": "test"}]}, ...]
But i'm totally stuck with mergeMap, forkJoin etc..
The thing i have tried:
getUserTeamMembers(): Observable<any> {
return this.http.get(`${environment.serverUrl}/user/profil`).pipe(
mergeMap((teamUsers: any) =>
this.http.get(
`api/user/${teamUsers.data._id}/team`
)
),
mergeMap((teamUsers: any) =>
forkJoin(
...teamUsers.data.map((user: any) =>
this.http.get(`api/user/${user.id}`)
)
)
),
map((res: any) =>
res
.map((user: any) => {
if (user.data) {
return {
firstname: user.data.local.firstname,
lastname: user.data.local.lastname,
email: user.data.local.email,
projects: user.data.local.projects,
language: user.data.local.lang
};
}
})
.filter((user: any) => user !== undefined)
),
tap(t => console.log(t)),
catchError(err => throwError(err))
);}
I try so many things, but i have no clue where to populate my array of projects which is currently only contains id's, not data related:
[{'username': 'richard', projects: ['id1', 'id2']}, ...]

have made a similar example hope it clear the idea.
of course did't use http instead made it local using of
and stored the final result in an array called results
we have a source to get the list of all users, another source that given the user id it will return the list of this user's projects' ids projectList, and finally a source that give one project id will returns it's details(projcectsDetails)
let users = of([ 1, 2, 3 ])
let projectsList = (userId) => of([ userId, userId*2 ])
let projectsDetails = (projectId) => of({ id: projectId, details: `details about ${projectId}` })
let results = [];
users
.pipe(
tap(users => {
users.forEach(userId => results.push({ userId, projects: [] }));
return users
}),
switchMap(users => forkJoin(users.map(userId => projectsList(userId)))),
switchMap(projectsArray => forkJoin(
projectsArray.map(
oneProjectArray =>
forkJoin(oneProjectArray.map(projectId => projectsDetails(projectId)))
)
)
),
tap(projectDetailsArray => {
projectDetailsArray.forEach((projectsArray, index) =>{
results[index]['projects'] = [ ...projectsArray ]
})
})
)
.subscribe(() => console.warn('final',results))
explanation
1- initalize the result array from the given users
[
{ userId: X, projcts: [] },
{ userId: Y, projcts: [] },
....
]
2- for each give users we want the projects ids he/she has
so the first switchMap will return (in order)
[
[ 1, 2, 3, ...] project ids for the first user,
[ 8, 12, 63, ...] project ids for the second user,
...
]
3- in the second switchMap we iterate over the previous array
(which is an array that each item in it is an array of projects ids)
so we needed two forkJoin the outer will iterate over each array of projects ids(the big outer
array previously mentioned)
the inner one will iterate over each single project id and resolve it's observable value(which is the
project details)
4- finally in the tap process we have an array of array like the 2nd step but this time each
array has the projects details and not the projects ids
[
{ userId:1, projects: [ { id:1, details: 'details about 1' }, { id:2, details: 'details about 2' }]},
{ userId:2, projects: [ { id:2, details: 'details about 2' }, { id:4, details: 'details about 4' }]},
{ userId:3, projects: [ { id:3, details: 'details about 3' }, { id:6, details: 'details about 6' }]}
]

Related

Modify Observable array before subscribing by rxjs operators

Having two models look like this:
export class Game {
id: number;
name: string;
platform: number;
}
export class Platform {
id: number;
name: string;
}
having an observable array of the Game object that has a property of platformId which is related to the Platform object. for better understand, I created two separate methods for getting a list of my games and another method for getting a platform based on id.
getGames(): Observable<Game[]> {
return of([
{
id: 1,
name: 'god of war',
platform: 1,
},
{
id: 2,
name: 'halo',
platform: 2,
},
]);
}
getPlatform(id): Observable<Platform> {
if (id === 1)
return of({
id: 1,
name: 'ps4',
});
if (id === 2)
return of({
id: 2,
name: 'xbox',
});
}
now I'm with help of two operators of rxjs (switchMap,forkJoin) reach to this point:
this.getGames()
.pipe(
switchMap((games: Game[]) =>
forkJoin(
games.map((game) =>
forkJoin([this.getPlatform(game.platform)]).pipe(
map(([platform]) => ({
game: game.name,
platform: platform.name,
}))
)
)
)
)
)
.subscribe((v) => {
console.log(v);
this.gamesV2 = v;
});
and my final result:
[
{
game: "god of war"
platform: "ps4"
},
{
game: "halo"
platform: "xbox"
}
]
is it possible to achieve this in a simpler way?
StackBlitz
By flattening an inner observable array of getPlatformV2:
this.getGames()
.pipe(
switchMap(games =>
combineLatest(
games.map(game =>
this.getPlatformV2(game.id).pipe(
map(platform => ({
game: game.name,
platform
}))
)
)
)
)
)
Extra: Regarding Game and Platform, you should use TS types or interfaces instead of classes if you won't implement a constructor on those.
I find another way in StackOverflow thanks to Daniel Gimenez and putting it here, if anyone has better and simpler, I really appreciate it to share it with me.
Create another method which returns the name of the platform:
getPlatformV2(id): Observable<string> {
const platforms = [
{
id: 1,
name: 'ps4',
},
{
id: 2,
name: 'xbox',
},
];
return of(platforms.find(x=>x.id===id).name);
}
}
and instead of using two forkjoin, I used concatMap
this.getGames()
.pipe(
switchMap((games) => games),
concatMap((game) =>
forkJoin({
game: of(game.name),
platform: this.getPlatformV2(game.platform),
})
),
toArray()
)
.subscribe(console.log);

RxJS group by field and return new observable

I've following interfaces and Observable<Machine[]>, what I want to achive is group by Machine symbol property in Observable<Machine[]> and return mapped observable Observable<Order[]>.
export interface Machine {
symbol: string;
price: number;
amount: number;
id: number;
}
export interface Order {
symbol: string;
machines: OrderMachine[];
}
export interface OrderMachine {
price: number;
amount: number;
id: number;
}
I've tried to use RxJS gropBy operator but it seems it return grouped array one by one.
machines: Machine[] = [
{ amount: 1, id: 1, symbol: "A", price: 1 },
{ amount: 1, id: 2, symbol: "A", price: 2 }
];
of(machines).pipe(
takeUntil(this.unsubscribe),
mergeMap(res => res),
groupBy(m => m.symbol),
mergeMap(group => zip(of(group.key), group.pipe(toArray()))),
map(x => { // here I have probably wrong model [string, Machine[]]
const orderMachines = x[1].map(y => { return <OrderMachine>{price: y.price, amount: y.amount, id: y.id }})
return <Order>{ symbol: x[0], machines: orderMachines } })
);
as in result I have Observable<Order> istead ofObservable<Order[]>.
expected result model:
orders: Order[] = [
{
symbol: "A",
machines: [
{ amount: 1, price: 1, id: 1 },
{ amount: 1, price: 2, id: 2 }
]
}
];
Here a possible solution based on your approach but with a few changes:
const machines = [
{ amount: 1, id: 1, symbol: "A", price: 1 },
{ amount: 1, id: 2, symbol: "A", price: 2 },
{ amount: 1, id: 3, symbol: "B", price: 3 }
];
from(machines) // (1)
.pipe(
// (2)
groupBy((m) => m.symbol),
mergeMap((group) => group.pipe(toArray())),
map((arr) => ({
symbol: arr[0].symbol, // every group has at least one element
machines: arr.map(({ price, amount, id }) => ({
price,
amount,
id
}))
})),
toArray(), // (3)
)
.subscribe(console.log);
(1) I changed of(machines) to from(machines) in order to emit the objects from machines one by one into the stream. Before that change the whole array was emitted at once and thus the stream was broken.
(2) I removed takeUntil(this.unsubscribe) and mergeMap(res => res) from the pipe since there is no reason to have them in your example. takeUntil wouldn't have any effect since the stream is finite and synchronous. An identity function (res => res) applied with mergeMap would make sense in a stream of streams which is not the case in your example. Or do you actually need these operators for your project because you have an infinite stream of observables?
(3) toArray() is what transforms Observable<Order> to Observable<Order[]>. It waits until the stream ends and emits all streamed values at once as an array.
edit:
The op has mentioned that he rather needs a solution that is compatible with an infinite stream but because toArray only works with finite streams the provided answer above would never emit anything in such scenario.
To solve this I would avoid using groupBy from rxjs. It cvan be a very powerful tool in other cases where you need to split one stream into several groups of streams but in your case you simply want to group an array and there are easier methods for that that.
this.store.pipe(
select(fromOrder.getMachines)
map((arr) =>
// (*) group by symbol
arr.reduce((acc, { symbol, price, amount, id }) => {
acc[symbol] = {
symbol,
machines: (acc[symbol] ? acc[symbol].machines : [])
.concat({ price, amount, id })
};
return acc;
}, {})
),
)
.subscribe((result) =>
// (**)
console.log(Object.values(result))
);
(*) you could use a vanilla groupBy implementation that returns an object of the shape {[symbol: string]: Order}.
(**) result is an object here but you can convert it to an array easily but applying Object.values(result)
#kruschid Thank you very much for your reply, it works properly but unfortynetelly, it doesn't work when I want to use it with my store (ngrx), type is ok but it stops to show log after mergeMap method:
this.store.pipe(select(fromOrder.getMachines),
mergeMap(res => res), // Machine[]
groupBy((m) => m.symbol),
tap(x => console.log(x)), //this shows object GroupedObservable {_isScalar: false, key: "A", groupSubject: Subject, refCountSubscription: GroupBySubscriber}
mergeMap((group) => group.pipe(toArray())),
tap(x => console.log(x)), // this is not printed in console
map((arr) => <Order>({
symbol: arr[0].symbol,
machines: arr.map(({ price, amount, id }) => ({
price,
amount,
id
}))
})),
toArray())) // (3)

RxJS logic which solves a filter/merge issue

This is more a logical problem then a RxJS problem, I guess, but I do not get it how to solve it.
[input 1]
From a cities stream, I will receive 1 or 2 objects (cities1 or cities2 are test fixtures).
1 object if their is only one language available, 2 objects for a city with both languages.
[input 2]
I do also have a selectedLanguage ("fr" or "nl")
[algo]
If the language of the object corresponds the selectedLanguage, I will pluck the city. This works for my RxJS when I receive 2 objects (cities2)
But since I also can receive 1 object, the filter is not the right thing to do
[question]
Should I check the cities stream FIRST if only one object exists and add another object. Or what are better RxJS/logical options?
const cities1 = [
{city: "LEUVEN", language: "nl"}
];
const cities2 = [
{city: "BRUSSEL", language: "nl"},
{city: "BRUXELLES", language: "fr"}
];
const selectedLang = "fr"
const source$ = from(cities1);
const result = source$.pipe(
mergeMap((city) => {
return of(selectedLang).pipe(
map(lang => {
return {
lang: city.language,
city: city.city,
selectedLang: lang
}
}),
filter(a => a.lang === selectedLang),
pluck('city')
)
}
)
);
result.subscribe(console.log)
If selectedLang is not an observable (i.e. you don't want this to change) then I think it would make it way easier if you keep it as a value:
const result = source$.pipe(
filter(city => city.language === selectedLang)
map(city => city.city)
);
There's nothing wrong from using external parameters, and it makes the stream easier to read.
Now, if selectedLang is an observable, and you want result to always give the city with that selectedLang, then you probably need to combine both streams, while keeping all the cities received so far:
const selectedLang$ = of(selectedLang); // This is actually a stream that can change value
const cities$ = source$.pipe(
scan((acc, city) => [...acc, city], [])
);
const result = combineLatest([selectedLang$, cities$]).pipe(
map(([selectedLang, cities]) => cities.find(city => city.language == selectedLang)),
filter(found => Boolean(found))
map(city => city.city)
)
Edit: note that this result will emit every time cities$ or selectedLang$ changes and one of the cities matches. If you don't want repeats, you can use the distinctUntilChanged() operator - Probably this could be optimised using an exhaustMap or something, but it makes it harder to read IMO.
Thanks for your repsonse. It's great value for me. Indeed I will forget about the selectedLang$ and pass it like a regular string. Problem 1 solved
I'll explain a bit more in detail my question. My observable$ cities$ in fact is a GET and will always return 1 or 2 two rows.
leuven:
[ { city: 'LEUVEN', language: 'nl', selectedLanguage: 'fr' } ]
brussel:
[
{ city: 'BRUSSEL', language: 'nl', selectedLanguage: 'fr' },
{ city: 'BRUXELLES', language: 'fr', selectedLanguage: 'fr' }
]
In case it returns two rows I will be able to filter out the right value
filter(city => city.language === selectedLang) => BRUXELLES when selectedLangue is "fr"
But in case I only receive one row, I should always return this city.
What is the best solution to this without using if statements? I've been trying to work with object destruct and scaning the array but the result is always one record.
// HTTP get
const leuven: City[] = [ {city: "LEUVEN", language: "nl"} ];
// same HTTP get
const brussel: City[] = [ {city: "BRUSSEL", language: "nl"},
{city: "BRUXELLES", language: "fr"}
];
mapp(of(brussel), "fr").subscribe(console.log);
function mapp(cities$: Observable<City[]>, selectedLanguage: string): Observable<any> {
return cities$.pipe(
map(cities => {
return cities.map(city => { return {...city, "selectedLanguage": selectedLanguage }}
)
}),
// scan((acc, value) => [...acc, { ...value, selectedLanguage} ])
)
}

NestJS | GraphQL - How to resolve a list of nested objects from multiple datasets

I'm setting up a NestJS service using GraphQL to be the middleman between the UI and multiple other services. For the sake of this example, I need to display a list of books with their relevant information from a single publisher.
The UI will hit the nest service with an id of a publisher. I need to then call a service and get a list of book Ids. When I have the list of Ids I need to hit two separate services, each return a list of objects for each book id. I then need to build a list of book objects from both datasets.
An example of the book models is:
export class BookModel {
#Field(type => BookInformationModel)
bookInfo: BookInformationModel;
#Field(type => BookSalesModel)
bookSales: BookSalesModel;
}
An example of the flow is:
UI hits Nest service with a publisher Id "pub1"
Nest service goes to the publisher-service which returns a list of books linked to the publisher ['book1', 'book2']
Nest service then hits the book-info-service that returns [{id: 'book1' title: ...}, {id: 'book2' title: ...}]
Nest service then hits the book-sales-service that returns [{price: 123 currency: ...}, {price: 456 currency: ...}]
Map both data sets to a list of BookModel
[{
bookInfo: {id: 'book1' title: ...}
bookSales: {price: 123 currency: ...}
}, {
bookInfo: {id: 'book2' title: ...}
bookSales: {price: 456 currency: ...}
}]
This is a simplified version of the resolver where I'm having trouble:
#Query(returns => [BookModel])
async getBooksByPublisherId(
#Args('id', { type: () => String }) id: string
) {
this.publisherService.getBookIds(id)
.then(response => {
return response.data;
})
}
#ResolveField('bookInfo', returns => BookInformationModel)
async getBookInfo() {
return this.bookInfoService.getBookInfo(bookIds)
}
#ResolveField('bookSales', returns => BookSalesModel)
async getBookSalesInfo() {
return this.bookSalesService.getSalesInfo(bookIds)
}
There are two issues I'm having:
How do I share the list of book Ids with the field resolvers?
I'm not sure how I write the field resolvers for bookInfo, bookSales as the services return a list of objects.
Figured it out. I needed to have the bookId on the BookModel. That way the field resolvers can access it via #Parent.
export class BookModel {
#Field()
bookId: string;
#Field(type => BookInformationModel)
bookInfo: BookInformationModel;
}
#Query(returns => [BookModel])
async getBooksByPublisherId(
#Args('id', { type: () => String }) id: string
) {
this.publisherService.getBookIds(id)
.then(response => {
return response.data.map(bookID => ({ bookId }));
})
}
#ResolveField('bookInfo', returns => BookInformationModel)
async getBookInfo(
#Parent() bookModel: BookModel
) {
return this.bookInfoService.getBookInfo(bookModel.bookId)
}

Using subscribe on a GroupedObservable in map

I have an array of objects like the follwing:
private questions: Question[] = [
{
title: "...",
category: "Technologie",
answer: `...`
},
{
title: "...",
category: "Technologie",
answer: `...`
},
{
title: "...",
category: "eID",
answer: `...`
}
];
And I would like to group them by categories, filter them based on a value and return the result as an array. Currently, I'm using this:
Observable
.from(this.questions)
.groupBy(q => q.category)
.map(go =>
{
let category: Category = { title: go.key, questions: [] };
go.subscribe(d => category.questions.push(d));
return category;
})
.filter(c => c.title.toLowerCase().indexOf(value.toLowerCase()) >= 0 || c.questions.filter(q => q.title.toLowerCase().indexOf(value.toLowerCase()) >= 0).length > 0)
.toArray()
This finds the question with the value in the category title but not the one with the value in the question title. I think that's because I'm using a subscribe in map, therefore, the questions are not yet available in the filter method, so I was wondering if there's a possibility to wait for the subscribe to end before going into filter. My research pointed me to flatMap but I can't get it to do what I want.
EDIT
I figured out that I can fix the issue like this:
Observable
.from(this.questions)
.filter(q => q.category.toLowerCase().indexOf(value.toLowerCase()) >= 0 || q.title.toLowerCase().indexOf(value.toLowerCase()) >= 0)
.groupBy(q => q.category)
.map(go =>
{
let category: Category = { title: go.key, questions: [] };
go.subscribe(d => category.questions.push(d));
return category;
})
.toArray()
But I'm still interested in the answer.
When you use groupBy, you get a grouped observable that can be flattened with operators like concatMap, mergeMap, switchMap etc. Within those operators, grouped observables can be transformed separately for each category, i.e. collect the questions together into an array with reduce, and then create the desired object with map.
Observable
.from(questions)
.groupBy(q => q.category)
.mergeMap(go => {
return go.reduce((acc, question) => { acc.push(question); return acc; }, [])
.map(questions => ({ title: go.key, questions }));
})
.filter(c => "...")
.toArray()

Resources