Related
I'm having trouble getting and updating the only document that matches filter in nest array of objects in mongoose, I'm using the findOneAndUpdate query in mongoose.
This is my data:
{
"_id": "62e87e193fe01f5068f9ae11",
"year": "2023",
"month": "1",
"department_id":"62e387d39ffb6ada6c590fbf",
"blocks": [
{
"name": "CEEDO Schedule Block",
"days": [
{
"day": 2,
"employees": [
{
"employee_id":"62cf92fb3a790000170062e3",
"schedule_type": "Day Off"
},
{
"employee_id": "62cf92fb3a790000170062e2",
"schedule_type": "Shifting"
},
{
"employee_id": "62cf92fb3a790000170062e4",
"schedule_type": "Regular"
}
],
"_id": "62e87e193fe01f5068f9ae13"
},
{
"day": 6,
"employees": [
{
"employee_id": "62cf92fb3a790000170062e3",
"schedule_type": "Day Off"
},
{
"employee_id": "62cf92fb3a790000170062e2",
"schedule_type": "Shifting"
},
{
"employee_id":"62cf92fb3a790000170062e4",
"schedule_type": "Regular"
}
],
"_id": "62e87e193fe01f5068f9ae14"
}
],
"_id": "62e87e193fe01f5068f9ae12"
}
]
}
And here is my query:
const update_block = await schedule_model.findOneAndUpdate({'blocks.days._id': '62e87e193fe01f5068f9ae13'},
{
$set: {"days":req.body.days, "employees":req.body.employees}
}
);
Thanks in advance.
try change '62e87e193fe01f5068f9ae13' to mongoose.Types.ObjectId('62e87e193fe01f5068f9ae13')
I finally found the answer by using the arrayFilter function in mongoose:
const update_block = await schedule_model.updateOne({
"_id": mongoose.Types.ObjectId('62e87e193fe01f5068f9ae11')
}, {
"$set": {
"blocks.$[i].days.$[j].day": 31
}
}, {
arrayFilters: [{
"i._id":mongoose.Types.ObjectId('62e87e193fe01f5068f9ae12')
}, {
"j._id": mongoose.Types.ObjectId('62e87e193fe01f5068f9ae14')
}]
})
console.log(update_block)
Thank you.
I am very new to MongoDB and I need to do a somewhat complex Update operation on my collection.
I have this kind of collection:
[
{
"Id": 1,
"extension": [
{
"keyName": "Name",
"value": "Bob"
},
{
"keyAge": "Age",
"value": 20
}
]
},
{
"Id": 2,
"extension": [
{
"keyName": "Name",
"value": "Sam"
},
{
"key": "Name",
"value": "Sam"
}
]
},
{
"Id": 3,
"extension": [
{
"keyName": "Age",
"value": 25
},
{
"key": "Age",
"value": 25
}
]
},
{
"Id": 4
}
]
I would like to update any items in the extension array of all documents
so that when an item is found with a key property, to rename it keyAge.
Here is the expected result:
[
{
"Id": 1,
"extension": [
{
"keyName": "Name",
"value": "Bob"
},
{
"keyAge": "Age",
"value": 20
}
]
},
{
"Id": 2,
"extension": [
{
"keyName": "Name",
"value": "Sam"
},
{
"keyAge": "Name",
"value": "Sam"
}
]
},
{
"Id": 3,
"extension": [
{
"keyName": "Age",
"value": 25
},
{
"keyAge": "Age",
"value": 25
}
]
},
{
"Id": 4
}
]
I tried to use $rename in a similar way to this question:
MongoDB rename database field within array
but I get the same error $rename source may not be dynamic array
I think this solution might also apply to me, I tried using it but it's not updating anything on my side, so I guess I cannot understand how to apply that answer to me...
https://stackoverflow.com/a/49193743/215553
Thanks for the help!
I tried to use $rename in a similar way to this question: MongoDB rename database field within array but I get the same error $rename source may not be dynamic array
There is a note in $rename:
$rename does not work if these fields are in array elements.
You can try update with aggregation pipeline starting from MongoDB 4.2,
check condition id key field is exists
$map to iterate loop of extension array
$map to iterate loop of array that is converted from extension object to array in key-value format
$cond check condition if k is key then return keyAge otherwise return current
$arrayToObject back to convert key-value array return from above map to object original format
db.collection.update(
{ "extension.key": { $exists: true } },
[{
$set: {
extension: {
$map: {
input: "$extension",
in: {
$arrayToObject: {
$map: {
input: { $objectToArray: "$$this" },
in: {
k: {
$cond: [
{ $eq: ["$$this.k", "key"] }, // check "key" field name
"keyAge", // update new name "keyAge"
"$$this.k"
]
},
v: "$$this.v"
}
}
}
}
}
}
}
}],
{ multi: true }
)
Playground
Question : How to Rename or add new key for existing array object key?
Answer : Inside projection of mongodb query we have map property which will resolve this.
Solution Example :
{
parents: {
$map: {
input: "$parents",
as: "parent",
in: {
caaUserId: "$$parent._id",
email: "$$parent.email",
countryCode: "$$parent.countryCode",
mobile: "$$parent.mobile",
reportType : "single"
}
}
}
}
In this example if we want to rename $parent._id as caaUserId in parents array for each Element.
Then we can use map and define caaUserId like $$parent._id. This whole code will work in mongoose projection of Query.
It should return following :
{
"parents" : [
{
"caaUserId" : "62d17fa164057000149e283f",
"email" : "john.doe#hotmail.com",
"countryCode" : 91,
"mobile": 9876543210,
"reportType":"single",
},
{
"caaUserId" : "6195d50f15ae2b001293c486",
"email" : "akka.ash#hotmail.com",
"countryCode" : 91,
"mobile": 9876543211,
"reportType":"multi",
},
]
}
This is something that works in your case. Might not be the most readable though.
import json
data = json.loads(strdata)
for entry in data:
if 'extension' in entry:
for x in entry['extension']:
for k, v in x.items():
if k == 'key':
x['keyAge'] = x.pop(k)
I have a single document which has user generated tags and also entries which has an array of tag IDs for each entry (or possibly none):
// Doc (with redacted items I would like to project too)
{
"_id": ObjectId("5ae5afc93e1d0d2965a4f2d7"),
"entries" : [
{
"_id" : ObjectId("5b159ebb0ed51064925dff24"),
// Desired:
// tags: {[
// "_id" : ObjectId("5b142ab7e419614016b8992d"),
// "name" : "Shit",
// "color" : "#95a5a6"
// ]}
"tags" : [
ObjectId("5b142ab7e419614016b8992d")
]
},
],
"tags" : [
{
"_id" : ObjectId("5b142608e419614016b89925"),
"name" : "Outdated",
"color" : "#3498db"
},
{
"_id" : ObjectId("5b142ab7e419614016b8992d"),
"name" : "Shit",
"color" : "#95a5a6"
},
],
}
How can I "fill up" the tag array for each entry with the corresponding value in the tags array? I tried $lookup and aggregate but it was too complicated to get right.
From the looks of your actual data, there is no need to populate() or $lookup here since the data you want to "join" is not only in the same collection but it's actually in the same document. What you want here instead is $map or even Array.map() to simply take values in one array of the document and merge them into the other.
Aggregate $map transform
The basic case of what you need to do here is $map to transform the each array in the output. These are "entries" and within each "entry" transforming the "tags" by matching values to those within the "tags" array of the parent document:
Project.aggregate([
{ "$project": {
"entries": {
"$map": {
"input": "$entries",
"as": "e",
"in": {
"someField": "$$e.someField",
"otherField": "$$e.otherField",
"tags": {
"$map": {
"input": "$$e.tags",
"as": "t",
"in": {
"$arrayElemAt": [
"$tags",
{ "$indexOfArray": [ "$tags._id", "$$t" ] }
]
}
}
}
}
}
}
}}
])
Note there the "someField" and "otherField" as placeholders for fields which "might" be present at that level within each "entry" document of the array. The only catch with $map is that what is specified within the "in" argument is the only output you actually get, so there is a need to explicitly name every single potential field that would be in your "variable keys" structure, and including the "tags".
The counter to this in modern releases since MongoDB 3.6 is to use $mergeObjects instead which allows a "merge" of the "re-mapped" inner array of "tags" into the "entry" document of each array member:
Project.aggregate([
{ "$project": {
"entries": {
"$map": {
"input": "$entries",
"as": "e",
"in": {
"$mergeObjects": [
"$$e",
{ "tags": {
"$map": {
"input": "$$e.tags",
"as": "t",
"in": {
"$arrayElemAt": [
"$tags",
{ "$indexOfArray": [ "$tags._id", "$$t" ] }
]
}
}
}}
]
}
}
}
}}
])
As for the actual $map on the "inner" array of "tags", here you can use the $indexOfArray operator to do a comparison with the "root level" field of "tags" based on where the _id property matches the value of the current entry of this "inner" array. With that "index" returned, the $arrayElemAt operator then "extracts" the actual array entry from that matched "index" position, and transplants the current array entry in the $map with that element.
The only point of care here is in the case where the two arrays in fact do not have matching entries for some reason. If you have already taken care of this, then the code here is fine. If there is a mismatch you might instead need to $filter to match the elements and take the $arrayElemAt at index 0 instead:
"in": {
"$arrayElemAt": [
{ "$filter": {
"input": "$tags",
"cond": { "$eq": [ "$$this._id", "$$t" ] }
}},
0
]
}
The reason being that doing that allows a null where there is no match, but $indexOfArray will return -1, and that used with $arrayElemAt returns the "last" array element. And the "last" element is of course in that scenario not the "matching" result, since there was no match.
Client side transformation
So from the perspective there where you are "only" returning the "entries" content "re-mapped" and discarding the "tags" from the root of the document, the aggregation process where possible is the better option since the server only returns the elements you actually want.
If you cannot do that or otherwise really don't care if the existing "tags" element is also returned, then aggregation transformation is really not necessary here at all. In fact the "server" need not do anything, and probably "should not" considering all the data is already in the document and "additional" transforms is just adding to the document size.
So this is all actually possible to do with the result once returned to the client, and for a simple transformation of the document just the same as was demonstrated with the above aggregation pipeline examples the only code you actually need is:
let results = await Project.find().lean();
results = results.map(({ entries, tags, ...r }) =>
({
...r,
entries: entries.map(({ tags: etags, ...e }) =>
({
...e,
tags: etags.map( tid => tags.find(t => t._id.equals(tid)) )
})
),
// tags
})
);
This gives you exactly the same results and even optionally keep the tags in there by removing the comment. It's even basically "exactly the same process" of using Array.map() on each array in order to do the transformation of each one.
The syntax to "merge" is much more simple with modern JavaScript object spread operations, and overall the language is far less terse. You use Array.find() in order to "lookup" the matching content of the two arrays for tags and the only other thing to be aware of is the ObjectId.equals() method, which is needed to actually compare these two values and built in to the returned types anyway.
Of course since you are "transforming" the documents, in order to make this possible you use lean() on any mongoose operation returning the results to manipulate so the data returned is in fact plain JavaScript objects rather than Mongoose Document types bound to the schema, which is the default return.
Conclusion and Demonstration
The general lesson here is that if you are looking to "reduce data" in the returned response, then the aggregate() method is for you. If however you decide that you want the "whole" document data anyway and just want to "augment" these other array entries in the response, then just take the data back to the "client" and transform it there instead. Ideally as "frontward" as possible considering that "additions" are just adding weight to the payload response in this case.
A full demonstration listing would be:
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');
const uri = 'mongodb://localhost/test';
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
const tagSchema = new Schema({
name: String,
color: String
});
const projectSchema = new Schema({
entries: [],
tags: [tagSchema]
});
const Project = mongoose.model('Project', projectSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri);
let db = conn.connections[0].db;
let { version } = await db.command({ buildInfo: 1 });
version = parseFloat(version.match(new RegExp(/(?:(?!-).)*/))[0]);
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
await Project.insertMany(data);
let pipeline = [
{ "$project": {
"entries": {
"$map": {
"input": "$entries",
"as": "e",
"in": {
"someField": "$$e.someField",
"otherField": "$$e.otherField",
"tags": {
"$map": {
"input": "$$e.tags",
"as": "t",
"in": {
"$arrayElemAt": [
"$tags",
{ "$indexOfArray": [ "$tags._id", "$$t" ] }
]
}
}
}
}
}
}
}}
];
let other = [
{
...(({ $project: { entries: { $map: { input, as, ...o } } } }) =>
({
$project: {
entries: {
$map: {
input,
as,
in: {
"$mergeObjects": [ "$$e", { tags: o.in.tags } ]
}
}
}
}
})
)(pipeline[0])
}
];
let tests = [
{ name: 'Standard $project $map', pipeline },
...(version >= 3.6) ?
[{ name: 'With $mergeObjects', pipeline: other }] : []
];
for ( let { name, pipeline } of tests ) {
let results = await Project.aggregate(pipeline);
log({ name, results });
}
// Client Manipulation
let results = await Project.find().lean();
results = results.map(({ entries, tags, ...r }) =>
({
...r,
entries: entries.map(({ tags: etags, ...e }) =>
({
...e,
tags: etags.map( tid => tags.find(t => t._id.equals(tid)) )
})
)
})
);
log({ name: 'Client re-map', results });
mongoose.disconnect();
} catch(e) {
console.error(e)
} finally {
process.exit()
}
})();
// Data
const data =[
{
"_id": ObjectId("5ae5afc93e1d0d2965a4f2d7"),
"entries" : [
{
"_id" : ObjectId("5b159ebb0ed51064925dff24"),
"someField": "someData",
"tags" : [
ObjectId("5b142ab7e419614016b8992d")
]
},
],
"tags" : [
{
"_id" : ObjectId("5b142608e419614016b89925"),
"name" : "Outdated",
"color" : "#3498db"
},
{
"_id" : ObjectId("5b142ab7e419614016b8992d"),
"name" : "Shitake",
"color" : "#95a5a6"
},
]
},
{
"_id": ObjectId("5b1b1ad07325c4c541e8a972"),
"entries" : [
{
"_id" : ObjectId("5b1b1b267325c4c541e8a973"),
"otherField": "otherData",
"tags" : [
ObjectId("5b142608e419614016b89925"),
ObjectId("5b142ab7e419614016b8992d")
]
},
],
"tags" : [
{
"_id" : ObjectId("5b142608e419614016b89925"),
"name" : "Outdated",
"color" : "#3498db"
},
{
"_id" : ObjectId("5b142ab7e419614016b8992d"),
"name" : "Shitake",
"color" : "#95a5a6"
},
]
}
];
And this would give full output ( with the optional output from a supporting MongoDB 3.6 instance ) as:
Mongoose: projects.remove({}, {})
Mongoose: projects.insertMany([ { entries: [ { _id: 5b159ebb0ed51064925dff24, someField: 'someData', tags: [ 5b142ab7e419614016b8992d ] } ], _id: 5ae5afc93e1d0d2965a4f2d7, tags: [ { _id: 5b142608e419614016b89925, name: 'Outdated', color: '#3498db' }, { _id: 5b142ab7e419614016b8992d, name: 'Shitake', color: '#95a5a6' } ], __v: 0 }, { entries: [ { _id: 5b1b1b267325c4c541e8a973, otherField: 'otherData', tags: [ 5b142608e419614016b89925, 5b142ab7e419614016b8992d ] } ], _id: 5b1b1ad07325c4c541e8a972, tags: [ { _id: 5b142608e419614016b89925, name: 'Outdated', color: '#3498db' }, { _id: 5b142ab7e419614016b8992d, name: 'Shitake', color: '#95a5a6' } ], __v: 0 } ], {})
Mongoose: projects.aggregate([ { '$project': { entries: { '$map': { input: '$entries', as: 'e', in: { someField: '$$e.someField', otherField: '$$e.otherField', tags: { '$map': { input: '$$e.tags', as: 't', in: { '$arrayElemAt': [ '$tags', { '$indexOfArray': [Array] } ] } } } } } } } } ], {})
{
"name": "Standard $project $map",
"results": [
{
"_id": "5ae5afc93e1d0d2965a4f2d7",
"entries": [
{
"someField": "someData",
"tags": [
{
"_id": "5b142ab7e419614016b8992d",
"name": "Shitake",
"color": "#95a5a6"
}
]
}
]
},
{
"_id": "5b1b1ad07325c4c541e8a972",
"entries": [
{
"otherField": "otherData",
"tags": [
{
"_id": "5b142608e419614016b89925",
"name": "Outdated",
"color": "#3498db"
},
{
"_id": "5b142ab7e419614016b8992d",
"name": "Shitake",
"color": "#95a5a6"
}
]
}
]
}
]
}
Mongoose: projects.aggregate([ { '$project': { entries: { '$map': { input: '$entries', as: 'e', in: { '$mergeObjects': [ '$$e', { tags: { '$map': { input: '$$e.tags', as: 't', in: { '$arrayElemAt': [Array] } } } } ] } } } } } ], {})
{
"name": "With $mergeObjects",
"results": [
{
"_id": "5ae5afc93e1d0d2965a4f2d7",
"entries": [
{
"_id": "5b159ebb0ed51064925dff24",
"someField": "someData",
"tags": [
{
"_id": "5b142ab7e419614016b8992d",
"name": "Shitake",
"color": "#95a5a6"
}
]
}
]
},
{
"_id": "5b1b1ad07325c4c541e8a972",
"entries": [
{
"_id": "5b1b1b267325c4c541e8a973",
"otherField": "otherData",
"tags": [
{
"_id": "5b142608e419614016b89925",
"name": "Outdated",
"color": "#3498db"
},
{
"_id": "5b142ab7e419614016b8992d",
"name": "Shitake",
"color": "#95a5a6"
}
]
}
]
}
]
}
Mongoose: projects.find({}, { fields: {} })
{
"name": "Client re-map",
"results": [
{
"_id": "5ae5afc93e1d0d2965a4f2d7",
"__v": 0,
"entries": [
{
"_id": "5b159ebb0ed51064925dff24",
"someField": "someData",
"tags": [
{
"_id": "5b142ab7e419614016b8992d",
"name": "Shitake",
"color": "#95a5a6"
}
]
}
]
},
{
"_id": "5b1b1ad07325c4c541e8a972",
"__v": 0,
"entries": [
{
"_id": "5b1b1b267325c4c541e8a973",
"otherField": "otherData",
"tags": [
{
"_id": "5b142608e419614016b89925",
"name": "Outdated",
"color": "#3498db"
},
{
"_id": "5b142ab7e419614016b8992d",
"name": "Shitake",
"color": "#95a5a6"
}
]
}
]
}
]
}
Note this includes some additional data to demonstrate the projection of "variable fields".
This question already has answers here:
How to Update Multiple Array Elements in mongodb
(16 answers)
Updating a Nested Array with MongoDB
(2 answers)
Closed 5 years ago.
Building a Nodejs app, I'm trying to pull all doubly nested records from a Mongo Database. Attempts that I've made only removed one doubly nested record or all nested records. As in the example data below I've been trying to remove all tickets that has the same keyId. I've reduced the example but tickets as an array there might be other elements with the same structure with different "keyIds" that shouldn't be removed. I've looked this question but it only refrains to removing one record of a doubly nested array, not all of them at once.
[
{
"_id": "59fe54098448d822f89a7e62",
"ownerId": "59b23449b20b7c1838eee1a3",
"name": "Home",
"keys": [
{
"id": "6d7435625564594f4a563554796c6a77",
"name": "Front Door"
}
],
"grants": [
{
"id": "307658775634774a556b677650347072",
"userId": "59b23449b20b7c1838eee1a3",
"tickets": [
{
"keyId": "6d7435625564594f4a563554796c6a77",
"iv": "b7090268bdaf9ab55270e133b5629e28"
}
]
},
{
"id": "37703369365765485763484a4358774d",
"userId": "59b23449b20b7c1838eee1a3",
"tickets": [
{
"keyId": "6d7435625564594f4a563554796c6a77",
"iv": "d2e2de0f9387c5d9b16424e8ac66a3c1"
}
]
},
{
"id": "3451483977564d755278397939593633",
"userId": "59b23449b20b7c1838eee1a3",
"tickets": [
{
"keyId": "6d7435625564594f4a563554796c6a77",
"iv": "582ff50ac3d337c62eb53094470e3161"
}
]
},
{
"id": "7059684f4e42754d55456e726b35664e",
"userId": "59b23449b20b7c1838eee1a3",
"tickets": [
{
"keyId": "6d7435625564594f4a563554796c6a77",
"iv": "b110ee5cb5da8941cc8ad6e1c3fe501c"
}
]
}
]
}
]
After removing all tickets with keyId=6d7435625564594f4a563554796c6a77 the intended data should look like this:
[
{
"_id": "59fe54098448d822f89a7e62",
"ownerId": "59b23449b20b7c1838eee1a3",
"name": "Home",
"keys": [
{
"id": "6d7435625564594f4a563554796c6a77",
"name": "Front Door"
}
],
"grants": [
{
"id": "307658775634774a556b677650347072",
"userId": "59b23449b20b7c1838eee1a3",
"tickets": []
},
{
"id": "37703369365765485763484a4358774d",
"userId": "59b23449b20b7c1838eee1a3",
"tickets": []
},
{
"id": "3451483977564d755278397939593633",
"userId": "59b23449b20b7c1838eee1a3",
"tickets": []
},
{
"id": "7059684f4e42754d55456e726b35664e",
"userId": "59b23449b20b7c1838eee1a3",
"tickets": []
}
]
}
]
This code remove all grants at once:
db.places.update({}, {
$pull: {
"grants": {
"tickets": {
$elemMatch: { "keyId": keyID }
}
}
}
}, { multi: true });
This pull out just the first ticket and with "$pullAll" doesn't do anything:
db.places.findAndModify(
{
ownerId: ownerID, "grants.tickets.keyId": keyID
},
[ ],
{ $pull: { "grants.$.tickets": { keyId: keyID } } },
{ multi: true },
next
);
And this throws me an error saying: cannot use the part (grants of grants.tickets.$*.keyId) to traverse the element
db.places.update({ "grants.tickets.keyId": keyID }, {
$pull: {
"grants.tickets.$*.keyId": keyID
}
}, { multi: true });
Cloudant is returning error message:
{"error":"invalid_key","reason":"Invalid key use-index for this request."}
whenever I try to query against an index with the combination operator, "$or".
A sample of what my documents look like is:
{
"_id": "28f240f1bcc2fbd9e1e5174af6905349",
"_rev": "1-fb9a9150acbecd105f1616aff88c26a8",
"type": "Feature",
"properties": {
"PageName": "A8",
"PageNumber": 1,
"Lat": 43.051523,
"Long": -71.498852
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
-71.49978935969642,
43.0508382914137
],
[
-71.49978564033566,
43.052210148524
],
[
-71.49791499857444,
43.05220740550381
],
[
-71.49791875962663,
43.05083554852429
],
[
-71.49978935969642,
43.0508382914137
]
]
]
}
}
The index that I created is for field "properties.PageName", which works fine when I'm just querying for one document, but as soon as I try for multiple ones, I would receive the error response as quoted in the beginning.
If it helps any, here is the call:
POST https://xyz.cloudant.com/db/_find
request body:
{
"selector": {
"$or": [
{ "properties.PageName": "A8" },
{ "properties.PageName": "M30" },
{ "properties.PageName": "AH30" }
]
},
"use-index": "pagename-index"
}
In order to perform an $or query you need to create a text (full text) index, rather than a json index. For example, I just created the following index:
{
"index": {
"fields": [
{"name": "properties.PageName", "type": "string"}
]
},
"type": "text"
}
I was then be able to perform the following query:
{
"selector": {
"$or": [
{ "properties.PageName": "A8" },
{ "properties.PageName": "M30" },
{ "properties.PageName": "AH30" }
]
}
}