How to delete in cascade in several models with mongoose? - mongodb

I have these 3 models in mongoose:
TravelSchema
var travelSchema = new mongoose.Schema({
name: String,
description: String,
mexican_currency_value: mongoose.Schema.Types.Decimal128
})
travelSchema.pre('deleteOne', function(next) {
const id = this.getQuery()['_id'];
Product.deleteMany({ travel: id }, (err, value) => {
});
next();
});
ProductSchema
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' }
})
InvoiceSchema
var invoiceSchema = new mongoose.Schema({
product: productSchema,
client: { type: mongoose.Schema.Types.ObjectId, ref: 'Client' },
purchased_amount: Number,
fch: String
});
Where Travel and Product have a one-to-many relationship and Product and Invoice have a one-to-many relationship.
I need the following:
When a Travel is deleted, all Products that are related to that Travel are also deleted.
When these Products are eliminated, all the Invoices related to each Product are also eliminated.
I have managed to eliminate all the products, but when I try to eliminate the invoices I do not obtain the ids of the Products.
invoiceSchema.pre('deleteMany', (next) => {
console.log(this);
// this print { n: 2, ok: 1, deletedCount: 2 }
})

I think you should start the other way when looking at deleting all the related docs. Like you have travel id, with it get all the products and store their id in array and then your first delete should be of the invoices where product._id: { $ in: _arrayOfProducIds }. Once that is complete then deleteMany your products since you already have their ids in the _arrayOfProducIds and lastly deal with the Travel:
travelSchema.pre('deleteOne', function(next) {
const id = this.getQuery()['_id']; // travel id check
// Query to get all the product ids based on the travel id and return array for `$in`
const productIds = this.DoYourQuery // [productIds] check
Invoice.deleteMany({'product._id': { $in: productIds }}, // ... etc
Product.deleteMany({ _id': { $in: productIds }}, // ... etc
next();
});
I would assume you do not have a large number of products and invoices ... like thousands since then $in might be somewhat of a performance issue. Hope this helps.

Related

BelongsTo in Sails.js

Hello, I need your help please with 2 questions.
I have 2 Models
One to Many
(One) Customer{ id, names, dni} -> Invoice {id, date, ....customer_id} (Many)
1. How can I get this?
I need to consume the api "GET /api/invoices" and that the json return of this, in turn, returns an array
[{
id: 1,
date: '2022-01-01',
....invoice
customer: {
dni: 1,
names: 'Example'
}
},
{
id: 2,
date: '2022-01-02',
....invoice
customer: {
dni: 2,
names: 'Example 2'
}
},
]
So far what I have found in the sailsjs documentation are only examples with POPULATE, where they only show how to list the User model with its corresponding created ones (hasMany)
//var users = await User.find().populate('pets');
// The users object would look something like the following
// [{
// id: 123,
// firstName: 'Foo',
// lastName: 'Bar',
// pets: [{
// id: 1,
// breed: 'labrador',
// type: 'dog',
// name: 'fido',
// user: 123
// }]
// }]
//---This is not what I need.
Is there a function or configuration that I have not found?
Or would I do something like this?
Invoices.find().exec(async(err, invoices)=>{
if(invoices){
for(i = 0; i< invoices.length; i++){
const customer = await Customer.find({id: invoices[i].customer_id});
invoices[i].customer = customer;
}
});
The point is that this takes much longer than doing a query with join
const invoices = await sails.sendNativeQuery('SELECT * from INVOICE A A inner join CUSTOMER B on A.customer_id=B.id ', []);
But I don't know how to get a JSON with the previous structure if I do it by query
2. What is the best option that can solve my problem?
The populate method works in both directions: oneToMany, manyToMany, and manyToOne:
https://sailsjs.com/documentation/reference/waterline-orm/queries/populate
If any condition is required, you could check the details on the section Populating a collection association:
var usersNamedFinn = await User.find({ name:'Finn' })
.populate('currentSwords', {
where: {
color: 'purple'
},
limit: 3,
sort: 'hipness DESC'
});

Removing reference from a child collection in MongooseJS

I have a schema that includes an array of child references:
const schemaSet = {
userSchema: new Schema({
name: {
type: String,
required: true
}
}),
groupSchema: new Schema({
name: {
type: String,
required: true
},
members: [ {
type: Schema.Types.ObjectId
ref: 'User'
}]
})
}
This is working fine in terms of being able to create groups and add users to them, but I find I can't remove a user from the group.
The closest I have got so far is this:
async removeUser(group, userId) {
console.log("Before: group has "+group.members.length+" members");
await group.members.pull({ _id : userId });
console.log("After: group has "+group.members.length+" members");
await group.save();
}
This logs out the correct size before and after the call and runs with no errors, but the next time I retrieve that group, the member is still there. I have tried using remove as well with much the same outcome.
I only want to remove the reference from the members collection, the User needs to persist. How do I persist the removal of the reference to a document?
It took me a while to discover this. If you have a group model (created using const Group = mongoose.model("Group", groupSchema)), and the id of the user you want to remove from the group, you can use the following syntax: const updatedGroup = await Group.findOneAndUpdate({ members: userId }, { $pull: { members: userId }}, {new: true, useFindAndModify: false});
The third parameter in that function indicates that the updated document should be returned, and ensures that you don't get a warning. Actually, that syntax will only work if the group to user relationship is one to many (i.e. any user can only be in one group). If the relationships is many to many, I guess you would have to use the following:
await Group.where({ members: userId}).update({ $pull: { members: userId }});

Relationship field within same collection?

I have a keystonejs model for product categories (MongoDB). Some categories should have subcategories. Currently I have set a relationship field "ChildCategoryOf", where I can manually select the Parent Category in admin panel. To have more functionality, I would like to create another Field called "ParentCategoryOf" that would consist an Array of subcategories. How is it possible to have a field that automatically stores Child categories in Array? I imagine it like this:
Current model:
let ProductCategory = new keystone.List('ProductCategory', {
autokey: {
from: 'name',
path: 'key',
unique: true
}
});
ProductCategory.add({
name: {
type: String,
required: true
},
ChildCategoryOf: {
type: Types.Relationship,
ref: 'ProductCategory',
many: false,
required: false,
},
IsParentCategory: Types.Boolean,
});
For categories in mongo you can use an inheritance like model, in which you can store the parentId and ancestor Id for each model and its children and then add a method to the mongoose model to add a child each time you call it on an instance, this would be like below code:
const ProductCategory = new Schema({
name: String,
parent: { type: Schema.Types.ObjectId, ref: 'ProductCategory'},
ancestors: [{type: Schema.Types.ObjectId, ref: 'ProductCategory'}],
children: [{type: Schema.Types.ObjectId, ref: 'ProductCategory'}]
});
ProductCategory.methods = {
addChild: function(child){
let that = this;
child.parent = this._id;
child.ancestors = this.ancestors.concat([this._id]);
return this.model('ProductCategory').create(child).addCallback
(function(child){
that.children.push(child._id);
that.save();
});
}
};
then later from where you want to find products by their categories you should concat the category id that you find with it's children ids and use this array in find query with $in operator to find all the products in a category with its children.

Mongoose Virtual Populate 4.5 One-to-Many

I'm using the Mongoose 4.5 virtual populate API and am unable to get the virtual field to populate on the one side of a one-to-many.
const FilmSchema = new Schema({
title: String,
slug: String,
director: String
});
const DirectorSchema = new Schema({
name: String,
slug: String
});
DirectorSchema.virtual('films', {
ref: 'Film',
localField: 'slug',
foreignField: 'director'
});
const Film = mongoose.model('Film', FilmSchema, 'film');
const Director = mongoose.model('Director', DirectorSchema, 'director');
Director.find({}).populate('films').exec()
.then(data => res.send(data))
.catch(err => console.log(err));
The director data is output as expected but without any mention of films and without throwing/logging any errors.
In the query log, it looks like Mongoose is trying to do what I ask:
Mongoose: director.find({}) { fields: undefined }
Mongoose: film.find({ director: { '$in': [ 'spielberg', 'zemeckis', 'nolan' ] } }) { fields: undefined }
I've tried several variations, such as:
setting a ref to Director with type: String on FilmSchema.director
setting a ref to Director with type ObjectId on FilmSchema.director
replacing slug with a custom _id String
...and various combinations of the above.
I'm using the docs example and Valeri's recent article as guides.
Does anyone see what I'm doing wrong?
Versions: Node: 6.3.0 / MongoDB: 3.2.5 / Mongoose: 4.5.8
Answered by vkarpov15 on GitHub issues:
Try res.send(data.toObject({ virtuals: true })); or setting schema.options.toJSON = { virtuals: true }. Virtuals are not included by default when you transform a mongoose doc into a pojo
If you have set schema.options.toJSON = { virtuals: true } and are still not seeing your populated child objects, try explicitly calling .toJSON() - I was simply console.logging my objects and the data was not showing up! DOH!
ie:
const director = await Director.findOne({}).populate('films');
console.log(director);
>> { _id: 5a5f598923294f047ae2f66f, name: 'spielberg', __v: 0};
but:
const director = await Director.findOne({}).populate('films');
console.log(director.toJSON());
>> { _id: 5a5f598923294f047ae2f66f, name: 'spielberg', __v: 0,
films: [{_id: 5a5f598923294f047ae2f6bf, title:"ET"},{_id: 5a5f598923294f047ae2f30v, title:"Jaws"}]
};

Mongoose: Populate a populated field

I'm using MongoDB as a log keeper for my app to then sync mobile clients. I have this models set up in NodeJS:
var UserArticle = new Schema({
date: { type: Number, default: Math.round((new Date()).getTime() / 1000) }, //Timestamp!
user: [{type: Schema.ObjectId, ref: "User"}],
article: [{type: Schema.ObjectId, ref: "Article"}],
place: Number,
read: Number,
starred: Number,
source: String
});
mongoose.model("UserArticle",UserArticle);
var Log = new Schema({
user: [{type: Schema.ObjectId, ref: "User"}],
action: Number, // O => Insert, 1 => Update, 2 => Delete
uarticle: [{type: Schema.ObjectId, ref: "UserArticle"}],
timestamp: { type: Number, default: Math.round((new Date()).getTime() / 1000) }
});
mongoose.model("Log",Log);
When I want to retrive the log I use the follwing code:
var log = mongoose.model('Log');
log
.where("user", req.session.user)
.desc("timestamp")
.populate("uarticle")
.populate("uarticle.article")
.run(function (err, articles) {
if (err) {
console.log(err);
res.send(500);
return;
}
res.json(articles);
As you can see, I want mongoose to populate the "uarticle" field from the Log collection and, then, I want to populate the "article" field of the UserArticle ("uarticle").
But, using this code, Mongoose only populates "uarticle" using the UserArticle Model, but not the article field inside of uarticle.
Is it possible to accomplish it using Mongoose and populate() or I should do something else?
Thank you,
From what I've checked in the documentation and from what I hear from you, this cannot be achieved, but you can populate the "uarticle.article" documents yourself in the callback function.
However I want to point out another aspect which I consider more important. You have documents in collection A which reference collection B, and in collection B's documents you have another reference to documents in collection C.
You are either doing this wrong (I'm referring to the database structure), or you should be using a relational database such as MySQL here. MongoDB's power relies in the fact you can embed more information in documents, thus having to make lesser queries (having your data in a single collection). While referencing something is ok, having a reference and then another reference doesn't seem like you're taking the full advantage of MongoDB here.
Perhaps you would like to share your situation and the database structure so we could help you out more.
You can use the mongoose-deep-populate plugin to do this. Usage:
User.find({}, function (err, users) {
User.deepPopulate(users, 'uarticle.article', function (err, users) {
// now each user document includes uarticle and each uarticle includes article
})
})
Disclaimer: I'm the author of the plugin.
I faced the same problem,but after hours of efforts i find the solution.It can be without using any external plugin:)
applicantListToExport: function (query, callback) {
this
.find(query).select({'advtId': 0})
.populate({
path: 'influId',
model: 'influencer',
select: { '_id': 1,'user':1},
populate: {
path: 'userid',
model: 'User'
}
})
.populate('campaignId',{'campaignTitle':1})
.exec(callback);
}
Mongoose v5.5.5 seems to allow populate on a populated document.
You can even provide an array of multiple fields to populate on the populated document
var batch = await mstsBatchModel.findOne({_id: req.body.batchId})
.populate({path: 'loggedInUser', select: 'fname lname', model: 'userModel'})
.populate({path: 'invoiceIdArray', model: 'invoiceModel',
populate: [
{path: 'updatedBy', select: 'fname lname', model: 'userModel'},
{path: 'createdBy', select: 'fname lname', model: 'userModel'},
{path: 'aircraftId', select: 'tailNum', model: 'aircraftModel'}
]});
how about something like:
populate_deep = function(type, instance, complete, seen)
{
if (!seen)
seen = {};
if (seen[instance._id])
{
complete();
return;
}
seen[instance._id] = true;
// use meta util to get all "references" from the schema
var refs = meta.get_references(meta.schema(type));
if (!refs)
{
complete();
return;
}
var opts = [];
for (var i=0; i<refs.length; i++)
opts.push({path: refs[i].name, model: refs[i].ref});
mongoose.model(type).populate(instance, opts, function(err,o){
utils.forEach(refs, function (ref, next) {
if (ref.is_array)
utils.forEach(o[ref.name], function (v, lnext) {
populate_deep(ref.ref_type, v, lnext, seen);
}, next);
else
populate_deep(ref.ref_type, o[ref.name], next, seen);
}, complete);
});
}
meta utils is rough... want the src?
or you can simply pass an obj to the populate as:
const myFilterObj = {};
const populateObj = {
path: "parentFileds",
populate: {
path: "childFileds",
select: "childFiledsToSelect"
},
select: "parentFiledsToSelect"
};
Model.find(myFilterObj)
.populate(populateObj).exec((err, data) => console.log(data) );