Mongoose nested object not updating 'cannot create field "foo" in element' - mongodb

I have a similar issue to this question.
I'm trying to create a new field using "findAndUpdate". I've tried all the methods, $set, $push, $addSet... none of them seem to be working and I keep getting the same error.
Here's the code:
router.post('/accept', auth, async (req, res) => {
const useremail = user.email
const originalEvent = await Event.findOneAndUpdate({eventId: 61469041, isOrganizer: true, "attendees.email": useremail},
{"$push":{"attendees.status" : "accepted"}},
{new: true})
res.status(200).json({originalEvent, event})
}
catch (e) {
res.status(400).json({ msg: e.message, success: false });
}
});
Here's the error code:
"Cannot create field 'status' in element {attendees: [ { _id: ObjectId('5f80a02a82dceb2810e0aa66'), email: "bob#gmail.com", name: "Bob" } ]}"
Here's the object I'm trying to update:
{
"organizer": {
"email": "alex#gmail.com",
"name": "Alex"
},
"_id": "5f80a02a82dceb2810e0aa65",
"title": "Go to the beach",
"eventId": 61469041,
"isOrganizer": true,
"user": "5f05f23417ca6ab69ccc4cf2",
"attendees": [
{
"_id": "5f80a02a82dceb2810e0aa66",
"email": "bob#gmail.com",
"name": "Bob"
}
],
"__v": 0,
}
Expected outcome:
{
"organizer": {
"email": "alex#gmail.com",
"name": "Alex"
},
"_id": "5f80a02a82dceb2810e0aa65",
"title": "Go to the beach",
"eventId": 61469041,
"isOrganizer": true,
"user": "5f05f23417ca6ab69ccc4cf2",
"attendees": [
{
"_id": "5f80a02a82dceb2810e0aa66",
"email": "bob#gmail.com",
"name": "Bob",
"status": "accepted"
}
],
"__v": 0,
}
SOLVED with this:
const originalEvent = await Event.findOneAndUpdate({eventId: eventId, "isOrganizer": true,
"attendees": {$elemMatch: {email: useremail}}
},
{ $set: { "attendees.$.status": "accepted"} }
)
res.status(200).json(originalEvent)
}

Referencing attendees.status doesn't make sense because in your schema attendees is not an object (with fields such as status) but an array. But you can do it differently. If you have the index of the attendee you want to mutate, you can do { $set: { "attendees.0.status": "accepted" } }, where 0 is the index in the array.
Also, with regards to the first half of your question, the error you're seeing is because $push works on arrays. So in order for your operation to work, you'd have to first initialize such an object {attendees: { status: [] } }.
If the field is not an array, the operation will fail. (docs)

Related

How to populate array of objects in mongoose

I have this code -
const getAllCourses = async (req, res) => {
const courses = await Course.find({});
try {
await courses.populate('professor')
res.send({ status: 200, data: { courses } });
} catch (err) {
res.status(500).send({ status: 500, message: "Internal server error." });
}
};
Also, this is the response I'm getting from postman -
{
"status": 200,
"data": {
"courses": [
{
"_id": "61dc47f58f88c1a7e9bd36b6",
"name": "Course1",
"professor": "61dc1299431cd669faad7d0f",
"students": [
{
"student": "61dc0b7f103b531f105e8e4c",
"_id": "61dc47f58f88c1a7e9bd36b7"
},
{
"student": "61dc220885886a9f1d8e94d0",
"_id": "61dc47f58f88c1a7e9bd36b8"
}
],
"createdAt": "2022-01-10T14:51:33.313Z",
"updatedAt": "2022-01-10T14:51:33.313Z",
"__v": 0
},
{
"_id": "61dc47fb8f88c1a7e9bd36bf",
"name": "Course2",
"professor": "61dc1299431cd669faad7d0f",
"students": [
{
"student": "61dc0b7f103b531f105e8e4c",
"_id": "61dc47fb8f88c1a7e9bd36c0"
},
{
"student": "61dc220885886a9f1d8e94d0",
"_id": "61dc47fb8f88c1a7e9bd36c1"
}
],
"createdAt": "2022-01-10T14:51:39.704Z",
"updatedAt": "2022-01-10T14:51:39.704Z",
"__v": 0
}
]
}
}
Now what I'm trying to do is to populate the professor and the students but it doesn't seem to work.
I tried populating "courses.professor", "course.professor", "professor" but nothing worked for me.
What am I missing?
Here is an example of how this can be done for a document with an array friends on it:
User.
findOne({ name: 'Val' }).
populate({
path: 'friends',
// Get friends of friends - populate the 'friends' array for every friend
populate: { path: 'friends' }
});
I just solved it by chaining the populate method directly after the Course.find({}) and it just worked, I'm not sure why though.
Solution:
const getAllCourses = async (req, res) => {
const courses = await Course.find({}).populate('professor').populate('students.student');
try {
res.send({ status: 200, data: { courses } });
} catch (err) {
res.status(500).send({ status: 500, message: err.message });
}
};

