How to update objects in list in mongoose (in NodeJS) - node.js

I have an object with the following model:
export const Address = mongoose.model('Address',
{
id: mongoose.SchemaTypes.ObjectId,
customer_id: String,
addresses: [{
address_type: String,
address_info: String,
}]
});
An example dataset is like this:
{
"data": {
"address": [
{
"customer_id": "12345123",
"addresses": [
{
"address_type": ANDROID,
"address_info": "dfjghjsdgf"
},
{
"address_type": IOS,
"address_info": "dwrw45345f"
}
]
},
{
"customer_id": "12345124",
"addresses": [
{
"address_type": SMS,
"address_info": "dfjghj231dgf"
},
{
"address_type": IOS,
"address_info": "ww242344234"
}
]
}
]
}
}
It's clear that the addresses field of the Address model is a list, within which each object has two fields : address_type, and address_info.
I wonder how to update an address_info with filters in customer_id and address_type? For example, if I would like to update the ANDROID address of customer_id = 12345123 to abcdefg, it's expected to see the database being updated into :
{
"data": {
"address": [
{
"customer_id": "12345123",
"addresses": [
{
"address_type": ANDROID,
"address_info": "abcdefg"
},
{
"address_type": IOS,
"address_info": "dwrw45345f"
}
]
},
{
"customer_id": "12345124",
"addresses": [
{
"address_type": SMS,
"address_info": "dfjghj231dgf"
},
{
"address_type": IOS,
"address_info": "ww242344234"
}
]
}
]
}
}
Reference: Mongoose :Find and filter nested array
This problem talked about findOne, but I'm more interested in update.

you can try like this
await Address.update(
{ customer_id: "12345123", "addresses.address_type": "ANDROID" },
{$set : { "addresses.$.address_info" : "abcdefg" }}
)
playground

Related

Push items into array of objects with array attribute in mongoose

Hello I am trying to add an element to an array that is inside an object and the object in turn inside an array, below is the structure.
// Schema called "Team" with mongoose
category: [
{
seasson: { type: String, required: true },
categories: [{ type: String, required: true }]
}]
// In code looks like:
[
{
seasson: "The seasson name 1",
categories: ["categoryOne", "categoryTwo"]
}
{
seasson: "The seasson name 2",
categories: ["categoryOne"] // I want to make push in this array the value "categoryTwo"
},
]
// I´m trying something like following code:
const status = await Team.updateOne(
{
_id: mongoose.Types.ObjectId(teamId),
},
{ $addToSet: { "category.$last.categories": "categoryTwo"} }
)
Whenever an array has to be pushed into the object, it will be in the last position of the main array. Honestly, I've been trying to find a way for a while, but I can't think of anything that works.
Thanks in advance.
There is no straight way to update the last element of the array without any identity, you can use update with aggregation pipeline starting from MongoDB 4.2,
$map to iterate loop of category array
$mergeObjects to merge current category object with updated categories field
$last to get the last element value from category.seasson
$cond check condition if above last element's value and current object session matches then do update operation otherwise return existing values
$setUnion to concat new value of categories, if it is present then it will do replace
let category = "categoryTwo";
const status = await Team.updateOne(
{ _id: mongoose.Types.ObjectId(teamId) },
[{
$set: {
category: {
$map: {
input: "$category",
in: {
$mergeObjects: [
"$$this",
{
categories: {
$cond: [
{
$eq: [
{ $last: "$category.seasson" },
"$$this.seasson"
]
},
{ $setUnion: ["$$this.categories", [category]] },
"$$this.categories"
]
}
}
]
}
}
}
}
}]
)
Playground
The bellow query,adds "categoryTwo" in categories,of the last member of array
category.I think this is what you want.
If you can next time give the document in the initial form,describe the query you want,and give the document in the final form,in valid JSON so people can help you easier.
You can try the code here
Its pipeline update,needs MongoDB >= 4.2
Data in(Collection)
[
{
"_id": 1,
"category": [
{
"seasson": "The seasson name 1",
"categories": [
"categoryOne",
"categoryTwo"
]
},
{
"seasson": "The seasson name 2",
"categories": [
"categoryOne"
]
},
]
}
]
Query
db.collection.update({
"_id": {
"$eq": 1
}
},
[
{
"$addFields": {
"category": {
"$let": {
"vars": {
"without_last": {
"$slice": [
"$category",
0,
{
"$subtract": [
{
"$size": "$category"
},
1
]
}
]
},
"last_member": {
"$arrayElemAt": [
{
"$slice": [
"$category",
-1,
1
]
},
0
]
}
},
"in": {
"$concatArrays": [
"$$without_last",
[
{
"$mergeObjects": [
"$$last_member",
{
"categories": {
"$concatArrays": [
"$$last_member.categories",
[
"categoryTwo"
]
]
}
}
]
}
]
]
}
}
}
}
}
])
Results
[
{
"_id": 1,
"category": [
{
"categories": [
"categoryOne",
"categoryTwo"
],
"seasson": "The seasson name 1"
},
{
"categories": [
"categoryOne",
"categoryTwo"
],
"seasson": "The seasson name 2"
}
]
}
]

