Is it possible to populate objects in map schema type? - javascript

I have schema type Map in my mongoose model. In this map, each element has reference to another model. I know that it's possible to populate attributes in array, but how about Map type? Be cause nesting like "map_type_attribute.some_attribute_to_populate" doesn't work. :)
This is my model:
const Mongoose = require('mongoose');
const parameter = Mongoose.Schema({
definition: {
type: Mongoose.Schema.Types.ObjectId,
ref: 'Definition',
},
value: {},
}, {_id: false});
const schema = Mongoose.Schema({
model: {
type: Mongoose.Schema.Types.ObjectId,
ref: 'Model'
},
name: String,
objectid: Number,
externalId: String,
properties: Mongoose.Schema.Types.Mixed,
parameters: {
type: Map,
of: parameter
}
});
module.exports = Mongoose.model('Element', schema);
This is how i'm trying to populate definition field:
const request = Element.find(query, projection);
request.populate('parameters.definition');
request.exec( (err, docs) => {
...

This functionality was added in Mongoose 5.10.3 (September 2020). You simply denote every element in the map with a $*.
In your example this would be:
const request = Element.find(query, projection);
request.populate('parameters.$*.definition');
request.exec( (err, docs) => {

I also trying to find answer on this question. It seems that deep-populate work, but only if you put keys from the Map to populate method/function. In your case, if you have data like:
{
model: ObjectId("111"),
name: "MyName",
objectid: 111,
externalId: "ExternalId",
properties: ...,
parameters:{
"parameter1":{
"definition":ObjectId("333"),
"value":"value of parameter 1"
},
"parameter2":{
"definition": ObjectId("444"),
"value": "value of parameter 2"
}
}
}
Then you may find and populate like this:
Element.find({}).populate("parameters.parameter1.definiton")
But it's not a good solution. It would be nice if we have something like regexp inside this populate path.
Currently I've managed only to grab all inner collection, and then manually work with Map to substitude collections.
It shouldn't be a huge overhead, since you have only 2 queries to DB. In you case it can be like:
const elements = Element.find({});
const parameters = Parameter.find({});
// go through the elements.parameters and replace it with appropriate value from parameters collection.

Related

MongoDB/Mongoose: AddToSet an array into an array field, but also addToSet a nested array field value if the parent object already exists

I am building an education application and I am trying to add/update a field which is an array of objects with addToSet from a javascript array, and if the object already exists (matched with objectId) I want to update the already existing object's array (addToSet) and change another field of that same object.
My model looks like this (simplified):
const course = new Schema(
{
events: [
{
type: Schema.Types.ObjectId,
ref: 'event'
}
],
students: [
{
user: {
type: Schema.Types.ObjectId,
ref: 'user'
},
status: {
type: String,
enum: ['notBooked', 'booked', 'attended', 'completed']
},
events: [
{
type: Schema.Types.ObjectId,
ref: 'event'
}
]
}
],
});
And ideally I would like to use an updateOne query to both addToSet to the course's list of events, while also updating the students list.
Currently I am using this code to accomplish my updates by first finding the course and then using javascript to update it, which works:
const studentsToAdd = this.attendees.map((attendee) => ({
user: attendee._id,
status: 'booked',
events: [this._id]
}));
const studentIds = studentsToAdd.map((student) => student.user);
const course = await courseModel.findById(this.course);
console.log(studentIds);
course.events.push(this._id);
course.students.forEach((student) => {
if (studentIds.some((s) => s.equals(student.user))) {
student.events.push(this._id);
student.status = 'booked';
studentsToAdd.splice(studentsToAdd.indexOf(studentsToAdd.find((s) => s.user.equals(student.user))), 1);
}
});
course.students.push(...studentsToAdd);
course.save();
However I am curious if it is possible to solve this using a single updateOne on the courseModel schema.
This is part of a mongoose middleware function, and "this" references an event object, which has its own _id, attendees of the event, and the course ID (named course).
The goal is to add the student object part of studentsToAdd to the students array of the course IF the student does not exist (signified by user being a reference by objectId), and if the student already exists then I want to add the eventId (this._id) to the events array for that particular student and set status for that student to "booked".
Is this possible? I have tried many iterations using $cond, $elemmatch, "students.$[]" and so forth but I am quite new to mongodb and am unsure how to go about this.

Mongoose: Count array elements

I have the following Schema with a array of ObjectIds:
const userSchema = new Schema({
...
article: [{
type: mongoose.Schema.Types.ObjectId,
}],
...
},
I will count the array elements in the example above the result should be 10.
I have tried the following but this doesn't worked for me. The req.query.id is the _id from the user and will filter the specific user with the matching article array.
const userData = User.aggregate(
[
{
$match: {_id: id}
},
{
$project: {article: {$size: '$article'}}
},
]
)
console.log(res.json(userData));
The console.log(article.length) give me currently 0. How can I do this? Is the aggregate function the right choice or is a other way better to count elements of a array?
Not sure why to use aggregate when array of ids is already with user object.
Define articles field as reference:
const {Schema} = mongoose.Schema;
const {Types} = Schema;
const userSchema = new Schema({
...
article: {
type: [Types.ObjectId],
ref: 'Article',
index: true,
},
...
});
// add virtual if You want
userSchema.virtual('articleCount').get(function () {
return this.article.length;
});
and get them using populate:
const user = await User.findById(req.query.id).populate('articles');
console.log(user.article.length);
or simply have array of ids:
const user = await User.findById(req.query.id);
console.log(user.article.length);
make use of virtual field:
const user = await User.findById(req.query.id);
console.log(user.articleCount);
P.S. I use aggregate when I need to do complex post filter logic which in fact is aggregation. Think about it like You have resultset, but You want process resultset on db side to have more specific information which would be ineffective if You would do queries to db inside loop. Like if I need to get users which added specific article by specific day and partition them by hour.

Populating subfield from other collections (not refs)

I am trying to populate subfields of a document, which are not defined as refs. The problem is that mongoose keeps returning null, whenever I try to fetch the document and populate the fields.
I will try to make this a generic question. I haven't found an answer anywhere online.
schemaA:
const schemaA = new Schema({
before: {
type: Object,
default: {}
},
after: {
type: Object,
default: {}
}
});
module.exports = SchemaA = mongoose.model("schemaA", schemaA);
schemaB:
const schemaB = new Schema({
someField: {
subFieldA: {
type: String
},
subFieldB: {
type: String
}
}
});
module.exports = SchemaB = mongoose.model("schemaB", schemaB);
And an example document that would exist in schemaA is:
_id: ObjectId('5e4ab79d9d3ce8633aedf524')
before: {
someField: {
subFieldA: ObjectId('5e4ab74f9d3ce8633aedf2eb'),
subFieldB: ObjectId('5e4ab74f9d3ce8633aedf2ep')
},
}
after: {
someField: {
subFieldA: ObjectId('5e4ab74f9d4ce8633aedf2eb'),
subFieldB: ObjectId('5e4ab74f9d3ce8639aedf2ep')
},
}
date: 2020-02-17T15:56:13.340+00:00
My query:
const schemaAs = await SchemaA.find()
.populate(
"before.someField.subFieldA, before.someField.subFieldB, after.someField.subFieldA, after.someField.subFieldB"
)
But this query returns null. What am I doing wrong?
You are looking for Dynamic References.
This lets you set what collection you are referencing as a property to each individual document, instead of hard coding it to one specific collection.
As far as I know, it is not possible to populate a property without any reference.

Creating an array INSIDE an array in the mongoose schema

I want to create an array of stores. And in each store, there would be sections. So one store maybe have a deli and produce section. Another store may just cashiers and a meat section.
var UserSchema = new Schema({
store: {
type: Array,
},
section: { // how do I put this inside the store array?
type: Array,
}
});
we can do it by by specifying type mixed, it will accept any kind of data structre
var UserSchema = new Schema({
store: [{
type: Schema.Types.Mixed
}]
});

Mongoose find/update subdocument

I have the following schemas for the document Folder:
var permissionSchema = new Schema({
role: { type: String },
create_folders: { type: Boolean },
create_contents: { type: Boolean }
});
var folderSchema = new Schema({
name: { type: string },
permissions: [ permissionSchema ]
});
So, for each Page I can have many permissions. In my CMS there's a panel where I list all the folders and their permissions. The admin can edit a single permission and save it.
I could easily save the whole Folder document with its permissions array, where only one permission was modified. But I don't want to save all the document (the real schema has much more fields) so I did this:
savePermission: function (folderId, permission, callback) {
Folder.findOne({ _id: folderId }, function (err, data) {
var perm = _.findWhere(data.permissions, { _id: permission._id });
_.extend(perm, permission);
data.markModified("permissions");
data.save(callback);
});
}
but the problem is that perm is always undefined! I tried to "statically" fetch the permission in this way:
var perm = data.permissions[0];
and it works great, so the problem is that Underscore library is not able to query the permissions array. So I guess that there's a better (and workgin) way to get the subdocument of a fetched document.
Any idea?
P.S.: I solved checking each item in the data.permission array using a "for" loop and checking data.permissions[i]._id == permission._id but I'd like a smarter solution, I know there's one!
So as you note, the default in mongoose is that when you "embed" data in an array like this you get an _id value for each array entry as part of it's own sub-document properties. You can actually use this value in order to determine the index of the item which you intend to update. The MongoDB way of doing this is the positional $ operator variable, which holds the "matched" position in the array:
Folder.findOneAndUpdate(
{ "_id": folderId, "permissions._id": permission._id },
{
"$set": {
"permissions.$": permission
}
},
function(err,doc) {
}
);
That .findOneAndUpdate() method will return the modified document or otherwise you can just use .update() as a method if you don't need the document returned. The main parts are "matching" the element of the array to update and "identifying" that match with the positional $ as mentioned earlier.
Then of course you are using the $set operator so that only the elements you specify are actually sent "over the wire" to the server. You can take this further with "dot notation" and just specify the elements you actually want to update. As in:
Folder.findOneAndUpdate(
{ "_id": folderId, "permissions._id": permission._id },
{
"$set": {
"permissions.$.role": permission.role
}
},
function(err,doc) {
}
);
So this is the flexibility that MongoDB provides, where you can be very "targeted" in how you actually update a document.
What this does do however is "bypass" any logic you might have built into your "mongoose" schema, such as "validation" or other "pre-save hooks". That is because the "optimal" way is a MongoDB "feature" and how it is designed. Mongoose itself tries to be a "convenience" wrapper over this logic. But if you are prepared to take some control yourself, then the updates can be made in the most optimal way.
So where possible to do so, keep your data "embedded" and don't use referenced models. It allows the atomic update of both "parent" and "child" items in simple updates where you don't need to worry about concurrency. Probably is one of the reasons you should have selected MongoDB in the first place.
In order to validate subdocuments when updating in Mongoose, you have to 'load' it as a Schema object, and then Mongoose will automatically trigger validation and hooks.
const userSchema = new mongoose.Schema({
// ...
addresses: [addressSchema],
});
If you have an array of subdocuments, you can fetch the desired one with the id() method provided by Mongoose. Then you can update its fields individually, or if you want to update multiple fields at once then use the set() method.
User.findById(userId)
.then((user) => {
const address = user.addresses.id(addressId); // returns a matching subdocument
address.set(req.body); // updates the address while keeping its schema
// address.zipCode = req.body.zipCode; // individual fields can be set directly
return user.save(); // saves document with subdocuments and triggers validation
})
.then((user) => {
res.send({ user });
})
.catch(e => res.status(400).send(e));
Note that you don't really need the userId to find the User document, you can get it by searching for the one that has an address subdocument that matches addressId as follows:
User.findOne({
'addresses._id': addressId,
})
// .then() ... the same as the example above
Remember that in MongoDB the subdocument is saved only when the parent document is saved.
Read more on the topic on the official documentation.
If you don't want separate collection, just embed the permissionSchema into the folderSchema.
var folderSchema = new Schema({
name: { type: string },
permissions: [ {
role: { type: String },
create_folders: { type: Boolean },
create_contents: { type: Boolean }
} ]
});
If you need separate collections, this is the best approach:
You could have a Permission model:
var mongoose = require('mongoose');
var PermissionSchema = new Schema({
role: { type: String },
create_folders: { type: Boolean },
create_contents: { type: Boolean }
});
module.exports = mongoose.model('Permission', PermissionSchema);
And a Folder model with a reference to the permission document.
You can reference another schema like this:
var mongoose = require('mongoose');
var FolderSchema = new Schema({
name: { type: string },
permissions: [ { type: mongoose.Schema.Types.ObjectId, ref: 'Permission' } ]
});
module.exports = mongoose.model('Folder', FolderSchema);
And then call Folder.findOne().populate('permissions') to ask mongoose to populate the field permissions.
Now, the following:
savePermission: function (folderId, permission, callback) {
Folder.findOne({ _id: folderId }).populate('permissions').exec(function (err, data) {
var perm = _.findWhere(data.permissions, { _id: permission._id });
_.extend(perm, permission);
data.markModified("permissions");
data.save(callback);
});
}
The perm field will not be undefined (if the permission._id is actually in the permissions array), since it's been populated by Mongoose.
just try
let doc = await Folder.findOneAndUpdate(
{ "_id": folderId, "permissions._id": permission._id },
{ "permissions.$": permission},
);

Categories

Resources