Type validations

This commit is contained in:
Adria Navarro 2025-03-03 16:07:05 +01:00
parent 052ee309bf
commit 4b3cc75bf4
4 changed files with 77 additions and 40 deletions

View File

@ -1,6 +1,11 @@
import dayjs from "dayjs" import dayjs from "dayjs"
import { FieldTypes } from "../../../constants"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import {
FieldConstraints,
FieldType,
Table,
UIFieldValidationRule,
} from "@budibase/types"
/** /**
* Creates a validation function from a combination of schema-level constraints * Creates a validation function from a combination of schema-level constraints
@ -12,19 +17,19 @@ import { Helpers } from "@budibase/bbui"
* @returns {function} a validator function which accepts test values * @returns {function} a validator function which accepts test values
*/ */
export const createValidatorFromConstraints = ( export const createValidatorFromConstraints = (
schemaConstraints, schemaConstraints: FieldConstraints | null | undefined,
customRules, customRules: UIFieldValidationRule[],
field, field: string,
definition definition: Table | undefined
) => { ) => {
let rules = [] let rules: UIFieldValidationRule[] = []
// Convert schema constraints into validation rules // Convert schema constraints into validation rules
if (schemaConstraints) { if (schemaConstraints) {
// Required constraint // Required constraint
if ( if (
field === definition?.primaryDisplay || field === definition?.primaryDisplay ||
schemaConstraints.presence?.allowEmpty === false || (schemaConstraints.presence as any)?.allowEmpty === false ||
schemaConstraints.presence === true schemaConstraints.presence === true
) { ) {
rules.push({ rules.push({
@ -106,7 +111,7 @@ export const createValidatorFromConstraints = (
rules = rules.concat(customRules || []) rules = rules.concat(customRules || [])
// Evaluate each constraint // Evaluate each constraint
return value => { return (value: any) => {
for (let rule of rules) { for (let rule of rules) {
const error = evaluateRule(rule, value) const error = evaluateRule(rule, value)
if (error) { if (error) {
@ -124,7 +129,7 @@ export const createValidatorFromConstraints = (
* @param value the value to validate against * @param value the value to validate against
* @returns {null|*} an error if validation fails or null if it passes * @returns {null|*} an error if validation fails or null if it passes
*/ */
const evaluateRule = (rule, value) => { const evaluateRule = (rule: UIFieldValidationRule, value: any) => {
if (!rule) { if (!rule) {
return null return null
} }
@ -150,14 +155,14 @@ const evaluateRule = (rule, value) => {
* @param type the type to parse * @param type the type to parse
* @returns {boolean|string|*|number|null|array} the parsed value, or null if invalid * @returns {boolean|string|*|number|null|array} the parsed value, or null if invalid
*/ */
const parseType = (value, type) => { const parseType = (value: any, type: `${FieldType}`) => {
// Treat nulls or empty strings as null // Treat nulls or empty strings as null
if (!exists(value) || !type) { if (!exists(value) || !type) {
return null return null
} }
// Parse as string // Parse as string
if (type === FieldTypes.STRING) { if (type === FieldType.STRING) {
if (typeof value === "string" || Array.isArray(value)) { if (typeof value === "string" || Array.isArray(value)) {
return value return value
} }
@ -168,7 +173,7 @@ const parseType = (value, type) => {
} }
// Parse as number // Parse as number
if (type === FieldTypes.NUMBER) { if (type === FieldType.NUMBER) {
if (isNaN(value)) { if (isNaN(value)) {
return null return null
} }
@ -176,7 +181,7 @@ const parseType = (value, type) => {
} }
// Parse as date // Parse as date
if (type === FieldTypes.DATETIME) { if (type === FieldType.DATETIME) {
if (value instanceof Date) { if (value instanceof Date) {
return value.getTime() return value.getTime()
} }
@ -185,7 +190,7 @@ const parseType = (value, type) => {
} }
// Parse as boolean // Parse as boolean
if (type === FieldTypes.BOOLEAN) { if (type === FieldType.BOOLEAN) {
if (typeof value === "string") { if (typeof value === "string") {
return value.toLowerCase() === "true" return value.toLowerCase() === "true"
} }
@ -193,7 +198,7 @@ const parseType = (value, type) => {
} }
// Parse attachments, treating no elements as null // Parse attachments, treating no elements as null
if (type === FieldTypes.ATTACHMENTS) { if (type === FieldType.ATTACHMENTS) {
if (!Array.isArray(value) || !value.length) { if (!Array.isArray(value) || !value.length) {
return null return null
} }
@ -202,8 +207,8 @@ const parseType = (value, type) => {
// Parse attachment/signature single, treating no key as null // Parse attachment/signature single, treating no key as null
if ( if (
type === FieldTypes.ATTACHMENT_SINGLE || type === FieldType.ATTACHMENT_SINGLE ||
type === FieldTypes.SIGNATURE_SINGLE type === FieldType.SIGNATURE_SINGLE
) { ) {
if (!value?.key) { if (!value?.key) {
return null return null
@ -212,7 +217,7 @@ const parseType = (value, type) => {
} }
// Parse links, treating no elements as null // Parse links, treating no elements as null
if (type === FieldTypes.LINK) { if (type === FieldType.LINK) {
if (!Array.isArray(value) || !value.length) { if (!Array.isArray(value) || !value.length) {
return null return null
} }
@ -220,7 +225,7 @@ const parseType = (value, type) => {
} }
// Parse array, treating no elements as null // Parse array, treating no elements as null
if (type === FieldTypes.ARRAY) { if (type === FieldType.ARRAY) {
if (!Array.isArray(value) || !value.length) { if (!Array.isArray(value) || !value.length) {
return null return null
} }
@ -229,7 +234,7 @@ const parseType = (value, type) => {
// For JSON we don't touch the value at all as we want to verify it in its // For JSON we don't touch the value at all as we want to verify it in its
// raw form // raw form
if (type === FieldTypes.JSON) { if (type === FieldType.JSON) {
return value return value
} }
@ -238,69 +243,74 @@ const parseType = (value, type) => {
} }
// Evaluates a required constraint // Evaluates a required constraint
const requiredHandler = value => { const requiredHandler = (value: any) => {
return value != null return value != null
} }
// Evaluates a min length constraint // Evaluates a min length constraint
const minLengthHandler = (value, rule) => { const minLengthHandler = (value: any, rule: UIFieldValidationRule) => {
const limit = parseType(rule.value, "number") const limit = parseType(rule.value, "number")
return value == null || value.length >= limit return value == null || value.length >= limit
} }
// Evaluates a max length constraint // Evaluates a max length constraint
const maxLengthHandler = (value, rule) => { const maxLengthHandler = (value: any, rule: UIFieldValidationRule) => {
const limit = parseType(rule.value, "number") const limit = parseType(rule.value, "number")
return value == null || value.length <= limit return value == null || value.length <= limit
} }
// Evaluates a max file size (MB) constraint // Evaluates a max file size (MB) constraint
const maxFileSizeHandler = (value, rule) => { const maxFileSizeHandler = (value: any, rule: UIFieldValidationRule) => {
const limit = parseType(rule.value, "number") const limit = parseType(rule.value, "number")
const check = attachment => attachment.size / 1000000 > limit const check = (attachment: { size: number }) =>
attachment.size / 1000000 > limit
return value == null || !(value?.key ? check(value) : value.some(check)) return value == null || !(value?.key ? check(value) : value.some(check))
} }
// Evaluates a max total upload size (MB) constraint // Evaluates a max total upload size (MB) constraint
const maxUploadSizeHandler = (value, rule) => { const maxUploadSizeHandler = (value: any, rule: UIFieldValidationRule) => {
const limit = parseType(rule.value, "number") const limit: number = parseType(rule.value, "number")
return ( return (
value == null || value == null ||
(value?.key (value?.key
? value.size / 1000000 <= limit ? value.size / 1000000 <= limit
: value.reduce((acc, currentItem) => acc + currentItem.size, 0) / : value.reduce(
(acc: number, currentItem: { size: number }) =>
acc + currentItem.size,
0
) /
1000000 <= 1000000 <=
limit) limit)
) )
} }
// Evaluates a min value constraint // Evaluates a min value constraint
const minValueHandler = (value, rule) => { const minValueHandler = (value: any, rule: UIFieldValidationRule) => {
// Use same type as the value so that things can be compared // Use same type as the value so that things can be compared
const limit = parseType(rule.value, rule.type) const limit = parseType(rule.value, rule.type)
return value == null || value >= limit return value == null || value >= limit
} }
// Evaluates a max value constraint // Evaluates a max value constraint
const maxValueHandler = (value, rule) => { const maxValueHandler = (value: any, rule: UIFieldValidationRule) => {
// Use same type as the value so that things can be compared // Use same type as the value so that things can be compared
const limit = parseType(rule.value, rule.type) const limit = parseType(rule.value, rule.type)
return value == null || value <= limit return value == null || value <= limit
} }
// Evaluates an inclusion constraint // Evaluates an inclusion constraint
const inclusionHandler = (value, rule) => { const inclusionHandler = (value: any, rule: UIFieldValidationRule) => {
return value == null || rule.value.includes(value) return value == null || (rule.value as any).includes(value)
} }
// Evaluates an equal constraint // Evaluates an equal constraint
const equalHandler = (value, rule) => { const equalHandler = (value: any, rule: UIFieldValidationRule) => {
const ruleValue = parseType(rule.value, rule.type) const ruleValue = parseType(rule.value, rule.type)
return value === ruleValue return value === ruleValue
} }
// Evaluates a not equal constraint // Evaluates a not equal constraint
const notEqualHandler = (value, rule) => { const notEqualHandler = (value: any, rule: UIFieldValidationRule) => {
const ruleValue = parseType(rule.value, rule.type) const ruleValue = parseType(rule.value, rule.type)
if (value == null && ruleValue == null) { if (value == null && ruleValue == null) {
return true return true
@ -309,7 +319,7 @@ const notEqualHandler = (value, rule) => {
} }
// Evaluates a regex constraint // Evaluates a regex constraint
const regexHandler = (value, rule) => { const regexHandler = (value: any, rule: UIFieldValidationRule) => {
const regex = parseType(rule.value, "string") const regex = parseType(rule.value, "string")
if (!value) { if (!value) {
value = "" value = ""
@ -318,23 +328,23 @@ const regexHandler = (value, rule) => {
} }
// Evaluates a not regex constraint // Evaluates a not regex constraint
const notRegexHandler = (value, rule) => { const notRegexHandler = (value: any, rule: UIFieldValidationRule) => {
return !regexHandler(value, rule) return !regexHandler(value, rule)
} }
// Evaluates a contains constraint // Evaluates a contains constraint
const containsHandler = (value, rule) => { const containsHandler = (value: any, rule: UIFieldValidationRule) => {
const expectedValue = parseType(rule.value, "string") const expectedValue = parseType(rule.value, "string")
return value && value.includes(expectedValue) return value && value.includes(expectedValue)
} }
// Evaluates a not contains constraint // Evaluates a not contains constraint
const notContainsHandler = (value, rule) => { const notContainsHandler = (value: any, rule: UIFieldValidationRule) => {
return !containsHandler(value, rule) return !containsHandler(value, rule)
} }
// Evaluates a constraint that the value must be a valid json object // Evaluates a constraint that the value must be a valid json object
const jsonHandler = value => { const jsonHandler = (value: any) => {
if (typeof value !== "object" || Array.isArray(value)) { if (typeof value !== "object" || Array.isArray(value)) {
return false return false
} }
@ -372,6 +382,6 @@ const handlerMap = {
* @param value the value to test * @param value the value to test
* @returns {boolean} whether the value exists or not * @returns {boolean} whether the value exists or not
*/ */
const exists = value => { const exists = <T = any>(value: T | null | undefined): value is T => {
return value != null && value !== "" return value != null && value !== ""
} }

View File

@ -0,0 +1 @@
export * from "./validationRules"

View File

@ -0,0 +1,25 @@
import { FieldType } from "../../documents"
export interface UIFieldValidationRule {
type: `${FieldType}`
constraint: FieldValidationRuleType
value?: string | number | string[]
error: string
}
export type FieldValidationRuleType =
| "required"
| "minLength"
| "maxLength"
| "minValue"
| "maxValue"
| "inclusion"
| "equal"
| "notEqual"
| "regex"
| "notRegex"
| "contains"
| "notContains"
| "json"
| "maxFileSize"
| "maxUploadSize"

View File

@ -5,3 +5,4 @@ export * from "./dataFetch"
export * from "./datasource" export * from "./datasource"
export * from "./common" export * from "./common"
export * from "./BudibaseApp" export * from "./BudibaseApp"
export * from "./fields"