Update field value in nested array object - javascript

I have objects that pretty much look like this:
{
...
someArray: [
{
id: 1548,
amount: 153,
done: 0
}
]
...
}
As these objects can become quite large, I can't just use set everytime I'm updating them, as sending 100kB everytime I need to update the document isn't an option.
In order to solve this, I decided to use update with what's called the "dots notation", example usage being :
update({
'a.b.c': true
})
Source: Difference between set with {merge: true} and update
So I decided to give it a try and it worked like a charm for "normal" nested fields, but I can't find how I can do this for objects that are nested inside arrays.
What I tried was this:
update({
'a.someArray.0.done': 153
})
update({
'a.someArray[0].done': 153
})
But both of these just erased the object and replaced it by the patch, meaning that the dots notation wasn't recognized properly.
How can I solve this? Is there a solution for this kind of approach or should I just refactor it using a subcollection?

I believe this could be what you are looking for:
var washingtonRef = db.collection("cities").doc("DC");
// Atomically add a new region to the "regions" array field.
washingtonRef.update({
regions: firebase.firestore.FieldValue.arrayUnion("greater_virginia")
});
https://firebase.google.com/docs/firestore/manage-data/add-data#update_elements_in_an_array

Related

Merge key/value into response.data inline?

This is probably a simple question, but my lack of JavaScript skills is probably preventing me from figuring out a one-liner to do this. Im using axios to request from an API, im getting a response back (This is for a "Todo" list app), however I need to merge another field into this response.
The snippet of code looks like this:
let mergedIsEditing = {...response.data, isEditing: false}
const lists = [ ...this.state.lists, mergedIsEditing ]
lists is basically what it sounds like (an array of lists), and the response.data is nothing more than an object with a title and description. I thought maybe using Object.assign but since im already spreading out this.state.lists im not sure how that would work. Essentially I just need to add the key/value pair isEditing: false onto that list coming in.
Not to encourage shorter code over readability, but to answer your question, this is how you could do the equivalent to your snippet in 1 line:
let mergedIsEditing = {...response.data, isEditing: false}
const lists = [ ...this.state.lists, mergedIsEditing ]
const lists = [ ...this.state.lists, {...response.data, isEditing: false} ]
Of course, you could prepend this.state.lists =, if that's the desired purpose.

How to find the changes between two arrays of objects (src and update), and apply the changes to the src array?

I have two arrays src and update. Both contain objects. Incoming JSON from an API, the objects in the arrays follow the same structure (see below).
I want to compare src against update and add/remove/update items in src that have represented changes in update.
My current process involves iterating both arrays and comparing items using JSON.stringify(), doing multiple passes. Pass one to add, pass two to delete, and pass three to detect changes for replacement (I replace the entire object rather than the fields).
Is there an easier/better way?
Is there a generic utility to help facilitate for this or must I write my own diff detection system?
EDIT: The objects inside the arrays look like (but not limited to):
[
{
id: String,
summary: String,
updates: [
{
date: String,
title: String
},
{ Repeated }
]
},
{ Repeated }
]
As Ali pointed out lodash _.isEqual is a good utility method for this.
Check out https://lodash.com/docs/4.17.4#isEqual
Lodash is really good for helper functions and dealing with data.
If I understand your question correctly both your 'updates' and 'src' array have the same structure - objects with two properties, 'date' and 'title'. You can compare them with nested loops. For example, something like this might be helpful...
updates.forEach(function(u) {
src.forEach(function(s) {
if (s.date === u.date && s.title === u.title) {
// You have a match
}
});
});
I presume you want to identify partial matches, so you can add/replace the if-condition in the inner loop and amend the src array accordingly...
if (s.date === u.date && s.title !== u.title) {
// same date, different title, so...
// change the title of the current
// item from the 'src' array;
s.title = u.title;
}
Or use if-else-if conditions as needed. Regardless, although it's not quite clear from your question which array should be the outer-loop and which should be the inner, the nested 'forEach' loops above will compare each item in the 'updates' array against every item in the 'src' array.
I hope that helped. :)

Trying to return a deep nested object with Lodash

