Access mongoose parent document for default values in subdocument - mongodb

I have a backend API for an Express/Mongo health tracking app.
Each user has an array of weighIns, subdocuments that contain a value, a unit, and the date recorded. If no unit is specified the unit defaults to 'lb'.
const WeighInSchema = new Schema({
weight: {
type: Number,
required: 'A value is required',
},
unit: {
type: String,
default: 'lb',
},
date: {
type: Date,
default: Date.now,
},
});
Each user also has a defaultUnit field, that can specify a default unit for that user. If that user posts a weighIn without specifying a unit, that weighIn should use the user's defaultUnit if present or else default to 'lb'.
const UserSchema = new Schema({
email: {
type: String,
unique: true,
lowercase: true,
required: 'Email address is required',
validate: [validateEmail, 'Please enter a valid email'],
},
password: {
type: String,
},
weighIns: [WeighInSchema],
defaultUnit: String,
});
Where is correct location for this logic?
I can easily do this in the create method of my WeighInsController, but this seems at best not best practice and at worst an anti-pattern.
// WeighInsController.js
export const create = function create(req, res, next) {
const { user, body: { weight } } = req;
const unit = req.body.unit || user.defaultUnit;
const count = user.weighIns.push({
weight,
unit,
});
user.save((err) => {
if (err) { return next(err); }
res.json({ weighIn: user.weighIns[count - 1] });
});
};
It doesn't seem possible to specify a reference to a parent document in a Mongoose schema, but I would think that a better bet would be in my pre('validate') middleware for the subdocument. I just can't see a way to reference the parent document in the subdocument middleware either.
NB: This answer does not work as I don't want to override all of the user's WeighIns' units, just when unspecified in the POST request.
Am I stuck doing this in my controller? I started with Rails so I have had 'fat models, skinny controllers' etched on my brain.

You can access the parent (User) from a sub-document (WeighIn) using the this.parent() function.
However, I'm not sure if it's possible to add a static to a sub-document, so that something like this would be possible:
user.weighIns.myCustomMethod(req.body)
Instead, you could create a method on the UserSchema, like addWeightIn:
UserSchema.methods.addWeightIn = function ({ weight, unit }) {
this.weightIns.push({
weight,
unit: unit || this.defaultUnit
})
}
Then just call the user.addWeightIn function within your controller and pass the req.body to it.
This way, you get 'fat models, skinny controllers'.

Related

Express.js PUT Request not updating MongoDB

I'm working on a project that is basically a super watered down social media website.
I have a chunk done already, but I'm having some issues creating a put request to my mongodb. Basically, I want to send a put request to update a numeric value to be able to have a like counter on each post.
What I'm trying do here is send a put request with a specific post id. I'm storing the post id in a hidden text box to reference it. This is pug formatted HTML:
input.form-control(type='hidden' value=item.id id='postId' placeholder='' name='postId' required='false')
form(method='PUT' action='/update/{{post._id}}')
button(type='submit') Like
Then in my router.js file I'm basically trying to take in that id and set the likes field in the Post schema to 1 (just for testing).
router.put('/update/:id', function (req, res, next) {
let id = {
_id: ObjectID(req.params.id)
};
Post.update({_id: id}, {$set:{'likes': 1}}, (err, result) => {
if(err) {
throw err;
}
res.send('user updated sucessfully');
});
});
Here is my post schema
var mongoose = require("mongoose");
var PostSchema = new mongoose.Schema({
postText: {
type: String,
unique: false,
required: true,
trim: true
},
usernameText: {
type: String,
unique: false,
required: true,
trim: true
},
likes:{
type: Number,
unique: false,
required: false
}
});
var Post = mongoose.model("Posts", PostSchema);
module.exports = Post;
Any and all help would be highly appreciated, thank you
You can't change the ObjectId. The ObjectId is generated by MongoDB and can't be changed by the user using query functions.
If you want to assign a unique id to each user for example, then create a separate field in your schema.
You cannot change the ID
'PUT' method is not supported directly as far as I know. You need method override

Mongoose Schema planning: using ObjectID reference or using array of type: [otherSchema]?