How to return only some columns of a relations with Typeorm

Ok, i'm having trouble with getting the relations with typeorm, when i run the service it returns me all the data from the relation, and i want only specific fields, like id and name.
Here's my code:
async findById(id: string): Promise<UsersUseOfferHistoric> {
return await this.repository.findOne({
where: { id },
relations: ['userId', 'offerId'],
});
}
Here's the json Output:
{
"id": "da0fd04e-17c6-4412-b342-a4361d191468",
"createdAt": "2020-01-07T19:48:30.840Z",
"userId": {
"id": "bdc00227-569f-44b5-9bdd-c8de03661ebd",
"name": "Alexandre Vieira",
"cpf": "10443771430",
"email": "av.souza2018#gmail.com",
"password": "asjdsifjdsfasf",
"imagePath": "/me.png",
"active": true,
"lastLogin": "2020-01-07T19:40:26.850Z",
"createdAt": "2020-01-07T19:40:26.850Z",
"updatedAt": "2020-01-07T19:40:26.850Z"
},
"offerId": {
"id": "e399560c-d2c2-4f4e-b2b1-94cae3af3779",
"offerDrescription": "Nova oferta top",
"discountCoupon": " Desconto top",
"discountValidity": "2020-01-07T14:18:19.803Z",
"discountPercentage": 20,
"discountQuantityLimit": 50,
"createdAt": "2020-01-07T19:45:33.589Z",
"updatedAt": "2020-01-07T19:45:33.589Z"
}
}
Here's the output i want:
{
"id": "da0fd04e-17c6-4412-b342-a4361d191468",
"createdAt": "2020-01-07T19:48:30.840Z",
"userId": {
"id": "bdc00227-569f-44b5-9bdd-c8de03661ebd",
"name": "Alexandre Vieira",
},
"offerId": {
"id": "e399560c-d2c2-4f4e-b2b1-94cae3af3779",
"offerDrescription": "Nova oferta top",
}
}
The findOne function accepts an select: ['id', 'createdAt'] property where you can filter the fields of the outgoing relation. To explicitly select the returned fields of a joined table (using the relations property does implicitly a left join) you have to use a query builder.
await getRepository(Foo).createQueryBuilder('foo')
.where({ id: 1})
.select(['foo.id', 'foo.createdAt', 'bar.id', 'bar.name'])
.leftJoin('foo.bars', 'bar') // bar is the joined table
.getMany();
Try something like that:
...findOne({
select: {
id: true,
createdAt: true,
userId: {
id: true,
name: true,
},
offerId: {
id: true,
offerDrescription: true,
},
},
...
where: {...},
})
You can do it like this if you rather use the repository API instead of the queryBuilder
return await this.repository.findOne({
where: { id },
select: {
userId: {
id: true,
name: true
},
offerId: {
id: true,
offerDrescription: true
}
},
relations: {
userId: true,
offerId: true,
}
});

