MongoDB - Update all entries in nested array only if they exist - javascript

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
}

Related

How to build names from related Lavels?

I like to build a list of names to an array. It needs to find the name from Level1 and then go through the chain to Level3. If it does not match from Level1 then try Level2, if still no match then try to get the Name from Level3
For example Ouput:
getNames("Office") to return ["Office", "Microsoft", "Software"]
getNames("Apple") to return ["Apple", "Software"]
getNames("Tesla") to return ["Tesla", "Car"]
Data
const data = {
"Level1": [ { "Names": [ "Office" ], "Level": "Level2#Microsoft" } ],
"Level2": [ { "Names": [ "Apple", "Microsoft" ], "Level": "Level3#Software" }, { "Names": [ "Tesla" ], "SubLevel": "Level3#Car" }],
"Level3": [ { "Names": [ "Software" ] }, { "Names": [ "Car" ] } ]
}
I am struggling with how to get data to the next level without writing many if conditions?
Incomplete usage:
function getNames(name) {
const namesBuild = [];
const foundName = data.Level1.find(row => {
return row.Names.find(rowName => rowName === name)
});
if (foundName) {
namesBuild.push(name);
const subLevel = foundName.Level.split("#");
const catLevel = subLevel[0];
const catName = subLevel[1];
// How to continue to find next chain from object without many if conditions?
} else {
// attempt to find next level Level2, if not found then try Level3
}
return namesBuild;
}

Filter nested of nested in Mongoose Populate

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.

Dynamically create multidimensional array from split input