How to query an object with a specific key/value pair in an array with Mongodb?

This is my data structure:
{
studentName: 'zzz',
teachers: [
{
teacherName: 'xxx',
feedbacks: []
}, {
teacherName: 'yyy',
feedbacks: []
}
]
}
Now I am trying to code a query to add an 'feedback' object to the 'feedbacks' array that belongs to the teacher named 'yyy'.
await collection.updateOne({
studentName: 'zzz',
teachers: {
$elemMatch: {
teacherName: {
$eq: 'yyy'
}
}
}
}, {
$push: {
'teachers.$.feedbacks': {}
}
})
The query part is faulty somehow. If I change '$' to 0 or 1, then the code works finally. Otherwise, the object cannot be pushed.
This update works fine, adds the string element "Nice work" to the teachers.feedbacks nested array.
db.test.updateOne(
{
studentName: "zzz",
"teachers.teacherName": "yyy"
},
{ $push: { "teachers.$.feedbacks" : "Nice work" } }
)
The $elemMatch syntax is not required, as the query condition for the array elements is for a single field.
The updated document:
{
"studentName" : "zzz",
"teachers" : [
{
"teacherName" : "xxx",
"feedbacks" : [ ]
},
{
"teacherName" : "yyy",
"feedbacks" : [
"Nice work"
]
}
]
}

MongoDB - Aggregation $filter in $filter / filtering array of subdocuments in subdocuments

I have the following structure of a document in a collection:
A Factory has many departments, which has many areas.
factoryName: "",
departments: [
{
departmentName: ""
areas: [
{
areaName: ""
}
]
}
]
When querying a document via the "areaName", I only want to get the area + respective parent department + respective parent factory.
As an example, please briefly see the below 2 documents.
db.factories.insertMany([{
factoryName: "San Francisco",
departments: [
{
departmentName: "Administration",
areas: [
{
areaName: "Phone Guys"
},
{
areaName: "Email Guys"
}
]
},
{
departmentName: "Development",
areas: [
{
areaName: "Dev Ops"
},
{
areaName: "Programming"
},
{
areaName: "Architecture"
}
]
}
]
},{
factoryName: "Chicago",
departments: [
{
departmentName: "Administration",
areas: [
{
areaName: "Phone Guys"
},
{
areaName: "Email Guys"
}
]
},
{
departmentName: "Logistics",
areas: [
{
areaName: "Delivery"
},
{
areaName: "Human Resources"
}
]
}
]
}])
I wish to query by areaName = "Architecture" and receive back the following:
factoryName: "San Francisco",
departments: [
{
departmentName: "Development"
areas: [
{
areaName: "Architecture"
}
]
}
]
Since usual query combinations via .find() with projection failed (so far), I've tried myself in aggregation.
In order to achieve the wished result, I've tried many things but failing when it comes down to filtering the areas. Filtering the departments' work.
Using MongoDB Compass Visual Aggregation feature, the most logical to me seemed:
db.factories.aggregate([
{
'$match': {
'departments.areas.areaName': 'Architecture'
}
}, {
'$addFields': {
'departments': {
'$filter': {
'input': '$departments',
'as': 'department',
'cond': {
'$and': [
{
'$eq': [
'$$department.departmentName', 'Development'
]
}, {
'$filter': {
'input': '$$department.areas',
'as': 'area',
'cond': {
'$eq': [
'$$area.areaName', 'Architecture'
]
}
}
}
]
}
}
}
}
}
])
It seems a $filter, inside a $filter does not work or I'm missing something here since all 3 areas for the "Development" department are being returned instead of only "Architecture" whereas the "Development" department is being filtered correctly.
How can I achieve this? What am I missing?
Any help is much appreciated.
Many thanks!
You need $filter and $map since you have nested arrays:
db.collection.aggregate([
{
"$match": {
"departments.areas.areaName": "Architecture"
}
},
{
$addFields: {
departments: {
$map: {
input: {
$filter: {
input: "$departments",
cond: {
$in: [ "Architecture", "$$this.areas.areaName" ]
}
}
},
in: {
departmentName: "$$this.departmentName",
areas: { $filter: { input: "$$this.areas", as: "d", cond: { $eq: [ "$$d.areaName", "Architecture" ] } } }
}
}
}
}
}
])
Mongo Playground

