Iterating over Promises recursively - javascript

I have an REST API interface which only gets me the first level of some information.
So for example I want to collect groups. Every Group can have subgroups. So for example "Group 1" has the Subgroups "Group A" and "Group B". "Group A" has the Subgroup "GroupX". And so on.
But the API only gives me the first level of Groups for a group name. So I pass "Group 1" to the API and it returns "Group A" and "Group B". To get the supgroups of Group A, I need to call the API again. But i don't know how many iterations of this it will have.
So I thought about using recursion but I haven't come far.
So far my Code:
getGroupChildren(group:string){ return this restService.getGroupChildren(group)}
getGroups():Promise<any>{
let collection:string[] = [];
return this.getGroupChildren("Group A").then((result)=> {
if(result.data.length !==0){
return this.getGroupChildren(data[0].groupName);
}
});
}
Now this will only return me the first Supgroups of the first element.
How can I accomplish it will always find every Supgroup no matter how many? Maybe is it good to use Observables?
Here an example structure of one API call:
{ "groupName" : "Group_1", "children" : ["Group_A", "Group_B"]}

You can achieve what you want with flatMap operator of Observable
getGroups(group: string) {
return this.http.get(`/group/{group}`).flatMap(response => {
if (response.children.length === 0) { // you hit a leaf, stop recursion here
return Observable.of(response);
} else { // there are more levels to go deeper
return this.getGroups(response.children[0].groupName);
}
});
}
Edit Using Promise
Let's say you use a GroupService which returns the data instead of HttpClient. You can convert a Promise to an Observable with fromPromise operator.
getGroups(group: string) {
return Observable.fromPromise(this.groupService.get(group)).flatMap(response => {
if (response.children.length === 0) { // you hit a leaf, stop recursion here
return Observable.of(response);
} else { // there are more levels to go deeper
return this.getGroups(response.children[0].groupName);
}
});
}
Edit 2 Using this service
Let's take a look at your example. You have following json
{
"groupName": "Group_1",
"children" : ["Group_A", "Group_B"]
}
In your component file, you call the service as follows
...
this.recursiveGroupService.getGroups("Group_1")
.subscribe(response => {
// at this point response will be `Group_A`
})
Edit 3 Getting the whole object
This time we'll use forkJoin and call getGroups for all of the children and collect the results in a children array.
Note: I haven't tested this code myself. It may contains some error. If it has, let me know.
import { forkJoin, of } from 'rxjs';
import { map } from 'rxjs/operators';
getGroups(group: string) {
let retVal;
return Observable.fromPromise(this.groupService.get(group)).flatMap(response => {
retVal = {
groupName: response.groupName
};
if (response.children.length === 0) { // you hit a leaf, stop recursion here
return of(retVal);
} else { // there are more levels to go deeper
// this will create list of observable for each child
const children$ = response.children.map(
child => this.getGroups(child));
// forkJoin will execute these observables in parallel
return forkJoin(children$).pipe(
map(results => {
// results is an array containing children data
retVal.children = results;
return retVal;
})
);
}
});
}

You could use Promise.all to recursively resolve deeper children, and then take the result (an array) to create an object to resolve the promise with:
getGroups(groupName = "Group A") {
return this.getGroupChildren(groupName).then((result) =>
Promise.all(result.data.map( ({groupName}) => this.getGroups(groupName) ))
).then(children => ({ groupName, children }));
}
So the promised value could be something like:
[{
groupName: "Group A",
children: [{
groupName: "Group A1",
children: []
}, {
groupName: "Group A2",
children: []
}]
}]

Related

Trying to write a recursive asynchronous search in JavaScript

