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.
Related
I have a Shop Model
const Shop = mongoose.Schema({
_id: mongoose.Schema.Types.ObjectId,
shop_name: { type: String },
products: {_id: mongoose.Schema.Types.ObjectId,type:Array},
});
and a product schema
const Product = mongoose.Schema({
_id: mongoose.Schema.Types.ObjectId,
title: { type: String },
description: { type: String },
shop: { type: mongoose.Schema.Types.ObjectId, ref: "Shop" },
});
I'm trying to access a product within the products array of the Shop model, so that I can update it.
I've looked online a lot but couldn't quite find what I'm looking for. I need to access a very specific product within the products array with the given parameters, which are the id of the shop and the id of the product.
This is what I tried to do
const item = await Product.findOne({_id} , 'products').find({"products._id" : productId})
But what this does is it gives a mongoose object if the second find method hits a match
[
{
products: [ [Object] ],
_id: 617f1bca39a5a43c1a981060,
butik: 'scsbutik',
butik_slug: 'egzbutikcom-1000010',
butik_image: 'https://webizade.com/bm/img/butik-10.jpg',
butik_points: '9.8',
butik_order_count: 45,
butik_success_order_count: 42,
butik_refund_count: 3,
is_butik_refund: true,
__v: 0,
login: []
}
]
I need to access the object INSIDE the products array and update that product.
Appreciate any help in advance.
don't use by default _id format I suggest declare a different value and then using populate method you can do it
const Product = mongoose.Schema({
shopId: mongoose.Schema.Types.ObjectId,
title: { type: String },
description: { type: String },
shop: { type: mongoose.Schema.Types.ObjectId, ref: 'Shop' },
});
THEN ROUTING
const item = await Product.find(_id).populate('shopId')
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.
I am getting some data in an array of object like this :
{
"success": true,
"result": {
"docs": [
{
"_id": "60a602901a74f62935a4898f",
"user": "607030ba3c82e235443db610",
"weekNum": 19,
"__v": 0,
"createdAt": "2021-05-20T06:32:48.742Z",
"data": [
{
"activity": "6063f898232d3f2acca5d2ae",
"_id": "60a6063668f27715b0f08753",
"project": "60702d1f3c82e235443db5ff",
"task": "60702d3d3c82e235443db601",
"workingDate": "2021-05-10T18:30:00.000Z",
"dayVal": 1,
"description": ""
}
],
"managersComment": "leleleleelelel",
"status": "Submitted",
"updatedAt": "2021-05-20T06:48:22.163Z"
}
],
"paginator": {
"itemCount": 1,
"offset": 0,
"perPage": 10000,
"pageCount": 1,
"currentPage": 1,
"slNo": 1,
"hasPrevPage": false,
"hasNextPage": false,
"prev": null,
"next": null
}
}
}
my schema for this collection in like this:
const timesheetSchema = new Schema({
managersComment: {
type: String
},
weekNum: {
type: Number
},
data:[{
project: {
type: Schema.ObjectId,
ref: projectModel
},
task: {
type: Schema.ObjectId,
ref: taskModel
},
activity: {
type: Schema.ObjectId,
default: null,
ref: activityModel
},
workingDate: {
type: Date
},
dayVal: {
type: Number
},
description: {
type: String
},
}],
user: { type: ObjectId, ref: userModel },
status: {
type: String,
enum: ['Saved', 'Submitted', 'Approved', 'Rejected', 'Reset']
},
}, { timestamps: true });
timesheetSchema.plugin(mongoosePaginate);
const timesheetModel = mongoose.model('timesheet', timesheetSchema);
my code for getting data is something like this:
try {
console.log('populateRequired --------------------------------------------------')
const populateArray = [
{ path: "task", select: "taskName" },
{ path: "project", select: "projectName" },
{ path: "activity", select: "title" },
];
const query = {
user: req.params.userId,
status: req.query.status,
};
const paginationParams = {
populate: populateArray,
customLabels: customLabels,
limit: req.query.limit,
};
console.log("USER QUERY ", query);
const userTimesheet = await getTimesheetDataByUserId(
query,
paginationParams
);
console.log(userTimesheet);
res.send({ success: true, result: userTimesheet });
} catch (err) {
console.log(err);
next(err);
}
But as shown in return data above i am not getting populate applied in data array. Please help not sure what to do.
According to the data you posted, I think that the issue is that you're not creating virtual fields to populate with your references. Your fields project, task and activity in each array element, or user, are meant to be ids referring to the corresponding models. But those ids alone will not implement the population, they are only the pointers that the population will need in order to be executed. To make that a little bit more clear, I would change those names to userId: { type: ObjectId, ref: userModel }.
After that, you will need to create the virtual fields:
timesheetSchema.virtual("user", {
ref: "userModel",
localField: "userId",
foreignField: "_id",
justOne: true,
});
Finally, if you want to have the virtual field timesheet.user populated each time you query your collection, you will have to add some middleware to your schema. For me, the most reasonable way to make this work is:
timesheetSchema.pre("find", function (next) {
this.populate("user");
next();
});
Just to have a complete solution: I think this will solve your problem for the timesheet.user field. But I don't think it will work in your data array. In fact, I'm not 100% sure the way you're defining it is really going to work: creating a timesheet with an array of imputations doesn't make too much sense to me. A more coherent approach would be creating a collection of all the imputations that looked like this:
const dataSchema = new Schema({
projectId: {
type: Schema.ObjectId,
ref: projectModel
},
taskId: {
type: Schema.ObjectId,
ref: taskModel
},
activityId: {
type: Schema.ObjectId,
default: null,
ref: activityModel
},
userId: {
type: ObjectId,
ref: userModel
},
workingDate: {
type: Date
},
dayVal: {
type: Number
},
description: {
type: String
},
});
With virtual fields like:
dataSchema.virtual("project", {
ref: projectModel,
localField: "projectId",
foreignField: "_id",
justOne: true
});
And so on. I would populate each field just like I showed you with the user example. Then, for the timesheet schema I would only reference userId, and populate data like this:
const timesheetSchema = new Schema({
managersComment: {
type: String
},
weekNum: {
type: Number
},
userId: {
type: ObjectId,
ref: dataModel
},
status: {
type: String,
enum: ['Saved', 'Submitted', 'Approved', 'Rejected', 'Reset']
},
}, { timestamps: true });
timesheetSchema.virtual("data", {
ref: dataModel,
localField: "userId",
foreignField: "userId"
});
timesheetSchema.virtual("user", {
ref: userModel,
localField: "userId",
foreignField: "_id",
justOne: true
});
This way you would have a collection with all the imputations for all the users, and you would be able to query and filter that collection for each userId, projectId or anything you would need. Having an array inside your timesheet collection would make this quite more difficult.
One simple solution I found on another SO post is like this:
const result = await timesheetModel.findOne(query).populate({
path: 'data.project data.activity data.task'
});
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.
I need to populate "contents" on lesson property, but the array return empty.
I'm using the plugin autopopulate and its work fine, but doesnt work with multiple levels. I tried the documentation, but 2 levels I didnt found a solutions.
It's my Schema.
const structure = {
name: {
type: String,
required: true
},
lessons: [{
name: String,
contents: [{
type: ObjectId,
ref: 'Content',
autopopulate: true
}]
}]
}
It's my query to get content.
Course.findById(contentData.course_id)
.populate({
path: 'lessons.contents',
model: 'Content'
})
.exec((err, a) => {
console.log(a);
res.status(201).json(a)
});
The array of contents in lessons return empty, but exit two registers on db.
"lessons": [
{
"contents": [],
"name": "Nova Aula"
}
],
Model your Schema like this...
const structure = {
name: {
type: String,
required: true
},
lessons: [{
name: String,
contents: [{
type: ObjectId,
ref: 'Content',
//remove this line
//autopopulate: true
}]
}]
}
Your code can look like this...
const result = Course.findById(contentData.course_id)
.populate('lessons.contents')
.select('_id x y z'); // x,y,z --> attributes name you want to select from populated collection
response.send(result);