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);
Related
allEmployees$ = this.http.get<IEmployees[]>('../../assets/employees').pipe(
map(allEmployees =>
allEmployees.map(Employee =>
<IEmployees>({
id: Employee.id,
name: Employee.name,
email: Employee.email,
gender: Employee.gender,
productid: Employee.productid,
productName: 'N/A',
})
)),
switchMap(data => data.reduce((acc, curr) => {
const exists = acc.find(v => v['name'] === curr['name']);
return exists ? acc : acc.concat(curr);
}, []))
);
Please see pic for more info:
It looks like the Typescript compiler is having trouble inferring the type of the array in the seed parameter of reduce. Try casting it to IEmployees[]
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)
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)
}
I started to learn GraphQL and I'm trying to create the following relationship:
type User {
id: ID!,
name: String!,
favoriteFoods: [Food]
}
type Food {
id: ID!
name: String!
recipe: String
}
So basically, a user can have many favorite foods, and a food can be the favorite of many users. I'm using graphql.js, here's my code:
const Person = new GraphQLObjectType({
name: 'Person',
description: 'Represents a Person type',
fields: () => ({
id: {type: GraphQLNonNull(GraphQLID)},
name: {type: GraphQLNonNull(GraphQLString)},
favoriteFoods: {type: GraphQLList(Food)},
})
})
const Food = new GraphQLObjectType({
name: 'Food',
description: 'Favorite food(s) of a person',
fields: () => ({
id: {type: GraphQLNonNull(GraphQLID)},
name: {type: GraphQLNonNull(GraphQLString)},
recipe: {type: GraphQLString}
})
})
And here's the food data:
let foodData = [
{id: 1, name: 'Lasagna', recipe: 'Do this then that then put it in the oven'},
{id: 2, name: 'Pancakes', recipe: 'If you stop to think about, it\'s just a thin, tasteless cake.'},
{id: 3, name: 'Cereal', recipe: 'The universal "I\'m not in the mood to cook." recipe.'},
{id: 4, name: 'Hashbrowns', recipe: 'Just a potato and an oil and you\'re all set.'}
]
Since I'm just trying things out yet, my resolver basically just returns a user that is created inside the resolver itself. My thought process was: put the food IDs in a GraphQLList, then get the data from foodData usind lodash function find(), and replace the values in person.favoriteFoods with the data found.
const RootQuery = new GraphQLObjectType({
name: 'RootQueryType',
description: 'Root Query',
fields: {
person: {
type: Person,
resolve(parent) {
let person = {
name: 'Daniel',
favoriteFoods: [1, 2, 3]
}
foodIds = person.favoriteFoods
for (var i = 0; i < foodIds.length; i++) {
person.favoriteFoods.push(_.find(foodData, {id: foodIds[i]}))
person.favoriteFoods.shift()
}
return person
}
}
}
})
But the last food is returning null. Here's the result of a query:
query {
person {
name
favoriteFoods {
name
recipe
}
}
}
# Returns
{
"data": {
"person": {
"name": "Daniel",
"favoriteFoods": [
{
"name": "Lasagna",
"recipe": "Do this then that then put it in the oven"
},
{
"name": "Pancakes",
"recipe": "If you stop to think about, it's just a thin, tasteless cake."
},
null
]
}
}
}
Is it even possible to return the data from the Food type by using only its ID? Or should I make another query just for that? In my head the relationship makes sense, I don't think I need to store the IDs of all the users that like a certain food in the foodData since it has an ID that I can use to fetch the data, so I can't see the problem with the code or its structure.
Calling shift and push on an array while iterating through that same array will invariably lead to some unexpected results. You could make a copy of the array, but it'd be much easier to just use map:
const person = {
name: 'Daniel',
favoriteFoods: [1, 2, 3],
}
person.favoriteFoods = person.favoriteFoods.map(id => {
return foodData.find(food => food.id === id)
})
return person
The other issue here is that if your schema returns a Person in another resolver, you'll have to duplicate this logic in that resolver too. What you really should do is just return the person with favoriteFoods: [1, 2, 3]. Then write a separate resolver for the favoriteFoods field:
resolve(person) {
return person.favoriteFoods.map(id => {
return foodData.find(food => food.id === id)
})
}
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' }]}
]