What is the best way to validate updators using multiple fields upon calling findOneAndUpdate?
#15525
Replies: 3 comments
|
basicsSchema.path('endYear').validate({
validator: async function (this: BasicsDocument | mongoose.Query<unknown, BasicsDocument>, value) {
if (this instanceof mongoose.Document) {
return true;
}
// `this` is a mongoose query object in update validators
const query = this;
// Get the existing doc to compare against `startYear`
const existingDoc = await query.findOne(query.getFilter());
if (existingDoc && value !== null && value < existingDoc.startYear) {
throw new Error('endYear must be >= startYear');
}
return true;
}
});
const Basics = mongoose.model('Basic', basicsSchema);
type BasicsDocument = ReturnType<(typeof Basics)['hydrate']>;This is likely good enough for your use case. There's a minor race condition where technically another update can update |
|
For cross-field validation in Mongoose you have two clean options. Option 1. Schema-level validate on a virtual or a computed field. userSchema.path("endYear").validate(function (value) {
return value == null || value >= this.startYear
}, "endYear must be greater than or equal to startYear")The validation runs on save and on calls to doc.validate(). Option 2. Pre-save hook for more complex logic across many fields. userSchema.pre("save", function (next) {
if (this.endYear != null && this.endYear < this.startYear) {
return next(new Error("endYear must be >= startYear"))
}
next()
})For NestJS with the @nestjs/mongoose decorators you can add the pre hook in a plugin or call it on the schema object directly in your module setup: @Module({
imports: [
MongooseModule.forFeatureAsync([
{
name: BasicsModel.name,
useFactory: () => {
const schema = BasicsSchema
schema.pre("save", function (next) {
if (this.endYear != null && this.endYear < this.startYear) {
return next(new Error("endYear must be >= startYear"))
}
next()
})
return schema
}
}
])
]
})This keeps the validation close to the schema and works with both create and update via findOneAndUpdate if you pass runValidators: true. |
|
The short answer is that update validators run with this bound to the update document, not the full document being saved, so accessing this.startYear from an endYear validator will not work with findOneAndUpdate. The two practical approaches are: The first is to use a pre findOneAndUpdate hook that fetches the full document and performs the cross-field check manually. In your schema definition, add a pre hook on findOneAndUpdate. Inside the hook, call this.model.findOne(this.getQuery()) to load the existing document, then merge the incoming update from this.getUpdate() to reconstruct the final state, and throw a validation error if endYear is set and is not greater than startYear. The advantage is that the validation lives in the schema and fires automatically for every findOneAndUpdate call. The disadvantage is an extra round-trip to the database. The second is to validate in your service layer before calling findOneAndUpdate. In updateByTconst, load the document first, apply the incoming dto fields to get the projected final state, run the check, and only call findOneAndUpdate if the check passes. This avoids the extra hook and keeps the logic visible at the call site, which is useful when the validation message needs to be customized per endpoint. The first approach is better if you want the constraint enforced everywhere the model is used. The second is simpler if this endpoint is the only place updates happen. A third option is to move this validation into the DTO class itself using class-validator. If your dto already carries both startYear and endYear, you can add a custom @ValidateIf decorator that checks endYear against startYear without touching Mongoose at all. This works when the DTO always includes both fields, but not when only one field is being patched at a time. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
I have a Mongoose schema with NestJS:
I have an endpoint that allows callers to update a basic entity as following:
I want to make sure that
endYearif not null is valid only if its value is greater thanstartYear.I want to use
Update validatorsto isolate my validations so this is what I came up with.It works but it feels clunky and anti pattern because I have to use
@ts-ignoreand many evil type casting. Is there any better way to achieve what I want?Here what I have tried:
Using
this.get("startYear")orthis.startYearin my validator function does not work because both returnundefined. In the case ofthis.get(), Mongoose states the following:So basically,
this.get("endYear")returns a value whilethis.get("startYear")does not.I avoid using
prehook middleware because I want to isolate the validations. You can assume that there are many more fields in the schema and there are multiple checks so I don't want to have a bloating hook. However, I'm opened to every argument that makes the hook function cleaner and more organized.All reactions