I'm currently planning out the database structure for an app I'm building, and something in this linked answer raised some questions for me.
In the structure that Shivam proposed, he sometimes references another collection directly, in other words within one Schema he defines the type of a field to be an array of another schema type. Example:
import { Schema } from "mongoose";
import { QuestionSchema } from "./question-schema";
const mongoose = require('mongoose');
export const QuestionSetSchema: Schema = new Schema({
questionSet: {
type: [QuestionSchema],
validate: {
validator: function(value: any) {
return value.length === 12;
},
message: 'Question set must be 12.'
}
},
}, {
timestamps: true
});
In other cases, he only uses an ObjectID reference to another schema/collection:
export const CandidateSchema: Schema = new Schema({
name: String,
email: String, // you can store other candidate related information here.
totalAttempt: {
type: Number,
default: 0,
validate: {
validator: function(value: number) {
return value === 3;
},
message: 'You have already done three attempts.'
}
},
candidateQuestionAnswers: {
type: [Schema.Types.ObjectId],
ref: 'CandidateQuesAnswer'
}
}, {
timestamps: true
});
What are the use cases for each of the above? Does the "type:[otherSchema]" method actually embed instances of that collection or does it only provide their properties to the Schema they are called from?

Transform JSON Response Field Name In Mongoose Model

My JSON response contains a field first_name but I want my Mongoose model to represent this field as firstName. Is this possible and if so then how?
You can create a new object with different property names from the one Mongoose returns. A nice way of doing this is to create a transform function. For example, let's say this is your schema:
{
firstName: { type: String, required: true },
lastName: { type: String, required: true }
}
Then you can use this function to create a new object with the desired property names:
function transformDocument (doc) {
return {
first_name: doc.firstName,
last_name: doc.lastName
}
}
Then, when you query the DB, you apply this function to the response:
Person.findOne({ firstName: 'John', lastName: 'Smith' })
.then(transformDocument)
Doug W has a good solution, but if you don't want to be using Promises and chaining .thens, then you can simply add options to the schema like this:
const mongoose = require ('mongoose'); // I am using v5.9.1 at the moment
const { Schema } = mongoose.Schema;
// Specify an options object
const options = {
toJSON: {
versionKey: false
}
// If you ever send the query result as an object,
// you may remove it from there, too, if you wish
// toObject: {
// versionKey: false
// }
};
// Attach the options object to the schema by
// passing it into Schema as the second argument
const mySchema = new Schema({
/** define your schema */
}, options);
This will still save __v to the document in the database. But it will not appear on the json/object when it is the result of a mongoose query.
Besides setting versionKey: false in the options, you may also specify a transform function:
/* ... */
// Specify an options object
const options = {
toJSON: {
// versionKey: false,
transform: function(doc, ret) {
// ret is the object that will be returned as the result
// (and then stringified before being sent)
delete ret.__v;
return ret;
}
}
};
/* ... */
I know this question is nearly two years old, but I needed an answer to this question, and google was not kind to me at the time. I figured it out, and now I'm hoping someone else will be looking for an answer here and find that they have options. Pun not originally intended.

Sails js should not return password and email

