mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-06-21 23:36:26 +03:00
replace async-validator
This commit is contained in:
@@ -6,7 +6,6 @@ import type { Theme } from '@mui/material/styles';
|
||||
|
||||
import * as AuthenticationApi from 'components/routing/authentication';
|
||||
import { useRequest } from 'alova/client';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import {
|
||||
LanguageSelector,
|
||||
ValidatedPasswordField,
|
||||
@@ -19,6 +18,7 @@ import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { SignInRequest } from 'types';
|
||||
import { onEnterCallback, updateValue } from 'utils';
|
||||
import { SIGN_IN_REQUEST_VALIDATOR, ValidationError, validate } from 'validators';
|
||||
import type { ValidateFieldsError } from 'validators/schema';
|
||||
|
||||
const SignIn = memo(() => {
|
||||
const authenticationContext = useContext(AuthenticationContext);
|
||||
|
||||
@@ -18,13 +18,13 @@ import {
|
||||
import { callAction } from '@/api/app';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { useRequest } from 'alova/client';
|
||||
import type Schema from 'async-validator';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import { ValidatedTextField } from 'components';
|
||||
import { toast } from 'components/toast';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { updateValue } from 'utils';
|
||||
import { ValidationError, validate } from 'validators';
|
||||
import type Schema from 'validators/schema';
|
||||
import type { ValidateFieldsError } from 'validators/schema';
|
||||
|
||||
import type { CommandItem } from './types';
|
||||
|
||||
|
||||
@@ -23,12 +23,12 @@ import {
|
||||
} from '@mui/material';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import type Schema from 'async-validator';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import { BlockFormControlLabel, ValidatedTextField } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { numberValue, updateValue } from 'utils';
|
||||
import { ValidationError, validate } from 'validators';
|
||||
import type Schema from 'validators/schema';
|
||||
import type { ValidateFieldsError } from 'validators/schema';
|
||||
|
||||
import { DeviceValueType, DeviceValueTypeNames, DeviceValueUOM_s } from './types';
|
||||
import type { EntityItem } from './types';
|
||||
|
||||
@@ -22,13 +22,13 @@ import {
|
||||
import { callAction } from '@/api/app';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { useRequest } from 'alova/client';
|
||||
import type Schema from 'async-validator';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import { ValidatedTextField } from 'components';
|
||||
import { toast } from 'components/toast';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { numberValue, updateValue } from 'utils';
|
||||
import { ValidationError, validate } from 'validators';
|
||||
import type Schema from 'validators/schema';
|
||||
import type { ValidateFieldsError } from 'validators/schema';
|
||||
|
||||
import { DeviceValueUOM, DeviceValueUOM_s } from './types';
|
||||
import type { DeviceValue } from './types';
|
||||
|
||||
@@ -22,12 +22,12 @@ import {
|
||||
} from '@mui/material';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import type Schema from 'async-validator';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import { BlockFormControlLabel, ValidatedTextField } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { updateValue } from 'utils';
|
||||
import { ValidationError, validate } from 'validators';
|
||||
import type Schema from 'validators/schema';
|
||||
import type { ValidateFieldsError } from 'validators/schema';
|
||||
|
||||
import { ScheduleFlag } from './types';
|
||||
import type { ScheduleItem } from './types';
|
||||
|
||||
@@ -18,12 +18,12 @@ import {
|
||||
} from '@mui/material';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import type Schema from 'async-validator';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import { ValidatedTextField } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { numberValue, updateValue } from 'utils';
|
||||
import { ValidationError, validate } from 'validators';
|
||||
import type Schema from 'validators/schema';
|
||||
import type { ValidateFieldsError } from 'validators/schema';
|
||||
|
||||
import { AnalogType, AnalogTypeNames, DeviceValueUOM_s } from './types';
|
||||
import type { AnalogSensor } from './types';
|
||||
|
||||
@@ -16,12 +16,12 @@ import {
|
||||
} from '@mui/material';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import type Schema from 'async-validator';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import { ValidatedTextField } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { numberValue, updateValue } from 'utils';
|
||||
import { ValidationError, validate } from 'validators';
|
||||
import type Schema from 'validators/schema';
|
||||
import type { ValidateFieldsError } from 'validators/schema';
|
||||
|
||||
import type { TemperatureSensor } from './types';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Schema from 'async-validator';
|
||||
import type { InternalRuleItem } from 'async-validator';
|
||||
import Schema from 'validators/schema';
|
||||
import type { InternalRuleItem } from 'validators/schema';
|
||||
import { IP_OR_HOSTNAME_VALIDATOR } from 'validators/shared';
|
||||
|
||||
import type {
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Button, Checkbox, MenuItem } from '@mui/material';
|
||||
|
||||
import * as APApi from 'api/ap';
|
||||
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
BlockNavigation,
|
||||
@@ -22,6 +21,7 @@ import type { APSettingsType } from 'types';
|
||||
import { APProvisionMode } from 'types';
|
||||
import { numberValue, updateValueDirty, useRest } from 'utils';
|
||||
import { ValidationError, createAPSettingsValidator, validate } from 'validators';
|
||||
import type { ValidateFieldsError } from 'validators/schema';
|
||||
|
||||
export const isAPEnabled = ({ provision_mode }: APSettingsType) =>
|
||||
provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
|
||||
|
||||
@@ -19,7 +19,6 @@ import { readSystemStatus } from 'api/system';
|
||||
|
||||
import { useRequest } from 'alova/client';
|
||||
import SystemMonitor from 'app/status/SystemMonitor';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
BlockNavigation,
|
||||
@@ -35,6 +34,7 @@ import { toast } from 'components/toast';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { numberValue, updateValueDirty, useRest } from 'utils';
|
||||
import { ValidationError, validate } from 'validators';
|
||||
import type { ValidateFieldsError } from 'validators/schema';
|
||||
|
||||
import { API, getBoardProfile, readSettings, writeSettings } from '../../api/app';
|
||||
import { BOARD_PROFILES } from '../main/types';
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
|
||||
import * as MqttApi from 'api/mqtt';
|
||||
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
BlockNavigation,
|
||||
@@ -32,6 +31,7 @@ import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { MqttSettingsType } from 'types';
|
||||
import { numberValue, updateValueDirty, useRest } from 'utils';
|
||||
import { ValidationError, createMqttSettingsValidator, validate } from 'validators';
|
||||
import type { ValidateFieldsError } from 'validators/schema';
|
||||
|
||||
import { callAction } from '../../api/app';
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import { readNTPSettings } from 'api/ntp';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { useRequest } from 'alova/client';
|
||||
import { updateState } from 'alova/client';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
BlockNavigation,
|
||||
@@ -38,6 +37,7 @@ import type { NTPSettingsType, Time } from 'types';
|
||||
import { formatLocalDateTime, updateValueDirty, useRest } from 'utils';
|
||||
import { ValidationError, validate } from 'validators';
|
||||
import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp';
|
||||
import type { ValidateFieldsError } from 'validators/schema';
|
||||
|
||||
import { TIME_ZONES, selectedTimeZone, useTimeZoneSelectItems } from './TZ';
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import { API } from 'api/app';
|
||||
|
||||
import { updateState, useRequest } from 'alova/client';
|
||||
import type { APIcall } from 'app/main/types';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
BlockNavigation,
|
||||
@@ -42,6 +41,7 @@ import type { NetworkSettingsType } from 'types';
|
||||
import { updateValueDirty, useRest } from 'utils';
|
||||
import { ValidationError, validate } from 'validators';
|
||||
import { createNetworkSettingsValidator } from 'validators/network';
|
||||
import type { ValidateFieldsError } from 'validators/schema';
|
||||
|
||||
import SystemMonitor from '../../status/SystemMonitor';
|
||||
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Button } from '@mui/material';
|
||||
|
||||
import * as SecurityApi from 'api/security';
|
||||
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import {
|
||||
BlockNavigation,
|
||||
ButtonRow,
|
||||
@@ -20,6 +19,7 @@ import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { SecuritySettingsType } from 'types';
|
||||
import { updateValueDirty, useRest } from 'utils';
|
||||
import { SECURITY_SETTINGS_VALIDATOR, ValidationError, validate } from 'validators';
|
||||
import type { ValidateFieldsError } from 'validators/schema';
|
||||
|
||||
const SecuritySettings = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
} from '@mui/material';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import type Schema from 'async-validator';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
ValidatedPasswordField,
|
||||
@@ -25,6 +23,8 @@ import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { UserType } from 'types';
|
||||
import { updateValue } from 'utils';
|
||||
import { ValidationError, validate } from 'validators';
|
||||
import type Schema from 'validators/schema';
|
||||
import type { ValidateFieldsError } from 'validators/schema';
|
||||
|
||||
interface UserFormProps {
|
||||
creating: boolean;
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { FC } from 'react';
|
||||
import { FormHelperText, TextField } from '@mui/material';
|
||||
import type { TextFieldProps } from '@mui/material';
|
||||
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import type { ValidateFieldsError } from 'validators/schema';
|
||||
|
||||
interface ValidatedFieldProps {
|
||||
fieldErrors?: ValidateFieldsError;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isAPEnabled } from 'app/settings/APSettings';
|
||||
import Schema from 'async-validator';
|
||||
import type { APSettingsType } from 'types';
|
||||
import Schema from 'validators/schema';
|
||||
|
||||
import { IP_ADDRESS_VALIDATOR } from './shared';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Schema from 'async-validator';
|
||||
import Schema from 'validators/schema';
|
||||
|
||||
export const SIGN_IN_REQUEST_VALIDATOR = new Schema({
|
||||
username: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Schema from 'async-validator';
|
||||
import type { MqttSettingsType } from 'types';
|
||||
import Schema from 'validators/schema';
|
||||
|
||||
import { IP_OR_HOSTNAME_VALIDATOR } from './shared';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Schema from 'async-validator';
|
||||
import type { NetworkSettingsType } from 'types';
|
||||
import Schema from 'validators/schema';
|
||||
|
||||
import { HOSTNAME_VALIDATOR, IP_ADDRESS_VALIDATOR } from './shared';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Schema from 'async-validator';
|
||||
import Schema from 'validators/schema';
|
||||
|
||||
import { IP_OR_HOSTNAME_VALIDATOR } from './shared';
|
||||
|
||||
|
||||
171
interface/src/validators/schema.ts
Normal file
171
interface/src/validators/schema.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
// Minimal drop-in replacement for the subset of `async-validator` used by this
|
||||
// app's form validators. It intentionally mirrors async-validator's runtime
|
||||
// semantics for the rule features in use (required, type string/number with
|
||||
// min/max/pattern, and custom `validator(rule, value, callback)` functions) so
|
||||
// the existing schema definitions and the `ValidateFieldsError` consumers keep
|
||||
// working unchanged.
|
||||
//
|
||||
// Notable async-validator semantics preserved:
|
||||
// - A non-required built-in rule is skipped when the value is "empty"
|
||||
// (undefined / null / ''); `0` is NOT empty.
|
||||
// - Custom `validator` functions are always invoked (they guard empties
|
||||
// themselves), matching async-validator's behavior for validator rules.
|
||||
// - `min`/`max` mean numeric bounds for `type: 'number'` and length bounds
|
||||
// otherwise.
|
||||
// - All rule errors for a field are collected (no early-exit per field).
|
||||
|
||||
export interface ValidateError {
|
||||
message?: string;
|
||||
field?: string;
|
||||
fieldValue?: unknown;
|
||||
}
|
||||
|
||||
export type ValidateFieldsError = Record<string, ValidateError[]>;
|
||||
|
||||
export interface InternalRuleItem {
|
||||
field?: string;
|
||||
fullField?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export type RuleValidator = (
|
||||
rule: InternalRuleItem,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
value: any,
|
||||
callback: (error?: string | Error) => void
|
||||
) => void | Promise<void>;
|
||||
|
||||
export interface RuleItem {
|
||||
type?: string;
|
||||
required?: boolean;
|
||||
pattern?: RegExp | string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
message?: string;
|
||||
validator?: RuleValidator;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export type Rules = Record<string, RuleItem | RuleItem[]>;
|
||||
|
||||
export interface ValidateOption {
|
||||
first?: boolean;
|
||||
firstFields?: boolean | string[];
|
||||
suppressWarning?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export type ValidateCallback = (
|
||||
errors: ValidateError[] | null,
|
||||
fields: ValidateFieldsError
|
||||
) => void;
|
||||
|
||||
const isEmpty = (value: unknown): boolean =>
|
||||
value === undefined || value === null || value === '';
|
||||
|
||||
const runValidator = async (
|
||||
rule: RuleItem,
|
||||
field: string,
|
||||
value: unknown
|
||||
): Promise<string | undefined> => {
|
||||
let captured: string | undefined;
|
||||
const callback = (error?: string | Error) => {
|
||||
if (error) captured = typeof error === 'string' ? error : error.message;
|
||||
};
|
||||
try {
|
||||
const result = rule.validator!(
|
||||
{ ...rule, field, fullField: field },
|
||||
value,
|
||||
callback
|
||||
);
|
||||
if (result instanceof Promise) {
|
||||
await result;
|
||||
}
|
||||
} catch (error) {
|
||||
captured = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
return captured;
|
||||
};
|
||||
|
||||
const runRule = async (
|
||||
rule: RuleItem,
|
||||
field: string,
|
||||
value: unknown
|
||||
): Promise<string | undefined> => {
|
||||
// Custom validators own their empty-value handling and run unconditionally.
|
||||
if (typeof rule.validator === 'function') {
|
||||
return runValidator(rule, field, value);
|
||||
}
|
||||
|
||||
const empty = isEmpty(value);
|
||||
|
||||
if (rule.required && empty) {
|
||||
return rule.message ?? `${field} is required`;
|
||||
}
|
||||
|
||||
// Non-required built-in rules don't validate empty values.
|
||||
if (empty) return undefined;
|
||||
|
||||
if (rule.type === 'number') {
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) return rule.message;
|
||||
if (typeof rule.min === 'number' && value < rule.min) return rule.message;
|
||||
if (typeof rule.max === 'number' && value > rule.max) return rule.message;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// type 'string' or any rule carrying length/pattern constraints.
|
||||
if (
|
||||
rule.type === 'string' ||
|
||||
typeof rule.min === 'number' ||
|
||||
typeof rule.max === 'number' ||
|
||||
rule.pattern != null
|
||||
) {
|
||||
const str = String(value);
|
||||
if (typeof rule.min === 'number' && str.length < rule.min) return rule.message;
|
||||
if (typeof rule.max === 'number' && str.length > rule.max) return rule.message;
|
||||
if (rule.pattern != null) {
|
||||
const re =
|
||||
rule.pattern instanceof RegExp ? rule.pattern : new RegExp(rule.pattern);
|
||||
if (!re.test(str)) return rule.message;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export default class Schema {
|
||||
private readonly rules: Rules;
|
||||
|
||||
constructor(descriptor: Rules) {
|
||||
this.rules = descriptor;
|
||||
}
|
||||
|
||||
// Mirrors async-validator's callback form. Always resolves (never rejects);
|
||||
// callers (validators/shared.ts) inspect the `errors` argument.
|
||||
async validate(
|
||||
source: Record<string, unknown>,
|
||||
_options?: ValidateOption,
|
||||
callback?: ValidateCallback
|
||||
): Promise<void> {
|
||||
const fields: ValidateFieldsError = {};
|
||||
const errors: ValidateError[] = [];
|
||||
|
||||
for (const field of Object.keys(this.rules)) {
|
||||
const ruleDef = this.rules[field];
|
||||
if (ruleDef === undefined) continue;
|
||||
const ruleList = Array.isArray(ruleDef) ? ruleDef : [ruleDef];
|
||||
const value = source[field];
|
||||
|
||||
for (const rule of ruleList) {
|
||||
const message = await runRule(rule, field, value);
|
||||
if (message !== undefined) {
|
||||
const error: ValidateError = { message, field, fieldValue: value };
|
||||
(fields[field] ??= []).push(error);
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback?.(errors.length > 0 ? errors : null, fields);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import Schema from 'async-validator';
|
||||
import type { InternalRuleItem } from 'async-validator';
|
||||
import type { UserType } from 'types';
|
||||
import Schema from 'validators/schema';
|
||||
import type { InternalRuleItem } from 'validators/schema';
|
||||
|
||||
const USERNAME_PATTERN = /^[a-zA-Z0-9_\\.]{1,24}$/;
|
||||
const JWT_SECRET_MAX_LENGTH = 64;
|
||||
|
||||
@@ -2,8 +2,8 @@ import type {
|
||||
InternalRuleItem,
|
||||
ValidateFieldsError,
|
||||
ValidateOption
|
||||
} from 'async-validator';
|
||||
import type Schema from 'async-validator';
|
||||
} from 'validators/schema';
|
||||
import type Schema from 'validators/schema';
|
||||
|
||||
export class ValidationError extends Error {
|
||||
readonly fieldErrors: ValidateFieldsError;
|
||||
|
||||
Reference in New Issue
Block a user