MongoDB update nested object dependent on json input - javascript

I have json data that is structured in the following form:
[
{"size":100,"year":2015,"geography":"London","age":"21","gender":"Female"},
{"size":80,"year":2015,"geography":"Cardiff","age":"38","gender":"Male"},
{"size":80,"year":2013,"geography":"Edinburgh","age":"36","gender":"All"}
]
And I am trying to add it to a database collection with the following schema:
const Schema = new Schema({
geography: String,
size: {
2013: {
male: {
age: {
}
}
female: {
age: {
}
}
all: {
age: {
}
}
}
}
});
I'm currently trying to set up my update to work in the following way:
query = { geography : geography};
update = { geography : geography, $set: { "year.gender.age" : size}
Schema.updateOne( query, update, { upsert: true };
The $set here obviously does not work as the update does not know to pull the values from the json data. I have tried making year, gender and age variables and then refactoring the set (see below) but this does not work either.
$set: { `${year}.${gender{.$age}` }
So the question is how can I use the values in my JSON data to determine which embedded field to update?

The better option here would be a different schema which is not dynamic, as suggested in this answer.
Nonetheless, if you are to go with the current schema, you can map the values in the JSON as follows:
For example, if you have an object like
const obj = {
"size": 100,
"year": 2015,
"geography": "London",
"age": "21",
"gender": "Female"
}
to transform it to an update object of the form
{
"geography": "London",
"2015.female.21": 100
}
requires the following map:
const obj = {
"size": 100,
"year": 2015,
"geography": "London",
"age": "21",
"gender": "Female"
}
const doc = Object.keys(obj).reduce((acc, curr) => {
if (curr === "geography") {
acc["geography"] = obj[curr];
return acc;
};
switch(curr) {
case "year":
case "gender":
case "age":
acc[`${obj['year']}.${obj['gender'].toLowerCase()}.${obj['age']}`] = obj['size']
}
return acc;
}, {});
console.log(JSON.stringify(doc, null, 4));
which you can then use in your update operation as
Model.updateOne( query, { '$set': doc }, { upsert: true }, callback );

in order to create object like this-
{"size":100,"year":2015,"geography":"London","age":"21","gender":"Female"}
I think you need to set your schema to:
const Schema = new Schema({
geography: String,
size: Number,
year,: String,
gender: String,
age: String
});
Then to update use something like:
update = { {$and: [{year:"2015"}, {age:"33"} /*and more*/]}, $set: { size : size}

Related

Update a nested object in JavaScript

This is the original data
const data = {
"field1": {
"name": 'Anuv',
"marks": {
"eng": 43,
"hindi": 23
},
"age": 21
},
"field2": {
"school": 'DAV'
}
}
I am trying to update the name
const updatedValue = {
"field1": {
"name": "Anuv Gupta"
}
}
This is the expected data. It should have all the field and the updated name value as well.
const expectedData = {
"field1": {
"name": 'Anuv Gupta',
"marks": {
"eng": 43,
"hindi": 23
},
"age": 21
},
"field2": {
"school": 'DAV'
}
}
I have tried using these
expectedData = Object.assign({}, data, updatedValue)
as well as
expectedData = { ...data, ...updatedValue },
both of them returns this object
const obj = {
"field1": {
"name": 'Anuv Gupta',
},
"field2": {
"school": 'DAV'
}
}
How do I fix this and get the expectedData object?
If you don't care about mutating your original data you can just do:
data.field1.name = 'Anuv Gupta';
console.log(data);
If you don't want your original data to mutate, just clone it first and do the new value assignment:
const dataClone = structuredClone(data);
dataClone.field1.name = 'Anuv Gupta';
console.log(dataClone);
Edit:
Like others suggested you can also achieve that by spreading your data object into a new one like so:
const newData = {
...data,
field1: {
...data.field1,
name: 'Anuv Gupta',
}
}
It works but it is not mutation proof as it only shallow clones the original data object - You can read more about shallow vs deep clone in this great blog post. if you care about not mutating your original data, I would use the second option I mentioned in my answer
Avi's answer is good. Just to add one more method that is strictly immutable, you could do the following:
const expectedData = {
...data,
"field1": {
...data.field1,
"name": 'Anuv Gupta',
}
}
You can access the name property directly:
data.field1.name = "new value"
If you're trying to avoid mutating the original data obj you can try:
data2 = Object.assign({}, data);
data2.field1.name = "new value"
just change it directly using an Object property
data.field1.name = 'anuv Gupta'
and don't use quotes for object keys just do this and it works just fine
const obj = {
field1: {
name: 'Anuv Gupta',
},
field2: {
"school": 'DAV'
}
}

MongoDB get ArrayFilters reference to update value

I recently started using MongoDB and I'm trying to update a few properties from a document but not being able to get the object reference to update a value.
Please consider the following data:
const data = {
weekplanId: 'someid',
days: [
{label: 'Monday', cost: 20, comment: "some comment" },
{label: 'Tuesday', cost: 40, comment: "..." }
]
}
const update = await weekplan.updateOne(
{
_id: new ObjectId(data.weekplanId),
},
{
$set: {
"weekdays.$[i].cost": data.days.$[i].cost,
"weekdays.$[i].comment": data.days.$[i].comment,
"weekdays.$[i].date": new Date(),
"weekdays.$[i].someproperty": "something",
}
},
{
arrayFilters: [
{
"i.label": {
$in: data.days.map(p => p.label),
},
}]
}
);
How can I reference the array object to set the property value?
I know data.days.$[i].cost and data.days.$[i].comment are wrong, they are just an example of what I'm trying to achieve.
Setting the date and someproperty works as expected, since the values are not dependent on the source data.
I would like to try to do this without using JS.
Is arrayFilters even appropriate for this? I'd appreciate some guidance as to where to look at.
Thanks in advance!
###EDIT:
Expected output:
"_id": {"someid"},
"weekdays": [
{
"day": "Monday",
"cost": 20,
"comment": "some comment",
"date": 2021-08-01T19:51:45.057Z
"someproperty": "something"
},
{
"day": "Tuesday",
"cost": 40,
"comment": "...",
"date": 2021-08-01T19:51:45.057Z
"someproperty": "something"
},
...
...
...
]
The rest of the week days (Wednesday, Thursday, Friday) would remain untouched on this update.
In that example code data.days.$[i].cost is evaluated on the client side, before the query is submitted, so whatever value (or lack thereof) that is has will be assigned to the corresponding field of the $set object when the server receives it.
The data object will not be sent to the server with the query, so even if it were able to do array lookup on input values, the input value would not be there.
The way to accomplish this is to iterate the array on the client side, and programmatically build the update query. Perhaps something like:
let labelChar = 105;
let setOps = {};
let filters = {};
data.days.forEach( function(day) {
let char = String.fromCharCode(labelChar++);
setOps['weekdays.$[' + char + '].cost'] = day.cost;
setOps['weekdays.$[' + char + '].comment'] = day.comment;
setOps['weekdays.$[' + char + '].date'] = new Date();
setOps['weekdays.$[' + char + '].someproperty'] = "something";
let filterObj = {};
filterObj[char + '.label'] = day.label;
filters.push(filterObj);
});
const update = await weekplan.updateOne(
{
_id: new ObjectId(data.weekplanId),
},
{
$set: setOps
},
{
arrayFilters: filters
}
);
For the provided sample input, this will give the update:
.updateOne(
{
_id: new ObjectId(data.weekplanId),
},
{
$set: {
'weekdays.$[i].cost': 20,
'weekdays.$[i].comment': 'some comment',
'weekdays.$[i].date': ISODate(),
'weekdays.$[i].someproperty': 'something',
'weekdays.$[j].cost': 40,
'weekdays.$[j].comment': '...',
'weekdays.$[j].date': ISODate(),
'weekdays.$[j].someproperty': 'something'
}
},
{
arrayFilters: [
{'i.label': 'Monday'},
{'j.label': 'Tuesday'}
]
}
);

Is there a better way to iterate through an object of unknown depth than using map statements nested to the maximum possible depth?

I have nested employee objects of different, unknown depths. Each object has a children property, which is an array of the employee objects who report to that employee. These child objects have the same attributes as the top level object and may or may not have employee objects in their own children property.
I need to go through each employee object's array of employee objects and add each of those objects to one of two different arrays, depending on whether the object has other employee objects in it's own "children" property. These arrays are also properties of the employee objects. Employees with empty "children" arrays will be added to their parent employee's nonManagersUnder array, while those with objects in their children array will be added to the managersUnder array.
The nested employee objects look like this:
{
id: "n1",
"isActive": true,
age: 38,
name: "Barb Smith",
"phone": "+1 (882) 547-3581",
"hired": "2016-08-08T12:46:19 +07:00",
children: [
{
id: "n10",
"isActive": true,
age: 37,
name: "Elsie MacDonald",
"phone": "+1 (958) 558-2389",
"hired": "2015-08-15T04:44:49 +07:00",
children: [
]
},
{
id: "n11",
"isActive": true,
age: 29,
name: "Peter Chen",
"phone": "+1 (881) 574-3927",
"hired": "2015-02-16T12:11:11 +08:00",
children: [
]
},
{
id: "n12",
"isActive": true,
age: 32,
name: "Ty Wilder",
"phone": "+1 (990) 506-2830",
"hired": "2019-09-17T06:29:16 +07:00",
children: [
]
}
}
This is a very simple example since I didn't want to put something several hundred lines long in my post, but the structure is the same. Just imagine that each of the secondary employee objects has its own children.
You'll notice that the nonManagersUnder and managersUnder arrays are not attributes of the employee objects to start with. That is because in my current solution they are dynamically assigned.
Here is that solution:
countManagers = (employee) => {
let midManagers = []
let nonManagers = []
employee.children.map(child =>{
if(child.children.length == 0) {
nonManagers.push(child);
}else {
midManagers.push(child);
child.children.map(grandChild => {
if(grandChild.children.length == 0){
nonManagers.push(grandChild);
}else {
midManagers.push(grandChild);
grandChild.children.map(greatGrand => {
if(greatGrand.children.length == 0){
nonManagers.push(greatGrand)
} else {
midManagers.push(greatGrand);
greatGrand.children.map(grand3 => {
if(grand3.children.length==0){
nonManagers.push(grand3);
} else {
midManagers.push(grand3);
grand3.children.map(grand4 => {
if(grand4.children.length==0){
nonManagers.push(grand4);
} else {
midManagers.push(grand4);
}
})
}
})
}
})
}
})
}
})
console.log(midManagers);
// console.log(nonManagers);
employee.managersUnder = (midManagers);
employee.nonManagersUnder=(nonManagers)
}
As you can see, it is simply nested map operators and some conditionals, nested to the maximum depth an employee object can be nested. This solution does work, but is very ugly and I'm almost certain there is a better way of doing this. A better solution would work for an object of any depth. This only works for objects where the depth is equal to or less than the number of nested map operators.
I wanted to refresh some recursion stuffs and came out with a solution for you query.
const values = [{
id: "n1",
children: [{
id: "n10",
children: [{
id: "n100",
children: []
}, ]
},
{
id: "n11",
children: []
},
{
id: "n12",
children: []
}
]
}]
const getAllManagers = (employees) => {
return employees.reduce((acc, emp) => {
return acc.concat(emp.children.length > 0 ? [emp, ...getAllManagers(emp.children)] : [])
}, [])
}
const getAllNonManagers = (employees) => {
return employees.reduce((acc, emp) => {
return acc.concat(emp.children.length > 0 ? getAllNonManagers(emp.children) : emp)
}, [])
}
console.log("Managers: ", getAllManagers(values))
console.log("NonManagers:", getAllNonManagers(values))

Filter out objects in array if one value is the same

I am fetching data from an api that, sometimes, gives me multiple objects with the same values, or very similar values, which I want to remove.
For example, I might get back:
[
{
"Name": "blah",
"Date": "1992-02-18T00:00:00.000Z",
"Language": "English",
},
{
"Name": "blahzay",
"Date": "1998-02-18T00:00:00.000Z",
"Language": "French",
}, {
"Name": "blah", // same name, no problem
"Date": "1999-02-18T00:00:00.000Z", // different date
"Language": "English", // but same language
},
]
So I want to check that no two objects have a key with the same "Language" value (in this case, "English").
I would like to get the general process of filtering out the entire object if it's "Language" value is duplicated, with the extra issue of not having the same number of objects returned each time. So, allowing for dynamic number of objects in the array.
There is an example here:
Unexpeected result when filtering one object array against two other object arrays
but it's assuming that you have a set number of objects in the array and you are only comparing the contents of those same objects each time.
I would be looking for a way to compare
arrayName[eachObject].Language === "English"
and keep one of the objects but any others (an unknown number of objects) should be filtered out, most probably using .filter() method along with .map().
The below snippets stores the languages that have been encountered in an array. If the current objects language is in the array then it is filtered out. It makes the assumption that the first object encountered with the language is stored.
const objs = [
{
"Name": "blah",
"Date": "1992-02-18T00:00:00.000Z",
"Language": "English",
},
{
"Name": "blahzay",
"Date": "1998-02-18T00:00:00.000Z",
"Language": "French",
}, {
"Name": "blah", // same name, no problem
"Date": "1999-02-18T00:00:00.000Z", // different date
"Language": "English", // but same language
},
],
presentLanguages = [];
let languageIsNotPresent;
const objsFilteredByLanguage = objs.filter(function (o) {
languageIsNotPresent = presentLanguages.indexOf(o.Language) == -1;
presentLanguages.push(o.Language);
return languageIsNotPresent;
});
console.log(objsFilteredByLanguage);
You could take a hash table and filter the array by checking Name and Language.
var array = [{ Name: "blah", Date: "1992-02-18T00:00:00.000Z", Language: "English" }, { Name: "blahzay", Date: "1998-02-18T00:00:00.000Z", Language: "French" }, { Name: "blah", Date: "1999-02-18T00:00:00.000Z", Language: "English" }],
hash = {},
result = array.filter(({ Name, Language }) => {
var key = `${Name}|${Language}`;
if (!hash[key]) return hash[key] = true;
});
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Using Set makes it easy to remove duplicates for as many keys as you like. I tried to be as verbose as possible so that each step was clear.
var objects = [{ "Name": "blah", "Date": "1992-02-18T00:00:00.000Z", "Language": "English", }, { "Name": "blah", "Date": "1998-02-18T00:00:00.000Z", "Language": "French", }, { "Name": "blah", "Date": "1999-02-18T00:00:00.000Z", "Language": "English" }];
function uniqueKeyVals(objects, key) {
const objVals = objects.map(object => object[key]); // ex. ["English", "French", "English"]
return objects.slice(0, new Set(objVals).size); // ex. { "English", "French" }.size = 2
}
function removeKeyDuplicates(objects, keys) {
keys.forEach(key => objects = uniqueKeyVals(objects, key));
return objects;
}
// can also use uniqueKeyVals(key) directly for just one key
console.log("Unique 'Language': \n", removeKeyDuplicates(objects, ["Language"]));
console.log("Unique ['Language', 'Name']: \n", removeKeyDuplicates(objects, ["Language", "Name"]));
I would use the underscore module for JavaScript and the unique function in this scenario. Here is a sample array of data objects:
let data = [{
name: 'blah',
date: Date.now(),
language: "en"
},
{
name: 'noblah',
date: Date.now(),
language: 'es'
},
{
name: 'blah',
date: Date.now(),
language: 'en'
}];
Then we can use the unique function in the underscore library to only return a copy of the data that has unique values associated with the language key:
const result = _.unique(data, 'language');

Postman get value from JSON where equals a value in array using javascript

Currently using the latest version of Postman: 6.7.4 (Latest)
I'm trying to get a value out of a JSON response body and store it in an environment variable BUT the value 'username' should be equal to my preferred username.
Normally I would extract a value like this:
var jsonData = pm.response.json();
pm.environment.set("useridToken", jsonData.Customers[0].userid);
This would give me the first item in the list but I do not wish to obtain the first nor the second item from the list. I wish to obtain the userid where username EQUAL "Billy" for example.
Output of the body response:
{
"Customers": [
{
"id": 24,
"userid": 73063,
"username": "BOB",
"firstname": "BOB",
"lastname": "LASTNAME
},
{
"id": 25,
"userid": 73139,
"username": "Billy",
"firstname": "Billy",
"lastname": "lasty"
}
]
}
Any tips?
I remember in SoapUI it was like this:
$.channels[?(#.is_archived=='false')].id[0]
I guess it's not possible to do this in JS in Postman?
You can use: Array.prototype.find():
const data = {
"Customers": [{
"id": 24,
"userid": 73063,
"username": "BOB",
"firstname": "BOB",
"lastname": "LASTNAME"
},
{
"id": 25,
"userid": 73139,
"username": "Billy",
"firstname": "Billy",
"lastname": "lasty"
}
]
}
const user = data.Customers.find(u => u.username === 'Billy')
const userid = user ? user.userid : 'not found'
console.log(user)
console.log(userid)
find() as another answer points out is the best solution here, but if the username is not unique and you want an array of users where username is 'Billy' then use filter()
const jsonData = {
"Customers": [{
"id": 24,
"userid": 73063,
"username": "BOB",
"firstname": "BOB",
"lastname": "LASTNAME"
},
{
"id": 25,
"userid": 73139,
"username": "Billy",
"firstname": "Billy",
"lastname": "lasty"
}
]
}
console.log(jsonData.Customers.filter(c => c.username === 'Billy'))
In Postnam test script, you can use some Javascript features. In your case, too many way to do.
I will show you how to solve your case with Array.find function:
var jsonData = pm.response.json();
var user = jsonData.Customers.find(function(user) {
return user.username === 'Billy';
// OR you could config username in postman env
// return user.username === pm.variables.get("username_to_find");
});
pm.environment.set("useridToken", user.userid);
Your userid can also be obtained using filter as follows -
const data = {
"Customers": [{
"id": 24,
"userid": 73063,
"username": "BOB",
"firstname": "BOB",
"lastname": "LASTNAME"
},
{
"id": 25,
"userid": 73139,
"username": "Billy",
"firstname": "Billy",
"lastname": "lasty"
}
]
};
const username = 'Billy';
const user = data.Customers.filter(obj => obj.username.toLowerCase() === username.toLowerCase())[0];
const userid = user ? user['userid'] : null;
console.log(userid);
Note: .toLowerCase() is optional here, you may use it depending on your condition.
Then you could simply set it as -
pm.environment.set("useridToken", userid);
This answer is inspired by the
other answer that outputs an array.
1
It is not clearly stated by the original poster whether the desired output
should be a single userid (presumably the first occurence?) - or
an array containing all userid:s matching "Billy".
This answer shows a solution to the latter case by using
Lodash.
const jsonData = {
Customers: [{
userid: 73063,
username: 'BOB'
}, {
userid: 73138,
username: 'Billy'
}, {
userid: 74139,
username: 'Billy'
}]
};
const userIds = [];
_.forEach(_.filter(jsonData.Customers, c => c.username === 'Billy'),
item => { userIds.push(item.userid); });
console.log(userIds);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.19/lodash.js"></script>
1 That answer is quite helpful as it hints how to filter out
the relevant objects of the Customers array. However, the original poster
wants (an array of) the userid(s) which is a number, and not an an array
of objects that contains the userid:s. This is how my answer here is
different.
Try this
const userid = data.Customers.find(u => u.username === 'Billy') || 'not found';
This answer shows a solution using the JavaScript library
Lodash.
This is not meant as a recommendation, but merely to prove that it is
possible to use Lodash.
It is inspired by
the other lodash answer.1
const jsonData = {
Customers: [{
id: 24,
userid: 73063,
username: 'BOB',
firstname: 'BOB',
lastname: 'LASTNAME'
}, {
id: 25,
userid: 73139,
username: 'Billy',
firstname: 'Billy',
lastname: 'lasty'
}]
};
const userId_Lodash = (name) => {
let userId;
_.forEach(jsonData.Customers, (item) => {
if (item.username === name) { userId = item.userid; }
});
return userId;
};
console.log('Lodash loop, userid of "Billy": ' + userId_Lodash('Billy'));
console.log('Lodash loop, userid of "Dave": ' + userId_Lodash('Dave'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.19/lodash.js"></script>
For the question at hand, I don't see any particular reason to use the Lodash
library.
But in other examples it could make all the more sense.
1 The posted question does not state whether the desired outcome is
the first userid matching Billy, or all such userid:s.
This answer gives the first hit.
Repeating
the currently highest voted answer,
slightly modified.
Also adding a solution inspired by
the other Lodash answer.1
const jsonData = {
Customers: [{
id: 24,
userid: 73063,
username: 'BOB',
firstname: 'BOB',
lastname: 'LASTNAME'
}, {
id: 25,
userid: 73139,
username: 'Billy',
firstname: 'Billy',
lastname: 'lasty'
}]};
for (const i in jsonData.Customers) {
console.log('userid of customer['+i+']: '+jsonData.Customers[i].userid);
}
const userId_UsingFind = (name) => {
const user = jsonData.Customers.find(item => item.username === name);
return user ? user.userid : user;
};
console.log('Using .find(), userid of "Billy": '+userId_UsingFind('Billy'));
console.log('Using .find(), userid of "Joe": '+userId_UsingFind('Joe'));
const userId_Native = (name) => {
for (const i in jsonData.Customers) {
if (jsonData.Customers[i].username === name) {
return jsonData.Customers[i].userid;
}
}
};
console.log('Native loop, userid of "Billy": '+userId_Native('Billy'));
console.log('Native loop, userid of "Joe": '+userId_Native('Joe'));
As the code shows, the solution using .find() is both short and elegant.
1 Assuming the desired outcome is the userid of the first
Billy. To retrieve an array of userid:s for all occurrences
ofBilly, see the answer that returns an array of userid:s
.

Categories