update two layer nested object based on the id - javascript

I have this structure in my Mother Model (this is a fixed structure and I just push cards or update them on these 3 array levels):
{
cards: {
starter: [],
intermediate: [],
advanced: [ {Object}, {Object}, {Object} ]
},
}
The Objects inside cards.advanced array above are like:
{
cards: [
{ // this is a single card object
title: 'this is a card',
id: 'main-2-1' // this is unique id only in advanced array, we may have exact id for a card in starter or in intermediate array
}
],
unit: 2 // this is the unit
}
Assuming I have access to Mother model like this:
const motherModel = await db.Mother.findOne({}); // this retrieves all data in the Model
How can we update a card object based on its id and the level it belongs to and replace the whole card object with newCard ?
const level = 'advanced'; // the level of the card we want to search for
const cardID = 'main-2-1'; // the exact id of the card we want to be replaced
const cardUnit = cardID.split('-')[1]; // I can calculate this as the unit in which the card exist inside
const newCard = { // new card to be replaced
title: 'this is our new updated card',
id: 'main-2-1'
}
I have tried this with no luck:
const updated = await db.Mother.update(
{ ["cards." + level + ".unit"]: cardUnit },
{ ["cards." + level + ".$.cards"]: newCard }
)
I have tried this one too but it doesn't change anything in the Model:
async function updateMotherCard(card, level) {
const cardID = card.id;
const cardUnit = cardID.split('-')[1];
const motherModel = await db.Mother.findOne({});
const motherLevel = motherModel.cards[level];
const selectedUnit = motherLevel.find(e => e.unit == cardUnit);
let selectedCard = selectedUnit.cards.find(e => e.id == cardID);
selectedCard = card;
const updated = await motherModel.save();
console.log(updated);
}

You can actually sort your problem out with the update method, but you have to do it in a different way if you are using MongoDB 4.2 or later. The second parameter can be the $set operation you want to perform or an aggregation pipeline. Using the later you have more liberty shaping the data. This is the way you can solve your problem, I will breakdown after:
db.collection.update({
"cards.advanced.unit": 2
},
[
{
$set: {
"cards.advanced": {
$map: {
input: "$cards.advanced",
as: "adv",
in: {
cards: {
$map: {
input: "$$adv.cards",
as: "advcard",
in: {
$cond: [
{
$eq: [
"$$advcard.id",
"main-2-1"
]
},
{
title: "this is a NEW updated card",
id: "$$advcard.id"
},
"$$advcard"
]
}
}
},
unit: "$$adv.unit"
}
}
}
}
}
],
{
new: true,
});
First with use the update method passing three parameters:
Filter query
Aggregation pipeline
Options. Here I just used new: true to return the updated document and make it easier to test.
This is the structure:
db.collection.update({
"cards.advanced.unit": 2
},
[
// Pipeline
],
{
new: true,
});
Inside the pipeline we only need one stage, the $set to replace the property advanced with an array we will create.
...
[
{
$set: {
"cards.advanced": {
// Our first map
}
}
}
]
...
We first map the advanced array to be able to map the nested cards array after:
...
[
{
$set: {
"cards.advanced": {
$map: {
input: "$cards.advanced",
as: "adv",
in: {
// Here we will map the nested array
}
}
}
}
}
]
...
We use the variable we declared on the first map and which contains the advanced array current item being mapped ( adv ) to access and map the nested "cards" array ( $$adv.cards ):
...
[
{
$set: {
"cards.advanced": {
$map: {
input: "$cards.advanced",
as: "adv",
in: {
cards: {
$map: {
input: "$$adv.cards",
as: "advcard",
in: {
// We place our condition to check for the chosen card here
}
}
},
unit: "$$adv.unit",
}
}
}
}
}
]
...
Lastly we check if the current card id is equal to the id being searched $eq: [ "$$advcard.id", "main-2-1" ] and return the new card if it matches or the current card:
...
{
$cond: [
{
$eq: [
"$$advcard.id",
"main-2-1"
]
},
{
title: "this is a NEW updated card",
id: "$$advcard"
},
"$$advcard"
]
}
...
Here is a working example of what is described:
https://mongoplayground.net/p/xivZGNeD8ng

Related

Return all values of nested arrays using string identifier

