el-form-core
Version:
Framework-agnostic form validation engine - schema-first validation core for TypeScript applications. Supports Zod, Yup, Valibot and custom validators.
648 lines (642 loc) • 19.5 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
// src/validation.ts
function parseZodErrors(error) {
const errors = {};
error.errors.forEach((err) => {
const path = err.path.join(".");
errors[path] = err.message;
});
return errors;
}
function flattenObject(obj, prefix = "") {
const flattened = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const newKey = prefix ? `${prefix}.${key}` : key;
if (typeof obj[key] === "object" && obj[key] !== null && !Array.isArray(obj[key])) {
Object.assign(flattened, flattenObject(obj[key], newKey));
} else {
flattened[newKey] = obj[key];
}
}
}
return flattened;
}
// src/utils.ts
function setNestedValue(obj, path, value) {
const result = { ...obj };
const normalizedPath = path.replace(/\[(\d+)\]/g, ".$1");
const keys = normalizedPath.split(".").filter((key) => key !== "");
let current = result;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
const nextKey = keys[i + 1];
if (!isNaN(Number(key))) {
if (!Array.isArray(current)) {
current = [];
}
if (!current[Number(key)]) {
current[Number(key)] = !isNaN(Number(nextKey)) ? [] : {};
} else {
current[Number(key)] = Array.isArray(current[Number(key)]) ? [...current[Number(key)]] : { ...current[Number(key)] };
}
current = current[Number(key)];
} else {
if (!isNaN(Number(nextKey))) {
if (!Array.isArray(current[key])) {
current[key] = [];
} else {
current[key] = [...current[key]];
}
} else {
if (typeof current[key] !== "object" || current[key] === null) {
current[key] = {};
} else {
current[key] = { ...current[key] };
}
}
current = current[key];
}
}
const finalKey = keys[keys.length - 1];
if (!isNaN(Number(finalKey))) {
current[Number(finalKey)] = value;
} else {
current[finalKey] = value;
}
return result;
}
function getNestedValue(obj, path) {
const normalizedPath = path.replace(/\[(\d+)\]/g, ".$1");
const keys = normalizedPath.split(".").filter((key) => key !== "");
return keys.reduce((current, key) => {
if (current === null || current === void 0) return void 0;
if (!isNaN(Number(key))) {
return Array.isArray(current) ? current[Number(key)] : void 0;
}
return current[key];
}, obj);
}
function removeArrayItem(obj, path, index) {
const result = { ...obj };
const normalizedPath = path.replace(/\[(\d+)\]/g, ".$1");
const keys = normalizedPath.split(".").filter((key) => key !== "");
let current = result;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!isNaN(Number(key))) {
if (Array.isArray(current)) {
current[Number(key)] = Array.isArray(current[Number(key)]) ? [...current[Number(key)]] : { ...current[Number(key)] };
current = current[Number(key)];
}
} else {
if (typeof current[key] === "object" && current[key] !== null) {
current[key] = Array.isArray(current[key]) ? [...current[key]] : { ...current[key] };
}
current = current[key];
}
}
const arrayKey = keys[keys.length - 1];
if (!isNaN(Number(arrayKey))) {
if (Array.isArray(current)) {
current.splice(Number(arrayKey), 1);
}
} else {
if (Array.isArray(current[arrayKey])) {
current[arrayKey] = [...current[arrayKey]];
current[arrayKey].splice(index, 1);
}
}
return result;
}
// src/validators/adapters.ts
function isZodSchema(schema) {
return schema && typeof schema === "object" && typeof schema.parse === "function" && typeof schema.safeParse === "function" && schema._def !== void 0;
}
function isYupSchema(schema) {
return schema && typeof schema === "object" && typeof schema.validate === "function" && typeof schema.validateSync === "function" && schema.__isYupSchema__ === true;
}
function isValibotSchema(schema) {
return schema && typeof schema === "object" && schema._types !== void 0 && schema.kind !== void 0;
}
function isArkTypeSchema(schema) {
return schema && typeof schema === "object" && typeof schema.assert === "function" && schema.kind !== void 0;
}
function isEffectSchema(schema) {
return schema && typeof schema === "object" && typeof schema.validate === "function" && schema._schema !== void 0;
}
function isValidatorFunction(validator) {
return typeof validator === "function";
}
function isStandardSchema(schema) {
return schema && typeof schema === "object" && typeof schema["~standard"] === "object";
}
var SchemaAdapter = class {
static validate(schema, value, context) {
try {
if (isValidatorFunction(schema)) {
return this.validateFunction(schema, value, context);
}
if (isStandardSchema(schema)) {
return this.validateStandardSchema(schema, value);
}
if (isZodSchema(schema)) {
return this.validateZod(schema, value);
}
if (isYupSchema(schema)) {
return this.validateYup(schema, value);
}
if (isValibotSchema(schema)) {
return this.validateValibot(schema, value);
}
if (isArkTypeSchema(schema)) {
return this.validateArkType(schema, value);
}
if (isEffectSchema(schema)) {
return this.validateEffect(schema, value);
}
throw new Error("Unsupported schema type");
} catch (error) {
return {
isValid: false,
errors: {
[context?.fieldName || "form"]: error instanceof Error ? error.message : "Validation failed"
}
};
}
}
static async validateAsync(schema, value, context) {
try {
if (isValidatorFunction(schema)) {
return await this.validateAsyncFunction(schema, value, context);
}
return this.validate(schema, value, context);
} catch (error) {
return {
isValid: false,
errors: {
[context?.fieldName || "form"]: error instanceof Error ? error.message : "Validation failed"
}
};
}
}
static validateFunction(validator, value, context) {
const result = validator(context || { value, values: {}, fieldName: "" });
if (result === void 0 || result === null) {
return { isValid: true, errors: {} };
}
if (typeof result === "string") {
return {
isValid: false,
errors: { [context?.fieldName || "form"]: result }
};
}
if (typeof result === "object" && result.fields) {
return {
isValid: false,
errors: result.fields
};
}
return {
isValid: false,
errors: { [context?.fieldName || "form"]: String(result) }
};
}
static async validateAsyncFunction(validator, value, context) {
const result = await validator(
context || { value, values: {}, fieldName: "" }
);
return this.validateFunction(() => result, value, context);
}
static validateStandardSchema(schema, value) {
const result = schema["~standard"].validate(value);
if (result.issues && result.issues.length > 0) {
const errors = {};
result.issues.forEach((issue) => {
const path = issue.path?.join(".") || "form";
errors[path] = issue.message;
});
return { isValid: false, errors };
}
return { isValid: true, errors: {} };
}
static validateZod(schema, value) {
const result = schema.safeParse(value);
if (!result.success) {
const errors = {};
result.error.errors.forEach((err) => {
const path = err.path.join(".") || "form";
errors[path] = err.message;
});
return { isValid: false, errors };
}
return { isValid: true, errors: {} };
}
static validateYup(schema, value) {
try {
schema.validateSync(value, { abortEarly: false });
return { isValid: true, errors: {} };
} catch (error) {
const errors = {};
if (error.inner && error.inner.length > 0) {
error.inner.forEach((err) => {
errors[err.path || "form"] = err.message;
});
} else {
errors[error.path || "form"] = error.message;
}
return { isValid: false, errors };
}
}
static validateValibot(schema, value) {
try {
if (schema.parse) {
schema.parse(value);
}
return { isValid: true, errors: {} };
} catch (error) {
return {
isValid: false,
errors: { form: error.message || "Validation failed" }
};
}
}
static validateArkType(schema, value) {
try {
schema.assert(value);
return { isValid: true, errors: {} };
} catch (error) {
return {
isValid: false,
errors: { form: error.message || "Validation failed" }
};
}
}
static validateEffect(schema, value) {
try {
const result = schema.validate(value);
if (result._tag === "Success") {
return { isValid: true, errors: {} };
} else {
return {
isValid: false,
errors: { form: "Validation failed" }
};
}
} catch (error) {
return {
isValid: false,
errors: { form: error.message || "Validation failed" }
};
}
}
};
// src/validators/engine.ts
var ValidationEngine = class {
constructor() {
__publicField(this, "debounceTimers", /* @__PURE__ */ new Map());
}
/**
* Validates a single field using the provided validator configuration
*/
async validateField(fieldName, value, values, config, event) {
const context = {
value,
values,
fieldName
};
const validatorKey = event.isAsync ? `${event.type}Async` : event.type;
const validator = config[validatorKey];
if (!validator) {
return { isValid: true, errors: {} };
}
if (event.isAsync) {
return this.validateAsync(validator, context, config, event);
} else {
return SchemaAdapter.validate(validator, value, context);
}
}
/**
* Validates the entire form using form-level validators
*/
async validateForm(values, config, event) {
const validatorKey = event.isAsync ? `${event.type}Async` : event.type;
const validator = config[validatorKey];
if (!validator) {
return { isValid: true, errors: {} };
}
const context = { value: values };
let result;
if (event.isAsync) {
result = await this.validateFormAsync(validator, context, config, event);
} else {
result = this.validateFormSync(validator, context);
}
return result;
}
/**
* Validates multiple fields at once
*/
async validateFields(fieldNames, values, fieldConfigs, event) {
const results = await Promise.all(
fieldNames.map((fieldName) => {
const config = fieldConfigs[fieldName];
if (!config) return { isValid: true, errors: {} };
return this.validateField(
fieldName,
values[fieldName],
values,
config,
event
);
})
);
const combinedErrors = {};
let isValid = true;
results.forEach((result) => {
if (!result.isValid) {
isValid = false;
Object.assign(combinedErrors, result.errors);
}
});
return { isValid, errors: combinedErrors };
}
/**
* Clears debounce timer for a specific field
*/
clearDebounce(fieldName, eventType) {
const key = `${fieldName}-${eventType}`;
const timer = this.debounceTimers.get(key);
if (timer) {
clearTimeout(timer);
this.debounceTimers.delete(key);
}
}
/**
* Clears all debounce timers
*/
clearAllDebounce() {
this.debounceTimers.forEach((timer) => clearTimeout(timer));
this.debounceTimers.clear();
}
async validateAsync(validator, context, config, event) {
const specificDebounceKey = `${event.type}AsyncDebounceMs`;
const debounceMs = config[specificDebounceKey] || config.asyncDebounceMs || 0;
if (debounceMs > 0) {
return this.validateWithDebounce(
validator,
context,
config,
event,
debounceMs
);
}
return SchemaAdapter.validateAsync(validator, context.value, context);
}
async validateWithDebounce(validator, context, _config, event, debounceMs) {
const key = `${context.fieldName}-${event.type}`;
this.clearDebounce(context.fieldName, event.type);
return new Promise((resolve) => {
const timer = setTimeout(async () => {
this.debounceTimers.delete(key);
const result = await SchemaAdapter.validateAsync(
validator,
context.value,
context
);
resolve(result);
}, debounceMs);
this.debounceTimers.set(key, timer);
});
}
validateFormSync(validator, context) {
if (typeof validator === "function") {
const result = validator(context);
if (result === void 0 || result === null) {
return { isValid: true, errors: {} };
}
if (typeof result === "string") {
return {
isValid: false,
errors: { form: result }
};
}
if (typeof result === "object" && result.fields) {
return {
isValid: false,
errors: result.fields
};
}
}
return SchemaAdapter.validate(validator, context.value);
}
async validateFormAsync(validator, context, config, event) {
const debounceMs = config.asyncDebounceMs || 0;
if (debounceMs > 0) {
const key = `form-${event.type}`;
this.clearDebounce("form", event.type);
return new Promise((resolve) => {
const timer = setTimeout(async () => {
this.debounceTimers.delete(key);
const result = await this.executeFormAsyncValidation(
validator,
context
);
resolve(result);
}, debounceMs);
this.debounceTimers.set(key, timer);
});
}
return this.executeFormAsyncValidation(validator, context);
}
async executeFormAsyncValidation(validator, context) {
if (typeof validator === "function") {
const result = await validator(context);
if (result === void 0 || result === null) {
return { isValid: true, errors: {} };
}
if (typeof result === "string") {
return {
isValid: false,
errors: { form: result }
};
}
if (typeof result === "object" && result.fields) {
return {
isValid: false,
errors: result.fields
};
}
}
return SchemaAdapter.validateAsync(validator, context.value);
}
};
var validationEngine = new ValidationEngine();
// src/validators/fileValidators.ts
function formatFileSize(bytes) {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
function getFileExtension(fileName) {
return fileName.slice((fileName.lastIndexOf(".") - 1 >>> 0) + 2);
}
function validateFile(file, options) {
if (options.maxSize && file.size > options.maxSize) {
return `File size must be less than ${formatFileSize(options.maxSize)}`;
}
if (options.minSize && file.size < options.minSize) {
return `File size must be at least ${formatFileSize(options.minSize)}`;
}
if (options.acceptedTypes && !options.acceptedTypes.includes(file.type)) {
return `File type ${file.type} is not allowed`;
}
if (options.acceptedExtensions) {
const ext = getFileExtension(file.name).toLowerCase();
if (!options.acceptedExtensions.includes(ext)) {
return `File extension .${ext} is not allowed`;
}
}
return void 0;
}
function validateFiles(files, options) {
const fileArray = Array.from(files);
if (options.maxFiles && fileArray.length > options.maxFiles) {
return `Maximum ${options.maxFiles} files allowed`;
}
if (options.minFiles && fileArray.length < options.minFiles) {
return `Minimum ${options.minFiles} files required`;
}
for (const file of fileArray) {
const error = validateFile(file, options);
if (error) return error;
}
return void 0;
}
function createFileValidator(options) {
return ({ value }) => {
if (!value) return void 0;
if (value instanceof File) {
return validateFile(value, options);
}
if (value instanceof FileList || Array.isArray(value)) {
return validateFiles(value, options);
}
return void 0;
};
}
var fileValidators = {
/**
* Image files (JPEG, PNG, GIF, WebP) up to 5MB
*/
image: createFileValidator({
acceptedTypes: ["image/jpeg", "image/png", "image/gif", "image/webp"],
maxSize: 5 * 1024 * 1024
// 5MB
}),
/**
* Avatar images (JPEG, PNG) up to 2MB, single file only
*/
avatar: createFileValidator({
acceptedTypes: ["image/jpeg", "image/png"],
maxSize: 2 * 1024 * 1024,
// 2MB
maxFiles: 1
}),
/**
* Document files (PDF, Word, Text) up to 10MB
*/
document: createFileValidator({
acceptedTypes: [
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"text/plain"
],
maxSize: 10 * 1024 * 1024
// 10MB
}),
/**
* Image gallery (multiple images) up to 5MB each, max 10 files
*/
gallery: createFileValidator({
acceptedTypes: ["image/jpeg", "image/png", "image/gif"],
maxSize: 5 * 1024 * 1024,
// 5MB each
maxFiles: 10
}),
/**
* Video files (MP4, WebM, MOV) up to 50MB
*/
video: createFileValidator({
acceptedTypes: ["video/mp4", "video/webm", "video/quicktime"],
maxSize: 50 * 1024 * 1024
// 50MB
}),
/**
* Audio files (MP3, WAV, OGG) up to 20MB
*/
audio: createFileValidator({
acceptedTypes: ["audio/mpeg", "audio/wav", "audio/ogg"],
maxSize: 20 * 1024 * 1024
// 20MB
})
};
function fileValidator(options) {
return createFileValidator(options);
}
// src/compatibility.ts
function validateForm(schema, data) {
const result = SchemaAdapter.validate(schema, data);
if (result.isValid) {
return { success: true, data };
} else {
return { success: false, errors: result.errors };
}
}
function createValidatorFromSchema(schema, events = ["onSubmit"]) {
const config = {};
events.forEach((event) => {
config[event] = schema;
});
return config;
}
function hasValidationErrors(result) {
return !result.isValid && Object.keys(result.errors).length > 0;
}
function getFirstValidationError(result) {
if (result.isValid) return void 0;
const firstKey = Object.keys(result.errors)[0];
return firstKey ? result.errors[firstKey] : void 0;
}
export {
SchemaAdapter,
ValidationEngine,
createFileValidator,
createValidatorFromSchema,
fileValidator,
fileValidators,
flattenObject,
getFirstValidationError,
getNestedValue,
hasValidationErrors,
isArkTypeSchema,
isEffectSchema,
isStandardSchema,
isValibotSchema,
isValidatorFunction,
isYupSchema,
isZodSchema,
parseZodErrors,
removeArrayItem,
setNestedValue,
validateFile,
validateFiles,
validateForm,
validationEngine
};
//# sourceMappingURL=index.mjs.map