I am trying to write some code that searches through a bunch of objects in a MongoDB database. I want to pull the objects from the database by ID, then those objects have ID references. The program should be searching for a specific ID through this process, first getting object from id, then ids from the object.
async function objectFinder(ID1, ID2, depth, previousList = []) {
let route = []
if (ID1 == ID2) {
return [ID2]
} else {
previousList.push(ID1)
let obj1 = await findObjectByID(ID1)
let connectedID = obj1.connections.concat(obj1.inclusions) //creates array of both references to object and references from object
let mapPromises = connectedID.map(async (id) => {
return findID(id) //async function
})
let fulfilled = await Promise.allSettled(mapPromises)
let list = fulfilled.map((object) => {
return object.value.main, object.value.included
})
list = list.filter(id => !previousList.includes(id))
for (id of list) {
await objectFinder(id, ID2, depth - 1, previousList).then(result => {
route = [ID1].concat(result)
if (route[route.length - 1] == ID2) {
return route
}})
}
}
if (route[route.length - 1] == ID2) {
return route
}
}
I am not sure how to make it so that my code works like a tree search, with each object and ID being a node.
I didn't look too much into your code as I strongly believe in letting your database do the work for you if possible.
In this case Mongo has the $graphLookup aggregation stage, which allows recursive lookups. here is a quick example on how to use it:
db.collection.aggregate([
{
$match: {
_id: 1,
}
},
{
"$graphLookup": {
"from": "collection",
"startWith": "$inclusions",
"connectFromField": "inclusions",
"connectToField": "_id",
"as": "matches",
}
},
{
//the rest of the pipeline is just to restore the original structure you don't need this
$addFields: {
matches: {
"$concatArrays": [
[
{
_id: "$_id",
inclusions: "$inclusions"
}
],
"$matches"
]
}
}
},
{
$unwind: "$matches"
},
{
"$replaceRoot": {
"newRoot": "$matches"
}
}
])
Mongo Playground
If for whatever reason you want to keep this in code then I would take a look at your for loop:
for (id of list) {
await objectFinder(id, ID2, depth - 1, previousList).then(result => {
route = [ID1].concat(result);
if (route[route.length - 1] == ID2) {
return route;
}
});
}
Just from a quick glance I can tell you're executing this:
route = [ID1].concat(result);
Many times at the same level. Additional I could not understand your bottom return statements, I feel like there might be an issue there.

loop through an array and choose specific to setstate

