Load only required plugin into the Schema when saving a model - javascript

Plugins is quite a powerful feature in Mongoose.js, but there is one thing I have got stuck with. I need to load only the required plugin into the Schema when saving a model. Because If I don't do it, the other unnecessary plugins are loaded automatically along with lots of validation errors.
Here is my schema
// models/user_collection.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var UserSchema = new Schema({
username: { type: String, required: true },
password: { type: String, required: true },
role: String // either 'memeber' or 'admin'
});
// Member User Plugin
UserSchema.plugin(function (schema) {
schema.add({
locality: { type: String, required: true },
contactNo: { type: Number, min: 13, max: 13 }
});
});
// Admin User Plugin
UserSchema.plugin(function (schema) {
schema.add({
accountNo: { type: String, required: true },
settingsArray: Array
});
});
mongoose.model('User', UserSchema);
Now whenever I try to save a record only for member user, the Schema automatically loads the Admin plugin, responding with validation errors.
So,
var member = new User();
member.username = 'XYZ';
member.password = createHash('ABC') // a hashing method;
member.role = 'member';
member.locality = 'USA';
member.contactNo = 123456;
member.save(function(err, user) {
if(err) { console.log(err); res.send(err); return; }
// if successful I do my stuff
});
As soon as the save method is executed, I get validation errors from Admin like
"accountNo is required", ( I am not gonna paste the stack trace here, it will get messy, but you got the point )
Now I know that it is not an issue or bug with Mongoose.js, but I am doing something wrong here. Can you guys please guide me how to do it correctly ?

The thing is you are applying both the plugins to the same schema. So all the plugin functionality is being added to the schema. So accountNo and locality is required. Plugins are used to define common code at one place and use it across different schemas.
The way you have used plugins here, you just have broken down the definition of the model to smaller parts. You could as well put everything under the schema and would have gotten the same effect.
Read about plugins again.
As you said, I guess Discriminators is the way to go.

Related

Mongoose and Next.js: Unhandled Runtime Error TypeError: Cannot read properties of undefined (reading 'Token')

I basically defined this Model, much like another which doesn't error out; So I am stumped as to why it's not working...
Here is a Minimal, Reproducible Example
Not working:
import mongoose from 'mongoose';
const TokenSchema = new mongoose.Schema({
_userId: { type: mongoose.Schema.Types.ObjectId, required: true, ref: 'User' },
token: { type: String, required: true },
createdAt: { type: Date, required: true, default: Date.now, expires: 43200 }
});
export default mongoose.models.Token || mongoose.model('Token', TokenSchema);
Working:
import mongoose from 'mongoose';
import emailValidator from 'email-validator'
import bcrypt from 'bcrypt'
import crypto from 'crypto'
const SALT_ROUNDS = 12;
const UserSchema = new mongoose.Schema(
{
username: {
type: String,
required: true,
trim: true,
lowercase: true,
index: { unique: true },
validate: {
validator: emailValidator.validate,
message: props => `${props.value} is not a valid email address!`
}
},
password: {
type: String,
required: true,
trim: true,
index: { unique: true },
minlength: 7,
maxlength: 11
},
roles: [{ type: 'String' }],
isVerified: { type: Boolean, default: false },
passwordResetToken: String,
resetPasswordExpires: Date
},
{
timestamps: true
}
);
UserSchema.pre('save', async function preSave(next) {
const user = this;
if (!user.isModified('password')) return next();
try {
const hash = await bcrypt.hash(user.password, SALT_ROUNDS);
user.password = hash;
return next();
} catch (err) {
return next(err);
}
});
UserSchema.methods.generatePasswordReset = function () {
this.resetPasswordToken = crypto
.randomBytes(20)
.toString('hex');
this.resetPasswordExpires = Date.now() + 3600000; // expires in an hour
};
UserSchema.methods.comparePassword = async function comparePassword(candidate) {
return bcrypt.compare(candidate, this.password);
};
export default mongoose.models.User || mongoose.model('User', UserSchema)
Also I'm following this example in the Next.js Examples repo.
Please help! :)
Apparently the TypeError: Cannot read properties of undefined (reading 'Token') is happening because the code is executing on the client side when it is intended to execute on the server side.
What is really happening is that mongoose has not initiated a connection to the database, and so mongoose.models is undefined. But the fix isn't to try to initiate a db connection:
I too was having the a similar issue, when I tried to define too many things in the same file, that is... In my case I'm trying to define Typescript interfaces that can be pulled into client-side code, but the mongoose model definitions end up executing as well...
I read that NextJS does a fair bit of work to split up code that gets sent to client-side and what stays on server-side... The developer should keep this in mind and try to split things that relate to client-side and server-side into different files.
In my case, I placed the Interface definitions and my custom Hooks in a different file from the Mongoose Schema definitions; and then imported only the Interfaces and Hooks when I need those, which made the errors go away.
Trying to keep everything in the same place sounds logical and neat, but doesn't work in this case.
I copied your code and it worked fine (went into the tokens collections versus token like expected possibly) one thing I noticed was the expires field on createdAt - was this a NextJS field? It's not a default field so just curious. Also, can you paste the exact error you are encountering, this will help someone track the issue down.
{
_userId: new ObjectId("5e1a0651741b255ddda996c4"),
token: 'abcd123',
createdAt: 2021-09-24T23:10:24.288Z,
_id: new ObjectId("614e5ae04c741f91ac062530"),
__v: 0
}
Also, consider using the timestamps options property when declaring the model as this will save you the headache of setting createdAt up (and you can also have updatedAt automatically update).
token: { type: String, required: true },
},
{
timestamps: true
}
);
I had the same error. Even though my schema file was correct. the issue was for some reason, I have been importing the model in react component file
import room from "../models/room";

