Mongoose remove subdocument - node.js

I am struggling to get subdocument removed from the parent.
I am using Mongoose findOneAndUpdate.
unitRouter.delete('/:id/contracts/:cid', async (req, res) => {
Unit.findOneAndUpdate(
{ id: req.params.id },
{$pull: {contracts: { id: req.params.cid }}},
function(err, data){
console.log(err, data);
});
res.redirect(`/units/${req.params.id}`);
});
Schema is as follows:
const unitSchema = new mongoose.Schema({
address: {
type: String,
required: true
}
contracts: [{type: mongoose.Schema.Types.ObjectId, ref: 'Contract'}]
});
And it doesn't remove it from the list, neither from the contract collection.
I have checked similar topics, but didn't got it to work. What am I missing?

First of all, your schema does not match with your query.
Your schema doesn't have any id. Do you mean _id created by default?
contracts field is an array of ObjectId, not an object like { id: XXX }
So, starting from the schema you can have a collection similar to this:
[
{
"contracts": [
"5a934e000102030405000000",
"5a934e000102030405000001",
"5a934e000102030405000002"
],
"_id": "613bd938774f3b0fa8f9c1ce",
"address": "1"
},
{
"contracts": [
"5a934e000102030405000000",
"5a934e000102030405000001",
"5a934e000102030405000002"
],
"_id": "613bd938774f3b0fa8f9c1cf",
"address": "2"
}
]
With this collection (which match with your schema) you need the following query:
Unit.updateOne({
"_id": req.params.id
},
{
"$pull": {
"contracts": req.params.cid
}
})
Example here.
Also, the inverse way, your query is ok but your schema doesn't. Then you need a schema similar to this:
new mongoose.Schema(
{
id:{
type: mongoose.Schema.Types.ObjectId,
required: true
},
address: {
type: String,
required: true
},
contracts: [{
id:{
type: mongoose.Schema.Types.ObjectId,
ref: 'Contract'
}
}]
});
Example here
By the way, take care to not confuse between id and _id. By default is created the field _id.

Related

How to query for sub-document in an array with Mongoose

