Array of inner subscriptions with rxjs - javascript

I'm trying to improve the flattening and reduce the chaining inside my Rxjs code.
A REST call of my service getAllItems() returns the following Items[]:
[{
id: 101,
name: 'Car',
owner: 1
},
{
id: 102,
name: 'Table',
owner: 2
}]
I have another endpoint getOwnerInfo(id:number) which provides information based in the "owner" id, information which I want to combine, so the final answer should look like:
[{
id: 101,
name: 'Car',
owner: {
id: 1,
username: 'Mikelinin',
name: 'Mikel',
},
},
{
id: 102,
name: 'Table',
owner: {
id: 2,
username: 'Thominin',
name: 'Thomas',
},
}]
My current implementation .subscribes once to getAllItems call and then in the subscription body it iterates per element subscribing to getOwnerInfo.
I've been looking through other flattening examples, but basically they "break" the array, and they never put it back together. I need the output to be an array.
I've tried to use from(), concatMap(), and mergeMap() but seems I am unable to combine both requests properly.

Map your array of items into an array of observables that each emit the updated item. Then join the array of observables together.
getAllItems().pipe(
// turn item[] into observable<updatedItem>[]
map(items => items.map(item => getOwnerInfo(item.id).pipe(
map(owner => ({...item, owner}))
),
// join observable<updatedItem>[] into updatedItem[]
switchMap(itemCalls => forkJoin(itemCalls))
).subscribe(console.log);
Or you can do the mapping and joining in one step:
getAllItems().pipe(
// source item[], do stuff, emit updatedItem[]
switchMap(items => forkJoin(
items.map(item => getOwnerInfo(item.id).pipe(
map(owner => ({...item, owner}))
))
))
).subscribe(console.log);

The following will use from to convert a distinct array of owner ids into a stream of owner ids. Then it will use toArray to convert that stream back into an array and map the items array to an array of items with their respective owner object.
this.getAllItems().pipe(
switchMap(items => {
const ownerIds = Array.from(new Set(items.map(x => x.owner)));
return from(ownerIds).pipe(
concatMap(x => this.getOwnerInfo(x))
toArray(),
map(owners => items.map(x => ({ ...x, owner: owners.find(y => y.id === x.owner) })))
)
})
)

Related

Filter array of dictionaries with array of dictionaries

I have two arrays of dictionaries, both contain an ID property, one is 'id' and the other is '_id'.
I am trying to filter an array that contains all books available, and grab data from it based on what books a user has.
const books = allbooks.filter(({ id }) =>
userBooks.findIndex((book) => book._id === id)
)
This is the code I am using right now, but it's not doing what I expect.
What the data looks like
allbooks [
{
id: 1,
name: 'Book 1'
},
{
id: 2,
name: 'Book 2'
}
]
userBooks [
{
_id: 1
}
]
Should I create an array from the 2nd array's _ids and filter with that? Trying to find the best practice way of doing this, I am using arrays of dictionaries a lot and find it tough sometimes.
You are sooo close my friend. The problem is Array.prototype.findIndex is returning the first index in which your lambda function returns true. The index of the matching book is index 0, which is falsy, which makes your filter ignore it because the result of the filter lambda function is false.
Instead, you can check if the result of your findIndex is > -1 since Array.prototype.findIndex returns -1 if the element is not found.
const allbooks = [
{
id: 1,
name: 'Book 1'
},
{
id: 2,
name: 'Book 2'
}
];
const userBooks = [
{
_id: 1
}
];
const books = allbooks.filter(({ id }) =>
userBooks.findIndex((book) => book._id === id) > -1
)
console.log(books);

Comparing two arrays with field in common then pushing to a new array with corresponding grouped fields

general programming problem here.
I have this array called SPACES
[
{
_id: 5e1c4689429a8a0decf16f69,
challengers: [
5dfa24dce9cbc0180fb60226,
5dfa26f46719311869ac1756,
5dfa270c6719311869ac1757
]
},
{
_id: 5e1c4eb9c9461510407d5e81,
challengers: [ 5dfa24dce9cbc0180fb60226, 5dfa26f46719311869ac1756 ],
}
]
And this array called USERS
[
{
_id: 5dfa24dce9cbc0180fb60226,
name: 'Account 1',
email: 'account1#gmail.com',
spaces: [ 5e1c4689429a8a0decf16f69, 5e1c4eb9c9461510407d5e81 ],
},
{
_id: 5dfa26f46719311869ac1756,
name: 'Account 2',
email: 'account2#gmail.com',
spaces: [ 5e1c4689429a8a0decf16f69, 5e1c4eb9c9461510407d5e81 ]
},
{
_id: 5dfa270c6719311869ac1757,
name: 'Account 3',
email: 'account3#gmail.com',
spaces: [ 5e1c4689429a8a0decf16f69 ]
}
]
What I want to do, is go through both, and instead of having the SPACES.challengers array be just IDS, I would like the array to contain each USER object.
So for example, if the USER has an ID that is inside the SPACES.challengers array, then push the user into that array (which will then be the entire object).
SO FAR I have tried this (I am not very good yet):
users.map( ( user ) => {
spaces.map( ( space ) => {
if ( user.spaces.includes( space._id ) ) {
space.challengers.push(user)
}
} );
} );
However, I am not getting inside the IF block. (Even if I did, not sure if it would work OR if this is even how to do it). It feels Odd doing double maps, as I get so many iterations, and it duplicates my push (cause I have no logic to see if it just has been pushed).
Assuming every entry in the Users array has a unique ID, we can build a Hashmap to store (id, index) pairs in order to search efficiently for an ID from Users array while looping through Spaces array.
let spaces = [{_id: '5e1c4689429a8a0decf16f69',challengers: ['5dfa24dce9cbc0180fb60226', '5dfa26f46719311869ac1756', '5dfa270c6719311869ac1757']},{_id: '5e1c4eb9c9461510407d5e81',challengers: [ '5dfa24dce9cbc0180fb60226', '5dfa26f46719311869ac1756' ],}]
let users = [{_id: '5dfa24dce9cbc0180fb60226',name: 'Account 1',email: 'account1#gmail.com',spaces: [ '5e1c4689429a8a0decf16f69', '5e1c4eb9c9461510407d5e81' ],},{_id: '5dfa26f46719311869ac1756',name: 'Account 2',email: 'account2#gmail.com',spaces: [ '5e1c4689429a8a0decf16f69', '5e1c4eb9c9461510407d5e81' ]},{_id: '5dfa270c6719311869ac1757',name: 'Account 3',email: 'account3#gmail.com',spaces: [ '5e1c4689429a8a0decf16f69' ]}]
let IDIndexMapping = {} // To store (_id, index) pairs, in order to improve search efficiency
for(let index in users) // Iterate through Users array using index
IDIndexMapping[users[index]._id] = index; // store (_id, index) pair in IDIndexMapping
// I'm avoiding using `map` and using vanilla `for` loop for space efficiency
// as map returns a new array but with `for` loop, we can perform changes in-place
for(let outerIndex in spaces){ // Iterate through `spaces` array using index
let challengers = spaces[outerIndex].challengers; // Get challengers array
for(let innerIndex in challengers){ // Iterate through challengers array using index
let ID = challengers[innerIndex]; // Get ID
if(ID in IDIndexMapping) // If ID exists in IDIndexMapping
spaces[outerIndex].challengers[innerIndex] = users[IDIndexMapping[ID]]; // Change ID to actual User object
}
}
console.log(spaces)
Output
[ { _id: '5e1c4689429a8a0decf16f69',
challengers: [ [Object], [Object], [Object] ] },
{ _id: '5e1c4eb9c9461510407d5e81',
challengers: [ [Object], [Object] ] } ]
.map and .find should work here. keep it simple.
var spaces = [
{
_id: "5e1c4689429a8a0decf16f69",
challengers: [
"5dfa24dce9cbc0180fb60226",
"5dfa26f46719311869ac1756",
"5dfa270c6719311869ac1757"
]
},
{
_id: "5e1c4eb9c9461510407d5e81",
challengers: ["5dfa24dce9cbc0180fb60226", "5dfa26f46719311869ac1756", "some non existent"]
}
],
users = [
{
_id: "5dfa24dce9cbc0180fb60226",
name: "Account 1",
email: "account1#gmail.com",
spaces: ["5e1c4689429a8a0decf16f69", "5e1c4eb9c9461510407d5e81"]
},
{
_id: "5dfa26f46719311869ac1756",
name: "Account 2",
email: "account2#gmail.com",
spaces: ["5e1c4689429a8a0decf16f69", "5e1c4eb9c9461510407d5e81"]
},
{
_id: "5dfa270c6719311869ac1757",
name: "Account 3",
email: "account3#gmail.com",
spaces: ["5e1c4689429a8a0decf16f69"]
}
],
result = spaces.map(({ _id, challengers }) => ({
_id,
challengers: challengers.map(challenger =>
users.find(user => user._id === challenger)
).filter(row => row)
}));
console.log(JSON.stringify(result, null, 2));
You can create a map of challengers for look-up and then put them in spaces.
//create user map for look-up
userMap = users.reduce((res, val) => ({
...res,
[val._id]: val
}), {});
//change challenger id with user object
inflatedSpaces = spaces.map(s => ({ ...s, challengers: s.challengers.map(c => userMap[c]) }));
You could map the users with a Map.
Beside the destructuring of the object for mapping this answer uses for this part
challengers: challengers.map(
Map.prototype.get, // cb with a prototype and using `this`
new Map(users.map(o => [o._id, o])) // thisArg
)
the above mentioned Map in two parts.
The lower part generates an instance of Map where _id of the users items is used as key and the whole object as value. This instance is uses as thisArg of Array#map, the second parameter.
The upper part is a prototype of Map, used as callback. And while an this object is supplied, a binding (Function#bind) is not necessary.
var spaces = [{ _id: '5e1c4689429a8a0decf16f69', challengers: ['5dfa24dce9cbc0180fb60226', '5dfa26f46719311869ac1756', '5dfa270c6719311869ac1757'] }, { _id: '5e1c4eb9c9461510407d5e81', challengers: ['5dfa24dce9cbc0180fb60226', '5dfa26f46719311869ac1756'] }],
users = [{ _id: '5dfa24dce9cbc0180fb60226', name: 'Account 1', email: 'account1#gmail.com', spaces: ['5e1c4689429a8a0decf16f69', '5e1c4eb9c9461510407d5e81'] }, { _id: '5dfa26f46719311869ac1756', name: 'Account 2', email: 'account2#gmail.com', spaces: ['5e1c4689429a8a0decf16f69', '5e1c4eb9c9461510407d5e81'] },{ _id: '5dfa270c6719311869ac1757', name: 'Account 3', email: 'account3#gmail.com', spaces: ['5e1c4689429a8a0decf16f69'] }],
result = spaces.map(({ _id, challengers }) => ({
_id,
challengers: challengers.map(
Map.prototype.get,
new Map(users.map(o => [o._id, o]))
)
}));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

rxjs: recursive Observable emission inside map

I am pulling my hair out a little with attempting to group data recursively in rxjs. There seems to be alot of good examples around with different use cases but I cant seem to refactor the code around to fit my requirements.
The central problem that I can infer is that in my map operator I have a conditional return which is either a non-observable or an observable. Is there a way I can refactor to account for this discrepency?
Ideally this function would group an original "flat" array by an arbitrary amount of columns that are passed in as the cols argument.
Each time the criteria is satisfied it would
append to an array in the format
{ key: col_name, elements : [...col_elements] } where elements would be another array of elements in the same format, or a straight list of elements.
The below function works when only grouping the columns once ( and thus never requiring the observable within map to emit ).
//group data n times based on passed string[] of column attributes
group_data(elements: Observable<any>, cols: string[], index=0) : Observable<any> {
let col = cols[index]
return elements.pipe(
//groupby column value
RxOp.groupBy((el:any) => this.get_groupingValue(el, col)),
//place key inside array
RxOp.mergeMap((group) => group.pipe(
RxOp.reduce((acc, cur) => [...acc, cur], ["" + group.key]))
),
// map to key:group
RxOp.map((arr:any) =>
cols.length <= index + 1?
// no more grouping req
{ 'key': arr[0], 'elements': arr.slice(1) } :
//group recursively, returns Observable instead of array:(
{ 'key': arr[0], 'elements':
this.group_data(from(arr.slice(1)), cols, index + 1)
.pipe(
RxOp.tap(t=> console.log(t)), // not logged
RxOp.reduce((acc, val) => [...acc, val], [])
)
}),
RxOp.toArray()
)
}
//simplified data example:
data = [{id: 'idA', type: 'project', parents: null },
{id: 'idB', type: 'project', parents: null },
{id: 'idC', type: 'episode', parents: ['idA'] },
{id: 'idD', type: 'episode', parents: ['idB'] },
{id: 'idE', type: 'scene', parents: ['idA', 'idC'] },
{id: 'idF', type: 'scene', parents: ['idB', 'idD'] }]
// 1 column passed works correctly as below
group_data(elements: from(data), ['project'])
/* outputted data:
[{key: 'idA',
elements: [ {id: 'idC', type: 'episode', parents: ['idA'] },
{id: 'idE', type: 'scene', parents: ['idA', 'idC'] }]},
{key: 'idB',
elements: [ {id: 'idD', type: 'episode', parents: ['idA'] },
{id: 'idF', type: 'scene', parents: ['idA', 'idC'] }]},
{key: null,
elements: [ {id: 'idA', type: 'project', parents: [] }
{id: 'idB', type: 'project', parents: [] }]}
]
*/
// 2 columns not working correctly
group_data(elements: from(data), ['project', 'episode'])
/*[{key: 'idA',
elements: Observable},
{key: 'idB',
elements: Observable},
{key: null,
elements: Observable}
]*/
The approach i needed to take was to restructure so that I could use mergeMap instead of map - which meant i needed two observables at the condition instead of one. From there i just needed to refactor so that the mapping to key, elements was done after the mergeMap.
This is still my first foray into rxjs, so my explanation isn't great and my summary of the problem also wasnt great. Importantly, at least its behaving as expected now.
//group data n times based on passed string[] of column attributes
group_data(elements: Observable<any>, cols: string[], index=0) : Observable<any> {
let col = cols[index]
let grouping = elements.pipe(
//groupby column value
RxOp.groupBy((el:any) => this.get_groupingValue(el, col)),
//place key inside array
RxOp.mergeMap((group) => group.pipe(
RxOp.reduce((acc, cur) => [...acc, cur], ["" + group.key]))
)
)
return grouping.pipe(
RxOp.mergeMap((arr) =>
(
cols.length <= (index +1) ?
//no more grouping required
of(arr.slice(1)) :
//group again
this.group_data(from(arr.slice(1)), cols, index + 1))
// reduce result and put the key back in
.pipe(
RxOp.reduce((acc, cur) => [...acc, cur], ["" + arr[0]])
)
),
// map to key:group
RxOp.map(arr => ({
key: arr[0],
elements: arr.slice(1)
})
),
RxOp.toArray()
)

Applying a property to each element in several arrays, then returning a flat map with one array

I have a collection that looks like this
[
{
count: 123,
description: 'some description',
articles: [
{...}
]
},
{
count: 234,
description: 'some description',
articles: [
{...}
]
}
]
Each object in the collection has a collection of articles. What I need is to apply the description to each article object in the respective collection in each element of the primary collection. I also want to end up with a flat array containing only articles. Clearly I'm using mergeMap incorrectly, but I'm not sure how to do it.
I have tried this
json$.pipe(
// Filter out empty arrays
filter(section => section.count > 0),
// Apply the descriptions
map(section => section.articles.map(a => (Object.assign(a, section.sectionName)))),
mergeMap(x => x.articles)
).subscribe(x => console.log(x));
But the articles do not have the description property in them, and it's not a flat array of articles. I've tried a few things but I'm unsure how to proceed
You only need to concatMap the outer observable, after adjusting each article.
const { Observable } = Rx;
const { map, concatMap, filter } = Rx.operators;
const json$ = Observable.from([
{
count: 123,
description: 'some description 123',
articles: [
{id: 1},
{id: 2},
]
},
{
count: 234,
description: 'some description 234',
articles: [
{id: 3},
{id: 4},
]
}
]);
const withDescription$ = json$.pipe(
filter(({count}) => count > 0),
concatMap(
({articles, description}) => Observable.from(articles).map(a => ({...a, description}))
)
);
withDescription$.subscribe(console.log);
<script src="https://unpkg.com/#reactivex/rxjs#^5/dist/global/Rx.min.js"></script>
If you don't need any special behavior on the inner observable, you could simplify to:
const withDescription$ = json$.pipe(
filter(({count}) => count > 0),
concatMap(
({articles, description}) => articles.map(a => ({...a, description}))
),
);

Cascading ajax calls with RxJs

How can I cascade RxJs calls to retrieve data from this contrived service.
First request goes to /customer/1
/customer/:id
Response:
{
Name: "Tom",
Invoices: [1,3],
Orders: [5,6]
}
In the customer response there are 2 InvoiceIds that we use to access second service:
/invoices/:id
Response
{
Id: 1,
Amount: 10
}
In the customer response there are 2 OrderIds that we use to access third service:
/orders/:id
Response
{
Id:2,
Date: '2016-11-12'
}
At the end I would like to kome up with an object looking like this:
{
Name: "Tom",
Invoices: [
{
Id: 1,
Amount: 10
},
{
Id: 3,
Amount: 5
}],
Orders: [
{
Id:5,
Date: '2016-11-12'
},
{
Id:6,
Date: '2016-11-12'
}]
}
How can I pass the ids through the pipeline so that the dependant objects are retrieved.
My gut feeling tells me I should probably use the flatMap operator, but I am totally uncertain how this could all work together.
var ajax = Rx.DOM.getJSON('/api/customers/1')
.flatMap(p => p.Invoices.map(x =>
Rx.DOM.getJSON('/api/invoices/' + x)
));
This is a typical use-case where you need to construct a response out of several HTTP calls:
const Observable = Rx.Observable;
var customerObs = Observable.create(observer => {
Observable.of({Name: "Tom", Invoices: [1,3], Orders: [5,6]})
.subscribe(response => {
var result = {
Name: response.Name,
Invoices: [],
Orders: [],
};
let invoicesObs = Observable.from(response.Invoices)
.flatMap(id => Observable.of({ Id: Math.round(Math.random() * 50), Amount: Math.round(Math.random() * 500) }).delay(500))
.toArray();
let ordersObs = Observable.from(response.Orders)
.flatMap(id => Observable.of({ Id: Math.round(Math.random() * 50), Amount: Math.round(Math.random() * 500) }).delay(400))
.toArray();
Observable.forkJoin(invoicesObs, ordersObs).subscribe(responses => {
result.Invoices = responses[0];
result.Orders = responses[1];
observer.next(result);
});
});
});
customerObs.subscribe(customer => console.log(customer));
See live demo: https://jsfiddle.net/martinsikora/uc1246cx/
I'm using Observable.of() to simulate HTTP requests, flatMap() to turn each invoice/order id into an Observable (another HTTP request) that are reemitted and then toArray() to collect all values emitted from an operator chain and reemit them in a single array (just because it's convenient).
Operator forkJoin() waits until all source Observables complete and then emits their last value as an array (so we haw array of arrays in responses).
See a similar question: Performing advanced http requests in rxjs

Categories

Resources