Mongoose, upsert an item in array in referenced subdocument - node.js

I have the following schemas and models:
const shelfSchema = new mongoose.Schema({
name: {
type: String,
required: true,
default: 'New Shelf'
},
books: {
type: [bookSchema],
required: false
}
}, {autoCreate: false})
const shelfModel = mongoose.model('Shelf', shelfSchema)
const librarySchema= new mongoose.Schema({
shelves: {
type: [shelfSchema],
required: false,
}
})
const libraryModel = mongoose.model('library', librarySchema)
const userSchema = new mongoose.Schema({
username: {
type: String,
required:true,
unique:true
}
library: {
type: mongoose.Schema.Types.ObjectId,
ref: 'library',
required: true
}
})
const userModel = mongoose.model('User', userSchema );
Every User has a unique username and a Library reference, and every library has one or more Shelves, each one with one or more Book.
When I add a book, I pass also the information of the shelf name I want to insert the book into, and if the shelf with that name is missing, it should be created.
Since I come from a sql mentality I'm having a bit of difficulties in understanding if I can manage an upsert the same way.
I thought that I could insert the book using at most two queries: one to create the self if it's missing and one to insert the book in the shelf.
My approach was then to use
UserModel.findOneAndUpdate({username: user.username, "library.shelves.name": shelfName},{}, {upsert: true})
but since it's a query in the UserModel, if it doesn't find a user with a shelf with that name it tries to create a new user, duplicating the username.
Am I right to assume that I have to split this first query in two parts, "Find a user with a shelf with that name in the library" and in case it's not found "Create that shelf in the library"?
Or is it possible to unite the queries in some way?

What you are doing right now is trying to update a UserModel that matches the user name, has the shelve name, that is why upsert: true creates a new user entry.
What you should do is to find out the library _id and then $push the book to the shelf you are searching for.

Related

Mongoose populate by field not _id

I have these two schema
const User = new mongoose.Schema({
username: {
type: String,
unique: true,
}
})
const Product = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User"
}
//...Other product information
})
and when I query product I can populate the product user like this
await database.Product.findOne({}).populate("user")
this will populate the user by his id but I want to populate the user by his username since it never gonna repeat and it's unique. I there is a way to reference a model by another field other than the _id
Do this:
await database.Product.findOne({}).populate('user',{path: 'username'})

Referencing another schema in mongoose

I'm creating an app with the merng stack that allows user to log a set they have done for an exercise to their account. I am currently working on mongoose schemas and have two schemas that are fairly similar, Workout and Exercise.
models/Workout.js
const { model, Schema } = require("mongoose");
const workoutSchema = new Schema({
workoutName: String,
username: String,
createdAt: Number,
exercises: [
{
exerciseName: String,
sets: [
{
weight: Number,
reps: Number,
createdAt: Number,
notes: String
}
]
}
],
notes: String
});
module.exports = model("Workout", workoutSchema);
models/Exercise.js
const { model, Schema } = require("mongoose");
const exerciseSchema = new Schema({
exerciseName: String,
username: String,
sets: [
{
weight: Number,
reps: Number,
createdAt: Number,
notes: String
}
]
});
module.exports = model("Exercise", exerciseSchema);
The workout document will only contain an array of exercises and the sets that were logged during a workout (each workout is limited to 4 hours). The exercise document will contain an array of all the sets a user has ever logged while using the app. The problem I am facing is that whenever a new workout document is created with an exercise they have already done before, it creates a new ID for the exercise instead of being the same as the one in the exercise document. I want the workout document to recognise that an ID already exists for an exercise if it has been logged previously.
After researching, I believe that I need to reference the exercise schema. However, even with the answers on SO, I'm still unsure on how to achieve this with my project.
You can reference by _id and don't need to store entire object exercise into workout collection.
And after that you can use populate defined as:
Population is the process of automatically replacing the specified paths in the document with document(s) from other collection(s).
So, your workout schema should be:
const workoutSchema = new Schema({
workoutName: String,
username: String,
createdAt: Number,
exercises: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Exercise',
}
],
notes: String
});
With this schema you can use populate() or $lookup to do a "SQL Join" to get all values from exercise into workout where _id are equals. You can use other field to compare too.

