Yup Child Schema accessing parent value (or passing the value to the child) - yup

I have the following schema, and I am trying to determine how to allow the nested Yup.object().shape({..}) to have access to the RuleSchema validationTypeCode (or pass it to the child schema)
export const RuleSchema = Yup.object().shape({
description: Yup.string().required('Required').min(2).max(25),
fieldId: Yup.number().required('Required'),
validationTypeCode: Yup.string().required('Required'),
failureAction: Yup.string().required('Required'),
failureActionValue: Yup.string().required('Required'),
dataEntryWorkFlowRuleValidationList: Yup.array().of(
Yup.object().shape({
//I need to be able to access the validationTypeCode from the parent schema
fieldValue: Yup.string().min(5)
})
)
})

I've managed to find an approach that works for my scenario, but I am not sure it is the best way to do it.
The schema is now:
const RuleSchema = Yup.object().shape({
description: Yup.string().required('Description Required').min(2).max(25),
fieldId: Yup.number().required('dRequired'),
validationTypeCode: Yup.string().required('Required'),
failureAction: Yup.string().required('Required'),
failureActionValue: Yup.string().required('Required'),
dataEntryWorkFlowRuleValidationList: Yup.array().of(
Yup.object().shape({
fieldValue: Yup.string().when("$rule", (rule, schema) => {
return rule.fieldLength ? schema.max(rule.fieldLength, 'Field Value is too long') : schema
}
)
})
)
})
I call the validation including the entire record being validated into the validation as context.
RuleSchema.validateSync(_rule, {context: {rule: _rule }})

Related

omit empty strings fields mongoose

I have the following schema:
const mySchema = new mongoose.Schema({
x: String,
y: String
})
when a user from the front-end requests in his body:
req.body = {
'x' : '',
'y': ''
}
this results in creating a field in MongoDB, but with an empty string.
I need a way to prevent this behavior by setting the empty strings to be undefined somehow.
Is there such an option in Mongoose? or do I have to predict my own middlewares for that?
You could use the set method for Mongoose Schemas:
const mySchema = new mongoose.Schema(
{
myAttribute: {
type: String,
set: (attribute: string) => attribute === '' ? undefined : attribute,
},
},
{ strict: 'throw' },
);
This will unset the field if the string equals ''.
Use this to trim the strings:
set: (a: string) => a?.trim() === '' ? undefined : a
You don't need mongoose, or a middleware to handle this. You can just write a quick few lines to check for empty values and exclude them from the MongoDB write operation.
Ex:
const newEntry = Object.entries(req.body).reduce((obj, [key, value]) => {
if (value) obj[key] = value
return obj
}, {})
In this example, I convert the req.body into an array using Object.entries and iterate over it with the Array.reduce method, wherein I add key:value pairs to a new object if there is a value to add. Since an empty string value is falsey I can do a simple if check on the value. I then assign the return of the reduce method to the variable newEntry. Then I would then take the new entry and create the MongoDB document with it.
This could be extracted into a helper method and reused in any of your routes that need to check remove empty values from an object.
Docs on Array.reduce
Docs on Object.entries

Yup conditionally extend Object Schema with other Schema

For a Project I use yup and I have some trouble extending a Schema when a property is set.
I build a Codesandbox to show my Problem but the basics are the following.
// Schema I want to extend by, I use it in other places which is why I want to spread it into the baseSchema
const baseSchema = yup.object({
firstName: yup.string().required()
}).required();
const extendedSchema = yup
.object({
hasOtherFields: yup
.mixed()
.required()
.oneOf([...TRUE_FLASE])
})
.when("hasOtherFields", {
is: (value?: typeof TRUE_FLASE[number]) =>
value != null && value === "true",
then: (schema) =>
schema
.shape({
...baseSchema.fields
})
.required()
})
.required();
So the property hasOtherFields needs to be set and needs to be either "true" or "false". If it is "true", I extend the extendedSchema with shape and pass in the fields from another the baseSchema which has a required string field.
Sadly this doesn't work ... If I create a nested object property inside the object declaration like so
const extendedSchema = yup
.object({
hasOtherFields: yup
.mixed()
.required()
.oneOf([...TRUE_FLASE]),
extendedFields: yup.object({}).when("hasOtherFields", {
is: (value?: typeof TRUE_FLASE[number]) =>
value != null && value === "true",
then: (schema) =>
schema
.shape({
...baseSchema.fields
})
.required()
})
})
it works, but that doesn't really work for my use case.
So my question is, is there a way ? I basically want the type to look like this
type Schema = {hasCoApplicant: "false"} | {hasCoApplicant: "true"; firstName: string};

Access mongoose parent document for default values in subdocument

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'.

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.

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.