Populating an Object with Information from a Child Object with Different Model (JS/Mongoose)

Here is what I have. I created a project model that references a user model for an array of members.
var ProjectSchema = new mongoose.Schema(
{title: {
type: String,
required: true
},
members: [
{user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'users'
}
}],
});
User Schema (I have code that creates a model from both of these schemas)
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true
}
});
In a separate file, I want to export the JSON of a found project and include the information of the users in the members array, but I am not sure how to do that. This is the code I have right now.
const project = await Project.findById(req.params.proj_id).populate(
'members'
);
res.json(project);
It has no trouble finding the project but the only information I can get on the members is their id. I tried using for loops to gather the information from members separately using the id that I can get from the project, but the code gets messy and I am hoping to find a simpler way to do it.
You can use mongoose populate query to get all members associated with a project. It should populate array of objects users associated to a project. You should be doing something like this:
const project = await Project.findById(req.params.proj_id)
await project.populate('members').execPopulate()
console.log(project.members)
Mongoose docs for the reference: Populate
You can give the model to your mongoose populate.
const project = await Project.findById(req.params.proj_id)
.populate({
'members',
model: UserModel
})
.exec()
res.json(project);
keep in mind you've to create UserModel just like
const UserModel = mongoose.model('User', userSchema);

What is the best way to keep track of changes of a document's property in MongoDB?

