Sequelizejs "isAfter" validation with other field - node.js

In the Sequelize's docs, they have mention following way to restrict a date field value to after a certain date.
validate: {
isAfter: '2018-10-02'
}
But I want to perform this validation against another date field from req.body
Like
validate: {
isAfter: anotherFieldName // fieldname instead of static value
}

The field validators do not have access to the model instance's other properties. In order to validate that two values on an instance pass your validation check, You should make use of Sequelize's custom validate object in the model options:
const SomeModel = db.define(
'some_model',
{
start_date: {
type: Sequelize.DATEONLY,
validate: {
isDate: true
}
},
end_date: {
type: Sequelize.DATEONLY,
validate: {
isDate: true
}
},
},
{
validate: {
startDateAfterEndDate() {
if (this.start_date.isAfter(this.end_date)) {
throw new Error('Start date must be before the end date.');
}
}
}
}
);

Related

Mongoose, virtuals and Schemas

I'm developing a NodeJS (Typescript) with Mongoose and when I try to add a virtual to my schema in one of the ways Mongoose's documentation suggests, I get an error saying that the prop virtuals doesn't exist in type SchemaOptions. And a correct error, even though the documentation suggests to use it.
This is what I find in the docs:
// That can be done either by adding it to schema options:
const personSchema = new Schema({
name: {
first: String,
last: String
}
}, {
virtuals: { //--------> I get the error here
fullName: {
get() {
return this.name.first + ' ' + this.name.last;
}
}
}
});
This is what I was trying:
const mySchema = new Schema<MyInterface>(
{
someProp: { type: Types.ObjectId, required: true, ref: "some-collection" },
myList: { type: [listSchema], required: true, default: [] },
},
{
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true },
virtuals: {
getByType: {
get: (type: string) => {
return this.myList.filter((item: Item) => item.type === type);
}
}
}
}
);
In the other hand, I can set my virtual this way:
mySchema.virtual("getByType").get((type: string) => {
return this.myList.filter((item: Item) => item.type === type);
});
I had to do a few workarounds to sort the issue about not resolving the this keyword, but so far I have no problem about it...
The problem is: I use findOne and then try to call my virtual get with my document, but I get a Type Error saying Property 'getByType' does not exist on type 'MyInterface & Document<any, any, MyInterface>'.
Looks like there is a mix of mistake on the documentation and a Typescript problem here.
What do you suggest?
Virtual is intended to be used by a Class. If you want to use it on an instance, use methods instead.
Virtual
mySchema.virtual("getByType").get((type: string) => {
return this.myList.filter((item: Item) => item.type === type);
});
// Usage
MySchema.getByType(type)
Methods
mySchema.methods.getByType = function (type, cb) {
return this.model('MySchema').find({
myList: { $elemMatch: { type } }, // Fill your logic here
}, cb)
};
// Usage
const instance = MySchema.findOne();
const result = instance.getByType(type);

how to have a field that conditionally MUST be null?

This isn't quite a "sometimes required" field, but a little different.
I need to have a field in a Mongoose document that, depending on other data in the document, must NEVER be populated.
Is there a way to define the schema such that if field a is populated, that field b MUST be null?
I would strongly prefer to NOT solve this with mongoose hooks...
It can be done using custom validators, this example is for mongoose version 5.x.
new Schema({
a: {
type: String
},
b: {
type: String,
default: null,
validate: {
validator: function (v) {
if (this.a && v === null) {
return true
}
return false
},
message: (props) => `${props.value} should be null!`
}
}
})

How to grab field value during a MongooseModel.bulkWrite operation?

Context:
I am trying to upsert in bulk an array of data, with an additional computed field: 'status'.
Status should be either :
- 'New' for newly inserted docs;
- 'Removed' for docs present in DB, but inexistent in incoming dataset;
- a percentage explaining the evolution for the field price, comparing the value in DB to the one in incoming dataset.
Implementations:
data.model.ts
import { Document, model, Model, models, Schema } from 'mongoose';
import { IPertinentData } from './site.model';
const dataSchema: Schema = new Schema({
sourceId: { type: String, required: true },
name: { type: String, required: true },
price: { type: Number, required: true },
reference: { type: String, required: true },
lastModified: { type: Date, required: true },
status: { type: Schema.Types.Mixed, required: true }
});
export interface IData extends IPertinentData, Document {}
export const Data: Model<IData> = models.Data || model<IData>('Data', dataSchema);
data.service.ts
import { Data, IPertinentData } from '../models';
export class DataService {
static async test() {
// await Data.deleteMany({});
const data = [
{
sourceId: 'Y',
reference: `y0`,
name: 'y0',
price: 30
},
{
sourceId: 'Y',
reference: 'y1',
name: 'y1',
price: 30
}
];
return Data.bulkWrite(
data.map(function(d) {
let status = '';
// #ts-ignore
console.log('price', this);
// #ts-ignore
if (!this.price) status = 'New';
// #ts-ignore
else if (this.price !== d.price) {
// #ts-ignore
status = (d.price - this.price) / this.price;
}
return {
updateOne: {
filter: { sourceId: d.sourceId, reference: d.reference },
update: {
$set: {
// Set percentage value when current price is greater/lower than new price
// Set status to nothing when new and current prices match
status,
name: d.name,
price: d.price
},
$currentDate: {
lastModified: true
}
},
upsert: true
}
};
}
)
);
}
}
... then in my backend controller, i just call it with some route :
try {
const results = await DataService.test();
return new HttpResponseOK(results);
} catch (error) {
return new HttpResponseInternalServerError(error);
}
Problem:
I've tried lot of implementation syntaxes, but all failed either because of type casting, and unsupported syntax like the $ symbol, and restrictions due to the aggregation...
I feel like the above solution might be closest to a working scenario but i'm missing a way to grab the value of the price field BEFORE the actual computation of status and the replacement with updated value.
Here the value of this is undefined while it is supposed to point to current document.
Questions:
Am i using correct Mongoose way for a bulk update ?
if yes, how to get the field value ?
Environment:
NodeJS 13.x
Mongoose 5.8.1
MongoDB 4.2.1
EUREKA !
Finally found a working syntax, pfeeeew...
...
return Data.bulkWrite(
data.map(d => ({
updateOne: {
filter: { sourceId: d.sourceId, reference: d.reference },
update: [
{
$set: {
lastModified: Date.now(),
name: d.name,
status: {
$switch: {
branches: [
// Set status to 'New' for newly inserted docs
{
case: { $eq: [{ $type: '$price' }, 'missing'] },
then: 'New'
},
// Set percentage value when current price is greater/lower than new price
{
case: { $ne: ['$price', d.price] },
then: {
$divide: [{ $subtract: [d.price, '$price'] }, '$price']
}
}
],
// Set status to nothing when new and current prices match
default: ''
}
}
}
},
{
$set: { price: d.price }
}
],
upsert: true
}
}))
);
...
Explanations:
Several problems were blocking me :
the '$field_value_to_check' instead of this.field with undefined 'this' ...
the syntax with $ symbol seems to work only within an aggregation update, using update: [] even if there is only one single $set inside ...
the first condition used for the inserted docs in the upsert process needs to check for the existence of the field price. Only the syntax with BSON $type worked...
Hope it helps other devs in same scenario.