MongoDB Mongoose aggregate query deeply nested array remove empty results and populate references

This question is a follow up to a previous question for which I have accepted an answer already. I have an aggregate query that returns the results of a deeply nested array of subdocuments based on a date range. The query returns the correct results within the specified date range, however it also returns an empty array for the results that do not match the query.
Technologies: MongoDB 3.6, Mongoose 5.5, NodeJS 12
Question 1:
Is there any way to remove the results that don't match the query?
Question 2:
Is there any way to 'populate' the Person db reference in the results? For example to get the Person Display Name I usually use 'populate' such as find().populate({ path: 'Person', select: 'DisplayName'})
Records schema
let RecordsSchema = new Schema({
RecordID: {
type: Number,
index: true
},
RecordType: {
type: String
},
Status: {
type: String
},
// ItemReport array of subdocuments
ItemReport: [ItemReportSchema],
}, {
collection: 'records',
selectPopulatedPaths: false
});
let ItemReportSchema = new Schema({
// ObjectId reference
ReportBy: {
type: Schema.Types.ObjectId,
ref: 'people'
},
ReportDate: {
type: Date,
required: true
},
WorkDoneBy: [{
Person: {
type: Schema.Types.ObjectId,
ref: 'people'
},
CompletedHours: {
type: Number,
required: true
},
DateCompleted: {
type: Date
}
}],
});
Query
Works but also returns empty results and also need to populate the Display Name property of the Person db reference
db.records.aggregate([
{
"$project": {
"ItemReport": {
$map: {
input: "$ItemReport",
as: "ir",
in: {
WorkDoneBy: {
$filter: {
input: "$$ir.WorkDoneBy",
as: "value",
cond: {
"$and": [
{ "$ne": [ "$$value.DateCompleted", null ] },
{ "$gt": [ "$$value.DateCompleted", new Date("2017-01-01T12:00:00.000Z") ] },
{ "$lt": [ "$$value.DateCompleted", new Date("2018-12-31T12:00:00.000Z") ] }
]
}
}
}
}
}
}
}
}
])
Actual Results
{
"_id": "5dcb6406e63830b7aa5427ca",
"ItemReport": [
{
"WorkDoneBy": [
{
"_id": "5dcb6406e63830b7aa53d8ea",
"PersonID": 111,
"ReportID": 8855,
"CompletedHours": 3,
"DateCompleted": "2017-01-20T05:00:00.000Z",
"Person": "5dcb6409e63830b7aa54fdba"
}
]
}
]
},
{
"_id": "5dcb6406e63830b7aa5427f1",
"ItemReport": [
{
"WorkDoneBy": [
{
"_id": "5dcb6406e63830b7aa53dcdc",
"PersonID": 4,
"ReportID": 9673,
"CompletedHours": 17,
"DateCompleted": "2017-05-18T04:00:00.000Z",
"Person": "5dcb6409e63830b7aa54fd69"
},
{
"_id": "5dcb6406e63830b7aa53dcdd",
"PersonID": 320,
"ReportID": 9673,
"CompletedHours": 3,
"DateCompleted": "2017-05-18T04:00:00.000Z",
"Person": "5dcb6409e63830b7aa54fe88"
}
]
}
]
},
{
"_id": "5dcb6406e63830b7aa5427f2",
"ItemReport": [
{
"WorkDoneBy": []
}
]
},
{
"_id": "5dcb6406e63830b7aa5427f3",
"ItemReport": [
{
"WorkDoneBy": []
}
]
},
{
"_id": "5dcb6406e63830b7aa5427f4",
"ItemReport": [
{
"WorkDoneBy": []
}
]
},
{
"_id": "5dcb6406e63830b7aa5427f5",
"ItemReport": [
{
"WorkDoneBy": []
}
]
},
Desired results
Note the results with an empty "WorkDoneBy" array are removed (question 1), and the "Person" display name is populated (question 2).
{
"_id": "5dcb6406e63830b7aa5427f1",
"ItemReport": [
{
"WorkDoneBy": [
{
"_id": "5dcb6406e63830b7aa53dcdc",
"CompletedHours": 17,
"DateCompleted": "2017-05-18T04:00:00.000Z",
"Person": {
_id: "5dcb6409e63830b7aa54fe88",
DisplayName: "Joe Jones"
}
},
{
"_id": "5dcb6406e63830b7aa53dcdd",
"CompletedHours": 3,
"DateCompleted": "2017-05-18T04:00:00.000Z",
"Person": {
_id: "5dcb6409e63830b7aa54fe88",
DisplayName: "Alice Smith"
}
}
]
}
]
},
First question is relatively easy to answer and there are multiple ways to do that. I would prefer using $anyElementTrue along with $map as those operators are pretty self-explanatory.
{
"$match": {
$expr: { $anyElementTrue: { $map: { input: "$ItemReport", in: { $gt: [ { $size: "$$this.WorkDoneBy" }, 0 ] } } } }
}
}
MongoPlayground
Second part is a bit more complicated but still possible. Instead of populate you need to run $lookup to bring the data from other collection. The problem is that your Person values are deeply nested so you need to prepare a list of id values before using $reduce and $setUnion. Once you get the data you need to merge your nested objects with people entities using $map and $mergeObjects.
{
$addFields: {
people: {
$reduce: {
input: "$ItemReport",
initialValue: [],
in: { $setUnion: [ "$$value", "$$this.WorkDoneBy.Person" ] }
}
}
}
},
{
$lookup: {
from: "people",
localField: "peopleIds",
foreignField: "_id",
as: "people"
}
},
{
$project: {
_id: 1,
ItemReport: {
$map: {
input: "$ItemReport",
as: "ir",
in: {
WorkDoneBy: {
$map: {
input: "$$ir.WorkDoneBy",
as: "wdb",
in: {
$mergeObjects: [
"$$wdb",
{
Person: { $arrayElemAt: [{ $filter: { input: "$people", cond: { $eq: [ "$$this._id", "$$wdb.Person" ] } } } , 0] }
}
]
}
}
}
}
}
}
}
}
Complete Solution

