I am trying to create a function that will loop over a Hierarchy of arrays and concat them together. I am struggling to think of a way to do this.
I call a Web API that returns me some data which has the same properties and layout but can differ in how many Hierarchy layer array there is.
Folders: A folder can contain 'Routes' but can also contain another folder inside of it which can then also contain more 'Routes' etc
Routes: A single object which is a route.
For example:
{
"id":1,
"folders":[
{
"id":2,
"folders":[
{
"id":3,
"folders":[],
"routes":[]
}
],
"routes":[
{
"id":1002,
"name":"Route3"
},
{
"id":1003,
"name":"Route4"
}
]
}
],
"routes":[
{
"id":1000,
"name":"Route1"
},
{
"id":1001,
"name":"Route2"
}
]
}
I need to be able to keep going deeper into the hierarchy and concat all of the Routes arrays with the Folders array, so I can bind the Kendo TreeView with a single child which will be the Folders array.
So far I have tried:
for (var i = 0; i < Folders.length; i++) {
if (Folder[i].Folders.length > 0) {
for (var e = 0; e < Folder[i].Folder[e].length; e++) {
if (Folder[i].Folders[e].length > 0) {
...
}
}
}
}
The problem with that method is that I will never know how many layer there will be and therefore is not a viable method.
The result I need is the above example to look like:
{
"id":1,
"folders":[
{
"id":2,
"folders":[
{
"id":3,
"folders":[
],
"routes":[
]
},
{
"id":1002,
"name":"Route3"
},
{
"id":1003,
"name":"Route4"
}
]
},
{
"id":1000,
"name":"Route1"
},
{
"id":1001,
"name":"Route2"
}
]
}
var flatRoutes = function(folders){
for(var i in folders){
var cur = folders[i];
if(cur.hasOwnProperty('folders')){
flatRoutes(cur.folders);
}
if(cur.hasOwnProperty('routes')){
for(var i in cur.routes){
cur.folders.push(cur.routes[i]);
}
delete cur.routes;
}
}
return folders;
}
var json = {
"id":1,
"folders":[
{
"id":2,
"folders":[
{
"id":3,
"folders":[],
"routes":[]
}
],
"routes":[
{
"id":1002,
"name":"Route3"
},
{
"id":1003,
"name":"Route4"
}
]
}
],
"routes":[
{
"id":1000,
"name":"Route1"
},
{
"id":1001,
"name":"Route2"
}
]
}
var routes = flatRoutes([json]);
console.log(routes);
To remove a property from ab object, you can make use of delete object.property.
I have taken the following step, I think it gives the correct result.
Create a modular function - formatData
Check if folders array has any data or not
If folders array have data, call formatData for every folder in it
Then, check for the routes array. If no routes, return the data
If routes array have data, just push each route into the folder array and remove the routes property from the object.
let data = {
"id": 1,
"folders": [{
"id": 2,
"folders": [{
"id": 3,
"folders": [],
"routes": []
}],
"routes": [{
"id": 1002,
"name": "Route3"
},
{
"id": 1003,
"name": "Route4"
}
]
}],
"routes": [{
"id": 1000,
"name": "Route1"
},
{
"id": 1001,
"name": "Route2"
}
]
};
function formatData(data) {
if (data.folders.length) {
data.folders.forEach(folder => {
return formatData(folder);
});
}
if (data.routes.length) {
data.routes.forEach(route => {
data.folders.push(route);
});
delete data.routes;
}
return data;
}
console.log(formatData(data));
You could use an iterative and recursive approach.
var data = { id: 1, folders: [{ id: 2, folders: [{ id: 3, folders: [], routes: [] }], routes: [{ id: 1002, name: "Route3" }, { id: 1003, name: "Route4" }] }], routes: [{ id: 1000, name: "Route1" }, { id: 1001, name: "Route2" }] },
result = [data].map(function iter(o) {
return {
id: o.id,
folders: (o.folders && o.folders.map(iter) || []).concat(o.routes || [])
};
})[0];
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Related
I have an object that looks like this: (that is the output of Mongoose query)
let systems = [
{
"maxUserLevel": 1,
"subsystems": [
{
"sections": [],
"name": "apple"
},
{
"sections": [
{
"name": "banana"
}
],
"name": "sun",
},
{
"sections": [],
"name": "orange"
}
],
"systemID": "12345"
},
{
"maxUserLevel": 3,
"subsystems": [
{
"sections": [],
"name": "blue"
},
{
"sections": [
{
"name": "pink"
}
],
"name": "red",
},
],
"systemID": "15654"
}];
The Mongoose query:
this.model.System.find({username: user.username}, {
_id: 0,
allowedOrganizations: 0,
name: 0,
updatedAt: 0,
createdAt: 0,
versionKey: 0
})
.populate(
{
path: "subsystems",
populate: {
path: "sections",
select: "name -_id",
match: {
allowedUsers: user.id
}
},
select: "name metadata -_id",
}
)
.exec((error, systems) => {
return res.status(200).json({
data: systems,
success: true
});
});
I'm looking for a way to removes the subsystems that do not have sections.
After hours of searching I think there's no way to filter populate based on nested populate, so I tried with some ways like this:
if (systems.subsystems.length > 0) {
let test = [];
systems.subsystems.forEach((value, index) => {
if (value.sections.length !== 0) {
test[index] = value;
}
if (systems.subsystems.length === index + 1) {
return test;
}
})
}
But I'm not sure if this is the correct way.
You can use an aggregate query with $filter like this:
db.collection.aggregate([
{
"$project": {
"_id": 1,
"maxUserLevel": 1,
"subsystems": {
"$filter": {
"input": "$subsystems",
"as": "s",
"cond": {
"$ne": [
"$$s.sections",
[]
]
}
}
}
}
}
])
Example here
Also your query should contains a $match stage (like your find stage) and $lookup.
I'm not sure this is the best way, but it solved my problem:
const _ = require('lodash');
systems.forEach((value, index) => {
systems[index].subsystems = _.filter(value.subsystems,
item => !item.sections.length == 0
);
if (systems.length === index + 1) {
return systems;
}
});
It removes all subsystems that do not have sections.
I have a multilevel nested document (its dynamic and some levels can be missing but maximum 3 levels). I want to update all the children and subchildren routes if any. The scenario is same as in any Windows explorer, where all subfolders' route need to change when a parent folder route is changed. For eg. In the below example, If I am at route=="l1/l2a" and it's name needs to be edited to "l2c", then I will update it's route as route="l1/l2c and I will update all childrens' route to say "l1/l2c/l3a".
{
"name":"l1",
"route": "l1",
"children":
[
{
"name": "l2a",
"route": "l1/l2a",
"children":
[
{
"name": "l3a",
"route": "l1/l2a/l3a"
}]
},
{
"name": "l2b",
"route": "l1/l2b",
"children":
[
{
"name": "l3b",
"route": "l1/l2b/l3b"
}]
}
]
}
Currently I am able to go to a point and I am able to change its name and ONLY its route in the following manner:
router.put('/navlist',(req,res,next)=>{
newname=req.body.newName //suppose l2c
oldname=req.body.name //suppose l2a
route=req.body.route // existing route is l1/l2a
id=req.body._id
newroute=route.replace(oldname,newname); // l1/l2a has to be changed to l1/l2c
let segments = route.split('/');
let query = { route: segments[0]};
let update, options = {};
let updatePath = "";
options.arrayFilters = [];
for(let i = 0; i < segments.length -1; i++){
updatePath += `children.$[child${i}].`;
options.arrayFilters.push({ [`child${i}.route`]: segments.slice(0, i + 2).join('/') });
} //this is basically for the nested children
updateName=updatePath+'name'
updateRoute=updatePath+'route';
update = { $setOnInsert: { [updateName]:newDisplayName,[updateRoute]:newroute } };
NavItems.updateOne(query,update, options)
})
The problem is I am not able to edit the routes of it's children if any i.e it's subfolder route as l1/l2c/l3a. Although I tried using the $[] operator as follows.
updateChild = updatePath+'.children.$[].route'
updateChild2 = updatePath+'.children.$[].children.$[].route'
//update = { $set: { [updateChild]:'abc',[updateChild2]:'abc' } };
Its important that levels are customizable and thus I don't know whether there is "l3A" or not. Like there can be "l3A" but there may not be "l3B". But my code simply requires every correct path else it gives an error
code 500 MongoError: The path 'children.1.children' must exist in the document in order to apply array updates.
So the question is how can I apply changes using $set to a path that actually exists and how can I edit the existing route part. If the path exists, it's well and good and if the path does not exist, I am getting the ERROR.
Update
You could simplify updates when you use references.Updates/Inserts are straightforward as you can only the update target level or insert new level without worrying about updating all levels. Let the aggregation takes care of populating all levels and generating route field.
Working example - https://mongoplayground.net/p/TKMsvpkbBMn
Structure
[
{
"_id": 1,
"name": "l1",
"children": [
2,
3
]
},
{
"_id": 2,
"name": "l2a",
"children": [
4
]
},
{
"_id": 3,
"name": "l2b",
"children": [
5
]
},
{
"_id": 4,
"name": "l3a",
"children": []
},
{
"_id": 5,
"name": "l3b",
"children": []
}
]
Insert query
db.collection.insert({"_id": 4, "name": "l3a", "children": []}); // Inserting empty array simplifies aggregation query
Update query
db.collection.update({"_id": 4}, {"$set": "name": "l3c"});
Aggregation
db.collection.aggregate([
{"$match":{"_id":1}},
{"$lookup":{
"from":"collection",
"let":{"name":"$name","children":"$children"},
"pipeline":[
{"$match":{"$expr":{"$in":["$_id","$$children"]}}},
{"$addFields":{"route":{"$concat":["$$name","/","$name"]}}},
{"$lookup":{
"from":"collection",
"let":{"route":"$route","children":"$children"},
"pipeline":[
{"$match":{"$expr":{"$in":["$_id","$$children"]}}},
{"$addFields":{"route":{"$concat":["$$route","/","$name"]}}}
],
"as":"children"
}}
],
"as":"children"
}}
])
Original
You could make route as array type and format before presenting it to user. It will greatly simplify updates for you. You have to break queries into multiple updates when nested levels don’t exist ( ex level 2 update ). May be use transactions to perform multiple updates in atomic way.
Something like
[
{
"_id": 1,
"name": "l1",
"route": "l1",
"children": [
{
"name": "l2a",
"route": [
"l1",
"l2a"
],
"children": [
{
"name": "l3a",
"route": [
"l1",
"l2a",
"l3a"
]
}
]
}
]
}
]
level 1 update
db.collection.update({
"_id": 1
},
{
"$set": {
"name": "m1",
"route": "m1"
},
"$set": {
"children.$[].route.0": "m1",
"children.$[].children.$[].route.0": "m1"
}
})
level 2 update
db.collection.update({
"_id": 1
},
{
"$set": {
"children.$[child].route.1": "m2a",
"children.$[child].name": "m2a"
}
},
{
"arrayFilters":[{"child.name": "l2a" }]
})
db.collection.update({
"_id": 1
},
{
"$set": {
"children.$[child].children.$[].route.1": "m2a"
}
},
{
"arrayFilters":[{"child.name": "l2a"}]
})
level 3 update
db.collection.update({
"_id": 1
},
{
"$set": {
"children.$[].children.$[child].name": "m3a"
"children.$[].children.$[child].route.2": "m3a"
}
},
{
"arrayFilters":[{"child.name": "l3a"}]
})
I don't think its possible with arrayFilted for first level and second level update, but yes its possible only for third level update,
The possible way is you can use update with aggregation pipeline starting from MongoDB 4.2,
I am just suggesting a method, you can simplify more on this and reduce query as per your understanding!
Use $map to iterate the loop of children array and check condition using $cond, and merge objects using $mergeObjects,
let id = req.body._id;
let oldname = req.body.name;
let route = req.body.route;
let newname = req.body.newName;
let segments = route.split('/');
LEVEL 1 UPDATE: Playground
// LEVEL 1: Example Values in variables
// let oldname = "l1";
// let route = "l1";
// let newname = "l4";
if(segments.length === 1) {
let result = await NavItems.updateOne(
{ _id: id },
[{
$set: {
name: newname,
route: newname,
children: {
$map: {
input: "$children",
as: "a2",
in: {
$mergeObjects: [
"$$a2",
{
route: { $concat: [newname, "/", "$$a2.name"] },
children: {
$map: {
input: "$$a2.children",
as: "a3",
in: {
$mergeObjects: [
"$$a3",
{ route: { $concat: [newname, "/", "$$a2.name", "/", "$$a3.name"] } }
]
}
}
}
}
]
}
}
}
}
}]
);
}
LEVEL 2 UPDATE: Playground
// LEVEL 2: Example Values in variables
// let oldname = "l2a";
// let route = "l1/l2a";
// let newname = "l2g";
else if (segments.length === 2) {
let result = await NavItems.updateOne(
{ _id: id },
[{
$set: {
children: {
$map: {
input: "$children",
as: "a2",
in: {
$mergeObjects: [
"$$a2",
{
$cond: [
{ $eq: ["$$a2.name", oldname] },
{
name: newname,
route: { $concat: ["$name", "/", newname] },
children: {
$map: {
input: "$$a2.children",
as: "a3",
in: {
$mergeObjects: [
"$$a3",
{ route: { $concat: ["$name", "/", newname, "/", "$$a3.name"] } }
]
}
}
}
},
{}
]
}
]
}
}
}
}
}]
);
}
LEVEL 3 UPDATE: Playground
// LEVEL 3 Example Values in variables
// let oldname = "l3a";
// let route = "l1/l2a/l3a";
// let newname = "l3g";
else if (segments.length === 3) {
let result = await NavItems.updateOne(
{ _id: id },
[{
$set: {
children: {
$map: {
input: "$children",
as: "a2",
in: {
$mergeObjects: [
"$$a2",
{
$cond: [
{ $eq: ["$$a2.name", segments[1]] },
{
children: {
$map: {
input: "$$a2.children",
as: "a3",
in: {
$mergeObjects: [
"$$a3",
{
$cond: [
{ $eq: ["$$a3.name", oldname] },
{
name: newname,
route: { $concat: ["$name", "/", "$$a2.name", "/", newname] }
},
{}
]
}
]
}
}
}
},
{}
]
}
]
}
}
}
}
}]
);
}
Why separate query for each level?
You could do single query but it will update all level's data whenever you just need to update single level data or particular level's data, I know this is lengthy code and queries but i can say this is optimized version for query operation.
you can't do as you want. Because mongo does not support it. I can offer you to fetch needed item from mongo. Update him with your custom recursive function help. And do db.collection.updateOne(_id, { $set: data })
function updateRouteRecursive(item) {
// case when need to stop our recursive function
if (!item.children) {
// do update item route and return modified item
return item;
}
// case what happen when we have children on each children array
}
let data = {
"rec": [{
"id": "25837",
"contentId": "25838"
},
{
"id": "25839",
"contentId": "25838"
},
{
"id": "25838"
},
{
"id": "25636",
"contentId": "25837"
}, {
"id": "25640",
"contentId": "25839"
}
]
};
I have a javascript object which I have to manipulate to below format.
{
"childern": [{
"id": "25838",
"childern": [{
"id": "25837",
"contentId": "25838",
"childern": [{
"id": "25636",
"contentId": "25837"
}]
},
{
"id": "25839",
"contentId": "25838",
"childern": [{
"id": "25640",
"contentId": "25839"
}]
}
]
}]
}
If any object dont have contentId it should be at parent level. then all the objects having contentId same as parent id should be at its child level and so on.
I have created a fiddle here but logic is not completed. Any idea or reference to achieve this.
You could create recursive function with reduce method to get the desired result.
let data = {"rec":[{"id":"25837","contentId":"25838"},{"id":"25839","contentId":"25838"},{"id":"25838"},{"id":"25636","contentId":"25837"},{"id":"25640","contentId":"25839"}]}
function nest(data, pid) {
return data.reduce((r, e) => {
if (pid == e.contentId) {
const obj = { ...e }
const children = nest(data, e.id);
if (children.length) obj.children = children
r.push(obj)
}
return r;
}, [])
}
const result = nest(data.rec);
console.log(result[0])
I am trying to return all objects that have a specific 'id' in the nested array. In the sample data, I'd like to return all person objects with hobbies id of 2 (hiking).
The other question addresses the problem of finding all values in an array based on an object value.
This question differs from the previous because I need to return all objects based on a value inside of a nested array.
[
{
"id":111222,
"name":"Faye",
"age":27,
"hobbies":[
{
"id":2,
"name":"hiking"
},
{
"id":3,
"name":"eating"
}
]
},
{
"id":223456789001,
"name":"Bobby",
"age":35,
"hobbies":[
{
"id":2,
"name":"hiking"
},
{
"id":4,
"name":"online gaming"
}
]
}
]
function hasHobby(person, hobbyId) {
return person.hobbies.some(function(hobby) {
return hobby.id === hobbyId;
});
}
function filterByHobby(people, hobbyId) {
return people.filter(function(person) {
return hasHobby(person, hobbyId);
});
}
If you wanna use the new cool ES6 syntax:
function filterByHobby(people, hobbyId) {
return people.filter(
person => person.hobbies.some(
hobby => hobby.id === hobbyId
)
);
}
var arr = [
{
"id":111222,
"name":"Faye",
"age":27,
"hobbies":[
{
"id":2,
"name":"hiking"
},
{
"id":3,
"name":"eating"
}
]
},
{
"id":223456789001,
"name":"Bobby",
"age":35,
"hobbies":[
{
"id":2,
"name":"hiking"
},
{
"id":4,
"name":"online gaming"
}
]
}
];
arr.filter(function(obj) {
var hobbies = obj.hobbies;
var x = hobbies.filter(function(hob) {
if (hob.id == "2") return true;
});
if (x.length > 0) return true;
});
Try this, I think its solve your proble:
var arr = [{
"id": 111222,
"name": "Faye",
"age": 27,
"hobbies": [{
"id": 2,
"name": "hiking"
}, {
"id": 3,
"name": "eating"
}]
}, {
"id": 223456789001,
"name": "Bobby",
"age": 35,
"hobbies": [{
"id": 2,
"name": "hiking"
}, {
"id": 4,
"name": "online gaming"
}]
}];
var x = arr.filter(function(el) {
var rnel = el.hobbies.filter(function(nel) {
return nel.id == 2;
});
return rnel.length > 0 ? true :false;
});
alert(x.length);
I have a JSON file with a list of categories:
"data": {
"categories": [
{
"id": 1,
"name": "Clothes",
"children": [
"Womens",
"Mens",
"Children",
"Baby",
]
},
{
"id": "13",
"name": "Womens",
"children": [
"Womens Tops",
"Womens Bottoms",
"Womens Accessories",
]
},
{
"id": "33",
"name": "Womens Tops",
"children": []
},
In the code below I tried/failed to iterate through each node and its children to build a path variable to also be stored with the data in the mongo db:
for(var i in obj.data.categories) {
var newCat = {
name: obj.data.categories[i].name,
children: obj.data.categories[i].children
};
//UPDATE OR CREATE
Category.findOneAndUpdate({name:obj.data.categories[i].name},newCat,{upsert: true},
function(err,cat) {
if(err) { return handleError(res, err); }
if(cat.children) {
//BUILD CHILDREN PATHS
for(var j=0;j<cat.children.length;j++) {
var newChildCat = {
name: cat.children[j],
};
newChildCat.path = cat.path ? cat.path+','+cat.name : cat.name;
Category.findOneAndUpdate({ name: newChildCat.name},newChildCat,{upsert: true},
function(childErr, newChildCat) {
if(childErr) { return handleError(res, childErr); }
}
);
}
}
}
);
}
However, because javascript runs asynchronously, some node paths are being stored before their parent has been stored.
I am still kind of a newb at this and am looking for the proper/best practice way to handle asynchronous importing of objects that are dependent upon the previous creation of one another like above.
Use async module to do this. The function you need is async.eachSeries
async.eachSeries(obj.data.categories, function(_cat, cb){
var newCat = {...};
Category.findOneAndUpdate({name:_cat.name},newCat,{upsert: true}, function(err,cat) {
if(cat.children) {
async.eachSeries(cat.children, function(_cat2, cb2){
// the code like above with cb2()
}, cb);
} else {
cb()
}
});
});