I have not been able to find a solution for this on StackoverlFlow and i have been working on this issue for some time now. Its a bit difficult so please let me know if i should provide more information.
My problem:
I have a JSON object of Lands, each land has an ID and a Blocks array associated with it and that blocks array has block with ID's too.
Preview:
var Lands = [{
'LandId':'123',
'something1':'qwerty',
'something2':'qwerty',
'Blocks': [
{
'id':'456',
'LandId':'123'
},
{
'BlockId':'789',
'LandId':'123'
}
]
},
...More Land Objects];
Note: The data is not setup the way i would have done it but this was done along time ago and i have to work with what i have for right now.
I am trying to write a lodash function that will take the blockId's that i have and match them and return the landId from the Blocks.
so the end result would be a list of LandId's that where returned from the Blocks.
I was using something like this and it was returning no results:
selectedLand = function(Lands, landIDs){
return _.filter(Lands, function(land){
return land === landIDs[index];
});
};
I know i am going about this the wrong way and would like to know the appropriate way to approach this and solve it. any help is much appreciated.
selectedLand = function(Lands, landIDs){
return _.filter(Lands, function(land){
return land === landIDs[index];
});
};
Note that index lacks any definition in this scope, so this function will essentially never return true, barring a lucky(?) accident with an externally defined index. And anytime you call _.filter(someArr,function(){return false;}) you'll get []. Undefined index aside, this does strict comparison of a land object against (maybe) a string in landIDs
I'm a bit unclear on the exact requirements of your selection, so you can tailor this filter to suit your needs. The function filters the lands array by checking if the .blocks array property has some values where the .landID property is included in the landsID array.
In closing, if you want to make the most out of lodash (or my favorite, ramda.js) I suggest you sit down and read the docs. Sounds deathly boring, but 75% of the battle with data transforms is knowing what's in your toolbox. Note how the English description of the process matches almost exactly with the example code (filter, some, includes).
var Lands = [{
'LandId': '123',
'something1': 'qwerty',
'something2': 'qwerty',
'Blocks': [{
'id': '456',
'LandId': '123'
}, {
'BlockId': '789',
'LandId': '123'
}]
}
];
// assuming lands is like the above example
// and landIDs is an array of strings
var selectLandsWithBlocks = function(lands, landIDs) {
return _.filter(lands, function(land) {
var blocks = land.Blocks;
var blockHasALandId = function(block) {
return _.includes(landIDs,block.LandId);
};
return _.some(blocks, blockHasALandId);
});
};
console.log(selectLandsWithBlocks(Lands,[]));
console.log(selectLandsWithBlocks(Lands,['mittens']));
console.log(selectLandsWithBlocks(Lands,['123']));
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/4.13.1/lodash.min.js"></script>

extracting a list of 2 property values

I have an object array:
user :[
{
name: String,
username: String
}
]
I want to view every change either to name or username.
I found underscore _.pluck only does the trick for one property (_.pluck(user, 'name')
Is there another way to have the list of both values?
With pluck you can only use one property, it simply isn't made to retrieve multiple. The method you would want to use is map, as suggested in this relevant question + answer: How to pluck multiple attributes from a Backbone collection?
Assuming you want the following output [['nameA','usernameA'],['nameB','usernameB'],...]], you could use map in the following manner:
var myResult = users.map(function(user) {
return [user.name, user.username];
});
NOTE: I changed the variable user to users to make more sense with your data.

Updating a Nested Array with MongoDB

