UNPKG

el-form-core

Version:

Framework-agnostic form validation engine - schema-first validation core for TypeScript applications. Supports Zod, Yup, Valibot and custom validators.

696 lines (689 loc) 21.5 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); // src/index.ts var src_exports = {}; __export(src_exports, { SchemaAdapter: () => SchemaAdapter, ValidationEngine: () => ValidationEngine, createFileValidator: () => createFileValidator, createValidatorFromSchema: () => createValidatorFromSchema, fileValidator: () => fileValidator, fileValidators: () => fileValidators, flattenObject: () => flattenObject, getFirstValidationError: () => getFirstValidationError, getNestedValue: () => getNestedValue, hasValidationErrors: () => hasValidationErrors, isArkTypeSchema: () => isArkTypeSchema, isEffectSchema: () => isEffectSchema, isStandardSchema: () => isStandardSchema, isValibotSchema: () => isValibotSchema, isValidatorFunction: () => isValidatorFunction, isYupSchema: () => isYupSchema, isZodSchema: () => isZodSchema, parseZodErrors: () => parseZodErrors, removeArrayItem: () => removeArrayItem, setNestedValue: () => setNestedValue, validateFile: () => validateFile, validateFiles: () => validateFiles, validateForm: () => validateForm, validationEngine: () => validationEngine }); module.exports = __toCommonJS(src_exports); // 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; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { 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.js.map