Add support for validating relationships

This commit is contained in:
Andrew Kingston 2021-08-11 14:34:22 +01:00
parent ce29f27c89
commit b79b75ad78
2 changed files with 87 additions and 70 deletions

View File

@ -25,6 +25,10 @@
label: "Required", label: "Required",
value: "required", value: "required",
}, },
MinLength: {
label: "Min length",
value: "minLength",
},
MaxLength: { MaxLength: {
label: "Max length", label: "Max length",
value: "maxLength", value: "maxLength",
@ -93,10 +97,10 @@
["attachment"]: [Constraints.Required], ["attachment"]: [Constraints.Required],
["link"]: [ ["link"]: [
Constraints.Required, Constraints.Required,
Constraints.Equal,
Constraints.NotEqual,
Constraints.Contains, Constraints.Contains,
Constraints.NotContains, Constraints.NotContains,
Constraints.MinLength,
Constraints.MaxLength,
], ],
} }
@ -255,7 +259,9 @@
bind:value={rule.valueType} bind:value={rule.valueType}
options={["Binding", "Value"]} options={["Binding", "Value"]}
/> />
{#if rule.valueType === "Binding"} {#if rule.valueType === "Binding"}
<!-- Bindings always get a bindable input -->
<DrawerBindableInput <DrawerBindableInput
placeholder="Constraint value" placeholder="Constraint value"
value={rule.value} value={rule.value}
@ -263,29 +269,38 @@
disabled={rule.constraint === "required"} disabled={rule.constraint === "required"}
on:change={e => (rule.value = e.detail)} on:change={e => (rule.value = e.detail)}
/> />
{:else if ["string", "number", "options", "longform"].includes(rule.type)} {:else if ["maxLength", "minLength", "regex", "notRegex", "contains", "notContains"].includes(rule.constraint)}
<!-- Certain constraints always need string values-->
<Input <Input
disabled={rule.constraint === "required"}
bind:value={rule.value} bind:value={rule.value}
placeholder="Constraint value" placeholder="Constraint value"
/> />
{:else if fieldType === "boolean"}
<Select
disabled={rule.constraint === "required"}
options={[
{ label: "True", value: "true" },
{ label: "False", value: "false" },
]}
bind:value={rule.value}
/>
{:else if fieldType === "datetime"}
<DatePicker
enableTime={false}
disabled={rule.constraint === "required"}
bind:value={rule.value}
/>
{:else} {:else}
<DrawerBindableInput disabled /> <!-- Otherwise we render a component based on the type -->
{#if ["string", "number", "options", "longform"].includes(rule.type) || ["contains"]}
<Input
disabled={rule.constraint === "required"}
bind:value={rule.value}
placeholder="Constraint value"
/>
{:else if fieldType === "boolean"}
<Select
disabled={rule.constraint === "required"}
options={[
{ label: "True", value: "true" },
{ label: "False", value: "false" },
]}
bind:value={rule.value}
/>
{:else if fieldType === "datetime"}
<DatePicker
enableTime={false}
disabled={rule.constraint === "required"}
bind:value={rule.value}
/>
{:else}
<DrawerBindableInput disabled />
{/if}
{/if} {/if}
<DrawerBindableInput <DrawerBindableInput
placeholder="Error message" placeholder="Error message"

View File

@ -38,7 +38,7 @@ export const createValidatorFromConstraints = (
type: "string", type: "string",
constraint: "length", constraint: "length",
value: length, value: length,
error: val => `Maximum length is ${val}`, error: `Maximum length is ${length}`,
}) })
} }
@ -49,7 +49,7 @@ export const createValidatorFromConstraints = (
type: "number", type: "number",
constraint: "minValue", constraint: "minValue",
value: min, value: min,
error: val => `Minimum value is ${val}`, error: `Minimum value is ${min}`,
}) })
} }
if (exists(schemaConstraints.numericality?.lessThanOrEqualTo)) { if (exists(schemaConstraints.numericality?.lessThanOrEqualTo)) {
@ -58,13 +58,13 @@ export const createValidatorFromConstraints = (
type: "number", type: "number",
constraint: "maxValue", constraint: "maxValue",
value: max, value: max,
error: val => `Maximum value is ${val}`, error: `Maximum value is ${max}`,
}) })
} }
// Inclusion constraint // Inclusion constraint
if (exists(schemaConstraints.inclusion)) { if (exists(schemaConstraints.inclusion)) {
const options = schemaConstraints.inclusion const options = schemaConstraints.inclusion || []
rules.push({ rules.push({
type: "string", type: "string",
constraint: "inclusion", constraint: "inclusion",
@ -76,26 +76,22 @@ export const createValidatorFromConstraints = (
// Date constraint // Date constraint
if (exists(schemaConstraints.datetime?.earliest)) { if (exists(schemaConstraints.datetime?.earliest)) {
const limit = schemaConstraints.datetime.earliest const limit = schemaConstraints.datetime.earliest
const limitString = flatpickr.formatDate(new Date(limit), "F j Y, H:i")
rules.push({ rules.push({
type: "datetime", type: "datetime",
constraint: "minValue", constraint: "minValue",
value: limit, value: limit,
error: val => { error: `Earliest date is ${limitString}`,
const date = flatpickr.formatDate(new Date(val), "F j Y, H:i")
return `Earliest date is ${date}`
},
}) })
} }
if (exists(schemaConstraints.datetime?.latest)) { if (exists(schemaConstraints.datetime?.latest)) {
const limit = schemaConstraints.datetime.latest const limit = schemaConstraints.datetime.latest
const limitString = flatpickr.formatDate(new Date(limit), "F j Y, H:i")
rules.push({ rules.push({
type: "datetime", type: "datetime",
constraint: "maxValue", constraint: "maxValue",
value: limit, value: limit,
error: val => { error: `Latest date is ${limitString}`,
const date = flatpickr.formatDate(new Date(val), "F j Y, H:i")
return `Latest date is ${date}`
},
}) })
} }
} }
@ -133,22 +129,12 @@ const evaluateRule = (rule, value) => {
return null return null
} }
// Coerce values into correct types // Coerce input value into correct type
const parsedValue = parseType(value, rule.type) value = parseType(value, rule.type)
const parsedRuleValue = parseType(rule.value, rule.type)
// Evaluate the rule // Evaluate the rule
const pass = handler(parsedValue, parsedRuleValue) const pass = handler(value, rule)
if (pass) { return pass ? null : rule.error || "Error"
return null
}
// Return an error if the validation failed
let error = rule.error
if (typeof error === "function") {
error = rule.error(parsedRuleValue)
}
return error || "Error"
} }
/** /**
@ -225,53 +211,66 @@ const requiredHandler = value => {
return value != null return value != null
} }
// Evaluates a min length constraint
const minLengthHandler = (value, rule) => {
const limit = parseType(rule.value, "number")
return value && value.length >= limit
}
// Evaluates a max length constraint // Evaluates a max length constraint
const maxLengthHandler = (value, maxLength) => { const maxLengthHandler = (value, rule) => {
if (value == null) { const limit = parseType(rule.value, "number")
return true return value == null || value.length <= limit
}
return value.length <= maxLength
} }
// Evaluates a min value constraint // Evaluates a min value constraint
const minValueHandler = (value, minValue) => { const minValueHandler = (value, rule) => {
if (value == null) { const limit = parseType(rule.value, "number")
return true return value && value >= limit
}
return value >= minValue
} }
// Evaluates a max value constraint // Evaluates a max value constraint
const maxValueHandler = (value, maxValue) => { const maxValueHandler = (value, rule) => {
if (value == null) { const limit = parseType(rule.value, "number")
return true return value == null || value <= limit
}
return value <= maxValue
} }
// Evaluates an inclusion constraint // Evaluates an inclusion constraint
const inclusionHandler = (value, options) => { const inclusionHandler = (value, rule) => {
return options.includes(value) return value == null || rule.value.includes(value)
} }
// Evaluates an equal constraint // Evaluates an equal constraint
const equalHandler = (value, referenceValue) => { const equalHandler = (value, rule) => {
return value === referenceValue const ruleValue = parseType(rule.value, rule.type)
return value === ruleValue
} }
// Evaluates a not equal constraint // Evaluates a not equal constraint
const notEqualHandler = (value, referenceValue) => { const notEqualHandler = (value, rule) => {
return !equalHandler(value, referenceValue) return !equalHandler(value, rule)
} }
// Evaluates a regex constraint // Evaluates a regex constraint
const regexHandler = (value, regex) => { const regexHandler = (value, rule) => {
const regex = parseType(rule.value, "string")
return new RegExp(regex).test(value) return new RegExp(regex).test(value)
} }
// Evaluates a not regex constraint // Evaluates a not regex constraint
const notRegexHandler = (value, regex) => { const notRegexHandler = (value, rule) => {
return !regexHandler(value, regex) return !regexHandler(value, rule)
}
// Evaluates a contains constraint
const containsHandler = (value, rule) => {
const expectedValue = parseType(rule.value, "string")
return value && value.includes(expectedValue)
}
// Evaluates a not contains constraint
const notContainsHandler = (value, rule) => {
return !containsHandler(value, rule)
} }
/** /**
@ -279,6 +278,7 @@ const notRegexHandler = (value, regex) => {
*/ */
const handlerMap = { const handlerMap = {
required: requiredHandler, required: requiredHandler,
minLength: minLengthHandler,
maxLength: maxLengthHandler, maxLength: maxLengthHandler,
minValue: minValueHandler, minValue: minValueHandler,
maxValue: maxValueHandler, maxValue: maxValueHandler,
@ -287,6 +287,8 @@ const handlerMap = {
notEqual: notEqualHandler, notEqual: notEqualHandler,
regex: regexHandler, regex: regexHandler,
notRegex: notRegexHandler, notRegex: notRegexHandler,
contains: containsHandler,
notContains: notContainsHandler,
} }
/** /**