I have a Schema of Project that looks like this:
const ProjectSchema = new mongoose.Schema({
name: {
type: String,
Required: true,
trim: true
},
description: {
type: String,
},
devices: [{
name: {type: String, Required: true},
number: {type: String, trim: true},
deck: {type: String},
room: {type: String},
frame: {type: String}
}],
cables: {
type: Array
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
adminsID: {
type: Array
},
createdAt: {
type: Date,
default: Date.now
}
I want to query an object from array of "devices".
I was able to add, delete and display all sub-documents from this array but I found it really difficult to get single object that matches _id criteria in the array.
The closest I got is this (I'm requesting: '/:id/:deviceID/edit' where ":id" is Project ObjectId.
let device = await Project.find("devices._id": req.params.deviceID).lean()
console.log(device)
which provides me with below info:
[
{
_id: 6009cfb3728ec23034187d3b,
cables: [],
adminsID: [],
name: 'Test project',
description: 'Test project description',
user: 5fff69af08fc5e47a0ce7944,
devices: [ [Object], [Object] ],
createdAt: 2021-01-21T19:02:11.352Z,
__v: 0
}
]
I know this might be really trivial problem, but I have tested for different solutions and nothing seemed to work with me. Thanks for understanding
This is how you can filter only single object from the devices array:
Project.find({"devices._id":req.params.deviceID },{ name:1, devices: { $elemMatch:{ _id:req.params.deviceID } }})
You can use $elemMatch into projection or query stage into find, whatever you want it works:
db.collection.find({
"id": 1,
"devices": { "$elemMatch": { "id": 1 } }
},{
"devices.$": 1
})
or
db.collection.find({
"id": 1
},
{
"devices": { "$elemMatch": { "id": 1 } }
})
Examples here and here
Using mongoose is the same query.
yourModel.findOne({
"id": req.params.id
},
{
"devices": { "$elemMatch": { "id": req.params.deviceID } }
}).then(result => {
console.log("result = ",result.name)
}).catch(e => {
// error
})
You'll need to use aggregate if you wish to get the device alone. This will return an array
Project.aggregate([
{ "$unwind": "$devices" },
{ "$match": { "devices._id": req.params.deviceID } },
{
"$project": {
name: "$devices.name",
// Other fields
}
}
])
You either await this or use .then() at the end.
Or you could use findOne() which will give you the Project + devices with only a single element
Or find, which will give you an array of object with the _id of the project and a single element in devices
Project.findOne({"devices._id": req.params.deviceID}, 'devices.$'})
.then(project => {
console.log(project.devices[0])
})
For now I worked it around with:
let project = await Project.findById(req.params.id).lean()
let device = project.devices.find( _id => req.params.deviceID)
It provides me with what I wanted but I as you can see I request whole project. Hopefuly it won't give me any long lasting troubles in the future.

find object by id which is in array mongoose

My chat object has got an array with two elements - users id. I have first id from url params, but second id I have in array of users. Now I want to get all chats where first id is this one from url, and second is in the array. I think that example how I tried to do it will be the best explanation of this problem :
Chat.find({
users: { $all: [req.params.id, { $in: usersIdArray }] }
})
where usersIdArray is for example:
[ 5f8777a01d8c122e74cb6f08, 5f8777721d8c122e74cb6f02 ]
they are numbers, not strings. I don't know if it is important...
The error I get now :
(node:12168) UnhandledPromiseRejectionWarning: CastError: Cast to ObjectId failed for value "{ '$in': [ 5f8777a01d8c122e74cb6f08, 5f8777721d8c122e74cb6f02 ] }" at path "users" for model "Chat"
And my chat schema:
// Create schema
const ChatSchema = new Schema({
users: {
type: [{
type: Schema.Types.ObjectId,
ref: 'Users',
}, {
type: Schema.Types.ObjectId,
ref: 'Users',
}],
required: [true, 'Podaj uczestników czatu.'],
},
lastMessage: {
type: Schema.Types.ObjectId,
ref: 'Message'
}
}, { timestamps: opts });
Since the length of your array is fixed (2), you can just query based on array position:
Chat.find({
"users.0": req.params.id,
"users.1": {$in: usersIdArray}
});
If that doesn't work then probably because usersIdArray are actually not ObjectIds, in which case you'd need to map them:
usersIdArray.map(x => ObjectId(x))
#Christian Fritz, I had to add $or to your solution and everything is fine now:
Chat.find({
$or: [
{
"users.1": req.params.id,
"users.0": { $in: usersIdArray }
}, {
"users.0": req.params.id,
"users.1": { $in: usersIdArray }
}]
})

Mongoose populating refs inside refs

I'm trying to save and retrieve a collection of game matches with associated games, and who played in it. My schema looks like this,
const TournamentSchema = new mongoose.Schema({
matches: [{
games: [{
type: mongoose.Schema.Types.Mixed,
ref: 'Game',
players: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Player',
}],
}],
}],
});
This is what the object in the database looks like,
{
"__v": 0,
"_id": "5a50ed6b267ddd32c4523327",
"matches": [
{
"_id": "5a50ed6b267ddd32c4523328",
"games": [
{
"players": [
{ "_id": "5a4fa908d9d55465ac4fdbe6" },
{ "_id": "5a50cf3d09176c2bb0f98fe1" }
],
"_id": "5a498918ffc6220edbe8a403"
},
{
"players": [
{ "_id": "5a50cf5609176c2bb0f98fe2" },
{ "_id": "5a50cf6009176c2bb0f98fe3" }
],
"_id": "5a50cf9007c2bb0c73f3783a"
}
]
}
],
}
I'm trying to retrieve it like this,
async function list(req, res, next) {
logger.log('info', 'Incoming request to retrieve all tournaments');
const tournaments = await Tournament.find().populate('matches.games.players');
return res.json(tournaments);
}
However, what I get from the database is the same as what was saved. i.e the refs don't get resolved. If I change type: Mixed from games to type: ObjectId it wont persist players, but populate will resolve games. How do I work with refs inside refs?
As requested this is what my Game schema looks like,
const GameSchema = new mongoose.Schema({
name: {
type: String,
unique: true,
required: true,
lowercase: true,
},
scoring: {
type: Object,
required: true,
rank: {
first_place: Number,
second_place: Number,
third_place: Number,
},
},
max_num_players: Number,
min_num_players: Number,
}, { runSettersOnQuery: true });
Each Game can have different scoring percentage per rank. For example for Counter Strike if you were first place, you would get 100% of points, second 80%, third 50%. However, for League of Legends first place would be 85%, second 60%, and third 50%.
I think the problem is that you name "game" both the game definition in the game collection and the tournament game (which actually is game + players).
I would write the schema like this (not tested):
const TournamentSchema = new mongoose.Schema({
matches: [{
games: [{
game: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Game',
},
players: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Player',
}],
}],
}],
});
And you would query like this:
Tournament.find().populate('matches.games.game matches.games.players')
I still find unclear what the Game schema contains and why the Game itself does not have a list of players.
Maybe it's a bit late, but I'll share what I've understood through researching of a similar case of nested populating (as the title says).
In the documentation it says that you could solve population across multiple levels in this way:
We have an Schema:
const CustomerSchema = new Schema({
orders: [
{
type: Schema.Types.ObjectId || null,
ref: "Order",
},
],
(...)
})
But the orders at the same time have products refs:
const OrderSchema = new Schema({
products: {
type: [{ type: Schema.Types.ObjectId, ref: "Product" }],
},
(...)
});
Now you could do a deep population like this:
const user = await CustomerModel.findById(id)
.populate("user", "-password")
.populate({ path: "orders", populate: { path: "products" } }) <---
.exec();
Hope this might help someone with a similar case.

Add elements in nested document then retrieve the _id

