Mongoose create child and associated parent at same time - node.js

So basically, in my application I have a employee, and a company model. This is just the basic information about these models, there is actually more information, so using nested objects rather than 2 schema's doesn't seem like a good option (I think)
var EmployeeSchema = new Schema(
{
name: { type: String, required: true, max: 100 },
company: { type: Schema.Types.ObjectId, ref: 'Company', required: true },
}
);
var CompanySchema = new Schema(
{
name: { type: String, required: true },
},
{
toJSON: { virtuals: true },
},
);
CompanySchema.virtual('employees', {
ref: 'Employee',
localField: '_id',
foreignField: 'company',
justOne: false,
});
And on a form to create a new employee, I want the option to either select a company, or create a new one.
So my API will send information like the following:
employee: {
name: 'John Bastien',
company: 5d44635fa5993c0424da8e07
}
or:
employee: {
name: 'Dan Smith',
company: {
name: 'ACME'
}
}
This of course can be changed, it was just what I had in mind.
So in my express app when I do var employee = await new Employee(req.body.employee).save(); How can I make it so that the company is created along with the employee. It works fine when sending an object ID, but how can I do it with just a JSON object for the associated document?

I ended up writing some middleware on my models that will handle this. This logic could be extracted out to make it more generic, but for my use case it hasn't needed to yet.
EmployeeSchema.virtual('company', {
ref: 'Company',
localField: 'companyId',
foreignField: '_id',
justOne: true,
}).set(function(company) {
this.companyId= company._id;
this.$company = company;
return this.$company;
});
EmployeeSchema.pre('validate', function(next) {
if (this.company && this.company instanceof Company) {
var err = this.company.validateSync();
if (err) {
// mergeErrors is a helper function that will merge the two exceptions into a nice format
err = mergeErrors(this.validateSync(), { company: err });
}
next(err);
}
next();
});
EmployeeSchema.pre('save', async function(next, saveOpts) {
if (this.company && this.company instanceof Company && this.company.isModified()) {
await this.company.save(saveOpts);
}
next();
});

Related

MongoDB relation between two collections by ID with the Express

I am facing a problem while making a relation between two collections (I am using MEAN stack)
I have two collections: Books and Authors
In frontend I want to make a CRUD menu, where I add a new book in the table and then from there i insert a few data about book and then I choose author from the dropdown menu (fetchin data from Authors collection)
So at the end my Book collection needs to have a few data about the book and then inside the object i need an array of data about those author.
Book schema:
const BookSchema = new mongoose.Schema({
owner: { type: String, required: true },
pagesNo: { type: String, required: true },
releaseDate: { type: String, required: true },
country: { type: String, required: true },
authorID: { type: String, required: true }, <-- HERE I NEED DATA ABOUT AUTHOR
});
Author schema:
const AuthorSchema = new mongoose.Schema({
name: { type: String, required: true },
surname: { type: String, required: true },
dateOfBirth: { type: String, required: true },
countryOfBirth: { type: String, required: true },
});
Book route: book.ts
router.get("/", async (req, res) => {
try {
const books= await Book.find();
let Author = await Author.find({
books: { $elemMatch: { _id: books.bookID } },
});
res.status(200).json(books);
} catch (err) {
res.status(404).json({ success: false, msg: "Booknot found" });
}
});
The problem is somewhere inside the find() function.. Is it even a good practice? I want that it can handle a lot of data.
Thanks to everyone!
Greetings.
Your Book schema would be like this:
const MongooseSchema = new mongoose.Schema({
owner: {
type: String,
required: true,
},
pagesNo: {
type: String,
required: true,
},
releaseDate: {
type: String,
required: true,
},
country: {
type: String,
required: true,
},
authorId: {
type: mongoose.Schema.ObjectId,
ref: 'User',
required: true,
},
});
And your Author Schema would remain the same (in order to link both schemas).
Your route would be like this (if you want to search all books along with their author names):
router.get('/', async (req, res) => {
try {
const books = await Book.find().populate('authorId');
res.status(200).json(books);
} catch (err) {
res.status(404).json({ success: false, msg: 'Booknot found' });
}
});
And in case you want to search for books with a specific author id then your route would be like this:
router.get('/', async (req, res) => {
try {
const books = await Book.find({ authorId }).populate('authorId');
res.status(200).json(books);
} catch (err) {
res.status(404).json({ success: false, msg: 'Booknot found' });
}
});
AuthorID should be type ObjectId, not string.
To join data from other table, you have to use an aggregate with a lookup.
let author = await Author.aggregate([
{
$lookup:
{
from: "books",
localField: "_id",
foreignField: "authorID",
as: "books"
}
}
]);

dynamic references by sending it from the route

I have been looking at the documentation and some other questions made, but I have not been able to do it, I need to consult another collection for an objectId, but these collections come dynamically in the routes.
The collections come req, as they are from many different clients
https://mongoosejs.com/docs/populate.html
//route
// example collection dynamic ssdf2451_users, ssdf2451_campus, ssdf2451_programs
router.get('/school/prueba/tipos/', async (req, res, next) => {
let users
let school
let prueba
try {
const Users = model(`ssdf2451_users`, UserSchema)
console.log(Users)
await Users.findOne({ _id: '5ef56f70d19aea6e70c82a50' })
.populate('schoolId')
.exec(function (err, usersDocuments) {
console.log(usersDocuments, err)
// handle err
prueba = usersDocuments
res.status(200).json(prueba)
})
} catch (err) {
next(err)
}
})
// Schema
import { Schema, model } from 'mongoose'
const UserSchema = new Schema({
state: {
type: Boolean,
default: false,
required: false
},
accountNumber: {
type: Number,
required: false
},
schoolId: {
type: Schema.Types.ObjectID,
required: true,
ref: 'schools'
},
campusId: {
type: Schema.Types.ObjectID,
required: true,
ref: dynamic
},
programsId: {
type: [Schema.Types.ObjectID],
required: false,
ref: dynamic
},
})
const User = model('users', UserSchema)
export { User, UserSchema }