I am trying to update a value in the nested array but can't get it to work.
My object is like this
{
"_id": {
"$oid": "1"
},
"array1": [
{
"_id": "12",
"array2": [
{
"_id": "123",
"answeredBy": [], // need to push "success"
},
{
"_id": "124",
"answeredBy": [],
}
],
}
]
}
I need to push a value to "answeredBy" array.
In the below example, I tried pushing "success" string to the "answeredBy" array of the "123 _id" object but it does not work.
callback = function(err,value){
if(err){
res.send(err);
}else{
res.send(value);
}
};
conditions = {
"_id": 1,
"array1._id": 12,
"array2._id": 123
};
updates = {
$push: {
"array2.$.answeredBy": "success"
}
};
options = {
upsert: true
};
Model.update(conditions, updates, options, callback);
I found this link, but its answer only says I should use object like structure instead of array's. This cannot be applied in my situation. I really need my object to be nested in arrays
It would be great if you can help me out here. I've been spending hours to figure this out.
Thank you in advance!
General Scope and Explanation
There are a few things wrong with what you are doing here. Firstly your query conditions. You are referring to several _id values where you should not need to, and at least one of which is not on the top level.
In order to get into a "nested" value and also presuming that _id value is unique and would not appear in any other document, you query form should be like this:
Model.update(
{ "array1.array2._id": "123" },
{ "$push": { "array1.0.array2.$.answeredBy": "success" } },
function(err,numAffected) {
// something with the result in here
}
);
Now that would actually work, but really it is only a fluke that it does as there are very good reasons why it should not work for you.
The important reading is in the official documentation for the positional $ operator under the subject of "Nested Arrays". What this says is:
The positional $ operator cannot be used for queries which traverse more than one array, such as queries that traverse arrays nested within other arrays, because the replacement for the $ placeholder is a single value
Specifically what that means is the element that will be matched and returned in the positional placeholder is the value of the index from the first matching array. This means in your case the matching index on the "top" level array.
So if you look at the query notation as shown, we have "hardcoded" the first ( or 0 index ) position in the top level array, and it just so happens that the matching element within "array2" is also the zero index entry.
To demonstrate this you can change the matching _id value to "124" and the result will $push an new entry onto the element with _id "123" as they are both in the zero index entry of "array1" and that is the value returned to the placeholder.
So that is the general problem with nesting arrays. You could remove one of the levels and you would still be able to $push to the correct element in your "top" array, but there would still be multiple levels.
Try to avoid nesting arrays as you will run into update problems as is shown.
The general case is to "flatten" the things you "think" are "levels" and actually make theses "attributes" on the final detail items. For example, the "flattened" form of the structure in the question should be something like:
{
"answers": [
{ "by": "success", "type2": "123", "type1": "12" }
]
}
Or even when accepting the inner array is $push only, and never updated:
{
"array": [
{ "type1": "12", "type2": "123", "answeredBy": ["success"] },
{ "type1": "12", "type2": "124", "answeredBy": [] }
]
}
Which both lend themselves to atomic updates within the scope of the positional $ operator
MongoDB 3.6 and Above
From MongoDB 3.6 there are new features available to work with nested arrays. This uses the positional filtered $[<identifier>] syntax in order to match the specific elements and apply different conditions through arrayFilters in the update statement:
Model.update(
{
"_id": 1,
"array1": {
"$elemMatch": {
"_id": "12","array2._id": "123"
}
}
},
{
"$push": { "array1.$[outer].array2.$[inner].answeredBy": "success" }
},
{
"arrayFilters": [{ "outer._id": "12" },{ "inner._id": "123" }]
}
)
The "arrayFilters" as passed to the options for .update() or even
.updateOne(), .updateMany(), .findOneAndUpdate() or .bulkWrite() method specifies the conditions to match on the identifier given in the update statement. Any elements that match the condition given will be updated.
Because the structure is "nested", we actually use "multiple filters" as is specified with an "array" of filter definitions as shown. The marked "identifier" is used in matching against the positional filtered $[<identifier>] syntax actually used in the update block of the statement. In this case inner and outer are the identifiers used for each condition as specified with the nested chain.
This new expansion makes the update of nested array content possible, but it does not really help with the practicality of "querying" such data, so the same caveats apply as explained earlier.
You typically really "mean" to express as "attributes", even if your brain initially thinks "nesting", it's just usually a reaction to how you believe the "previous relational parts" come together. In reality you really need more denormalization.
Also see How to Update Multiple Array Elements in mongodb, since these new update operators actually match and update "multiple array elements" rather than just the first, which has been the previous action of positional updates.
NOTE Somewhat ironically, since this is specified in the "options" argument for .update() and like methods, the syntax is generally compatible with all recent release driver versions.
However this is not true of the mongo shell, since the way the method is implemented there ( "ironically for backward compatibility" ) the arrayFilters argument is not recognized and removed by an internal method that parses the options in order to deliver "backward compatibility" with prior MongoDB server versions and a "legacy" .update() API call syntax.
So if you want to use the command in the mongo shell or other "shell based" products ( notably Robo 3T ) you need a latest version from either the development branch or production release as of 3.6 or greater.
See also positional all $[] which also updates "multiple array elements" but without applying to specified conditions and applies to all elements in the array where that is the desired action.
I know this is a very old question, but I just struggled with this problem myself, and found, what I believe to be, a better answer.
A way to solve this problem is to use Sub-Documents. This is done by nesting schemas within your schemas
MainSchema = new mongoose.Schema({
array1: [Array1Schema]
})
Array1Schema = new mongoose.Schema({
array2: [Array2Schema]
})
Array2Schema = new mongoose.Schema({
answeredBy": [...]
})
This way the object will look like the one you show, but now each array are filled with sub-documents. This makes it possible to dot your way into the sub-document you want. Instead of using a .update you then use a .find or .findOne to get the document you want to update.
Main.findOne((
{
_id: 1
}
)
.exec(
function(err, result){
result.array1.id(12).array2.id(123).answeredBy.push('success')
result.save(function(err){
console.log(result)
});
}
)
Haven't used the .push() function this way myself, so the syntax might not be right, but I have used both .set() and .remove(), and both works perfectly fine.

Categories

Resources