mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-13 05:19:00 +08:00
feat: add zod (#17277)
This commit is contained in:
parent
713902dc47
commit
b4aa1900e2
40
web/app/components/base/with-input-validation/index.spec.tsx
Normal file
40
web/app/components/base/with-input-validation/index.spec.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import '@testing-library/jest-dom'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import withValidation from '.'
|
||||||
|
|
||||||
|
describe('withValidation HOC', () => {
|
||||||
|
// schema for validation
|
||||||
|
const schema = z.object({ name: z.string() })
|
||||||
|
type Props = z.infer<typeof schema> & {
|
||||||
|
age: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const TestComponent = ({ name, age }: Props) => (
|
||||||
|
<div>{name} - {age}</div>
|
||||||
|
)
|
||||||
|
const WrappedComponent = withValidation(TestComponent, schema)
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.spyOn(console, 'error').mockImplementation(() => { })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the component when validation passes', () => {
|
||||||
|
render(<WrappedComponent name='Valid Name' age={30} />)
|
||||||
|
expect(screen.getByText('Valid Name - 30')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the component when props is invalid but not in schema ', () => {
|
||||||
|
render(<WrappedComponent name='Valid Name' age={'aaa' as any} />)
|
||||||
|
expect(screen.getByText('Valid Name - aaa')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render the component when validation fails', () => {
|
||||||
|
render(<WrappedComponent name={123 as any} age={30} />)
|
||||||
|
expect(screen.queryByText('123 - 30')).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
24
web/app/components/base/with-input-validation/index.tsx
Normal file
24
web/app/components/base/with-input-validation/index.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
'use client'
|
||||||
|
import React from 'react'
|
||||||
|
import type { ZodSchema } from 'zod'
|
||||||
|
|
||||||
|
function withValidation<T extends Record<string, unknown>, K extends keyof T>(
|
||||||
|
WrappedComponent: React.ComponentType<T>,
|
||||||
|
schema: ZodSchema<Pick<T, K>>,
|
||||||
|
) {
|
||||||
|
return function EnsuredComponent(props: T) {
|
||||||
|
const partialProps = Object.fromEntries(
|
||||||
|
Object.entries(props).filter(([key]) => key in (schema._def as any).shape),
|
||||||
|
) as Pick<T, K>
|
||||||
|
|
||||||
|
const checkRes = schema.safeParse(partialProps)
|
||||||
|
if (!checkRes.success) {
|
||||||
|
console.error(checkRes.error)
|
||||||
|
// Maybe there is a better way to handle this, like error logic placeholder
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return <WrappedComponent {...props} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withValidation
|
173
web/utils/zod.spec.ts
Normal file
173
web/utils/zod.spec.ts
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import { ZodError, z } from 'zod'
|
||||||
|
|
||||||
|
describe('Zod Features', () => {
|
||||||
|
it('should support string', () => {
|
||||||
|
const stringSchema = z.string()
|
||||||
|
const numberLikeStringSchema = z.coerce.string() // 12 would be converted to '12'
|
||||||
|
const stringSchemaWithError = z.string({
|
||||||
|
required_error: 'Name is required',
|
||||||
|
invalid_type_error: 'Invalid name type, expected string',
|
||||||
|
})
|
||||||
|
|
||||||
|
const urlSchema = z.string().url()
|
||||||
|
const uuidSchema = z.string().uuid()
|
||||||
|
|
||||||
|
expect(stringSchema.parse('hello')).toBe('hello')
|
||||||
|
expect(() => stringSchema.parse(12)).toThrow()
|
||||||
|
expect(numberLikeStringSchema.parse('12')).toBe('12')
|
||||||
|
expect(numberLikeStringSchema.parse(12)).toBe('12')
|
||||||
|
expect(() => stringSchemaWithError.parse(undefined)).toThrow('Name is required')
|
||||||
|
expect(() => stringSchemaWithError.parse(12)).toThrow('Invalid name type, expected string')
|
||||||
|
|
||||||
|
expect(urlSchema.parse('https://dify.ai')).toBe('https://dify.ai')
|
||||||
|
expect(uuidSchema.parse('123e4567-e89b-12d3-a456-426614174000')).toBe('123e4567-e89b-12d3-a456-426614174000')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support enum', () => {
|
||||||
|
enum JobStatus {
|
||||||
|
waiting = 'waiting',
|
||||||
|
processing = 'processing',
|
||||||
|
completed = 'completed',
|
||||||
|
}
|
||||||
|
expect(z.nativeEnum(JobStatus).parse(JobStatus.waiting)).toBe(JobStatus.waiting)
|
||||||
|
expect(z.nativeEnum(JobStatus).parse('completed')).toBe('completed')
|
||||||
|
expect(() => z.nativeEnum(JobStatus).parse('invalid')).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support number', () => {
|
||||||
|
const numberSchema = z.number()
|
||||||
|
const numberWithMin = z.number().gt(0) // alias min
|
||||||
|
const numberWithMinEqual = z.number().gte(0)
|
||||||
|
const numberWithMax = z.number().lt(100) // alias max
|
||||||
|
|
||||||
|
expect(numberSchema.parse(123)).toBe(123)
|
||||||
|
expect(numberWithMin.parse(50)).toBe(50)
|
||||||
|
expect(numberWithMinEqual.parse(0)).toBe(0)
|
||||||
|
expect(() => numberWithMin.parse(-1)).toThrow()
|
||||||
|
expect(numberWithMax.parse(50)).toBe(50)
|
||||||
|
expect(() => numberWithMax.parse(101)).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support boolean', () => {
|
||||||
|
const booleanSchema = z.boolean()
|
||||||
|
expect(booleanSchema.parse(true)).toBe(true)
|
||||||
|
expect(booleanSchema.parse(false)).toBe(false)
|
||||||
|
expect(() => booleanSchema.parse('true')).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support date', () => {
|
||||||
|
const dateSchema = z.date()
|
||||||
|
expect(dateSchema.parse(new Date('2023-01-01'))).toEqual(new Date('2023-01-01'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support object', () => {
|
||||||
|
const userSchema = z.object({
|
||||||
|
id: z.union([z.string(), z.number()]),
|
||||||
|
name: z.string(),
|
||||||
|
email: z.string().email(),
|
||||||
|
age: z.number().min(0).max(120).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
type User = z.infer<typeof userSchema>
|
||||||
|
|
||||||
|
const validUser: User = {
|
||||||
|
id: 1,
|
||||||
|
name: 'John',
|
||||||
|
email: 'john@example.com',
|
||||||
|
age: 30,
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(userSchema.parse(validUser)).toEqual(validUser)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support object optional field', () => {
|
||||||
|
const userSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
optionalField: z.optional(z.string()),
|
||||||
|
})
|
||||||
|
type User = z.infer<typeof userSchema>
|
||||||
|
|
||||||
|
const user: User = {
|
||||||
|
name: 'John',
|
||||||
|
}
|
||||||
|
const userWithOptionalField: User = {
|
||||||
|
name: 'John',
|
||||||
|
optionalField: 'optional',
|
||||||
|
}
|
||||||
|
expect(userSchema.safeParse(user).success).toEqual(true)
|
||||||
|
expect(userSchema.safeParse(userWithOptionalField).success).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support object intersection', () => {
|
||||||
|
const Person = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const Employee = z.object({
|
||||||
|
role: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const EmployedPerson = z.intersection(Person, Employee)
|
||||||
|
const validEmployedPerson = {
|
||||||
|
name: 'John',
|
||||||
|
role: 'Developer',
|
||||||
|
}
|
||||||
|
expect(EmployedPerson.parse(validEmployedPerson)).toEqual(validEmployedPerson)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support record', () => {
|
||||||
|
const recordSchema = z.record(z.string(), z.number())
|
||||||
|
const validRecord = {
|
||||||
|
a: 1,
|
||||||
|
b: 2,
|
||||||
|
}
|
||||||
|
expect(recordSchema.parse(validRecord)).toEqual(validRecord)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support array', () => {
|
||||||
|
const numbersSchema = z.array(z.number())
|
||||||
|
const stringArraySchema = z.string().array()
|
||||||
|
|
||||||
|
expect(numbersSchema.parse([1, 2, 3])).toEqual([1, 2, 3])
|
||||||
|
expect(stringArraySchema.parse(['a', 'b', 'c'])).toEqual(['a', 'b', 'c'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support promise', () => {
|
||||||
|
const promiseSchema = z.promise(z.string())
|
||||||
|
const validPromise = Promise.resolve('success')
|
||||||
|
|
||||||
|
expect(promiseSchema.parse(validPromise)).resolves.toBe('success')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support unions', () => {
|
||||||
|
const unionSchema = z.union([z.string(), z.number()])
|
||||||
|
|
||||||
|
expect(unionSchema.parse('success')).toBe('success')
|
||||||
|
expect(unionSchema.parse(404)).toBe(404)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support functions', () => {
|
||||||
|
const functionSchema = z.function().args(z.string(), z.number(), z.optional(z.string())).returns(z.number())
|
||||||
|
const validFunction = (name: string, age: number, _optional?: string): number => {
|
||||||
|
return age
|
||||||
|
}
|
||||||
|
expect(functionSchema.safeParse(validFunction).success).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support undefined, null, any, and void', () => {
|
||||||
|
const undefinedSchema = z.undefined()
|
||||||
|
const nullSchema = z.null()
|
||||||
|
const anySchema = z.any()
|
||||||
|
|
||||||
|
expect(undefinedSchema.parse(undefined)).toBeUndefined()
|
||||||
|
expect(nullSchema.parse(null)).toBeNull()
|
||||||
|
expect(anySchema.parse('anything')).toBe('anything')
|
||||||
|
expect(anySchema.parse(3)).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should safeParse would not throw', () => {
|
||||||
|
expect(z.string().safeParse('abc').success).toBe(true)
|
||||||
|
expect(z.string().safeParse(123).success).toBe(false)
|
||||||
|
expect(z.string().safeParse(123).error).toBeInstanceOf(ZodError)
|
||||||
|
})
|
||||||
|
})
|
Loading…
x
Reference in New Issue
Block a user