How to multiply NumberDecimal values in mongodb

I have the following structure:
{
"_id": "5d0118f0f57a282f89bc5f71",
"product": {
"_id": "5cfed37375a13067dd01ddb7",
"name": "My product",
"description": "My description",
"purchased_amount": 15,
"unit_price_mex": "45",
"unit_price_to_sell": "5",
"travel": "5cf58713d6f7f1657e2d8302",
"__v": 0,
"id": "5cfed37375a13067dd01ddb7"
},
"client": {
"_id": "5cf1778efffb651fad89d8b6",
"name": "Client name",
"description": "",
"__v": 0
},
"purchased_amount": 3,
"fch": "13/6/2019",
"__v": 0
},
{
"_id": "5d0151afda1a446008f1817b",
"product": {
"_id": "5cfed1995eaf2665c45efd82",
"name": "Camisa",
"description": "Camisas buenas",
"purchased_amount": 10,
"unit_price_mex": "100",
"unit_price_to_sell": "15",
"travel": "5cf56b04462a865264fabb9d",
"__v": 0,
"id": "5cfed1995eaf2665c45efd82"
},
"client": {
"_id": "5cf1778efffb651fad89d8b6",
"name": "Randy",
"description": "El que trabaja aqui",
"__v": 0
},
"purchased_amount": 34,
"fch": "12/6/2019",
"__v": 0
},
Where client and product are of type ObjectId. This is the Schema:
Client Model
var mongoose = require('mongoose');
const mongoosePaginate = require('mongoose-paginate-v2');
var clientSchema = new mongoose.Schema({
name: String,
description: String
}).plugin(mongoosePaginate);
var Client = mongoose.model('Client', clientSchema);
module.exports = Client;
Product Model
var mongoose = require('mongoose');
const mongoosePaginate = require('mongoose-paginate-v2');
var productSchema = new mongoose.Schema({
name: String,
description: String,
purchased_amount: Number,
unit_price_mex: mongoose.Schema.Types.Decimal128,
unit_price_to_sell: mongoose.Schema.Types.Decimal128,
travel: { type: mongoose.Schema.Types.ObjectId, ref: 'Travel' }
}).plugin(mongoosePaginate);
productSchema.set('toJSON', {
getters: true,
transform: (doc, ret) => {
if (ret.unit_price_mex) {
ret.unit_price_mex = ret.unit_price_mex.toString();
}
if ( ret.unit_price_to_sell ) {
ret.unit_price_to_sell = ret.unit_price_to_sell.toString();
}
}
})
var Product = mongoose.model('Product', productSchema);
module.exports = Product;
I need to get the multiplication sum of purchased_amount by product.unit_price_to_sell. My code is the following but always returns 0. Apparently, "$product.unit_price_to_sell" does not return the decimal value.
var aggregate = InvoiceModel.aggregate([
{ $match: { client: mongoose.Types.ObjectId( id ) } },
{ $group: { _id: null, total: { $sum: { $multiply: [ "$purchased_amount", "$product.unit_price_to_sell" ] } } } }
]);
InvoiceModel.aggregatePaginate(aggregate, {}, (error, aggs) => {
InvoiceModel.paginate({ client: id },{ page, limit, populate: 'client product' }, (err, value) => {
return res.status(200).send({
results: value.docs,
totalPages: value.totalPages,
totalDocs: value.totalDocs,
purchase_amount_total : aggs.docs[0].total
})
})
})
MongoDB cannot use string values in arithmetic expressions. You must either store the values using their numeric non-string representations, or you must use an aggregation operator like $toDecimal to convert the values to their numeric representations first.
Modifying your $group stage to something like the following should work:
{ $group: { _id: null, total: { $sum: { $multiply: [ "$purchased_amount", { $toDecimal: "$product.unit_price_to_sell" } ] } } }
Please note, however, that this will only work for MongoDB versions >= 4.0. If you're using an older version of MongoDB, you will either need to upgrade it to at least version 4.0 or begin converting your existing values from strings to numbers.

Multiple array elements are not updating in mongodb [duplicate]

I need to modify a document inside an array that is inside another array.
I know MongoDB doesn't support multiple '$' to iterate on multiple arrays at the same time, but they introduced arrayFilters for that.
See: https://jira.mongodb.org/browse/SERVER-831
MongoDB's sample code:
db.coll.update({}, {$set: {“a.$[i].c.$[j].d”: 2}}, {arrayFilters: [{“i.b”: 0}, {“j.d”: 0}]})
Input: {a: [{b: 0, c: [{d: 0}, {d: 1}]}, {b: 1, c: [{d: 0}, {d: 1}]}]}
Output: {a: [{b: 0, c: [{d: 2}, {d: 1}]}, {b: 1, c: [{d: 0}, {d: 1}]}]}
Here's how the documents are set:
{
"_id" : ObjectId("5a05a8b7e0ce3444f8ec5bd7"),
"name" : "support",
"contactTypes" : {
"nonWorkingHours" : [],
"workingHours" : []
},
"workingDays" : [],
"people" : [
{
"enabled" : true,
"level" : "1",
"name" : "Someone",
"_id" : ObjectId("5a05a8c3e0ce3444f8ec5bd8"),
"contacts" : [
{
"_id" : ObjectId("5a05a8dee0ce3444f8ec5bda"),
"retries" : "1",
"priority" : "1",
"type" : "email",
"data" : "some.email#email.com"
}
]
}
],
"__v" : 0
}
Here's the schema:
const ContactSchema = new Schema({
data: String,
type: String,
priority: String,
retries: String
});
const PersonSchema = new Schema({
name: String,
level: String,
priority: String,
enabled: Boolean,
contacts: [ContactSchema]
});
const GroupSchema = new Schema({
name: String,
people: [PersonSchema],
workingHours: { start: String, end: String },
workingDays: [Number],
contactTypes: { workingHours: [String], nonWorkingHours: [String] }
});
I need to update a contact. This is what I tried using arrayFilters:
Group.update(
{},
{'$set': {'people.$[i].contacts.$[j].data': 'new data'}},
{arrayFilters: [
{'i._id': mongoose.Types.ObjectId(req.params.personId)},
{'j._id': mongoose.Types.ObjectId(req.params.contactId)}]},
function(err, doc) {
if (err) {
res.status(500).send(err);
}
res.send(doc);
}
);
The document is never updated and I get this response:
{
"ok": 0,
"n": 0,
"nModified": 0
}
What am I doing wrong?
So the arrayFilters option with positional filtered $[<identifier>] does actually work properly with the development release series since MongoDB 3.5.12 and also in the current release candidates for the MongoDB 3.6 series, where this will actually be officially released. The only problem is of course is that the "drivers" in use have not actually caught up to this yet.
Re-iterating the same content I have already placed on Updating a Nested Array with MongoDB:
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.
All this means is that the current "driver" implementation of .update() actually "removes" the necessary arguments with the definition of arrayFilters. For NodeJS this will be addressed in the 3.x release series of the driver, and of course "mongoose" will then likely take some time after that release to implement it's own dependencies on the updated driver, which would then no longer "strip" such actions.
You can however still run this on a supported server instance, by dropping back to the basic "update command" syntax usage, since this bypassed the implemented driver method:
const mongoose = require('mongoose'),
Schema = mongoose.Schema,
ObjectId = mongoose.Types.ObjectId;
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
const uri = 'mongodb://localhost/test',
options = { useMongoClient: true };
const contactSchema = new Schema({
data: String,
type: String,
priority: String,
retries: String
});
const personSchema = new Schema({
name: String,
level: String,
priority: String,
enabled: Boolean,
contacts: [contactSchema]
});
const groupSchema = new Schema({
name: String,
people: [personSchema],
workingHours: { start: String, end: String },
workingDays: { type: [Number], default: undefined },
contactTypes: {
workingHours: { type: [String], default: undefined },
contactTypes: { type: [String], default: undefined }
}
});
const Group = mongoose.model('Group', groupSchema);
function log(data) {
console.log(JSON.stringify(data, undefined, 2))
}
(async function() {
try {
const conn = await mongoose.connect(uri,options);
// Clean data
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.remove() )
);
// Create sample
await Group.create({
name: "support",
people: [
{
"_id": ObjectId("5a05a8c3e0ce3444f8ec5bd8"),
"enabled": true,
"level": "1",
"name": "Someone",
"contacts": [
{
"type": "email",
"data": "adifferent.email#example.com"
},
{
"_id": ObjectId("5a05a8dee0ce3444f8ec5bda"),
"retries": "1",
"priority": "1",
"type": "email",
"data": "some.email#example.com"
}
]
}
]
});
let result = await conn.db.command({
"update": Group.collection.name,
"updates": [
{
"q": {},
"u": { "$set": { "people.$[i].contacts.$[j].data": "new data" } },
"multi": true,
"arrayFilters": [
{ "i._id": ObjectId("5a05a8c3e0ce3444f8ec5bd8") },
{ "j._id": ObjectId("5a05a8dee0ce3444f8ec5bda") }
]
}
]
});
log(result);
let group = await Group.findOne();
log(group);
} catch(e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})()
Since that sends the "command" directly through to the server, we see the expected update does in fact take place:
Mongoose: groups.remove({}, {})
Mongoose: groups.insert({ name: 'support', _id: ObjectId("5a06557fb568aa0ad793c5e4"), people: [ { _id: ObjectId("5a05a8c3e0ce3444f8ec5bd8"), enabled: true, level: '1', name: 'Someone', contacts: [ { type: 'email', data: 'adifferent.email#example.com', _id: ObjectId("5a06557fb568aa0ad793c5e5") }, { _id: ObjectId("5a05a8dee0ce3444f8ec5bda"), retries: '1', priority: '1', type: 'email', data: 'some.email#example.com' } ] } ], __v: 0 })
{ n: 1,
nModified: 1,
opTime:
{ ts: Timestamp { _bsontype: 'Timestamp', low_: 3, high_: 1510364543 },
t: 24 },
electionId: 7fffffff0000000000000018,
ok: 1,
operationTime: Timestamp { _bsontype: 'Timestamp', low_: 3, high_: 1510364543 },
'$clusterTime':
{ clusterTime: Timestamp { _bsontype: 'Timestamp', low_: 3, high_: 1510364543 },
signature: { hash: [Object], keyId: 0 } } }
Mongoose: groups.findOne({}, { fields: {} })
{
"_id": "5a06557fb568aa0ad793c5e4",
"name": "support",
"__v": 0,
"people": [
{
"_id": "5a05a8c3e0ce3444f8ec5bd8",
"enabled": true,
"level": "1",
"name": "Someone",
"contacts": [
{
"type": "email",
"data": "adifferent.email#example.com",
"_id": "5a06557fb568aa0ad793c5e5"
},
{
"_id": "5a05a8dee0ce3444f8ec5bda",
"retries": "1",
"priority": "1",
"type": "email",
"data": "new data" // <-- updated here
}
]
}
]
}
So right "now"[1] the drivers available "off the shelf" don't actually implement .update() or it's other implementing counterparts in a way that is compatible with actually passing through the necessary arrayFilters argument. So if you are "playing with" a development series or release candiate server, then you really should be prepared to be working with the "bleeding edge" and unreleased drivers as well.
But you can actually do this as demonstrated in any driver, in the correct form where the command being issued is not going to be altered.
[1] As of writing on November 11th 2017 there is no "official" release of MongoDB or the supported drivers that actually implement this. Production usage should be based on official releases of the server and supported drivers only.
I had a similar use case. But my second level nested array doesn't have a key. While most examples out there showcase an example with arrays having a key like this:
{
"id": 1,
"items": [
{
"name": "Product 1",
"colors": ["yellow", "blue", "black"]
}
]
}
My use case is like this, without the key:
{
"colors": [
["yellow"],
["blue"],
["black"]
]
}
I managed to use the arrayfilters by ommiting the label of the first level of the array nest. Example document:
db.createCollection('ProductFlow')
db.ProductFlow.insertOne(
{
"steps": [
[
{
"actionType": "dispatch",
"payload": {
"vehicle": {
"name": "Livestock Truck",
"type": "road",
"thirdParty": true
}
}
},
{
"actionType": "dispatch",
"payload": {
"vehicle": {
"name": "Airplane",
"type": "air",
"thirdParty": true
}
}
}
],
[
{
"actionType": "store",
"payload": {
"company": "Company A",
"is_supplier": false
}
}
],
[
{
"actionType": "sell",
"payload": {
"reseller": "Company B",
"is_supplier": false
}
}
]
]
}
)
In my case, I want to:
Find all documents that have any steps with payload.vehicle.thirdParty=true and actionType=dispatch
Update the actions set payload.vehicle.thirdParty=true only for the actions that have actionType=dispatch.
My first approach was withour arrayfilters. But it would create the property payload.vehicle.thirdParty=true inside the steps with actionType store and sell.
The final query that updated the properties only inside the steps with actionType=dispatch:
Mongo Shell:
db.ProductFlow.updateMany(
{"steps": {"$elemMatch": {"$elemMatch": {"payload.vehicle.thirdParty": true, "actionType": "dispatch"}}}},
{"$set": {"steps.$[].$[i].payload.vehicle.thirdParty": false}},
{"arrayFilters": [ { "i.actionType": "dispatch" } ], multi: true}
)
PyMongo:
query = {
"steps": {"$elemMatch": {"$elemMatch": {"payload.vehicle.thirdParty": True, "actionType": "dispatch"}}}
}
update_statement = {
"$set": {
"steps.$[].$[i].payload.vehicle.thirdParty": False
}
}
array_filters = [
{ "i.actionType": "dispatch" }
]
NOTE that I'm omitting the label on the first array at the update statement steps.$[].$[i].payload.vehicle.thirdParty. Most examples out there will use both labels because their objects have a key for the array. I took me some time to figure that out.

