flnxi-Wu/lib/upload-template-save-validation.ts
2026-06-01 11:24:54 +08:00

427 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
detectCircularDependencies,
getAllOperators,
parseFieldConditions,
type FieldConditions,
} from './field-conditions'
import {
parseValidationRules,
validateValidationRules,
getSupportedValidations,
type ValidationRule,
} from './field-validation'
const ALLOWED_TEMPLATE_STATUSES = new Set(['draft', 'active', 'archived'])
const ALLOWED_FIELD_TYPES = new Set([
'text',
'textarea',
'number',
'select',
'checkbox',
'radio',
'date',
'uppercase',
])
const OPTION_FIELD_TYPES = new Set(['select', 'radio'])
const CONDITION_OPERATORS = new Set(getAllOperators().map((operator) => operator.value))
const CONDITION_OPERATORS_REQUIRING_VALUE = new Set(
getAllOperators()
.filter((operator) => operator.needsValue)
.map((operator) => operator.value)
)
export type FormattedUploadTemplateField = {
id?: string | null
key: string
label: string
type: string
required: boolean
orderIndex: number
layoutWidth: string
validations: string | null
optionsSource: string | null
optionsStatic: string | null
allowedExtensions: string | null
visibleOnHomeFilter: boolean
isSearchable: boolean
isReadOnly: boolean
addToCategory: boolean
conditions: string | null
}
export type UploadTemplateSaveValidationResult =
| { valid: true; fields: FormattedUploadTemplateField[] }
| {
valid: false
error: string
field: string
details?: Record<string, unknown>
}
export function isValidUploadTemplateStatus(status: unknown): status is string {
return typeof status === 'string' && ALLOWED_TEMPLATE_STATUSES.has(status)
}
function normalizeStaticOptions(value: unknown): { value: string | null; invalid: boolean } {
if (value === null || value === undefined) {
return { value: null, invalid: false }
}
if (Array.isArray(value)) {
const options = value
.map((option) => String(option ?? '').trim())
.filter(Boolean)
return {
value: options.length > 0 ? JSON.stringify(Array.from(new Set(options))) : null,
invalid: false,
}
}
if (typeof value === 'string') {
const trimmed = value.trim()
if (!trimmed) {
return { value: null, invalid: false }
}
try {
const parsed = JSON.parse(trimmed)
return normalizeStaticOptions(parsed)
} catch {
const options = trimmed
.split('\n')
.map((option) => option.trim())
.filter(Boolean)
return {
value: options.length > 0 ? JSON.stringify(Array.from(new Set(options))) : null,
invalid: false,
}
}
}
return { value: null, invalid: true }
}
function parseNormalizedOptions(value: string | null): string[] {
if (!value) {
return []
}
try {
const parsed = JSON.parse(value)
return Array.isArray(parsed)
? parsed.map((option) => String(option ?? '').trim()).filter(Boolean)
: []
} catch {
return []
}
}
function normalizeValidations(
value: unknown,
fieldType: string,
): { value: string | null; invalid?: string; rules?: ValidationRule } {
if (value === null || value === undefined) {
return { value: null }
}
if (typeof value === 'string' && value.trim() === '') {
return { value: null }
}
let raw: unknown = value
if (typeof value === 'string') {
try {
raw = JSON.parse(value)
} catch {
return { value: null, invalid: '验证规则必须是合法 JSON 对象' }
}
}
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
return { value: null, invalid: '验证规则必须是对象' }
}
const parsedRules = parseValidationRules(JSON.stringify(raw))
if (!parsedRules) {
return { value: null, invalid: '验证规则必须是对象' }
}
const validation = validateValidationRules(parsedRules)
if (!validation.valid) {
return { value: null, invalid: validation.error || '验证规则配置无效' }
}
const supportedValidations = new Set(getSupportedValidations(fieldType))
const unsupportedRules = [
['minLength', parsedRules.minLength],
['maxLength', parsedRules.maxLength],
['pattern', parsedRules.pattern],
['min', parsedRules.min],
['max', parsedRules.max],
]
.filter(([ruleKey, ruleValue]) => ruleValue !== undefined && !supportedValidations.has(String(ruleKey)))
.map(([ruleKey]) => String(ruleKey))
if (unsupportedRules.length > 0) {
return {
value: null,
invalid: `字段类型 ${fieldType} 不支持验证规则:${unsupportedRules.join(', ')}`,
}
}
return {
value: JSON.stringify(parsedRules),
rules: parsedRules,
}
}
function normalizeConditions(
value: unknown,
): { value: string | null; invalid: boolean; conditions?: FieldConditions } {
if (value === null || value === undefined) {
return { value: null, invalid: false }
}
if (typeof value === 'string' && value.trim() === '') {
return { value: null, invalid: false }
}
let raw: any
try {
raw = typeof value === 'string' ? JSON.parse(value) : value
} catch {
return { value: null, invalid: true }
}
if (!raw || typeof raw !== 'object') {
return { value: null, invalid: true }
}
if (raw.logic !== undefined && raw.logic !== 'AND' && raw.logic !== 'OR') {
return { value: null, invalid: true }
}
if (!Array.isArray(raw.conditions)) {
return { value: null, invalid: true }
}
if (raw.conditions.length === 0) {
return { value: null, invalid: false }
}
for (const condition of raw.conditions) {
if (!condition || typeof condition !== 'object') {
return { value: null, invalid: true }
}
if (!condition.fieldKey || !condition.operator) {
return { value: null, invalid: true }
}
}
const parsed = parseFieldConditions(raw as FieldConditions)
if (!parsed || parsed.conditions.length !== raw.conditions.length) {
return { value: null, invalid: true }
}
return { value: JSON.stringify(parsed), invalid: false, conditions: parsed }
}
export function validateAndFormatUploadTemplateFields(
fields: unknown,
): UploadTemplateSaveValidationResult {
if (!Array.isArray(fields)) {
return {
valid: false,
error: '字段列表必须是数组',
field: 'fields',
}
}
const fieldKeys = new Set<string>()
const duplicateKeys: string[] = []
const formattedFields: FormattedUploadTemplateField[] = []
for (let index = 0; index < fields.length; index += 1) {
const rawField = fields[index]
const field = (rawField || {}) as Record<string, any>
const key = String(field.key || '').trim()
if (!key) {
return {
valid: false,
error: '字段key不能为空',
field: 'fields',
}
}
if (fieldKeys.has(key)) {
duplicateKeys.push(key)
} else {
fieldKeys.add(key)
}
const label = String(field.label || '').trim() || `字段${index + 1}`
const type = String(field.type || 'text').trim() || 'text'
const addToCategory = !!field.addToCategory
const normalizedOptions = normalizeStaticOptions(field.optionsStatic)
const optionsStatic = normalizedOptions.value
const normalizedValidations = normalizeValidations(field.validations, type)
const normalizedConditions = normalizeConditions(field.conditions)
const conditions = normalizedConditions.value
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(key)) {
return {
valid: false,
error: `字段“${label}”的 key 必须以字母开头,并且只能包含字母、数字和下划线`,
field: 'fields',
details: { fieldKey: key },
}
}
if (!ALLOWED_FIELD_TYPES.has(type)) {
return {
valid: false,
error: `字段“${label}”的类型不支持:${type}`,
field: 'fields',
details: {
fieldKey: key,
fieldType: type,
allowedTypes: Array.from(ALLOWED_FIELD_TYPES),
},
}
}
if (OPTION_FIELD_TYPES.has(type)) {
if (normalizedOptions.invalid) {
return {
valid: false,
error: `字段“${label}”的选项必须是数组或每行一个选项`,
field: 'fields',
details: { fieldKey: key },
}
}
const options = parseNormalizedOptions(optionsStatic)
if (field.required && !addToCategory && options.length === 0) {
return {
valid: false,
error: `字段“${label}”是必填${type === 'radio' ? '单选' : '下拉'}字段,至少需要配置一个选项`,
field: 'fields',
details: { fieldKey: key },
}
}
}
if (normalizedValidations.invalid) {
return {
valid: false,
error: `字段“${label}”的验证规则不正确:${normalizedValidations.invalid}`,
field: 'fields',
details: { fieldKey: key },
}
}
if (normalizedConditions.invalid) {
return {
valid: false,
error: `字段“${label}”的显示条件格式不正确`,
field: 'fields',
details: { fieldKey: key },
}
}
formattedFields.push({
id: field.id ?? null,
key,
label,
type,
required: !!field.required,
orderIndex: field.orderIndex ?? index,
layoutWidth: field.layoutWidth || 'full',
validations: normalizedValidations.value,
optionsSource: field.optionsSource === null || field.optionsSource === undefined ? null : String(field.optionsSource),
optionsStatic,
allowedExtensions: field.allowedExtensions === null || field.allowedExtensions === undefined ? null : String(field.allowedExtensions),
visibleOnHomeFilter: !!field.visibleOnHomeFilter,
isSearchable: !!field.isSearchable,
isReadOnly: !!field.isReadOnly,
addToCategory,
conditions,
})
}
if (duplicateKeys.length > 0) {
return {
valid: false,
error: `字段key重复:${duplicateKeys.join(', ')}`,
field: 'fields',
details: { duplicateKeys },
}
}
const availableKeys = new Set(formattedFields.map((field) => field.key))
for (const field of formattedFields) {
const conditions = parseFieldConditions(field.conditions)
if (!conditions) {
continue
}
for (const condition of conditions.conditions) {
if (!CONDITION_OPERATORS.has(condition.operator)) {
return {
valid: false,
error: `字段“${field.label}”的显示条件包含不支持的判断方式`,
field: 'fields',
details: { fieldKey: field.key, operator: condition.operator },
}
}
if (!availableKeys.has(condition.fieldKey)) {
return {
valid: false,
error: `字段“${field.label}”的显示条件引用了不存在的字段:${condition.fieldKey}`,
field: 'fields',
details: { fieldKey: field.key, dependencyKey: condition.fieldKey },
}
}
if (condition.fieldKey === field.key) {
return {
valid: false,
error: `字段“${field.label}”的显示条件不能引用自己`,
field: 'fields',
details: { fieldKey: field.key },
}
}
if (
CONDITION_OPERATORS_REQUIRING_VALUE.has(condition.operator) &&
String(condition.value ?? '').trim() === ''
) {
return {
valid: false,
error: `字段“${field.label}”的显示条件缺少比较值`,
field: 'fields',
details: { fieldKey: field.key, operator: condition.operator },
}
}
}
}
const circularFields = detectCircularDependencies(formattedFields)
if (circularFields.length > 0) {
return {
valid: false,
error: `字段显示条件存在循环依赖:${circularFields.join(', ')}`,
field: 'fields',
details: { circularFields },
}
}
return {
valid: true,
fields: formattedFields,
}
}