I have the following collection definition:
// Includes
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
// Create required sub schemas
const subSchema0 = new Schema({
value: String,
});
const subSchema = new Schema({
idWordsLibraryName: {
type: Schema.Types.ObjectId,
ref: 'WordsLibrary1_0',
},
type: String,
values: [
subSchema0,
],
});
const schema = new Schema({
version_: String,
idWordsLibraryName: {
type: Schema.Types.ObjectId,
ref: 'WordsLibrary1_0',
},
idsDads: [{
type: Schema.Types.ObjectId,
ref: 'LocationStructure1_0',
}],
params: [
subSchema,
],
});
Summary -> One document with nested parameters with nested values.
I have the following request that add some values into a particular parameter
this.findOneAndUpdate({
_id: data.idLocationStructure,
'params._id': data.idLocationStructureParameter,
}, {
$push: {
'params.$.values': {
$each: dataToPush,
},
},
}, {
new: true,
});
It works as expected.
What I want now is to get the _id of pushed elements, but without loading all values of the parameter.
I have tried to use the select option of findOneAndUpdate but it don't work using the projection:
this.findOneAndUpdate({
_id: data.idLocationStructure,
'params._id': data.idLocationStructureParameter,
}, {
$push: {
'params.$.values': {
$each: dataToPush,
},
},
}, {
new: true,
select: {
'params.$.values': 1,
},
});
It gets me:
{
"_id": "57273904135f829c3b0739dd",
"params": [
{},
{},
{},
{},
],
},
I have tried to perform a second request to get the _ids as well, but it don't work either:
this.find({
_id: data.idLocationStructure,
'params._id': data.idLocationStructureParameter,
}, {
_id: 1,
'params.$.values': {
$slice: -nbAdded,
},
});
If you have any idea of how retrieving the _id of the pushed values without loading all values of the parameter, you are very welcome :)
Well after tons of researches all over the web and stack overflow <3 I have found a solution, which is:
this.aggregate([{
$match: {
_id: new mongoose.Types.ObjectId(data.idLocationStructure),
},
},
{
$unwind: '$params',
}, {
$match: {
'params._id': new mongoose.Types.ObjectId(data.idLocationStructureParameter),
},
},
{
$unwind: '$params.values',
},
{
$sort: {
'params.values._id': -1
},
},
{
$limit: nbAdded,
},
{
$project: {
_id: '$params.values._id',
},
},
]);
If you experience the same problem, here is the explaination:
$match makes me taking the good high level document
$unwind makes me to go into the params array in the document we $match
$match makes me taking the good parameter
$unwind makes me to go into the values array
I $sort all values by _id DESC
I $limit to the number of values I added previsoulsy
I change the name of the _id (like an alias)
So I got as result an array that contains the last added values _ids

On the same findByIdAndUpdate, use $push and $pull

In the code below I am trying to findByIdAndUpdate and for updating I want to add to two arrays of objectIds and pull from another. However, only the first command for the update is being executed. So $push: {Group1: groupId, Group2: groupId} is being executed but $pull: {Group3: groupId} is not.
Is there a way to make both operations work together?
Code
User.findByIdAndUpdate(
userId,
{
$push: {Group1: groupId, Group2: groupId},
$pull: {Group3: groupId},
},
{'new': true, 'multi':true},
function (err, user) {
if (err)
console.log(err);
else {
res.json({
success: true,
message: 'Success.'
});
}
}
);
Schema
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var bcrypt = require('bcrypt-nodejs');
var ObjectId = Schema.Types.ObjectId;
//user schema
var UserSchema = new Schema({
username: {type: String, required: true, index:{unique:true}},
phoneNumber: {type: String, required: true, index:{unique:true}},
password: {type: String, required: true, select: false},
name: String,
Group1: [{ type : ObjectId, ref: 'Group' }],
Group2: [{ type : ObjectId, ref: 'Group' }],
Group3: [{ type : ObjectId, ref: 'Group' }],
});
module.exports = mongoose.model('User', UserSchema);
here is the json representation on the User object
{
"_id": ObjectId('564a4a24f63d409f526659c4'),
"password": "$2a$10$YYEdr4kavrB2w8dRWHqWC.hAUd1pRzKM6YQt9iLtkrLDk0cRg24Wa",
"phoneNumber": "222",
"username": "222",
"Group1": [
ObjectId('564a2ac4e982c8bb5122a96e')
],
"Group2": [],
"Group3": [],
"__v": 0
}
when I execute my code the json object looks like
{
"_id": ObjectId('564a4a24f63d409f526659c4'),
"password": "$2a$10$YYEdr4kavrB2w8dRWHqWC.hAUd1pRzKM6YQt9iLtkrLDk0cRg24Wa",
"phoneNumber": "222",
"username": "222",
"Group1": [
ObjectId('564a2ac4e982c8bb5122a96e')
],
"Group2": [
ObjectId('564a2acae982c8bb5122a970')
],
"Group3": [
ObjectId('564a2acae982c8bb5122a970')
],
"__v": 0
}
So the objectID from Group3 is not being removed.
My thought on the matter was that i am not using the correct findByIdAndUpdate correctly when it comes to doing a pull and a push at the same time.
This makes me lean towards just using a findById and then doing the update in the callback. Would there be a disadvantage in doing that vs using findByIdAndUpdate?

Resources