I am trying to create CRUD app in sails js, and i am able to post data to my DB what i noticed is when i insert data on success sails return whole object. But if we don't want certain fields in response then how can we restrict it. Please help thanks.
module.exports = {
attributes : {
username : {
type: 'string',
required: true
},
password : {
type: 'string',
required: true
},
email : {
type: 'string',
required: true,
unique: true
}
},
toJson: function() {
var obj = this.toObject();
delete obj.password;
return obj;
},
beforeCreate: function(attribute, callback) {
console.log(attribute.password);
require('bcrypt').hash(attribute.password, 10, function(err, encryptedPassword) {
sails.log(err);
attribute.password = encryptedPassword;
sails.log(encryptedPassword);
callback();
});
}
};
#arbuthnott is partly correct above -- you do need toJSON rather than toJson -- but more importantly, the function needs to go inside the attributes dictionary, since it is an instance method:
attributes : {
username : {
type: 'string',
required: true
},
password : {
type: 'string',
required: true
},
email : {
type: 'string',
required: true,
unique: true
},
toJSON: function() {
var obj = this.toObject();
delete obj.password;
return obj;
}
}
I think the responses through sails default REST api for models runs them through .toJSON before returning, so you are doing this the right way.
However, you may have a case issue, like you should define .toJSON with uppercase instead of .toJson. Try making that switch and see if it solves your problem.
UPDATE
Sounds like this is not solving your issue. The sails docs from here say:
The real power of toJSON relies on the fact every model instance sent out via res.json is first passed through toJSON. Instead of writing custom code for every controller action that uses a particular model (including the "out of the box" blueprints), you can manipulate outgoing records by simply overriding the default toJSON function in your model. You would use this to keep private data like email addresses and passwords from being sent back to every client.
That sounds pretty explicitly like what we are trying to do, so maybe this is a sails bug. Perhaps it applies to find, but not create. Is that password returned when simply finding an existing user?
If you must, a sure way around this would be to override the default create action in your UserController:
create: function(req, res) {
User.create(req.body).exec(function(err, user) {
if (err) {
return res.json(err);
}
// explicitly call your own toJSON() to be sure
return res.send(user.toJSON());
});
},
This isn't ideal, especially if you have many model properties you want to hide in many api calls. But it will get the job done.
password: { type: 'string', required: true, protected: true }
protected:true is now deprecated on sails v1.0
You can use instead of that customToJSON
customToJSON: function() {
// Return a shallow copy of this record with the password and ssn removed.
return _.omit(this, ['password', 'ssn'])
}
password: { type: 'string', required: true, protected: true }
You can do this also.

mongoose - how to validate specific fields only?

I have following mongoose model and routing file.
user.js
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
ObjectId = Schema.ObjectId,
var userSchema = new Schema({
nick_name: {
type: String,
unique: true
},
email: {
type: String,
unique: true
},
first_name: String,
last_name: String,
birth_date: {
type: Date
},
password: {
type: String,
select: true
},
user_type: {
type: Number,
},
is_active: {
type: Number,
default: -1
}
}, { collection: 'user' });
/*
*Validations
*/
userSchema.path('nick_name').required(true, 'nick name is required!');
userSchema.path('email').required(true, 'email is required!');
userSchema.path('password').required(true, 'password is required!');
userSchema.path('user_type').required(true, 'user type is required!');
userSchema.path('is_active').required(true, 'is active is required!');
userSchema.path('is_close').required(true, 'is close is required!');
userSchema.path('first_name').required(true, 'first name is required!');
userSchema.path('last_name').required(true, 'last name is required!');
userSchema.path('birth_date').required(true, 'birth date is required!');
var User = module.exports = mongoose.model("User", userSchema);
router.js
var express = require('express');
var router = express.Router();
var User = require('../models/user');
router
.route('/api/user/register')
.post(
function(req, res, next) {
var user_ = new User(req.body);
/*
*here all validations are required
*/
user_.validate(function(err) {
if (err) {
res.json({ "status": 0, "error": err });
} else {
user_.save(function(err) {
if (err) {
res.json({ "status": 0, "error": { "other": "Oops! something went wrong, please try again later." } });
} else {
res.json({ error: 1, message: 'User registered' });
}
});
}
}
});
}
});
In above routing file I can validate all fields by using validate() method but, I have need validation as following conditions
->When user register, following fields are required
nick_name
email
password
user_type
is_active
->When user edit his profile (after register), all fields are required.
Can anybody help me to solve this issue ?
I just found myself in this situation, want to update a comment model and want a specific field validation for field 'content'.
Im thinking about a hack, pull off that full comment document from the database, then create a new schema object with the same properties from the comment document that i just pulled off from the database and validate this document model copy as if i were to create a new document, but i wont, i wont use the save() method. If there is an error with the 'content' field, which is the only one i care, i would know after validation, if there is no errors then i forget about that new object schema copy that i created by pulling off the comment document from the database, ill forget about it since i already know my 'content' field is valid since no errors where shown, so ill proceed with my flow.
Perhaps instead of pulling off that document from the database i can just create a new object with some fake but valid fields... Then pass the real value i want to test which in my case is 'content', i wouldnt fake that value since i already have it.
NOTE: my comment model has property 'createdAt' so i would replace that for the current date, cause i could have errors at validation saying new comment must be from current date and no from past dates, but since i wont be saving that new date to the database i can add the current date, recall that i will forget about that new object, i wont save it to the database, all i care is the 'content' field validation and see if there is any errors.