-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
CMDCT-4190: adding validation and schemaMap files with tests
- Loading branch information
1 parent
a57d63b
commit 4e059c0
Showing
7 changed files
with
645 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,255 @@ | ||
import { | ||
array, | ||
boolean, | ||
mixed, | ||
number as numberSchema, | ||
object, | ||
string, | ||
StringSchema, | ||
} from "yup"; | ||
|
||
const error = { | ||
REQUIRED_GENERIC: "A response is required", | ||
INVALID_EMAIL: "Response must be a valid email address", | ||
INVALID_URL: "Response must be a valid hyperlink/URL", | ||
INVALID_DATE: "Response must be a valid date", | ||
INVALID_END_DATE: "End date can't be before start date", | ||
NUMBER_LESS_THAN_ONE: "Response must be greater than or equal to one", | ||
NUMBER_LESS_THAN_ZERO: "Response must be greater than or equal to zero", | ||
INVALID_NUMBER: "Response must be a valid number", | ||
INVALID_NUMBER_OR_NA: 'Response must be a valid number or "N/A"', | ||
INVALID_RATIO: "Response must be a valid ratio", | ||
}; | ||
|
||
const isWhitespaceString = (value?: string) => value?.trim().length === 0; | ||
|
||
// CHECKBOX | ||
export const checkbox = () => | ||
array() | ||
.min(0) | ||
.of(object({ key: text(), value: text() })); | ||
export const checkboxOptional = () => checkbox(); | ||
export const checkboxSingle = () => boolean(); | ||
|
||
// TEXT | ||
export const text = (): StringSchema => string(); | ||
export const textOptional = () => text(); | ||
|
||
// DATE | ||
export const dateFormatRegex = | ||
/^((0[1-9]|1[0-2])\/(0[1-9]|1\d|2\d|3[01])\/(19|20)\d{2})|((0[1-9]|1[0-2])(0[1-9]|1\d|2\d|3[01])(19|20)\d{2})|\s$/; | ||
|
||
export const date = () => | ||
string().test({ | ||
message: error.INVALID_DATE, | ||
test: (value) => !!value?.match(dateFormatRegex) || value?.trim() === "", | ||
}); | ||
|
||
export const dateOptional = () => date(); | ||
export const endDate = (startDateField: string) => | ||
date().test( | ||
"is-after-start-date", | ||
error.INVALID_END_DATE, | ||
(endDateString, context) => { | ||
return isEndDateAfterStartDate( | ||
context.parent[startDateField], | ||
endDateString as string | ||
); | ||
} | ||
); | ||
|
||
export const isEndDateAfterStartDate = ( | ||
startDateString: string, | ||
endDateString: string | ||
) => { | ||
const startDate = new Date(startDateString); | ||
const endDate = new Date(endDateString!); | ||
return endDate >= startDate; | ||
}; | ||
|
||
// DROPDOWN | ||
export const dropdown = () => object({ label: text(), value: text() }); | ||
|
||
// DYNAMIC | ||
export const dynamic = () => array().min(0).of(mixed()); | ||
export const dynamicOptional = () => array().notRequired().nullable(); | ||
|
||
export const email = () => text().email(error.INVALID_EMAIL); | ||
export const emailOptional = () => email(); | ||
|
||
// NUMBER | ||
export const validNAValues = [ | ||
"N/A", | ||
"NA", | ||
"na", | ||
"n/a", | ||
"N/a", | ||
"Data not available", | ||
]; | ||
|
||
/** This regex must be at least as permissive as the one in ui-src */ | ||
const validNumberRegex = /^\.$|[0-9]/; | ||
|
||
export const number = () => | ||
string().test({ | ||
message: error.INVALID_NUMBER_OR_NA, | ||
test: (value) => { | ||
if (value) { | ||
const isValidStringValue = validNAValues.includes(value); | ||
const isValidNumberValue = validNumberRegex.test(value); | ||
return isValidStringValue || isValidNumberValue; | ||
} else return true; | ||
}, | ||
}); | ||
|
||
export const numberNotLessThanOne = () => | ||
string() | ||
.required(error.REQUIRED_GENERIC) | ||
.test({ | ||
test: (value) => validNumberRegex.test(value!), | ||
message: error.INVALID_NUMBER, | ||
}) | ||
.test({ | ||
test: (value) => parseInt(value!) >= 1, | ||
message: error.NUMBER_LESS_THAN_ONE, | ||
}); | ||
|
||
export const numberNotLessThanZero = () => | ||
string() | ||
.required(error.REQUIRED_GENERIC) | ||
.test({ | ||
test: (value) => validNumberRegex.test(value!), | ||
message: error.INVALID_NUMBER, | ||
}) | ||
.test({ | ||
test: (value) => parseFloat(value!) >= 0, | ||
message: error.NUMBER_LESS_THAN_ZERO, | ||
}); | ||
|
||
export const numberOptional = () => number(); | ||
|
||
const validNumberSchema = () => | ||
string().test({ | ||
message: error.INVALID_NUMBER, | ||
test: (value) => { | ||
return typeof value !== "undefined" | ||
? validNumberRegex.test(value) | ||
: false; | ||
}, | ||
}); | ||
|
||
export const validNumber = () => | ||
validNumberSchema() | ||
.required(error.REQUIRED_GENERIC) | ||
.test({ | ||
test: (value) => !isWhitespaceString(value), | ||
message: error.REQUIRED_GENERIC, | ||
}); | ||
|
||
export const validNumberOptional = () => | ||
validNumberSchema().notRequired().nullable(); | ||
|
||
// OBJECT ARRAY | ||
export const objectArray = () => array().of(mixed()); | ||
|
||
// RADIO | ||
export const radio = () => | ||
array() | ||
.min(0) | ||
.of(object({ key: text(), value: text() })); | ||
export const radioOptional = () => radio(); | ||
|
||
// Number - Ratio | ||
const valueCleaningNumberSchema = (value: string, charsToReplace: RegExp) => { | ||
return numberSchema().transform((_value) => { | ||
return Number(value.replace(charsToReplace, "")); | ||
}); | ||
}; | ||
|
||
export const ratio = () => | ||
mixed().test({ | ||
message: error.INVALID_RATIO, | ||
test: (val: any) => { | ||
// allow if blank | ||
if (val === "" || !val) return true; | ||
|
||
const replaceCharsRegex = /[,.:]/g; | ||
const ratio = val.split(":"); | ||
|
||
// Double check and make sure that a ratio contains numbers on both sides | ||
if ( | ||
ratio.length != 2 || | ||
ratio[0].trim().length == 0 || | ||
ratio[1].trim().length == 0 | ||
) { | ||
return false; | ||
} | ||
|
||
// Check if the left side of the ratio is a valid number | ||
const firstTest = valueCleaningNumberSchema( | ||
ratio[0], | ||
replaceCharsRegex | ||
).isValidSync(val); | ||
|
||
// Check if the right side of the ratio is a valid number | ||
const secondTest = valueCleaningNumberSchema( | ||
ratio[1], | ||
replaceCharsRegex | ||
).isValidSync(val); | ||
|
||
// If both sides are valid numbers, return true! | ||
return firstTest && secondTest; | ||
}, | ||
}); | ||
|
||
// URL | ||
export const url = () => text().url(error.INVALID_URL); | ||
export const urlOptional = () => url(); | ||
|
||
// NESTED | ||
export const nested = ( | ||
fieldSchema: Function, | ||
parentFieldName: string, | ||
parentOptionId: string | ||
) => { | ||
const fieldTypeMap = { | ||
array: array(), | ||
string: string(), | ||
date: date(), | ||
object: object(), | ||
}; | ||
const fieldType: keyof typeof fieldTypeMap = fieldSchema().type; | ||
const baseSchema: any = fieldTypeMap[fieldType]; | ||
return baseSchema.when(parentFieldName, { | ||
is: (value: any[]) => | ||
// look for parentOptionId in checked Choices | ||
value?.find((option: any) => option.key === parentOptionId), | ||
then: () => fieldSchema(), // returns standard field schema (required) | ||
otherwise: () => baseSchema, // returns not-required Yup base schema | ||
}); | ||
}; | ||
|
||
export const schemaMap: any = { | ||
checkbox: checkbox(), | ||
checkboxOptional: checkboxOptional(), | ||
checkboxSingle: checkboxSingle(), | ||
date: date(), | ||
dateOptional: dateOptional(), | ||
dropdown: dropdown(), | ||
dynamic: dynamic(), | ||
dynamicOptional: dynamicOptional(), | ||
email: email(), | ||
emailOptional: emailOptional(), | ||
number: number(), | ||
numberNotLessThanOne: numberNotLessThanOne(), | ||
numberOptional: numberOptional(), | ||
objectArray: objectArray(), | ||
radio: radio(), | ||
radioOptional: radioOptional(), | ||
ratio: ratio(), | ||
text: text(), | ||
textOptional: textOptional(), | ||
url: url(), | ||
urlOptional: urlOptional(), | ||
}; |
Oops, something went wrong.