How to change nodejs schema according to my requirement

I have following data I want put data like below but my schema does not allow me what I do, to send data like below please check. when I put only single {} body it's working fine but I want more then one bodies
//Request body
{
"title" : "test10",
"sch_start": "2017-04-3",
"sch_end":"2017-04-3"
},
{
"title" : "test11",
"sch_start": "2017-04-4",
"sch_end":"2017-04-4"
}
import mongoose, {Schema} from 'mongoose';
/**
* Model to store Calendar entries of Engineer
*/
var EngineerScheduleSchema = new Schema({
title: { type: String, default: ''},
available: { type: Boolean, default: false },
sch_start: { type:Date, default: Date.now },
sch_end: { type:Date, default: Date.now }
});
export default mongoose.model('engineer_schedule', EngineerScheduleSchema);
// Main Model Mongoose Schema
schedule: [EngineerSchedule.schema]
//API Method
export function updateSchedulecalendar(req, res) {
var responseSchedule;
//Insert
return Engineer.findOneAndUpdate({ _id: req.params.id }, { $addToSet: { schedule: req.body } }, { new: true, upsert: true, setDefaultsOnInsert: true, runValidators: true }).exec()
.then((entity) => {
if (entity) {
responseSchedule = entity.schedule;
return EngineerEvents.emit(engineerEvents.updatedSchedule, req.user, entity);
}
else {
return res.status(404).end();
}
})
.then(()=> { return res.status(200).json(responseSchedule); })
.catch(handleError(res));
}
First, Since you are trying to send many schedules, your request body should look like:
[{
"title" : "test10",
"sch_start": "2017-04-3",
"sch_end":"2017-04-3"
},
{
"title" : "test11",
"sch_start": "2017-04-4",
"sch_end":"2017-04-4"
}]
Second, you should take a look at findOneAndUpdate documentation, cause it seems to me that you are sending two schedule objects (test10 and test11 ) to be updated on a single schedule specified by req.params.id. It does not make much sense.
If you are looking to update multiple schedules in a single request, maybe you should implement a bulk update. Take a look at bulk functionality, I would implement something like this:
export function updateSchedulecalendar(req, res) {
var responseSchedule;
var bulk = db.items.initializeUnorderedBulkOp();
// Iterate over every schedule sent by client
for(schedule in req.body) {
// Generate a new update statement for that specific document
bulk.find( { title: schedule.title } ).update( { $set: { sch_start: schedule.sch_start, ... } } );
}
// Execute all updates
bulk.execute().then(function() {
// Validation
});
}

Sequelize validations aways returns false

I'm creating a model that needs some validations, but the validations handle an error when I input the correct or wrong data.
What is the right way to configure model validations?
My model is:
return sequelize.define('books', {
id : { type: DataTypes.BIGINT , primaryKey:true, autoIncrement:true },
isbn_10 : { type: DataTypes.BIGINT , allowNull: false,
validate: {
min:10,
max:10
}
},
title : { type: DataTypes.STRING , allowNull: false,
validate: {
isNumeric:true,
min:2,
max:255
}
}, { underscored :true, freezeTableName:true});
And I'm validation on this way:
models.Book.create(req.body)
.success(function(book) { console.log(book); res.render('books/create', { books:models.Book }); })
.error(function(errors) {
console.log(errors);
});
The form input data is:
{ Book:
{ isbn_10: '123123131',
title: '',
cadastrar: 'Submit Query' }}
Thanks.
The problem was solved, I update the sequelize version form 2.0.0.alpha.0 to 2.0.0.dev11 and the validations start to work again.
Thanks.

Resources