Given an object searchable, is there a simple way of returning all the id values using lodash or underscore.js (or equivalent) where I can define the path to id?
const searchable = {
things: [
{
id: 'thing-id-one',
properties: [
{ id: 'd1-i1' },
{ id: 'd1-i2' },
]
},
{
id: 'thing-id-two',
properties: [
{ id: 'd2-i1' },
{ id: 'd2-i2' },
]
}
]
}
I am looking to see if this is possible in a manner similar to how we can use lodash.get e.g. if we wanted to return the things array from searchable we could do
const things = _.get(searchable, 'things');
I can't seem to find anything similar in the documentation. I am looking for something
that could contain an implementation similar to:
_.<some_function>(searchable, 'things[].properties[].id')
Note: I am well aware of functions like Array.map etc and there are numerous ways of extracting the id property - it is this specific use case that I am trying to figure out, what library could support passing a path as a string like above or does lodash/underscore support such a method.
Found a solution using the package jsonpath
const jp = require('jsonpath');
const result = jp.query(searchable, '$.things[*].properties[*].id')
console.log(result);
// outputs: [ 'd1-i1', 'd1-i2', 'd2-i1', 'd2-i2' ]
you can do it easily in plain js
like this
const searchable = {
things: [
{
id: 'thing-id-one',
properties: [
{ id: 'd1-i1' },
{ id: 'd1-i2' },
]
},
{
id: 'thing-id-two',
properties: [
{ id: 'd2-i1' },
{ id: 'd2-i2' },
]
}
]
}
const search = (data, k) => {
if(typeof data !== 'object'){
return []
}
return Object.entries(data).flatMap(([key, value]) => key === k ? [value]: search(value, k))
}
console.log(search(searchable, 'id'))
_.map and _.flatten together with iteratee shorthands let you expand nested properties. Every time you need to expand into an array, just chain another map and flatten:
const searchable = {
things: [
{
id: 'thing-id-one',
properties: [
{ id: 'd1-i1' },
{ id: 'd1-i2' },
]
},
{
id: 'thing-id-two',
properties: [
{ id: 'd2-i1' },
{ id: 'd2-i2' },
]
}
]
}
// Let's say the path is "things[].properties[].id"
const result = _.chain(searchable)
.get('things').map('properties').flatten()
.map('id').value();
console.log(result);
<script src="https://cdn.jsdelivr.net/npm/underscore#1.13.4/underscore-umd-min.js"></script>

Update multiple elements of array in mongodb document with appropriate element of javascript array

I have array of objects with following structure:
const myArr = [{
'name':'question1',
'grade':6
},
{
'name':'question2',
'grade':7
}]
Question collection:
{
_id:623749f845844e7d273d801c,
questions:[
{
'name':'question1',
'grade':10,
'someInfo':'blabla',
_id:623749f845844e7d273d801m
},
{
'name':'question2',
'grade':10,
'someInfo':'blabla',
_id:623749f845844e7d273d801a
},
{
'name':'question3',
'grade':10,
'someInfo':'blabla',
_id:623749f845844e7d273d801f
}
]
}
I just want to update all objects in array questions in collection which i have provided in myArr array. So the desired result should be:
{
_id:623749f845844e7d273d801c,
questions:[
{
'name':'question1',
'grade':6,
'someInfo':'blabla',
_id:623749f845844e7d273d801m
},
{
'name':'question2',
'grade':7,
'someInfo':'blabla',
_id:623749f845844e7d273d801a
},
{
'name':'question3',
'grade':10,
'someInfo':'blabla',
_id:623749f845844e7d273d801f
}
]
}
What i've found so far:
I managed to update grade when i specify the question name with positional operator:
await Prijave.updateOne(
{
_id: mongoose.Types.ObjectId(q_id),
'questions.name': 'question1',
},
{
$set: {
'questions.$.grade': '10',
},
}
)
But how to do this for every element in myArr ? I could make forEach loop which iterates through myArrand call updateOne for every element but i dont think it should be done that way because it will make multiple connection to database instead of one.

Create or update object if exists in a nested array

The following code works great in that it updates the object in the nested array.
However, I'm struggling to find a way to push a new object (Ex. {"locale" : "ar" , value:"مرحبا"}) if locale does not exist or update value if locale already exists (Ex. {"locale" : "en" , value:"hello"})
Update code:
Project.findOneAndUpdate(
{_id:projectId, 'sections._id': sectionId},
{ "$set": { "sections.$.subheader": {"locale":args.lang,"value":args.title} }},
{ upsert : true, new: true, useFindAndModify: false },
(err, section) => {
}
)
Object structure:
"project": {
"name": "project name",
"sections": [
{
"subheader": [{
'locale' : "en",
'value' : "Helle"
},
{
'locale' : "fr",
'value' : "salut"
}]
}
]
}
Unfortunately, this is not possible to do in one go. The upsert option only works on objects in the collection, not on nested objects.
You could solve this by first trying to update the element in the array, then check if the object in the nested array was matched. If there was no match, you can insert it into the nested array using $addToSet.
Additionally, you need to use positional operators to match the nested arrays:
Project.findOneAndUpdate(
// match item in subheader array
{ _id: projectId, 'sections._id': sectionId, 'sections.subheader.locale': args.lang },
// update existing item in subheader array
{ "$set": { "sections.$[section].subheader.$[subheader].value": args.title } },
// we use arrayFilters here, don't use upsert now
{ arrayFilters: [{ 'section._id': sectionId }, { 'subheader.locale': args.lang }], useFindAndModify: false },
(err, section) => {
// check if section was found
if (!section) {
// add new object to array if it wasn't found yet
Project.findOneAndUpdate(
// match section
{ _id: projectId, 'sections._id': sectionId},
// add new object to array
{ "$addToSet": { "sections.$.subheader": {"locale": args.lang,"value": args.title } }},
(err, section) => {
console.log('created new locale')
}
)
} else {
console.log('updated existing locale')
}
}
)

