|
@@ -1,7 +1,7 @@
|
|
|
-import { z } from 'zod';
|
|
|
|
|
-import type { DynamicValue, DataModel, LogicExpression } from './types';
|
|
|
|
|
-import { DynamicValueSchema, resolveDynamicValue } from './types';
|
|
|
|
|
-import { LogicExpressionSchema, evaluateLogicExpression } from './visibility';
|
|
|
|
|
|
|
+import { z } from "zod";
|
|
|
|
|
+import type { DynamicValue, DataModel, LogicExpression } from "./types";
|
|
|
|
|
+import { DynamicValueSchema, resolveDynamicValue } from "./types";
|
|
|
|
|
+import { LogicExpressionSchema, evaluateLogicExpression } from "./visibility";
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* Validation check definition
|
|
* Validation check definition
|
|
@@ -22,7 +22,7 @@ export interface ValidationConfig {
|
|
|
/** Array of checks to run */
|
|
/** Array of checks to run */
|
|
|
checks?: ValidationCheck[];
|
|
checks?: ValidationCheck[];
|
|
|
/** When to run validation */
|
|
/** When to run validation */
|
|
|
- validateOn?: 'change' | 'blur' | 'submit';
|
|
|
|
|
|
|
+ validateOn?: "change" | "blur" | "submit";
|
|
|
/** Condition for when validation is enabled */
|
|
/** Condition for when validation is enabled */
|
|
|
enabled?: LogicExpression;
|
|
enabled?: LogicExpression;
|
|
|
}
|
|
}
|
|
@@ -41,7 +41,7 @@ export const ValidationCheckSchema = z.object({
|
|
|
*/
|
|
*/
|
|
|
export const ValidationConfigSchema = z.object({
|
|
export const ValidationConfigSchema = z.object({
|
|
|
checks: z.array(ValidationCheckSchema).optional(),
|
|
checks: z.array(ValidationCheckSchema).optional(),
|
|
|
- validateOn: z.enum(['change', 'blur', 'submit']).optional(),
|
|
|
|
|
|
|
+ validateOn: z.enum(["change", "blur", "submit"]).optional(),
|
|
|
enabled: LogicExpressionSchema.optional(),
|
|
enabled: LogicExpressionSchema.optional(),
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -50,7 +50,7 @@ export const ValidationConfigSchema = z.object({
|
|
|
*/
|
|
*/
|
|
|
export type ValidationFunction = (
|
|
export type ValidationFunction = (
|
|
|
value: unknown,
|
|
value: unknown,
|
|
|
- args?: Record<string, unknown>
|
|
|
|
|
|
|
+ args?: Record<string, unknown>,
|
|
|
) => boolean;
|
|
) => boolean;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -72,7 +72,7 @@ export const builtInValidationFunctions: Record<string, ValidationFunction> = {
|
|
|
*/
|
|
*/
|
|
|
required: (value: unknown) => {
|
|
required: (value: unknown) => {
|
|
|
if (value === null || value === undefined) return false;
|
|
if (value === null || value === undefined) return false;
|
|
|
- if (typeof value === 'string') return value.trim().length > 0;
|
|
|
|
|
|
|
+ if (typeof value === "string") return value.trim().length > 0;
|
|
|
if (Array.isArray(value)) return value.length > 0;
|
|
if (Array.isArray(value)) return value.length > 0;
|
|
|
return true;
|
|
return true;
|
|
|
},
|
|
},
|
|
@@ -81,7 +81,7 @@ export const builtInValidationFunctions: Record<string, ValidationFunction> = {
|
|
|
* Check if value is a valid email address
|
|
* Check if value is a valid email address
|
|
|
*/
|
|
*/
|
|
|
email: (value: unknown) => {
|
|
email: (value: unknown) => {
|
|
|
- if (typeof value !== 'string') return false;
|
|
|
|
|
|
|
+ if (typeof value !== "string") return false;
|
|
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
|
},
|
|
},
|
|
|
|
|
|
|
@@ -89,9 +89,9 @@ export const builtInValidationFunctions: Record<string, ValidationFunction> = {
|
|
|
* Check minimum string length
|
|
* Check minimum string length
|
|
|
*/
|
|
*/
|
|
|
minLength: (value: unknown, args?: Record<string, unknown>) => {
|
|
minLength: (value: unknown, args?: Record<string, unknown>) => {
|
|
|
- if (typeof value !== 'string') return false;
|
|
|
|
|
|
|
+ if (typeof value !== "string") return false;
|
|
|
const min = args?.min;
|
|
const min = args?.min;
|
|
|
- if (typeof min !== 'number') return false;
|
|
|
|
|
|
|
+ if (typeof min !== "number") return false;
|
|
|
return value.length >= min;
|
|
return value.length >= min;
|
|
|
},
|
|
},
|
|
|
|
|
|
|
@@ -99,9 +99,9 @@ export const builtInValidationFunctions: Record<string, ValidationFunction> = {
|
|
|
* Check maximum string length
|
|
* Check maximum string length
|
|
|
*/
|
|
*/
|
|
|
maxLength: (value: unknown, args?: Record<string, unknown>) => {
|
|
maxLength: (value: unknown, args?: Record<string, unknown>) => {
|
|
|
- if (typeof value !== 'string') return false;
|
|
|
|
|
|
|
+ if (typeof value !== "string") return false;
|
|
|
const max = args?.max;
|
|
const max = args?.max;
|
|
|
- if (typeof max !== 'number') return false;
|
|
|
|
|
|
|
+ if (typeof max !== "number") return false;
|
|
|
return value.length <= max;
|
|
return value.length <= max;
|
|
|
},
|
|
},
|
|
|
|
|
|
|
@@ -109,9 +109,9 @@ export const builtInValidationFunctions: Record<string, ValidationFunction> = {
|
|
|
* Check if string matches a regex pattern
|
|
* Check if string matches a regex pattern
|
|
|
*/
|
|
*/
|
|
|
pattern: (value: unknown, args?: Record<string, unknown>) => {
|
|
pattern: (value: unknown, args?: Record<string, unknown>) => {
|
|
|
- if (typeof value !== 'string') return false;
|
|
|
|
|
|
|
+ if (typeof value !== "string") return false;
|
|
|
const pattern = args?.pattern;
|
|
const pattern = args?.pattern;
|
|
|
- if (typeof pattern !== 'string') return false;
|
|
|
|
|
|
|
+ if (typeof pattern !== "string") return false;
|
|
|
try {
|
|
try {
|
|
|
return new RegExp(pattern).test(value);
|
|
return new RegExp(pattern).test(value);
|
|
|
} catch {
|
|
} catch {
|
|
@@ -123,9 +123,9 @@ export const builtInValidationFunctions: Record<string, ValidationFunction> = {
|
|
|
* Check minimum numeric value
|
|
* Check minimum numeric value
|
|
|
*/
|
|
*/
|
|
|
min: (value: unknown, args?: Record<string, unknown>) => {
|
|
min: (value: unknown, args?: Record<string, unknown>) => {
|
|
|
- if (typeof value !== 'number') return false;
|
|
|
|
|
|
|
+ if (typeof value !== "number") return false;
|
|
|
const min = args?.min;
|
|
const min = args?.min;
|
|
|
- if (typeof min !== 'number') return false;
|
|
|
|
|
|
|
+ if (typeof min !== "number") return false;
|
|
|
return value >= min;
|
|
return value >= min;
|
|
|
},
|
|
},
|
|
|
|
|
|
|
@@ -133,9 +133,9 @@ export const builtInValidationFunctions: Record<string, ValidationFunction> = {
|
|
|
* Check maximum numeric value
|
|
* Check maximum numeric value
|
|
|
*/
|
|
*/
|
|
|
max: (value: unknown, args?: Record<string, unknown>) => {
|
|
max: (value: unknown, args?: Record<string, unknown>) => {
|
|
|
- if (typeof value !== 'number') return false;
|
|
|
|
|
|
|
+ if (typeof value !== "number") return false;
|
|
|
const max = args?.max;
|
|
const max = args?.max;
|
|
|
- if (typeof max !== 'number') return false;
|
|
|
|
|
|
|
+ if (typeof max !== "number") return false;
|
|
|
return value <= max;
|
|
return value <= max;
|
|
|
},
|
|
},
|
|
|
|
|
|
|
@@ -143,8 +143,8 @@ export const builtInValidationFunctions: Record<string, ValidationFunction> = {
|
|
|
* Check if value is a number
|
|
* Check if value is a number
|
|
|
*/
|
|
*/
|
|
|
numeric: (value: unknown) => {
|
|
numeric: (value: unknown) => {
|
|
|
- if (typeof value === 'number') return !isNaN(value);
|
|
|
|
|
- if (typeof value === 'string') return !isNaN(parseFloat(value));
|
|
|
|
|
|
|
+ if (typeof value === "number") return !isNaN(value);
|
|
|
|
|
+ if (typeof value === "string") return !isNaN(parseFloat(value));
|
|
|
return false;
|
|
return false;
|
|
|
},
|
|
},
|
|
|
|
|
|
|
@@ -152,7 +152,7 @@ export const builtInValidationFunctions: Record<string, ValidationFunction> = {
|
|
|
* Check if value is a valid URL
|
|
* Check if value is a valid URL
|
|
|
*/
|
|
*/
|
|
|
url: (value: unknown) => {
|
|
url: (value: unknown) => {
|
|
|
- if (typeof value !== 'string') return false;
|
|
|
|
|
|
|
+ if (typeof value !== "string") return false;
|
|
|
try {
|
|
try {
|
|
|
new URL(value);
|
|
new URL(value);
|
|
|
return true;
|
|
return true;
|
|
@@ -205,10 +205,10 @@ export interface ValidationContext {
|
|
|
*/
|
|
*/
|
|
|
export function runValidationCheck(
|
|
export function runValidationCheck(
|
|
|
check: ValidationCheck,
|
|
check: ValidationCheck,
|
|
|
- ctx: ValidationContext
|
|
|
|
|
|
|
+ ctx: ValidationContext,
|
|
|
): ValidationCheckResult {
|
|
): ValidationCheckResult {
|
|
|
const { value, dataModel, customFunctions } = ctx;
|
|
const { value, dataModel, customFunctions } = ctx;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Resolve args
|
|
// Resolve args
|
|
|
const resolvedArgs: Record<string, unknown> = {};
|
|
const resolvedArgs: Record<string, unknown> = {};
|
|
|
if (check.args) {
|
|
if (check.args) {
|
|
@@ -216,10 +216,11 @@ export function runValidationCheck(
|
|
|
resolvedArgs[key] = resolveDynamicValue(argValue, dataModel);
|
|
resolvedArgs[key] = resolveDynamicValue(argValue, dataModel);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Find the validation function
|
|
// Find the validation function
|
|
|
- const fn = builtInValidationFunctions[check.fn] ?? customFunctions?.[check.fn];
|
|
|
|
|
-
|
|
|
|
|
|
|
+ const fn =
|
|
|
|
|
+ builtInValidationFunctions[check.fn] ?? customFunctions?.[check.fn];
|
|
|
|
|
+
|
|
|
if (!fn) {
|
|
if (!fn) {
|
|
|
console.warn(`Unknown validation function: ${check.fn}`);
|
|
console.warn(`Unknown validation function: ${check.fn}`);
|
|
|
return {
|
|
return {
|
|
@@ -228,9 +229,9 @@ export function runValidationCheck(
|
|
|
message: check.message,
|
|
message: check.message,
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
const valid = fn(value, resolvedArgs);
|
|
const valid = fn(value, resolvedArgs);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
return {
|
|
return {
|
|
|
fn: check.fn,
|
|
fn: check.fn,
|
|
|
valid,
|
|
valid,
|
|
@@ -243,11 +244,11 @@ export function runValidationCheck(
|
|
|
*/
|
|
*/
|
|
|
export function runValidation(
|
|
export function runValidation(
|
|
|
config: ValidationConfig,
|
|
config: ValidationConfig,
|
|
|
- ctx: ValidationContext & { authState?: { isSignedIn: boolean } }
|
|
|
|
|
|
|
+ ctx: ValidationContext & { authState?: { isSignedIn: boolean } },
|
|
|
): ValidationResult {
|
|
): ValidationResult {
|
|
|
const checks: ValidationCheckResult[] = [];
|
|
const checks: ValidationCheckResult[] = [];
|
|
|
const errors: string[] = [];
|
|
const errors: string[] = [];
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Check if validation is enabled
|
|
// Check if validation is enabled
|
|
|
if (config.enabled) {
|
|
if (config.enabled) {
|
|
|
const enabled = evaluateLogicExpression(config.enabled, {
|
|
const enabled = evaluateLogicExpression(config.enabled, {
|
|
@@ -258,7 +259,7 @@ export function runValidation(
|
|
|
return { valid: true, errors: [], checks: [] };
|
|
return { valid: true, errors: [], checks: [] };
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Run each check
|
|
// Run each check
|
|
|
if (config.checks) {
|
|
if (config.checks) {
|
|
|
for (const check of config.checks) {
|
|
for (const check of config.checks) {
|
|
@@ -269,7 +270,7 @@ export function runValidation(
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
return {
|
|
return {
|
|
|
valid: errors.length === 0,
|
|
valid: errors.length === 0,
|
|
|
errors,
|
|
errors,
|
|
@@ -281,53 +282,56 @@ export function runValidation(
|
|
|
* Helper to create validation checks
|
|
* Helper to create validation checks
|
|
|
*/
|
|
*/
|
|
|
export const check = {
|
|
export const check = {
|
|
|
- required: (message = 'This field is required'): ValidationCheck => ({
|
|
|
|
|
- fn: 'required',
|
|
|
|
|
|
|
+ required: (message = "This field is required"): ValidationCheck => ({
|
|
|
|
|
+ fn: "required",
|
|
|
message,
|
|
message,
|
|
|
}),
|
|
}),
|
|
|
-
|
|
|
|
|
- email: (message = 'Invalid email address'): ValidationCheck => ({
|
|
|
|
|
- fn: 'email',
|
|
|
|
|
|
|
+
|
|
|
|
|
+ email: (message = "Invalid email address"): ValidationCheck => ({
|
|
|
|
|
+ fn: "email",
|
|
|
message,
|
|
message,
|
|
|
}),
|
|
}),
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
minLength: (min: number, message?: string): ValidationCheck => ({
|
|
minLength: (min: number, message?: string): ValidationCheck => ({
|
|
|
- fn: 'minLength',
|
|
|
|
|
|
|
+ fn: "minLength",
|
|
|
args: { min },
|
|
args: { min },
|
|
|
message: message ?? `Must be at least ${min} characters`,
|
|
message: message ?? `Must be at least ${min} characters`,
|
|
|
}),
|
|
}),
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
maxLength: (max: number, message?: string): ValidationCheck => ({
|
|
maxLength: (max: number, message?: string): ValidationCheck => ({
|
|
|
- fn: 'maxLength',
|
|
|
|
|
|
|
+ fn: "maxLength",
|
|
|
args: { max },
|
|
args: { max },
|
|
|
message: message ?? `Must be at most ${max} characters`,
|
|
message: message ?? `Must be at most ${max} characters`,
|
|
|
}),
|
|
}),
|
|
|
-
|
|
|
|
|
- pattern: (pattern: string, message = 'Invalid format'): ValidationCheck => ({
|
|
|
|
|
- fn: 'pattern',
|
|
|
|
|
|
|
+
|
|
|
|
|
+ pattern: (pattern: string, message = "Invalid format"): ValidationCheck => ({
|
|
|
|
|
+ fn: "pattern",
|
|
|
args: { pattern },
|
|
args: { pattern },
|
|
|
message,
|
|
message,
|
|
|
}),
|
|
}),
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
min: (min: number, message?: string): ValidationCheck => ({
|
|
min: (min: number, message?: string): ValidationCheck => ({
|
|
|
- fn: 'min',
|
|
|
|
|
|
|
+ fn: "min",
|
|
|
args: { min },
|
|
args: { min },
|
|
|
message: message ?? `Must be at least ${min}`,
|
|
message: message ?? `Must be at least ${min}`,
|
|
|
}),
|
|
}),
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
max: (max: number, message?: string): ValidationCheck => ({
|
|
max: (max: number, message?: string): ValidationCheck => ({
|
|
|
- fn: 'max',
|
|
|
|
|
|
|
+ fn: "max",
|
|
|
args: { max },
|
|
args: { max },
|
|
|
message: message ?? `Must be at most ${max}`,
|
|
message: message ?? `Must be at most ${max}`,
|
|
|
}),
|
|
}),
|
|
|
-
|
|
|
|
|
- url: (message = 'Invalid URL'): ValidationCheck => ({
|
|
|
|
|
- fn: 'url',
|
|
|
|
|
|
|
+
|
|
|
|
|
+ url: (message = "Invalid URL"): ValidationCheck => ({
|
|
|
|
|
+ fn: "url",
|
|
|
message,
|
|
message,
|
|
|
}),
|
|
}),
|
|
|
-
|
|
|
|
|
- matches: (otherPath: string, message = 'Fields must match'): ValidationCheck => ({
|
|
|
|
|
- fn: 'matches',
|
|
|
|
|
|
|
+
|
|
|
|
|
+ matches: (
|
|
|
|
|
+ otherPath: string,
|
|
|
|
|
+ message = "Fields must match",
|
|
|
|
|
+ ): ValidationCheck => ({
|
|
|
|
|
+ fn: "matches",
|
|
|
args: { other: { path: otherPath } },
|
|
args: { other: { path: otherPath } },
|
|
|
message,
|
|
message,
|
|
|
}),
|
|
}),
|