mongoose user bulk search by phone numbers

I have UserSchema which contains PhoneNumberSchema as below.
var PhoneNumberSchema = new Schema({
country: {
type: String
},
country_code: {
type: Number
},
local_number: {
type: Number
}
});
And here is sample json format of phone_number.
"phone_number": {
"country": "US",
"country_code": "1",
"local_number": "04152341"
}
What I want to do is to search users by phone numbers with / without country code.
Well, if request is
"phone_numbers": [ "104152341", "124254364" ]
then I want to get users who has exactly matched phone number which belongs in the request phone numbers array with/without country code.
So, I tried as below, but got error "invalid operator '$in'".
User.aggregate(
[
{ "$redact": {
"$cond": [
{
"$in": [ { "$concat": [ "$phone_number.country_code", "$phone_number.local_number" ] }, req.body.phone_numbers]
},
"$$KEEP",
"$$PRUNE"
]
}}
],
function(err, users) {
// Do something
if (err) {
return res.json({ success: false, err: err });
}
res.json({ success: true, users: users });
}
)
I hope to know how to handle my issue.
Please help me !!
Use $setIsSubset as your condition expression:
{ "$redact": {
"$cond": [
{
"$setIsSubset": [
[
{
"$concat": [
"$phone_number.country_code",
"$phone_number.local_number"
]
}
],
req.body.phone_numbers
]
},
"$$KEEP",
"$$PRUNE"
]
}}

Resources