@tuentyfaiv/svelte-form
Version:
A form library for Svelte. It is built on top of Svelte and Typescript. Inspired by Formik and React Hook Form.
345 lines (344 loc) • 15.4 kB
JavaScript
import { SchemaError, SchemaErrorList } from "./errors.js";
import { Adapter } from "../typing/stores/form.js";
const fieldsSchema = {
string: "",
number: 0,
boolean: false,
date: new Date(),
file: null,
};
const primitives = ["string", "number", "boolean", "date", "file"];
export function bridge(schema) {
function resolveSchema(field, required = false) {
if (required) {
return fieldsSchema[field instanceof RegExp ? "string" : field];
}
return null;
}
function isFieldProps(three) {
const hasFieldProps = (Object.hasOwn(three, "required") && typeof three.required === "boolean")
|| (Object.hasOwn(three, "min") && typeof three.min === "number")
|| (Object.hasOwn(three, "max") && typeof three.max === "number");
return Object.hasOwn(three, "type")
&& ((typeof three.type === "string" && three.type !== "array") || three.type instanceof RegExp)
&& (primitives.includes(three.type instanceof RegExp ? "string" : three.type)
|| hasFieldProps);
}
function isArraySchema(three) {
return Object.hasOwn(three, "type") && three.type === "array" && Object.hasOwn(three, "item");
}
function resolveFieldProps({ type, required }) {
return resolveSchema(type, required);
}
function resolveArray({ item, required }) {
if (typeof item === "string" || item instanceof RegExp) {
return required ? [] : null;
}
if (typeof item === "object" && !(item instanceof RegExp) && isArraySchema(item)) {
return required ? [resolveArray(item)] : null;
}
if (typeof item === "object" && !(item instanceof RegExp) && isFieldProps(item)) {
return required ? [] : null;
}
return required ? [] : null;
}
function resolveDeep(three) {
if (typeof three === "string" || three instanceof RegExp) {
return resolveSchema(three);
}
if (isFieldProps(three)) {
return resolveFieldProps(three);
}
if (isArraySchema(three)) {
return resolveArray(three);
}
const resolveSelf = Object.keys(three).reduce((acc, key) => ({
...acc,
[key]: resolveDeep(three[key]),
}), {});
return resolveSelf;
}
function typeError({ type, ...config }) {
return new SchemaError({
...config,
message: `The value is not a ${type}.`,
reason: `The type is a ${type}, but the value is ${config.value}.`,
});
}
function invalidError(config) {
return new SchemaError({
...config,
message: "The type is invalid.",
reason: "The type is invalid, check the schema.",
});
}
function requiredError(config) {
return new SchemaError({
...config,
message: "The field is required.",
reason: "The field is required, but the value is empty.",
});
}
function rangeError({ label, range, ...config }) {
const compare = label === "min" ? "less" : "more";
let legend = typeof config.value === "string" ? " characters" : "";
let rangeLegend = range;
if (config.value instanceof Date) {
legend = " date";
}
if (config.value instanceof File || config.value instanceof Blob) {
legend = " bytes";
}
if (range instanceof Date) {
rangeLegend = ` ${range.toISOString()}`;
}
return new SchemaError({
...config,
message: `The value is ${compare} than ${rangeLegend}${legend}.`,
reason: `The value is ${compare} than ${rangeLegend}${legend}. Check the ${label} property in the schema.`,
});
}
class InternalAdapter extends Adapter {
#schema;
constructor() {
super();
this.#schema = schema;
Object.freeze(this);
}
#validateSchema = (fieldSchema, field, value) => {
const correctSchema = fieldSchema instanceof RegExp
|| fieldSchema === "string"
|| fieldSchema === "number"
|| fieldSchema === "boolean"
|| fieldSchema === "date"
|| fieldSchema === "file";
const error = { field: String(field), schema: fieldSchema, value };
if (correctSchema) {
if (typeof value === "string") {
if (fieldSchema instanceof RegExp) {
if (fieldSchema.test(value))
return value;
return new SchemaError({
...error,
schema: fieldSchema.source,
message: "The value is not match with the pattern.",
reason: `Value: "${value}" is not match with the pattern: ${fieldSchema.source}`,
});
}
return value;
}
const correct = value === null
|| typeof value === "undefined"
|| typeof value === "string"
|| typeof value === "number"
|| typeof value === "boolean"
|| value instanceof Date
|| value instanceof File
|| value instanceof Blob;
if (correct)
return value;
const dateType = fieldSchema === "date" ? "Date" : fieldSchema;
const fileType = dateType === "file" ? "File | Blob" : dateType;
const errorType = fileType instanceof RegExp ? "RegExp" : fileType;
return typeError({ ...error, type: errorType });
}
return invalidError(error);
};
#validateFieldPropsSchema = (fieldSchema, field, value) => {
const { type, required, min, max } = fieldSchema;
const validated = this.#validateSchema(type, field, value);
const isEmpty = typeof validated === "undefined" || validated === null;
const correctSchema = type instanceof RegExp
|| type === "string"
|| type === "number"
|| type === "boolean"
|| type === "date"
|| type === "file";
const invalid = { field: String(field), schema: fieldSchema, value };
if (correctSchema) {
const error = { ...invalid, value: validated };
const errorMin = { ...error, label: "min", range: min, value: validated };
const errorMax = { ...error, label: "max", range: max, value: validated };
if (required && ((typeof validated === "string" && validated.length === 0) || isEmpty)) {
return requiredError(error);
}
if (typeof validated === "string") {
if (min && validated.length < min) {
return rangeError(errorMin);
}
if (max && validated.length > max) {
return rangeError(errorMax);
}
}
if (typeof validated === "number") {
if (min && validated < min) {
return rangeError(errorMin);
}
if (max && validated > max) {
return rangeError(errorMax);
}
}
if (validated instanceof Date) {
if (min && validated.getTime() < min) {
return rangeError({ ...errorMin, range: new Date(min) });
}
if (max && validated.getTime() > max) {
return rangeError({ ...errorMax, range: new Date(max) });
}
}
if ((validated instanceof File || validated instanceof Blob)) {
if (min && validated.size < min) {
return rangeError(errorMin);
}
if (max && validated.size > max) {
return rangeError(errorMax);
}
}
return validated;
}
return invalidError(invalid);
};
#validateArraySchema = (fieldSchema, field, value) => {
const { item, required, min, max } = fieldSchema;
const isEmpty = typeof value === "undefined" || value === null || (Array.isArray(value) && value.length === 0);
const error = { field: String(field), schema: fieldSchema, value };
if (!Array.isArray(value) && typeof value !== "undefined" && value !== null) {
return invalidError(error);
}
if (required && isEmpty) {
return requiredError(error);
}
if (value && min && value.length < min) {
return rangeError({ ...error, label: "min", range: min, value });
}
if (value && max && value.length > max) {
return rangeError({ ...error, label: "max", range: max, value });
}
if ((typeof item === "string" || item instanceof RegExp)) {
const incorrect = value?.find((itemValue) => {
const validated = this.#validateSchema(item, field, itemValue);
return validated instanceof SchemaError;
});
if (incorrect) {
const isRegExp = item instanceof RegExp ? "string" : item;
const isFile = isRegExp === "file" ? "File | Blob" : isRegExp;
const isDate = isFile === "date" ? "Date" : isFile;
return typeError({ ...error, type: `${isDate}[]`, value: incorrect });
}
return value;
}
if (typeof item === "object" && !(item instanceof RegExp) && isArraySchema(item)) {
const incorrect = value?.find((subArray) => {
const validated = this.#validateArraySchema(item, field, subArray);
return validated instanceof SchemaError;
});
if (incorrect) {
return typeError({ ...error, type: "array", value: incorrect });
}
return value;
}
if (typeof item === "object" && !(item instanceof RegExp) && isFieldProps(item)) {
const incorrect = value?.find((itemValue) => {
const validated = this.#validateFieldPropsSchema(item, field, itemValue);
return validated instanceof SchemaError;
});
if (incorrect) {
return typeError({ ...error, type: "array", value: incorrect });
}
return value;
}
if (typeof item === "object" && !(item instanceof RegExp)) {
const incorrect = value?.find((itemValue) => {
const validated = this.#validateDeepSchema(item, field, itemValue);
return validated instanceof SchemaError;
});
if (incorrect) {
return typeError({ ...error, type: "array", value: incorrect });
}
return value;
}
return invalidError(error);
};
#validateDeepSchema = (fieldSchema, field, value) => {
if (typeof fieldSchema === "string" || fieldSchema instanceof RegExp) {
return this.#validateSchema(fieldSchema, field, value);
}
if (isArraySchema(fieldSchema)) {
return this.#validateArraySchema(fieldSchema, field, value);
}
if (isFieldProps(fieldSchema)) {
return this.#validateFieldPropsSchema(fieldSchema, field, value);
}
const validateSelf = Object.keys(fieldSchema).reduce((acc, key) => ({
...acc,
[key]: this.#validateDeepSchema(fieldSchema[key], key, value[key]),
}), {});
if (Object.values(validateSelf).some((item) => (item instanceof SchemaError || item instanceof SchemaErrorList))) {
const errors = Object.values(validateSelf)
.filter((item) => (item instanceof SchemaError || item instanceof SchemaErrorList))
.flatMap((item) => (item instanceof SchemaErrorList ? item.errors : item));
return new SchemaErrorList(errors);
}
return validateSelf;
};
#setError = async (field, errors, error) => {
let message = null;
if (error instanceof SchemaError) {
message = error.reason;
}
if (error instanceof SchemaErrorList) {
message = error.errors.reduce((acc, current) => (`${acc}${current.reason}. `), "");
}
await errors.update((prev) => ({ ...prev, [field]: message }));
};
initial = () => {
const start = Object.entries(this.#schema).reduce((acc, [key, three]) => ({
fields: {
...acc.fields,
[key]: resolveDeep(three),
},
errors: {
...acc.errors,
[key]: null,
},
}), {
fields: {},
errors: {},
});
return start;
};
validate = async (data) => {
if (typeof data === "object" && !Array.isArray(data) && data !== null) {
const guard = Object.keys(data).map((key) => (this.#validateDeepSchema(this.#schema[key], key, data[key])));
if (guard.some((item) => item instanceof SchemaError || item instanceof SchemaErrorList)) {
const errors = guard
.filter((item) => item instanceof SchemaError || item instanceof SchemaErrorList)
.flatMap((item) => (item instanceof SchemaErrorList ? item.errors : item));
throw new SchemaErrorList(errors);
}
}
};
field = async (field, value, errors) => {
try {
const guard = await this.#validateDeepSchema(this.#schema[field], field, value);
if (guard instanceof SchemaError || guard instanceof SchemaErrorList)
throw guard;
await this.#setError(field, errors);
}
catch (error) {
await this.#setError(field, errors, error);
}
};
errors = async (error, errors, handle) => {
await handle?.(error);
if (error instanceof SchemaErrorList) {
const newErrors = error.errors.reduce((acc, err) => ({
...acc,
[err.field]: err.reason,
}), {});
await errors.update((prev) => ({ ...prev, ...newErrors }));
}
};
}
return new InternalAdapter();
}