Mongoose populate ObjectID from multiple possible collections

I have a mongoose model that looks something like this
var LogSchema = new Schema({
item: {
type: ObjectId,
ref: 'article',
index:true,
},
});
But 'item' could be referenced from multiple collections. Is it possible to do something like this?
var LogSchema = new Schema({
item: {
type: ObjectId,
ref: ['article','image'],
index:true,
},
});
The idea being that 'item' could be a document from the 'article' collection OR the 'image' collection.
Is this possible or do i need to manually populate?
Question is old, but maybe someone else still looks for similar issues :)
I found in Mongoose Github issues this:
mongoose 4.x supports using refPath instead of ref:
var schema = new Schema({
name:String,
others: [{ value: {type:mongoose.Types.ObjectId, refPath: 'others.kind' } }, kind: String }]
})
In #CadeEmbery case it would be:
var logSchema = new Schema({
item: {type: mongoose.Types.ObjectId, refPath: 'kind' } },
kind: String
})
But I did't try it yet...
First of all some basics
The ref option says mongoose which collection to get data for when you use populate().
The ref option is not mandatory, when you do not set it up, populate() require you to give dynamically a ref to him using the model option.
#example
populate({ path: 'conversation', model: Conversation }).
Here you say to mongoose that the collection behind the ObjectId is Conversation.
It is not possible to gives populate or Schema an array of refs.
Some others Stackoverflow people asked about it.
Soluce 1: Populate both (Manual)
Try to populate one, if you have no data, populate the second.
Soluce 2: Change your schema
Create two link, and set one of them.
var LogSchema = new Schema({
itemLink1: {
type: ObjectId,
ref: 'image',
index: true,
},
itemLink2: {
type: ObjectId,
ref: 'article',
index: true,
},
});
LogSchema.find({})
.populate('itemLink1')
.populate('itemLink2')
.exec()
Dynamic References via refPath
Mongoose can also populate from multiple collections based on the value of a property in the document. Let's say you're building a schema for storing comments. A user may comment on either a blog post or a product.
body: { type: String, required: true },
on: {
type: Schema.Types.ObjectId,
required: true,
// Instead of a hardcoded model name in `ref`, `refPath` means Mongoose
// will look at the `onModel` property to find the right model.
refPath: 'onModel'
},
onModel: {
type: String,
required: true,
enum: ['BlogPost', 'Product']
}
});
const Product = mongoose.model('Product', new Schema({ name: String }));
const BlogPost = mongoose.model('BlogPost', new Schema({ title: String }));
const Comment = mongoose.model('Comment', commentSchema);

Mongo user document structure with three user types