I would like to know how to keep track of the values of a document in MongoDB.
It's a MongoDB Database with a Node and Express backend.
Say I have a document, which is part of the Patients collection.
{
"_id": "4k2lK49938d82kL",
"firstName": "John",
"objective": "Burn fat"
}
Then I edit the "objective" property, so the document results like this:
{
"_id": "4k2lK49938d82kL",
"firstName": "John",
"objective": "Gain muscle"
}
What's the best/most efficient way to keep track of that change? In other words, I would like to know that the "objective" property had the value "Burn fat" in the past, and access it in the future.
Thanks a lot!
Maintaining/tracking history in the same document is not all recommended. As the document size will keep on increasing leading to
probably if there are too many updates, 16mb document size limit
Performance degrades
Instead, you should maintain a separate collection for history. You might have use hibernates' Javers or envers for auditing for your relational databases. if not you can check how they work. A separate table (xyz_AUD) is maintained for each table (xyz). For each row (with primary key abc) in xyz table, there exist multiple rows in xyz_AUD table, where each row is version of that row.
Moreover, Javers also support MongoDB auditing. If you are using java you can directly use it. No need to write your own logic.
Refer - https://nullbeans.com/auditing-using-spring-boot-mongodb-and-javers/
One more thing, Javers Envers Hibernate are java libraries. But I'm sure for other programming languages also, similar libraries will be present.
There is a mongoose plugin as well -
https://www.npmjs.com/package/mongoose-audit (quite oudated 4 years)
https://github.com/nassor/mongoose-history#readme (better)
Maybe you can change the type of "objective" to array and track the changes in it. the last one of the array is the latest value.
Maintain it as a sub-document like below
{
"_id": "4k2lK49938d82kL",
"firstName": "John",
"objective": {
obj1: "Gain muscle",
obj2: "Burn fat"
}
}
You can also maintain it as an array field but remember, mongodb doesn't allow you to maintain uniqueness in an array field and if you plan to index the "objective" field, you'll have to create a multi key index
I think the simplest solution would be to use and update an array:
const patientSchema = new Schema({
firstName: { type: String, required: true },
lastName: { type: String, required: true },
objective: { type: String, required: true }
notes: [{
date: { type: Date, default: Date.now() },
note: { type: String, required: true }
}],
});
Then when you want to update the objective...
const updatePatientObjective = async (req, res) => {
try {
// check if _id and new objective exist in req.body
const { _id, objective, date } = req.body;
if (!_id || !objective) throw "Unable to update patient's objective.";
// make sure provided _id is valid
const existingPatient = await Patient.findOne({ _id });
if (!existingPatient) throw "Unable to locate that patient.";
// pull out objective as previousObjective
const { objective: previousObjective } = existingPatient;
// update patient's objective while pushing
// the previous objective into the notes sub document
await existingPatient.updateOne({
// update current objective
$set { objective },
// push an object with a date and note (previouseObjective)
// into a notes array
$push: {
notes: {
date,
note: previousObjective
},
},
}),
);
// send back response
res
.status(201)
.json({ message: "Successfully updated your objective!" });
} catch (err) {
return res.status(400).json({ err: err.toString() });
}
};
Document will look like:
firstName: "John",
lastName: "Smith",
objective: "Lose body fat.",
notes: [
{
date: 2019-07-19T17:45:43-07:00,
note: "Gain muscle".
},
{
date: 2019-08-09T12:00:38-07:00,
note: "Work on cardio."
}
{
date: 2019-08-29T19:00:38-07:00,
note: "Become a fullstack web developer."
}
...etc
]
Alternatively, if you're worried about document size, then create a separate schema for patient history and reference the user's id (or just store the patient's _id as a string instead of referencing an ObjectId, whichever you prefer):
const patientHistorySchema = new Schema({
_id: { type: Schema.Types.ObjectId, ref: "Patient", required: true },
objective: { type: String, required: true }
});
Then create a new patient history document when the objective is updated...
PatientHistory.create({ _id, objective: previousObjective });
And if you need to access to the patient history documents...
PatientHistory.find({ _id });

JS library to check if extra data field returned in http response payload?

I’m currently working on building end-to-end testing for an API another team is working on, and I was wondering if anyone perhaps knows about a JS library that I could use to test whether an extra field is returned in HTTP response body? The purpose of this functionality would be to keep the QA team informed when the dev team makes changes to the api via the tests, instead of the developers manually having to let us know they’ve created updates. I know this can be implemented manually but if the wheel already exists, I’d prefer to avoid recreating it lol.
Example scenario:
API call: GET user
- returns : user name, user ID and user birthday.
With proposed functionality, if the dev team made updates to the Get user call, and it returns the following
- return : user name, user ID, user birthday AND user address.
A test would fail to let me know that an extra field that wasn't expected (user address) was returned.
Schema validation seems to be what you are looking for. Besides the library mentioned in another answer, you may also want check a similar one: joi
const Joi = require('joi');
const schema = Joi.object().keys({
userName: Joi.string().alphanum().required(),
userId: Joi.number().required(),
userBirthDay: Joi.number().required(),
})
const result = Joi.validate({
userName: 'johndoe',
userId: 1234567,
userBirthDay: 1970,
userAddress: 'John Doe St.'
}, schema);
if (result.error) {
console.log(result.error.details);
}
In the spec you can make assertion on existence of error key in result object using the assertion library of your choice.
The example above assumes that you are using nodejs as an environment to run tests, but browser version of joi also exists: joi-browser
You need schema validation, there are libraries out there like ajv.
var ajv = new Ajv({ allErrors: true }); // options can be passed, e.g. {allErrors: true}
// API call: GET user - returns : user name, user ID and user birthday.
// With proposed functionality, if the dev team made updates to the Get user call, and it returns the following - return : user name, user ID, user birthday AND user address.
var schema = {
type: "object",
properties: {
userName: {
type: "string",
},
userId: {
type: "string",
},
userBirthdate: {
type: "string",
},
},
required: ["userName", "userId", "userBirthdate"],
additionalProperties: false,
};
var validate = ajv.compile(schema);
var validUser = {
userName: "John",
userId: "john",
userBirthdate: "01012000",
};
var invalidUser = {
userName: "John",
userId: "john",
userBirthdate: "01012000",
userAddress: "World",
};
var valid = validate(validUser);
console.log(`Valid user is valid: ${valid}`);
valid = validate(invalidUser);
console.log(`Invalid user is valid: ${valid}`);
console.log('Validate errors:', validate.errors);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ajv/6.6.2/ajv.min.js"></script>

Mongoose JS findOne always returns null

I've been fighting with trying to get Mongoose to return data from my local MongoDB instance; I can run the same command in the MongoDB shell and I get results back. I have found a post on stackoverflow that talks about the exact problem I'm having here; I've followed the answers on this post but I still can't seem to get it working. I created a simple project to try and get something simple working and here's the code.
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var userSchema = new Schema({
userId: Number,
email: String,
password: String,
firstName: String,
lastName: String,
addresses: [
{
addressTypeId: Number,
address: String,
address2: String,
city: String,
state: String,
zipCode: String
}
],
website: String,
isEmailConfirmed: { type: Boolean, default: false },
isActive: { type: Boolean, default: true },
isLocked: { type: Boolean, default: false },
roles: [{ roleName: String }],
claims: [{ claimName: String, claimValue: String }]
});
var db = mongoose.connect('mongodb://127.0.0.1:27017/personalweb');
var userModel = mongoose.model('user', userSchema);
userModel.findOne({ email: 'test#test.com' }, function (error, user) {
console.log("Error: " + error);
console.log("User: " + user);
});
And here is the response of the 2 console.log statements:
Error: null
User: null
When the connect method is called I see the connection being made to my Mongo instance but when the findOne command is issued nothing appears to happen. If I run the same command through the MongoDB shell I get the user record returned to me. Is there anything I'm doing wrong?
Thanks in advance.
Mongoose pluralizes the name of the model as it considers this good practice for a "collection" of things to be a pluralized name. This means that what you are currently looking for in code it a collection called "users" and not "user" as you might expect.
You can override this default behavior by specifying the specific name for the collection you want in the model definition:
var userModel = mongoose.model('user', userSchema, 'user');
The third argument there is the collection name to be used rather than what will be determined based on the model name.
I know 7 years pass, however I'm starting to develop in node JS with MongoDB using mongoose, so searching for solution to the same problem, result = null.
After read this post and all the answers, I release that I totally forget to include the DB name in the string of the connection, 'mongodb://localhost:27017/DB name' so that solved my case. I guessed this can be help other clueless like me! :)

Categories