Mongoose - search a new collection based on the first collection result

I have two collections as follows :
This is my vehicle schema
const VehicleSchema = new schema({
user: {
type: schema.Types.ObjectId,
ref: "users"
},
name: {
type: String,
required: true
},
brand: {
type: String,
required: true
},
plate: {
type: String,
required: true
},
IMEI: {
type: String,
required: true
}
});
This is my Trip schema
const TripSchema = new schema({
date: {
type: Date,
default: Date.now
},
IMEI: String,
data: [
{
lat: String,
lon: String
}
]
});
Trip and Vehicle schemas share the the key IMEI, so when the user is logged in I wanna grab the trips based on the IMEI number, what is the most efficient way to approach this problem ? here is a look at my router
router.get(
"/",
passport.authenticate("jwt", { session: false }),
(req, res) => {
Vehicles.find({ user: req.user.id }).then(vehicles => {
if (!vehicles) {
return res.json({ error: "you do not have any vehicles yet" });
}
Trips.find().then(trips => {
// ??
});
});
}
);
You can simply do a $lookup and do a left join on trips with vehicles:
db.vechicles.aggregate([
{
$lookup:
{
from: "trips",
localField: "IMEI",
foreignField: "IMEI",
as: "trips"
}
}
])
This should get you what you want in one call.
Note this is available in mongodb versions 3.2+

Mongoose virtual return null

I have a Person schema :
const person = new mongoose.Schema({
name: String,
birthday: Date,
sex: String
},
{
toObject: { virtuals: true },
toJSON: { virtuals: true }
});
person.virtual('tasks', {
ref: 'task',
localField: '_id',
foreignField: 'person'
});
export default mongoose.model('person', person);
And a task one which has the person field :
const schema = new mongoose.Schema({
person: {
type: mongoose.Schema.Types.ObjectId,
ref: 'person',
required: true
},
name: String
});
export default mongoose.model('task', schema);
And my API to retrieve one person :
api.get('/person/:id', async (req, res) => {
let personID = req.params.id;
if (personID) {
let person = await Person.findById(personID);
if (person)
res.json(person);
else
res.status(404).end();
}
else {
res.status(400).end();
}
});
When I query the API for one person, tasks is always null, any ideas why?
Thanks
Update
I tried with :
let person = await Person.findById(personID).populate('tasks').exec();
and it's still returning null.
Use populate in your query.
Can you please check one of the below query
let person = await Person.findOne({ _id: personID })
.populate('tasks')
.exec((error, personData)
=> personData);
OR
let person = await Person.findById(personID)
.populate('tasks')
.exec((error, personData) => personData);
reference Link : http://thecodebarbarian.com/mongoose-virtual-populate
Ok I found the issue, I was registering the model before setting up the virtual (in some wrapper I wrote around mongoose.Schema).

How to trigger a function whenever a mongoose document is updated

I have a user model schema in mongoose which contains a list of friends and groups and stats info like so...
var user = new Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true, select: false },
roles: [{ type: String, required: true }],
friends: [{ type: Schema.Types.ObjectId, ref: 'User' }],
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
stats : {
nbrFriends: { type: Number, required: false },
nbrGroups: { type: Number, required: false }
}
}, {
timestamps: true
});
I need to update the users stats whenever a change is made to the friends or groups fields to contain the new number of friends or groups etc. For example, when the following function is called on a user:
var addGroup = function(user, group, cb) {
user.groups.push(group);
User.findOneAndUpdate({ _id: user._id }, { $set: { groups: user.groups }}, { new: true }, function(err, savedResult) {
if(err) {
return cb(err);
}
console.log('updated user: ' + JSON.stringify(savedResult));
return cb(null, savedResult);
});
};
How could I make sure the stats is automatically updated to contain the new number of groups the user has? It seems like a middleware function would be the best approach here. I tried the following but this never seems to get called...
user.pre('save', function(next) {
var newStats = {
nbrGroups: this.groups.length,
nbrPatients: this.friends.length
};
this.stats = newStats;
this.save(function(err, result) {
if(err) {
console.log('error saving: ' + err);
} else {
console.log('saved');
}
next();
});
});
You need to use the middleware a.k.a. hooks:
Middleware (also called pre and post hooks) are functions which are passed control during execution of asynchronous functions.
See the docs:
http://mongoosejs.com/docs/middleware.html
From version 3.6, you can use change streams.
Like:
const Users = require('./models/users.js')
var filter = [{
$match: {
$and: [{
$or:[
{ "updateDescription.updatedFields.friends": { $exists: true } },
{ "updateDescription.updatedFields.groups": { $exists: true } },
]
{ operationType: "update" }]
}
}];
var options = { fullDocument: 'updateLookup' };
let userStream = Users.watch(filter,options)
userStream.on('change',next=>{
//Something useful!
})
You should update with vanilla JS and then save the document updated to trigger the pre-save hooks.
See Mongoose docs
If you have many keys to update you could loop through the keys in the body and update one by one.
const user = await User.findById(id);
Object.keys(req.body).forEach(key => {
user[key] = req.body[key];
}
const saved = await user.save();

Resources