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
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.
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 }
}]
})
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.
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
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?