I have an array of ojects which all have a path and a name property.
Like
[
{
"id": "1",
"path": "1",
"name": "root"
},
{
"id": "857",
"path": "1/857",
"name": "Animals"
},
{
"id": "1194",
"path": "1/857/1194",
"name": "Dinasours"
},
...and so on
]
Here are some path examples
1/1279/1282
1/1279/1281
1/1279/1280
1/857
1/857/1194
1/857/1194/1277
1/857/1194/1277/1278
I want to turn this into a multidimensional array like:
const data = {
id: "1",
name: "Root",
children: [
{
id: "1279",
name: "Toys",
},
{
id: "857",
name: "Animals",
children: [
{
id: "1194",
name: "Dinasours",
children: [
{
id: "1277",
name: "T-Rex",
children: [
{
id: "1278",
name: "Superbig T-Rex",
},
],
},
],
},
],
},
],
};
As you can understand the amount of data is much larger.
Is there a neat way to transform this data?
I wonder if this would be sufficient for your needs?
I'll refer to the objects as nodes (just because I'm a graph theory person, and that's how I roll).
Build an index mapping each id to the object itself using a Map. (Purely for efficiency. You could technically find each node from scratch by id each time you need it.)
Split the path to obtain the second last path fragment which should be the id of the direct parent of the node. (Assuming there's only one and that there is guaranteed to be a node corresponding to that id?)
Add the child to the parent's list of children. We'll be careful not to add it multiple times.
This will result in nodes that have no children literally having no children property (as opposed to having a children property that is just []). I also did not remove/delete the path property from the objects.
As a note of caution, if there are path fragments that do not have corresponding objects, this will not work.
const nodes = [
{ id: '1', path: '1', name: 'root' },
{ id: '857', path: '1/857', name: 'Animals' },
{ id: '1194', path: '1/857/1194', name: 'Dinasours' }
//...and so on
];
const index = new Map();
for (let node of nodes) {
index.set(node.id, node)
}
for (let node of nodes) {
const fragments = node.path.split('/');
const parentId = fragments[fragments.length - 2];
const parent = index.get(parentId);
if (parent !== undefined) {
parent.children = parent.children || [];
if (!parent.children.includes(node)) {
parent.children.push(node);
}
}
}
// TODO: Decide which node is the root.
// Here's one way to get the first (possibly only) root.
const root = index.get(nodes[0].path.split('/')[0]);
console.dir(root, { depth: null });
Assuming that the root is always the same I came up with this code, it took me some time but it was fun to think about it.
var data = {};
list.forEach(item => {
var path = item.path.split("/");
let parent = data;
path.forEach((id) => {
if (!parent.id) {
parent.id = id;
parent.children = [];
if (id != item.id) {
let next = {}
parent.children.push(next);
parent = next;
}
} else if (parent.id != id) {
let next = parent.children.find(child => child.id == id);
if (!next) {
next = { id: id, children: [] }
parent.children.push(next);
}
parent = next;
}
});
parent.id = item.id;
parent.name = item.name
});
output:
{
"id": "1",
"children": [
{
"id": "857",
"children": [
{
"id": "1194",
"children": [
{
"id": "1277",
"children": [
{ "id": "1278", "children": [], "name": "Superbig T-Rex" }
],
"name": "T-Rex"
}
],
"name": "Dinasours"
}
],
"name": "Animals"
},
{ "id": "1279", "children": [], "name": "Toys" }
],
"name": "Root"
}
I think that having more roots here may need some fixing. Although I think the problem would be different if we were talking about multiple roots since your data variable is an object
Also, if you think in a recursive way it can be more understandable, but no comments on performance.

Transform an array to another array

I'd like to transform this array to another array using lodash 2.4.2:
authorities will be mapped in roles. and we don't touch the other properties.
From:
{
"items": [
{
"_id": "admin",
"authorities": [
{
"name": "ROLE_ADMIN"
}
]
},
{
"_id": "user",
"authorities": [
{
"name": "ROLE_USER"
}
]
}
]
}
to this array
{
"items": [
{
"_id": "admin",
"roles": [
"ROLE_ADMIN"
]
},
{
"_id": "user",
"role": [
"ROLE_USER"
]
}
]
}
can you help me please?
As charlietfl said, in the future posts you must provide examples of code, that you've written to solve your problem.
Here's the solution, it's pretty simple:
const initialObject = {
"items": [
{
"_id": "admin",
"authorities": [
{
"name": "ROLE_ADMIN"
}
]
},
{
"_id": "user",
"authorities": [
{
"name": "ROLE_USER"
}
]
}
]
}
const processedObject = _
.map(initialObject.items, item => {
return {
_id: item._id,
roles: _.map(item.authorities, a => a.name)
}
})
It is just a re-mapping of the key name. For performance reasons I would try to avoid the use of delete. You can see the performance differences in this benchmark, only use 'delete' if the object gets persisted later on.
for (var item of items) {
item.roles = item.authorities
// possible but slow
// delete item.authorities
item.authorities = undefined
}
Edit: #vanelizarov code is even better if you want to avoid mutation of the object itself and clean properties. but it is also slower.
vanelizarov code: x 5,025,370 ops/sec ±1.30% (90 runs sampled)
my code: x 31,583,811 ops/sec ±0.95% (92 runs sampled)

How to Sort by Weighted Values

I have this problem that I want to sort the result of a query based on the field values from another collection,
Problem: I want to first get the user 123 friends and then get their posts and then sort the post with the friends strength value,
I have this :
POST COLLECTON:
{
user_id: 8976,
post_text: 'example working',
}
{
user_id: 673,
post_text: 'something',
}
USER COLLECTON:
{
user_id: 123,
friends: {
{user_id: 673,strength:4}
{user_id: 8976,strength:1}
}
}
Based on the information you have retrieved from your user you essentially want to come out to an aggregation framework query that looks like this:
db.posts.aggregate([
{ "$match": { "user_id": { "$in": [ 673, 8976 ] } } },
{ "$project": {
"user_id": 1,
"post_text": 1,
"weight": {
"$cond": [
{ "$eq": [ "$user_id", 8976 ] },
1,
{ "$cond": [
{ "$eq": [ "$user_id", 673 ] },
4,
0
]}
]
}
}},
{ "$sort": { "weight": -1 } }
])
So why aggregation when this does not aggregate? As you can see, the aggregation framework does more than just aggregate. Here it is being used to "project" a new field into the document an populate it with a "weight" to sort on. This allows you to get the results back ordered by the value you want them to be sorted on.
Of course, you need to get from your initial data to this form in a "generated" way that you do do for any data. This takes a few steps, but here I'll present the JavaScript way to do it, which should be easy to convert to most languages
Also presuming your actual "user" looks more like this, which would be valid:
{
"user_id": 123,
"friends": [
{ "user_id": 673, "strength": 4 },
{ "user_id": 8976, "strength": 1 }
]
}
From an object like this you then construct the aggregation pipeline:
// user is the structure shown above
var stack = [];
args = [];
user.friends.forEach(function(friend) {
args.push( friend.user_id );
var rec = {
"$cond": [
{ "$eq": [ "user_id", friend.user_id ] },
friend.strength
]
};
if ( stack.length == 0 ) {
rec["$cond"].push(0);
} else {
var last = stack.pop();
rec["$cond"].push( last );
}
stack.push( rec );
});
var pipeline = [
{ "$match": { "user_id": { "$in": args } } },
{ "$project": {
"user_id": 1,
"post_text": 1,
"weight": stack[0]
}},
{ "$sort": { "weight": -1 } }
];
db.posts.aggregate(pipeline);
And that is all there is to it. Now you have some code to go through the list of "friends" for a user and construct another query to get all posts from those friends weighted by the "strength" value for each.
Of course you could do much the same things with a query for all posts by just removing or changing the $match, but keeping the "weight" projection you can "float" all of the "friends" posts to the top.

Categories

Resources