flnxi-Wu/lib/upload-template-save-validation.ts
2026-05-28 15:50:54 +08:00

320 lines
8.9 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'
const ALLOWED_TEMPLATE_STATUSES = new Set(['draft', 'active', 'archived'])
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 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')
const addToCategory = !!field.addToCategory
const normalizedOptions = normalizeStaticOptions(field.optionsStatic)
const optionsStatic = normalizedOptions.value
const normalizedConditions = normalizeConditions(field.conditions)
const conditions = normalizedConditions.value
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 (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: field.validations === null || field.validations === undefined ? null : String(field.validations),
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,
}
}