Meteor Mongo add subdocument

I have a collection documents MasterPropinsis like this :
{
"_id": "4HSb7bbjFBzRSftXu",
"nama": "Yogyakarta",
"kabupaten": [
{
"id": "KulonProgo",
"nama": "Kulon Progo",
"kecamatan": [{ "nama": "Kalibawang" },{ "nama": "Nanggulan" }]
},
{
"id": "Sleman",
"nama": "Sleman",
"kecamatan": [{ "nama": "Depok" },{ "nama": "Berbah" }]
},
{
"id": "Bantul",
"nama": "Bantul",
"kecamatan": []
}
]
}
At kabupaten:Bantul, I want to Add subdocument kecamantan:XXX, with this code :
Masterpropinsis.update(
{
_id: Session.get('idKabupaten').toString(),
'kabupaten.id': Session.get('idKecamatan').replace(" ", "")
},
{
$addToSet: {
'kabupaten.kecamatan': {
nama: nama,
createdAt: new Date(),
createBy: CreateBy,
createByID: CreateByid
}
}
},
{
validate: true
});
But I get this error:
Uncaught Error: Not permitted. Untrusted code may only update documents by ID. [403]
Here is an example try this
var docId = Session.get('idKabupaten'); // usally toString is not nesserry unless you do something unusual
Masterpropinsis.update({_id: docId }, {
$push: {
'kabupaten.kecamatan': {
nama: nama,
createdAt: new Date(),
createBy: CreateBy,
createByID: CreateByid
}
}
}); // validate true also is the default unless again you do something unusual
you can see we only supply {_id: docId} in the selector the error says in client you can only update by the ID in the server you can do any selector. Also hopefully you can have Allow/Deny rules to allow updates from the client? http://docs.meteor.com/#/full/allow