I'm setting up a Mongo database in Express with Mongoose and I'm trying to decide how to model the users. I've never modeled multiple users in the MEAN stack before and thought I'd reach out for some best-practices - I'm an instructor and need to be able to teach my students best practices. I haven't been able to find a whole lot out there, but perhaps I'm searching for the wrong things.
The app will have 3 user types, student, staff, and admin. Each user type will require some of the same basics - email, password, first and last names, phone, etc. If the user is a student, they will need to provide additional info like their high school name, grade, age, gender, etc, which ideally will be required.
This is what I've come up with so far - a single user model that requires all the basic information, but also has schema set up to allow for the additional information that students will need to include. Then I also have a pre-save hook set up to remove the "studentInfo" subdocument if the user being saved doesn't have a "student" role:
var mongoose = require("mongoose");
var Schema = mongoose.Schema;
var ethnicityList = [
"White",
"Hispanic or Latino",
"Black or African American",
"Native American or American Indian",
"Asian / Pacific Islander",
"Other"
];
var userSchema = new Schema({
firstName: {
type: String,
required: true
},
lastName: {
type: String,
required: true
},
phone: {
type: Number,
required: true
},
email: {
type: String,
required: true,
lowercase: true,
unique: true
},
password: {
type: String,
required: true
},
preferredLocation: {
type: String,
enum: ["provo", "slc", "ogden"]
},
role: {
type: String,
enum: ["student", "staff", "admin"],
required: true
},
studentInfo: {
school: String,
currentGrade: Number,
ethnicity: {
type: String,
enum: ethnicityList
},
gender: {
type: String,
enum: ["male", "female"]
}
}
}, {timestamps: true});
userSchema.pre("save", function (next) {
var user = this;
if (Object.keys(user.studentInfo).length === 0 && user.role !== "student") {
delete user.studentInfo;
next();
}
next();
});
module.exports = mongoose.model("User", userSchema);
Question 1: Is this an okay way to do this, or would it be better just to create two different models and keep them totally separate?
Question 2: If I am going to be to restrict access to users by their user type, this will be easy to check by the user's role property with the above setup. But if it's better to go with separated models/collections for different user types, how do I check whether its a "Staff" or "Student" who is trying to access a protected resource?
Question 3: It seems like if I do the setup as outlined above, I can't do certain validation on the subdocument - I want to require students to fill out the information in the subdocument, but not staff or admin users. When I set any of the fields to required, it throws an error when they're not included, even though the subdocument itself isn't required. (Which makes sense, but I'm not sure how to get around. Maybe custom validation pre-save as well? I've never written that before so I'm not sure how, but I can look that up if that's the best way.)
Well, Here are my two cents.
You would be better off creating separate schema models and then injecting the models on a need to basis.
for e.g.
If I have a blog schema as follows:
var createdDate = require('../plugins/createdDate');
// define the schema
var schema = mongoose.Schema({
title: { type: String, trim: true }
, body: String
, author: { type: String, ref: 'User' }
})
// add created date property
schema.plugin(createdDate);
Notice that author is referring to User and there is an additional field createdData
And here is the User Schema:
var mongoose = require('mongoose');
var createdDate = require('../plugins/createdDate');
var validEmail = require('../helpers/validate/email');
var schema = mongoose.Schema({
_id: { type: String, lowercase: true, trim: true,validate: validEmail }
, name: { first: String, last: String }
, salt: { type: String, required: true }
, hash: { type: String, required: true }
, created: {type:Date, default: Date.now}
});
// add created date property
schema.plugin(createdDate);
// properties that do not get saved to the db
schema.virtual('fullname').get(function () {
return this.name.first + ' ' + this.name.last;
})
module.exports = mongoose.model('User', schema);
And the created Property which is being refereed in both User and Blogspot
// add a "created" property to our documents
module.exports = function (schema) {
schema.add({ created: { type: Date, default: Date.now }})
}
If you want to restrict access based on the user types, you would have to write custom validation like in the User schema we had written for emails:
var validator = require('email-validator');
module.exports = function (email) {
return validator.validate(email);
}
And then add an if-else based on whatever validations you do.
2 and 3. So, Yes custom validations pre-save as well.
Since you are an instructor I preferred to just point out the practices that are used instead of elaborating on your specific problem.
Hope this helps! :)

Using Mongoose to findbyid and update sibling

I'm trying to locate a reference to another schema and update a sibling field. Specifically, I'm trying to manipulate the hasResponded field below based on a particular 'survey' ObjectId.
My schema looks like this:
var userSchema = new mongoose.Schema({
// some other stuff
surveys: [{
survey: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Survey'
},
hasResponded: {
type: Boolean,
default: false
}
}]
});
If you have a survey id, simply search for all the users that have this particular id in the array.
Something like that:
Users.find({surveys: { "$elemMatch": { type: <ID> } } });
Then, iterate through the users and their corresponding survey array to find the ones that match the id you gave.
Got to say I would structure this db a little different if this query takes place often.
Make a new Schema - UserSurveys that holds the id of the user and the survey + hasResponded. Like this:
var UserSurveySchema = new mongoose.Schema({
user_id: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
survey_id: {type: mongoose.Schema.Types.ObjectId, ref: 'Survey'}
hasResponded: {type:Boolean, 'default':false}
...
});
You might also want to keep an index on the user and survey ids.
Then, it will be much easier to update the field, requests will take much shorter times.
Hope this helps.

Resources