Updated nested object by matching ID

I have an array with nested objects that I need to update from another array of objects, if they match.
Here is the data structure I want to update:
const invoices = {
BatchItemRequest: [
{
bId: "bid10",
Invoice: {
Line: [
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "10110" },
},
},
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "11110" },
},
Amount: 2499,
},
],
},
},
{
bId: "bid10",
Invoice: {
Line: [
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "10110" },
},
},
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "10111" },
},
Amount: 2499,
},
],
},
},
],
};
Here is the array of objects I want to update it from:
const accounts = [
{ AccountCode: "10110", Id: "84" },
{ AccountCode: "11110", Id: "5" },
{ AccountCode: "10111", Id: "81" },
];
I want to update invoices, using accounts, by inserting Id if AccountCode matches, to get the following structure:
const invoices = {
BatchItemRequest: [
{
bId: "bid10",
Invoice: {
Line: [
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "10110", Id: "84" },
},
},
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "11110", Id: "5" },
},
Amount: 2499,
},
],
},
},
{
bId: "bid10",
Invoice: {
Line: [
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "10110", Id: "84" },
},
},
{
SalesItemLineDetail: {
ItemAccountRef: { AccountCode: "10111", Id: "81" },
},
Amount: 2499,
},
],
},
},
],
};
I have tried various methods, such as the following:
const mapped = invoices.BatchItemRequest.map((item1) => {
return Object.assign(
item1,
accounts.find((item2) => {
return item2 && item1.Invoice.Line.ItemAccountRef.AccountCode === item2.AccountCode;
})
);
});
Problem with this approach (it doesn't work as I think I need to do another nested map), but it also creates a new array, only including the nested elements of invoices.
Does anyone know a good approach to this?
This isn't the cleanest of code but it gets the job done:
function matchInvoiceWithAccount(invoices, accounts) {
const mappedInvoices = invoices.BatchItemRequest.map((request) => {
// Shouldn't modify input parameter, could use Object.assign to create a copy and modify the copy instead for purity
request.Invoice.Line = request.Invoice.Line.map((line) => {
const accountCode = line.SalesItemLineDetail.ItemAccountRef.AccountCode;
// If accounts was a map of AccountCode to Id you would't need to search for it which would be more effective
const account = accounts.find((account) => account.AccountCode === accountCode);
if (account) {
line.SalesItemLineDetail.ItemAccountRef.Id = account.Id;
}
return line;
});
return request;
});
return {
BatchItemRequest: mappedInvoices,
};
}
What you could and probably should do to improve this is to not modify the input parameters of the function, but that requires that you in a better way copy the original, either using Object.assign or spread operator.
At first, it will be good to create Map from your accounts array. We will go one time for array with O(n) and then will read ids by code with O(1). And nested fors is O(m*n), that will be much more slower at big arrays.
const idsByAccountCodes = new Map();
accounts.forEach((data) => {
idsByAccountCodes.set(data.AccountCode, data.Id);
})
or shorter:
const idsByAccountCode = new Map(accounts.map((data) => [data.AccountCode, data.Id]))
then if you want to mutate original values you can go through all nesting levels and add values
for ( const {Invoice:{ Line: line }} of invoices.BatchItemRequest){
for ( const {SalesItemLineDetail: {ItemAccountRef: item}} of line){
item.Id = idsByAccountCodes.get(item.AccountCode) || 'some default value'
// also if you don't have ids for all codes you need to define logic for that case
}
}
If you don't need to mutate original big object "invoices" and all of nested objects, then you can create recursive clone of if with something like lodash.cloneDeep

How to build tree array from flat array of object with category and subCategrie properties

I am trying to build tree array from flat array, each item in the flat array has two property need to be used to build the tree array, they are 1. category. 2. subCategrie which is array of string.
let data = [
{
id: 1,
name: "Zend",
category: "php",
subCategory: ["framework"]
},
{
id: 2,
name: "Laravel",
category: "php",
subCategory: ["framework"]
},
{
id: 3,
name: "Vesion 5",
category: "php",
subCategory: ["versions"]
},
{
id: 4,
name: "Angular",
category: "frontend",
subCategory: ["framework", "typescript"]
},
{
id: 5,
name: "Aurelia",
category: "frontend",
subCategory: ["framework", "typescript"]
},
{
id: 6,
name: "JQuery",
category: "frontend",
subCategory: []
}
];
It should be
let tree = [
{
name: "php",
children: [
{
name: "framework",
children: [
{
id: 1,
name: "Zend"
},
{
id: 2,
name: "Laravel"
}
]
},
{
name: "versions",
children: [
{
id: 3,
name: "Vesion 5"
}
]
}
]
}
// ...
];
Is there any article, link solving similar problem?
I gave it many tries but stuck when trying to build the sub categories children.
Here's my last attempt which throws error and I know it's wrong but it's for the ones who want to see my attempts
const list = require('./filter.json')
let tree = {};
for (let filter of list) {
if (tree[filter.category]) {
tree[filter.category].push(filter);
} else {
tree[filter.category] = [filter];
}
}
function buildChildren(list, subcategories, category, index) {
let tree = {}
for (let filter of list) {
if (filter.subcategory.length) {
for (let i = 0; i < filter.subcategory.length; i++) {
let branch = list.filter(item => item.subcategory[i] === filter.subcategory[i]);
branch.forEach(item =>{
if (tree[filter.subcategory[i]]){
tree[filter.subcategory[i]] = tree[filter.subcategory[i]].push(item)
}else{
tree[item.subcategory[i]] = [item]
}
})
}
}
}
console.log('tree ', tree);
}
Heads up, For javascript I usually use Lodash (usually written as _ in code) but most of these methods should also be built in to the objects in javascript (i.e. _.forEach = Array.forEach())
const tree = [];
// First Group all elements of the same category (PHP, Frontend, etc.)
data = _.groupBy(data, 'category');
_.forEach(data, function (categoryElements, categoryName) {
// Each Category will have it's own subCategories that we will want to handle
let categorySubCategories = {};
// The categoryElements will be an array of all the objects in a given category (php / frontend / etc..)
categoryElements.map(function (element) {
// For each of these categoryies, we will want to grab the subcategories they belong to
element.subCategory.map(function (subCategoryName) {
// Check if teh category (PHP) already has already started a group of this subcategory,
// else initialize it as an empty list
if (!categorySubCategories[subCategoryName]) { categorySubCategories[subCategoryName] = []; }
// Push this element into the subcategory list
categorySubCategories[subCategoryName].push({id: element.id, name: element.name});
});
});
// Create a category map, which will be a list in the format {name, children}, created from
// our categorySubCategories object, which is in the format {name: children}
let categoryMap = [];
_.forEach(categorySubCategories, function (subCategoryElements, subCategoryName) {
categoryMap.push({name: subCategoryName, children: subCategoryElements});
});
// Now that we've grouped the sub categories, just give the tree it's category name and children
tree.push({name: categoryName, children: categoryMap});
});
};
The key to success here is to create an interim format that allows for easy lookups. Because you work with children arrays, you end up having to use filter and find whenever you add something new, to prevent duplicates and ensure grouping.
By working with a format based on objects and keys, it's much easier to do the grouping.
We can create the groups in a single nested loop, which means we only touch each item once for the main logic. The group has this format:
{ "categoryName": { "subCategoryName": [ { id, name } ] } }
Then, getting to the required { name, children } format is a matter of one more loop over the entries of this tree. In this loop we move from { "categoryName": catData } to { name: "categoryName", children: catData }
Here's an example that shows the two steps separately:
const data=[{id:1,name:"Zend",category:"php",subCategory:["framework"]},{id:2,name:"Laravel",category:"php",subCategory:["framework"]},{id:3,name:"Vesion 5",category:"php",subCategory:["versions"]},{id:4,name:"Angular",category:"frontend",subCategory:["framework","typescript"]},{id:5,name:"Aurelia",category:"frontend",subCategory:["framework","typescript"]},{id:6,name:"JQuery",category:"frontend",subCategory:[]}];
// { category: { subCategory: [ items ] } }
const categoryOverview = data.reduce(
(acc, { id, name, category, subCategory }) => {
// Create a top level group if there isn't one yet
if (!acc[category]) acc[category] = {};
subCategory.forEach(sc => {
// Create an array for this subCat if there isn't one yet
acc[category][sc] = (acc[category][sc] || [])
// and add the current item to it
.concat({ id, name });
});
return acc;
},
{}
)
const nameChildrenMap = Object
.entries(categoryOverview)
// Create top level { name, children } objects
.map(([cat, subCats]) => ({
name: cat,
children: Object
.entries(subCats)
// Create sub level { name, children } objects
.map(([subCat, items]) => ({
name: subCat,
children: items
}))
}))
console.log(nameChildrenMap);

Categories

Resources