New react developer here, here i have two API's, first one gives an object :
{ id: "98s7faf", isAdmin: true, name: "james"}
second one gives an array of objects :
[
{ billingName: "trump", driverName: "james" },
{ billingName: "putin", driverName: "alex" },
{ billingName: "kalle", driverName: "james" },
{ billingName: "sussu", driverName: "trump" },
{ billingName: "vladimir", driverName: "james" },
]
my question is, when user goes to the page, the page should automatically check both API'S, from first api name and from second api driverName, and if those two have same value then take that specific object from an array and pass it to these:
setOrders(res);
setRenderedData(res);
so at the moment there are three objects (those which have value of james) inside an array which matches name from first api, so it should pass those, and the rest which have different value it should not let them pass. here is my code, what have i done wrong ? instead of some(({ driverName }) i need some kind of filter ?
useEffect(() => {
api.userApi.apiUserGet().then((res1?: User ) => {
return api.caApi.apiCaGet(request).then((res?: CaDto[])
=> {
if (res.some(({ driverName }) => driverName === res1?.name)) {
setOrders(res);
setRenderedData(res);
console.log(res);
}
});
});
}, [api.caApi, api.userApi]);
you need to filter your response to extract the right objects given your condition.
useEffect(() => {
api.userApi.apiUserGet().then((res1?: User ) => {
return api.caApi.apiCaGet(request).then((res?: CaDto[])
=> {
const filteredData = res?.filter(({ driverName }) => driverName === res1?.name);
if(filteredData?.length) {
setOrders(filteredData);
setRenderedData(filteredData);
console.log(filteredData);
}
});
});
}, [api.caApi, api.userApi]);
note: having 2 states that holds the same values (orders, renderedData) is not a good practice, you might consider to refactor your logic.

Javascript - filtering a list: how can I find an intersection between an array with objects including array and an array?

How can I filter a list (array with objects) with a filter list (array) and find intersections? I add to the filter array every time a user checks the checkbox clicking on particular filter. When user unchecks the checkbox I remove from filter array. Somehow whateever i try doing, i always return the entire reviews array including ALL not filtered items. Why? Thanks!!
const reviews = [
{
title: "item 1",
filter_results: {
features: ["message", "call"],
pricing: ["Business", "Free", "Whatever"],
rating: [1]
}
},
{
title: "item 2",
filter_results: {
features: ["call", "copy", "paste"],
pricing: ["Business"],
rating: [1]
}
},
{
title: "item 3",
filter_results: {
features: ["copy", "connect", "wifi"],
pricing: ["Free",
rating: [2]
}
}
]
const filteredReviews = {
pricing_options: ["Business"],
popular_features: ["copy, call"],
rating: [1, 2]
}
const update = (reviews, categoryName) => {
if (categoryName) {
return reviews.filter(review => {
return review.filter_results[categoryName].filter(value => {
if (filteredReviews[categoryName].includes(value)) {
return review
}
})
})
} else {
return reviews
}
}
update(reviews, "pricing")
Return a boolean on filter callback, and do a better filtering mechanism:
const update = (reviews, filters) => {
if (filters) {
return reviews.filter(review =>
Object.entries(filters)
// Change to `some` if the filters are OR'ed instead of AND'ed
.every(
([filter_key, filter_values]) =>
// Change `some` to `every` if all elements in the
// userFilter[*] array MUST be matched instead of some of them
filter_values.some( (filter_value) =>
review.filter_results[filter_key]
.includes(filter_value)
)
)
)
} else {
return reviews
}
}
// Fix variables names:
// - `userFilters` contains the filters selected by the user
// - `filteredReviews` contains the array of reviews, resulting from
// filtering the reviews using the `userFilters`
// Fix key names: Use same keys than in reviews, instead of:
// - `pricing_options` => `pricing`
// - `popular_features` => `features`
const userFilters = {
pricing: ["Business"],
// Transformed/fixed to 2 values. Was it a typo?
features: ["copy", "call"],
};
const filteredReviews = update(reviews, userFilters);
Filter callback function should return a "boolean", you are returning arrays which evaluate always to "true".

How to use rxjs map to filter out an observable list of object by another list of objects of different type

I have an observable list that I'm returning from an api and i want to filter it base on a property value of another list that I have in local session storage. It's basically an inner join.
is there a better way than using rxjs map / filter?
This filter works for a single value but I'm not sure how to go about filtering it based on a property value of a list
loadTable() {
this.service.getSomeDataFromAPI()
.pipe(
map(
items => items.filter(item => item.branchCode.indexOf(StaticListOfObjects.branchCode) > -1)
)
)
.subscribe(response => { ... }
);
}
This is my staticListOfObject and I need to filter for all the objects that matches branchCode.
[
{
branchCode: "AV1",
summaryCount: 10,
},
{
branchCode: "AV2",
summaryCount: 5,
},
{
branchCode: "BR1",
summaryCount: 4,
},
];
Since API call returns a list (I presume an array) you just need to map it and filter it like so;
let filterArray = [
{
branchCode: "AV1",
summaryCount: 10
},
{
branchCode: "AV2",
summaryCount: 5
},
{
branchCode: "BR1",
summaryCount: 4
}]
loadTable() {
this.service.getSomeDataFromAPI().pipe(
map(
items => items.filter( item => {
if (filterArray.find(filterItem => filterItem.branchCode == item.branchCode)) {
return true
} else {
return false
}
})
)
).subscribe(response => { ... });
}
Considering
const StaticListOfObjects = [
{
branchCode: "AV1",
summaryCount: 10,
},
{
branchCode: "AV2",
summaryCount: 5,
},
{
branchCode: "BR1",
summaryCount: 4,
},
];
another possible solution if your observable completes after sending the first array (and you are willing to use ES6) could be
loadTable() {
const valid_values = StaticListOfObjects.map(x => x.branchCode);
this.service.getSomeDataFromAPI()
.pipe(
concatMap(y => y),
filter(y => valid_value.includes(y.branchCode)),
toArray(),
)
.subscribe(response => { ... }
}
concatMap gets your array and emits observables of all its values, then you filter on each object with the filter operator and finally toArray bundles up all those filtered into an array when the observable returned by getSomeDataFromAPI() completes (this is the reason why it only works if the observable completes after emitting the first array, if it doesn't then the pipe will keep filtering newly emitted arrays until the observable completes returning all filtered values from all received source arrays into a single result array).

Find matching item across multiple store arrays in VueX

Currently when I want to find single item in an array that is in store I use this:
this.matched = this.$store.state.itemlist.find(itemId=> {
return itemId.id == "someid";
});
Lets says I want to go over multiple arrays to find the matching item given provided ID? Like i have itemlist1 itemlist2 itemgetter()... Some of the arrays are getters ( but I think it doesnt change much). So basically I want to search over different state and getter items in this component instead of searching over one as in example above.
if you just want to find if its exist in one the arrays you can simply write function like this
function find(search,...arrs){
return arrs.flat(1).find(item => item == search)
}
this function merge all arrays to one long array and search in it
example of usage
let a=[1,2,3,4]
let b=[5,6,7,8]
let c=[9,10,11,12]
let i=find(6,a,b)
console.log(i)
Using one object to group all the arrays, so that will be possible to iterate over them. The idea is something like below:
const store = new Vuex.Store({
state: {
itemsGroupArrays: {
items1: [{ id: 1, text: "item1 - 1" }, { id: 2, text: "item1 - 2" }],
items2: [{ id: 3, text: "item2 - 1" }, { id: 4, text: "item2 - 2" }]
}
},
getters: {
getItemByIdFromStateGroupArrays: state => (id) => {
let returnedItem = null;
Object.values(state.itemsGroupArrays).forEach((itemStateArray) => {
if (itemStateArray.some(item => item.id === id)) {
returnedItem = itemStateArray.find(item => item.id === id);
}
})
return returnedItem;
}
}
});

Categories