I'm doing my own custom validations on certain fields, so that only certain values are accepted (depending on the field) and the rest rejected. I would like to write a "filter" function that checks what attribute called the validation and from there decide what words the attribute is allowed to use. So the model would look something like this:
module.exports = {
types: {
filter: function(attribute) {
if (attribute === 'number') {
switch(attribute.value) {
case 'one':
return true;
case 'two':
return true;
default:
return false;
}
} else if (attribute === 'color') {
switch(attribute.value) {
case 'red':
return true;
case 'blue':
return true;
default:
return false;
}
}
},
},
attributes: {
number: {
type: 'string',
required: true,
filter: true
},
color: {
type: 'string',
required: true,
filter: true
}
}
};
Of course, in normal Sails.js behaviour, "attribute" would not be the attribute, but the value of the attribute. (And attribute.value was just an example, meaning, I want the attribute value in there).
So, I want attribute to be the actual attribute that called the validation rule. Is this possible with Sails? I mean, I could write a function for each field in the model, but it would be nice to have a function that fits them all (I have many of them).
Thanks.
Ok so I will answer your question, but this may not be exactly what you want. An attribute can have an "enum" which is how we'd achieve your end goal:
attributes: {
state: {
type: 'string',
enum: ['pending', 'approved', 'denied']
}
}
But I will assume that this code is just a contrived example. Here's a way that I think would work.
module.exports = {
types: {
filter: function(attribute) {
if (attribute === 'number') {
switch(attribute.value) {
case 'one':
return true;
case 'two':
return true;
default:
this.validationErrors.push(attribute);
return false;
}
} else if (attribute === 'color') {
switch(attribute.value) {
case 'red':
return true;
case 'blue':
return true;
default:
this.validationErrors.push(attribute);
return false;
}
}
},
},
attributes: {
validationErrors:(function(){
var errors = [];
return {
push:function(attr){
errors.push(attr);
},
get:function(){
return errors;
}
};
})(),
number: {
type: 'string',
required: true,
filter: true
},
color: {
type: 'string',
required: true,
filter: true
}
}
};
Edit:Used an attribute method instead of an attribute
There are potentially a couple problems with this answer. I'm not sure how waterline handles "this" within these custom type functions. Is "this" bound to the model? Or the instance of the model we're creating? There's a lot of questions to be asked here but maybe this can give you some ideas and create a discussion.
Related
I'm trying to perform simple validation on a JSON input, modelled by one of my DTOs.
One of the object properties is of type Map<string, number>. an example input:
{
"type": "CUSTOM",
"is_active": true,
"current_plan_day": 1,
"custom_warmup_plan": {
"1": 123,
"2": 456
}
On my controller I'm using a DTO to specify the body type. the class, together with class-validator decorators is this:
export class CreateWarmupPlanRequestDto {
#IsEnum(WarmupPlanType)
type: string;
#IsOptional()
#IsNumber({ allowInfinity: false, allowNaN: false, maxDecimalPlaces: 0 })
#IsPositive()
hard_cap: number | null;
#IsBoolean()
is_active: boolean;
#IsNumber({ allowInfinity: false, allowNaN: false, maxDecimalPlaces: 0 })
#IsPositive()
current_plan_day: number;
#IsOptional()
#IsNumber({ allowInfinity: false, allowNaN: false, maxDecimalPlaces: 0 })
#IsPositive()
previous_plan_day: number | null;
#IsOptional()
#IsNumber({ allowInfinity: false, allowNaN: false, maxDecimalPlaces: 0 }, { each: true })
#IsPositive({ each: true })
custom_warmup_plan: Map<string, number>; // PROBLEM HERE
}
I'm looking to validate each value of custom_warmup_plan to be an existing positive integer.
Validation of the other properties of the object works just fine and as expected, but for my example input I keep getting errors (2 error messages, joined):
{
"message": "each value in custom_warmup_plan must be a positive number. |#| each value in custom_warmup_plan must be a number conforming to the specified constraints",
"statusCode": 400,
"timestamp": "2021-07-29T13:18:29.331Z",
"path": "/api/warmup-plan/bc4c3f0e-8e77-46de-a46a-a908edbdded5"
}
Documentation for this seems to be pretty straight forward, but I just cant get it to work.
I've also played around with a simple Map<string, string> and the #IsString(each: true) validator, but that does not seem to work either.
any ideas?
versions:
"#nestjs/common": "^8.0.0",
"#nestjs/core": "^8.0.0",
"#nestjs/mapped-types": "^1.0.0",
"#nestjs/platform-express": "^8.0.0",
"class-transformer": "^0.4.0",
"class-validator": "^0.13.1",
It is necessary to convert plain object to map. Use Transform decorator from class-transformer
#IsOptional()
#IsNumber(undefined, { each: true })
#Transform(({ value }) => new Map(Object.entries(value)))
prop?: Map<string, number>;
From the docs
If your field is an array and you want to perform validation of each item in the array you must specify a special each: true decorator option
If you want to be able to validate maps you could write a custom decorator and pass in a list of class-validator functions to validate the keys and values. For example the below decorator takes as input a list of validation functions for both the keys and values (e.g. passing in isString, isObject, etc..., class-validator has a corresponding function you can call for all the validation decorators they provide)
export function IsMap(
key_validators: ((value: unknown) => boolean)[],
value_validators: ((value: unknown) => boolean)[],
validationOptions?: ValidationOptions
) {
return function (object: unknown, propertyName: string) {
registerDecorator({
name: 'isMap',
target: (object as any).constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate(value: unknown, args: ValidationArguments) {
if (!isObject(value)) return false;
const keys = Object.keys(value);
const is_invalid = keys.some((key) => {
const is_key_invalid = key_validators.some((validator) => !validator(key));
if (is_key_invalid) return false;
const is_value_invalid = value_validators.some((validator) => !validator(value[key]));
return is_value_invalid;
});
return !is_invalid;
},
},
});
};
}
And you can use this decorator in your example like this
import { isInt } from 'class-validator'
export class CreateWarmupPlanRequestDto {
#IsOptional()
#IsMap([], [isInt])
custom_warmup_plan: Map<string, number>;
}
Using the same approach with #Daniel, I modified the code little bit so that the focus is on 'isValid' rather than 'IsInvalid'. So that we could avoid double negation.
Additionally, the coming object is transformed to map in the DTO.
#Transform(({ value }) => new Map(Object.entries(value)))
import {
registerDecorator,
ValidationArguments,
ValidationOptions,
} from 'class-validator';
import * as $$ from 'lodash';
export function IsMap(
keyValidators: ((value: unknown) => boolean)[],
valueValidators: ((value: unknown) => boolean)[],
validationOptions?: ValidationOptions,
) {
return function (object: unknown, propertyName: string) {
/**
* ** value is expected to be a MAP already, we are just checking types of keys and values...
*/
registerDecorator({
name: 'isMap',
target: (object as any).constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate(value: Map<any, any>, args: ValidationArguments) {
if (!$$.isMap(value)) {
return false;
}
const keys = Array.from(value.keys());
return $$.every(keys, (key) => {
// checking if keys are valid...
const isKeyInvalid = keyValidators.some(
(validator) => !validator(key),
);
if (isKeyInvalid) {
return false;
}
// checking if values are valid...
const isValueInvalid = valueValidators.some(
(validator) => !validator(value.get(key)),
);
if (isValueInvalid) {
return false;
} else {
return true;
}
});
},
},
});
};
}
I am upgrading jquery and saw that the "async: false" option has been deprecated. This makes sense and in 99.9% of cases I agree with the rationale, but I have a case where I think I really need it and I cannot for the life of me figure out how to make this work with a purely async ajax call no matter how I use promises or async/await.
My use case is in a Vue component and I have an array of contacts. What I need to do is map over the contacts and validate them. One such validation requires a quick check of email validity via a "check_email" ajax endpoint.
Once I validate (or not) the list, I then submit the list (if valid) or show error messages (if invalid).
My code is something like this
sendContacts: function() {
valid = this.validateContacts()
if (valid) {
// send the contacts
} else {
return // will display error messages on contacts objects
}
},
validateContacts: function() {
this.contacts = this.contacts.map((contact) => {
if (!contact.name) {
contact.validDetails.name = false
contact.valid = false
return contact
}
if (!contact.email) {
contact.validDetails.emailExists = false
contact.valid = false
return contact
}
if (!check_email(email)) { // THIS IS ASYNC NOW WHAT DO I DO
contact.valid = false
contact.validDetails.emailFormat = false
}
return contact
}
var validData = this.contacts.map(c => {
return c.valid
})
return !validData.includes(false)
}
function check_email(email) {
const url = `/api/v1/users/check-email?email=${email}`
let valid = false
$.ajax({
url: url,
type: 'POST',
async: false, // I can't do this anymore
headers: {
'X-CSRFToken': csrfToken
},
success: resp => {
valid = true
},
error: err => {
}
})
return valid
}
my data function:
data: function() {
return {
contacts: [this.initContact()],
showThanks: false,
emailError: false,
blankEmail: false,
blankName: false
}
},
methods: {
initContact: function() {
return {
name: null,
email: null,
title: null,
validDetails: this.initValidDetails(),
valid: true,
}
},
initValidDetails: function() {
return {
emailDomain: true,
emailExists: true,
emailFormat: true,
name: true
}
}
}
Again, I have tried async/await in every place I could think of and I cannot get this to validate properly and then perform correct logic regarding whether the send contacts function part of the function should fire. Please help!
Once any part of your validation is asynchronous, you must treat the entire thing as asynchronous. This includes when calling validateContacts in sendContacts.
First, you should change check_email to return Promise<bool>. It's usually a bad idea to include jQuery in a Vue project so let's use fetch instead (Axios being another popular alternative).
async function check_email(email) {
const params = new URLSearchParams({ email })
const res = await fetch(`/api/v1/users/check-email?${params}`, {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken
}
})
return res.ok
}
As for your async validation logic, it's best to map your contacts to an array of promises and wait for them all with Promise.all.
async validateContacts () {
const validationPromises = this.contacts.map(async contact => {
if (!contact.name) {
return {
...contact,
valid: false,
validDetails: {
...contact.validDetails,
name: false
}
}
}
if (!contact.email) {
return {
...contact,
valid: false,
validDetails: {
...contact.validDetails,
emailExists: false
}
}
}
if (await check_email(contact.email)) { // await here
return {
...contact,
valid: false,
validDetails: {
...contact.validDetails,
emailFormat: false
}
}
}
return { ...contact, valid: true }
})
// now wait for all promises to resolve and check for any "false" values
this.contacts = await Promise.all(validationPromises)
return this.contacts.every(({ valid }) => valid)
}
As mentioned, now you need to treat this asynchronously in sendContacts
async sendContacts () {
if (await this.validateContacts()) {
// send the contacts
}
}
I have this custom validator:
export const mealTypesValidator = (mealSelected: boolean) => {
return (control: FormControl) => {
var mealTypes = control.value;
if (mealTypes) {
if (mealTypes.length < 1 && mealSelected) {
return {
mealTypesValid: { valid: false }
};
}
}
return null;
};
};
If I use it like this it works:
ngOnInit() {
this.findForm = this.formBuilder.group({
categories: [null, Validators.required],
mealTypes: [[], mealTypesValidator(true)],
distanceNumber: null,
distanceUnit: 'kilometers',
keywords: null,
});
}
The catch is, mealSelected is a property on my component - that changes when the user selects and deselects a meal.
How I call the validator above is using static true which can never change.
How can I get the validator to work when I use the component.mealSelected value as the parameter eg:
ngOnInit() {
this.findForm = this.formBuilder.group({
categories: [null, Validators.required],
mealTypes: [[], mealTypesValidator(this.mealSelected)],
distanceNumber: null,
distanceUnit: 'kilometers',
keywords: null,
});
}
Because if i do it as above, it evaluates this.mealSelected instantly which is false at the time - and then when the user selects a meal, it doesn't then go ahead and pass true into the custom validator.
Solution was to move the validator inside my component and use this.mealSelected to check against. Then I had an issue with the validator not being triggered when a meal was selected/deselected and I used this.findForm.controls['mealTypes'].updateValueAndValidity(); to trigger the validation.
Code (can probably be refactored to remove the parameter from the custom validator):
ngOnInit() {
this.findForm = this.formBuilder.group({
categories: [null, Validators.required],
mealTypes: [[], this.mealTypesValidator(true)],
distanceNumber: null,
distanceUnit: 'kilometers',
keywords: null,
});
}
mealTypesValidator = (mealSelected: boolean) => {
return (control: FormControl) => {
var mealTypes = control.value;
if (mealTypes) {
if (mealTypes.length < 1 && this.mealSelected) {
return {
mealTypesValid: { valid: false }
};
}
}
return null;
};
};
However It would still be nice to be able to have a seperate validation module to centralise validation, so if anyone knows how to have a changing parameter value such as a component field as a parameter to a custom validator - like I initially asked, then I'd appreciate an answer that goes with that technique.
What I Need
I would like to sort my grid/store by the Parent field, but because the Parent field that is fetched is an object, it fails to fetch any records when I put a sorter based on the Parent property. Even if I add a sorter function, it is not called. I am using a rallygrid, not sure if that makes a difference
sorters: [{
property: 'Parent',
direction: 'DESC',
sorterFn: function(one, two) {
console.log('one',one);
console.log('two',two); // console never shows these
return -1;
}
}]
What I have tried
To get around displaying the object, I have added a renderer function to the Parent column. I tried adding a doSort to the column, and that function is called, but sorting the store does not call my sorterFn, it only uses the property and direction (similar to the console.log() that fails to run above)
Here is an example of a custom App that works properly. The key is setting the default storeConfig of remoteSort to false!
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function() {
App = this;
Rally.data.ModelFactory.getModel({
type: 'PortfolioItem/Feature',
success: function(model) {
App.add({
xtype: 'rallygrid',
id : 'grid',
model: model,
columnCfgs: [
'FormattedID',
'Name',
{dataIndex: 'Parent', name: 'Parent',
doSort: function(state) {
var ds = this.up('grid').getStore();
var field = this.getSortParam();
console.log('field',field);
ds.sort({
property: field,
direction: state,
sorterFn: function(v1, v2){
v1 = v1.get(field);
v2 = v2.get(field);
console.log('v1',v1);
console.log('v2',v2);
if (!v1 && !v2) {
return 0;
} else if (!v2) {
return 1;
} else if (!v1) {
return -1;
}
return v1.Name.localeCompare(v2.Name);
}
});
},
renderer: function(value, meta, record) {
var ret = record.raw.Parent;
if (ret) {
return ret.Name;
} else {
return record.data.Name;
}
}
}
],
storeConfig: {
remoteSort: false
}
});
}
});
}
});
I want to reuse the following code for custom_func:
function validLen(value,colName){
if(value.length === 8){
return [true,""];
}
else{
return [false,"fail"];
}
}
I tried giving it an additional parameter as follows:
function validLen(value,colName,length){
if(value.length === length){
return [true,""];
}
else{
return [false,"fail"];
}
}
And calling it like this:
{name:'cntrct_id', editrules:{custom: true, custom_func:validLen(8)} },
Didn't work. The previous code DOES work, but as stated, I want a reusable function. Is there a workaround for this? Am I doing it wrong?
I would recommend you to use
editoptions: { maxlength: 8}
instead of the custom validation which you use. In the case the input element will be created with the maxlength attribute directly. So the user will not able to type more as characters as specified by maxlength.
UPDATED: You can't change the interface of any callback function, but you can do share common code of different custom_func in the following way. You define your custom validation function having three parameters like
function validLen (value, colName, valueLength) {
if (value.length === valueLength) {
return [true, ""];
}
else {
return [false, "fail"];
}
}
and use it in the following way
{
name: 'cntrct_id',
editrules: {
custom: true,
custom_func: function (value, colName) {
return validLen(value, colName, 8);
}
}
If you need to use this inside of custom_func then you can change return validLen(value, colName, 8); to return validLen.call(this, value, colName, 8);.