mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-18 16:05:53 +08:00
feat: replace ajv with jsonschema for JSON validation and update related components
This commit is contained in:
parent
0319e35b4d
commit
9ee8fa644e
@ -4,7 +4,7 @@ import cn from '@/utils/classnames'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { RiCloseLine } from '@remixicon/react'
|
import { RiCloseLine } from '@remixicon/react'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import { checkDepth } from '../../utils'
|
import { checkJsonDepth } from '../../utils'
|
||||||
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
|
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
|
||||||
import CodeEditor from './code-editor'
|
import CodeEditor from './code-editor'
|
||||||
import ErrorMessage from './error-message'
|
import ErrorMessage from './error-message'
|
||||||
@ -59,7 +59,7 @@ const JsonImporter: FC<JsonImporterProps> = ({
|
|||||||
setParseError(new Error('Root must be an object, not an array or primitive value.'))
|
setParseError(new Error('Root must be an object, not an array or primitive value.'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const maxDepth = checkDepth(parsedJSON)
|
const maxDepth = checkJsonDepth(parsedJSON)
|
||||||
if (maxDepth > JSON_SCHEMA_MAX_DEPTH) {
|
if (maxDepth > JSON_SCHEMA_MAX_DEPTH) {
|
||||||
setParseError({
|
setParseError({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
@ -72,10 +72,10 @@ const JsonImporter: FC<JsonImporterProps> = ({
|
|||||||
setOpen(false)
|
setOpen(false)
|
||||||
}
|
}
|
||||||
catch (e: any) {
|
catch (e: any) {
|
||||||
if (e instanceof SyntaxError)
|
if (e instanceof Error)
|
||||||
setParseError(e)
|
setParseError(e)
|
||||||
else
|
else
|
||||||
setParseError(new Error('Unknown error'))
|
setParseError(new Error('Invalid JSON'))
|
||||||
}
|
}
|
||||||
}, [onSubmit, json])
|
}, [onSubmit, json])
|
||||||
|
|
||||||
|
@ -9,12 +9,20 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import VisualEditor from './visual-editor'
|
import VisualEditor from './visual-editor'
|
||||||
import SchemaEditor from './schema-editor'
|
import SchemaEditor from './schema-editor'
|
||||||
import { convertBooleanToString, getValidationErrorMessage, jsonToSchema, validateSchemaAgainstDraft7 } from '../../utils'
|
import {
|
||||||
|
checkJsonSchemaDepth,
|
||||||
|
convertBooleanToString,
|
||||||
|
getValidationErrorMessage,
|
||||||
|
jsonToSchema,
|
||||||
|
preValidateSchema,
|
||||||
|
validateSchemaAgainstDraft7,
|
||||||
|
} from '../../utils'
|
||||||
import { MittProvider, VisualEditorContextProvider } from './visual-editor/context'
|
import { MittProvider, VisualEditorContextProvider } from './visual-editor/context'
|
||||||
import ErrorMessage from './error-message'
|
import ErrorMessage from './error-message'
|
||||||
import { useVisualEditorStore } from './visual-editor/store'
|
import { useVisualEditorStore } from './visual-editor/store'
|
||||||
import Toast from '@/app/components/base/toast'
|
import Toast from '@/app/components/base/toast'
|
||||||
import { useGetLanguage } from '@/context/i18n'
|
import { useGetLanguage } from '@/context/i18n'
|
||||||
|
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
|
||||||
|
|
||||||
type JsonSchemaConfigProps = {
|
type JsonSchemaConfigProps = {
|
||||||
defaultSchema?: SchemaRoot
|
defaultSchema?: SchemaRoot
|
||||||
@ -74,18 +82,26 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
|
|||||||
if (currentTab === value) return
|
if (currentTab === value) return
|
||||||
if (currentTab === SchemaView.JsonSchema) {
|
if (currentTab === SchemaView.JsonSchema) {
|
||||||
try {
|
try {
|
||||||
const parsedJson = JSON.parse(json)
|
const schema = JSON.parse(json)
|
||||||
const schema = convertBooleanToString(parsedJson)
|
|
||||||
setParseError(null)
|
setParseError(null)
|
||||||
const ajvError = validateSchemaAgainstDraft7(schema)
|
const result = preValidateSchema(schema)
|
||||||
if (ajvError.length > 0) {
|
if (!result.success) {
|
||||||
setValidationError(getValidationErrorMessage(ajvError))
|
setValidationError(result.error.message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
else {
|
const schemaDepth = checkJsonSchemaDepth(schema)
|
||||||
setJsonSchema(schema)
|
if (schemaDepth > JSON_SCHEMA_MAX_DEPTH) {
|
||||||
setValidationError('')
|
setValidationError(`Schema exceeds maximum depth of ${JSON_SCHEMA_MAX_DEPTH}.`)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
convertBooleanToString(schema)
|
||||||
|
const validationErrors = validateSchemaAgainstDraft7(schema)
|
||||||
|
if (validationErrors.length > 0) {
|
||||||
|
setValidationError(getValidationErrorMessage(validationErrors))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setJsonSchema(schema)
|
||||||
|
setValidationError('')
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
setValidationError('')
|
setValidationError('')
|
||||||
@ -135,7 +151,7 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
|
|||||||
|
|
||||||
const handleResetDefaults = useCallback(() => {
|
const handleResetDefaults = useCallback(() => {
|
||||||
if (currentTab === SchemaView.VisualEditor) {
|
if (currentTab === SchemaView.VisualEditor) {
|
||||||
setHoveringProperty('')
|
setHoveringProperty(null)
|
||||||
advancedEditing && setAdvancedEditing(false)
|
advancedEditing && setAdvancedEditing(false)
|
||||||
isAddingNewField && setIsAddingNewField(false)
|
isAddingNewField && setIsAddingNewField(false)
|
||||||
}
|
}
|
||||||
@ -153,15 +169,24 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
|
|||||||
try {
|
try {
|
||||||
schema = JSON.parse(json)
|
schema = JSON.parse(json)
|
||||||
setParseError(null)
|
setParseError(null)
|
||||||
const ajvError = validateSchemaAgainstDraft7(schema)
|
const result = preValidateSchema(schema)
|
||||||
if (ajvError.length > 0) {
|
if (!result.success) {
|
||||||
setValidationError(getValidationErrorMessage(ajvError))
|
setValidationError(result.error.message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
else {
|
const schemaDepth = checkJsonSchemaDepth(schema)
|
||||||
setJsonSchema(schema)
|
if (schemaDepth > JSON_SCHEMA_MAX_DEPTH) {
|
||||||
setValidationError('')
|
setValidationError(`Schema exceeds maximum depth of ${JSON_SCHEMA_MAX_DEPTH}.`)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
convertBooleanToString(schema)
|
||||||
|
const validationErrors = validateSchemaAgainstDraft7(schema)
|
||||||
|
if (validationErrors.length > 0) {
|
||||||
|
setValidationError(getValidationErrorMessage(validationErrors))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setJsonSchema(schema)
|
||||||
|
setValidationError('')
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
setValidationError('')
|
setValidationError('')
|
||||||
|
@ -47,14 +47,13 @@ const GeneratedResult: FC<GeneratedResultProps> = ({
|
|||||||
const jsonSchema = useMemo(() => formatJSON(schema), [schema])
|
const jsonSchema = useMemo(() => formatJSON(schema), [schema])
|
||||||
|
|
||||||
const handleApply = useCallback(() => {
|
const handleApply = useCallback(() => {
|
||||||
const ajvError = validateSchemaAgainstDraft7(schema)
|
const validationErrors = validateSchemaAgainstDraft7(schema)
|
||||||
if (ajvError.length > 0) {
|
if (validationErrors.length > 0) {
|
||||||
setValidationError(getValidationErrorMessage(ajvError))
|
setValidationError(getValidationErrorMessage(validationErrors))
|
||||||
}
|
return
|
||||||
else {
|
|
||||||
onApply()
|
|
||||||
setValidationError('')
|
|
||||||
}
|
}
|
||||||
|
onApply()
|
||||||
|
setValidationError('')
|
||||||
}, [schema, onApply])
|
}, [schema, onApply])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -47,6 +47,15 @@ const TYPE_OPTIONS = [
|
|||||||
{ value: ArrayType.object, text: 'array[object]' },
|
{ value: ArrayType.object, text: 'array[object]' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const MAXIMUM_DEPTH_TYPE_OPTIONS = [
|
||||||
|
{ value: Type.string, text: 'string' },
|
||||||
|
{ value: Type.number, text: 'number' },
|
||||||
|
// { value: Type.boolean, text: 'boolean' },
|
||||||
|
{ value: ArrayType.string, text: 'array[string]' },
|
||||||
|
{ value: ArrayType.number, text: 'array[number]' },
|
||||||
|
// { value: ArrayType.boolean, text: 'array[boolean]' },
|
||||||
|
]
|
||||||
|
|
||||||
const EditCard: FC<EditCardProps> = ({
|
const EditCard: FC<EditCardProps> = ({
|
||||||
fields,
|
fields,
|
||||||
depth,
|
depth,
|
||||||
@ -63,7 +72,8 @@ const EditCard: FC<EditCardProps> = ({
|
|||||||
const { emit, useSubscribe } = useMittContext()
|
const { emit, useSubscribe } = useMittContext()
|
||||||
const blurWithActions = useRef(false)
|
const blurWithActions = useRef(false)
|
||||||
|
|
||||||
const disableAddBtn = depth >= JSON_SCHEMA_MAX_DEPTH || (currentFields.type !== Type.object && currentFields.type !== ArrayType.object)
|
const maximumDepthReached = depth === JSON_SCHEMA_MAX_DEPTH
|
||||||
|
const disableAddBtn = maximumDepthReached || (currentFields.type !== Type.object && currentFields.type !== ArrayType.object)
|
||||||
const hasAdvancedOptions = currentFields.type === Type.string || currentFields.type === Type.number
|
const hasAdvancedOptions = currentFields.type === Type.string || currentFields.type === Type.number
|
||||||
const isAdvancedEditing = advancedEditing || isAddingNewField
|
const isAdvancedEditing = advancedEditing || isAddingNewField
|
||||||
|
|
||||||
@ -205,7 +215,7 @@ const EditCard: FC<EditCardProps> = ({
|
|||||||
/>
|
/>
|
||||||
<TypeSelector
|
<TypeSelector
|
||||||
currentValue={currentFields.type}
|
currentValue={currentFields.type}
|
||||||
items={TYPE_OPTIONS}
|
items={maximumDepthReached ? MAXIMUM_DEPTH_TYPE_OPTIONS : TYPE_OPTIONS}
|
||||||
onSelect={handleTypeChange}
|
onSelect={handleTypeChange}
|
||||||
popupClassName={'z-[1000]'}
|
popupClassName={'z-[1000]'}
|
||||||
/>
|
/>
|
||||||
|
@ -19,7 +19,7 @@ const VisualEditor: FC<VisualEditorProps> = (props) => {
|
|||||||
schema={schema}
|
schema={schema}
|
||||||
required={false}
|
required={false}
|
||||||
path={[]}
|
path={[]}
|
||||||
depth={1}
|
depth={0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -23,28 +23,31 @@ type SchemaNodeProps = {
|
|||||||
|
|
||||||
// Support 10 levels of indentation
|
// Support 10 levels of indentation
|
||||||
const indentPadding: Record<number, string> = {
|
const indentPadding: Record<number, string> = {
|
||||||
1: 'pl-0',
|
0: 'pl-0',
|
||||||
2: 'pl-[20px]',
|
1: 'pl-[20px]',
|
||||||
3: 'pl-[40px]',
|
2: 'pl-[40px]',
|
||||||
4: 'pl-[60px]',
|
3: 'pl-[60px]',
|
||||||
5: 'pl-[80px]',
|
4: 'pl-[80px]',
|
||||||
6: 'pl-[100px]',
|
5: 'pl-[100px]',
|
||||||
7: 'pl-[120px]',
|
6: 'pl-[120px]',
|
||||||
8: 'pl-[140px]',
|
7: 'pl-[140px]',
|
||||||
9: 'pl-[160px]',
|
8: 'pl-[160px]',
|
||||||
10: 'pl-[180px]',
|
9: 'pl-[180px]',
|
||||||
|
10: 'pl-[200px]',
|
||||||
}
|
}
|
||||||
|
|
||||||
const indentLeft: Record<number, string> = {
|
const indentLeft: Record<number, string> = {
|
||||||
2: 'left-0',
|
0: 'left-0',
|
||||||
3: 'left-[20px]',
|
1: 'left-[20px]',
|
||||||
4: 'left-[40px]',
|
2: 'left-[40px]',
|
||||||
5: 'left-[60px]',
|
3: 'left-[60px]',
|
||||||
6: 'left-[80px]',
|
4: 'left-[80px]',
|
||||||
7: 'left-[100px]',
|
5: 'left-[100px]',
|
||||||
8: 'left-[120px]',
|
6: 'left-[120px]',
|
||||||
9: 'left-[140px]',
|
7: 'left-[140px]',
|
||||||
10: 'left-[160px]',
|
8: 'left-[160px]',
|
||||||
|
9: 'left-[180px]',
|
||||||
|
10: 'left-[200px]',
|
||||||
}
|
}
|
||||||
|
|
||||||
const SchemaNode: FC<SchemaNodeProps> = ({
|
const SchemaNode: FC<SchemaNodeProps> = ({
|
||||||
@ -61,13 +64,13 @@ const SchemaNode: FC<SchemaNodeProps> = ({
|
|||||||
const isAddingNewField = useVisualEditorStore(state => state.isAddingNewField)
|
const isAddingNewField = useVisualEditorStore(state => state.isAddingNewField)
|
||||||
const advancedEditing = useVisualEditorStore(state => state.advancedEditing)
|
const advancedEditing = useVisualEditorStore(state => state.advancedEditing)
|
||||||
|
|
||||||
const { run: setHoveringPropertyDebounced } = useDebounceFn((path: string) => {
|
const { run: setHoveringPropertyDebounced } = useDebounceFn((path: string | null) => {
|
||||||
setHoveringProperty(path)
|
setHoveringProperty(path)
|
||||||
}, { wait: 50 })
|
}, { wait: 50 })
|
||||||
|
|
||||||
const hasChildren = useMemo(() => getHasChildren(schema), [schema])
|
const hasChildren = useMemo(() => getHasChildren(schema), [schema])
|
||||||
const type = useMemo(() => getFieldType(schema), [schema])
|
const type = useMemo(() => getFieldType(schema), [schema])
|
||||||
const isHovering = hoveringProperty === path.join('.') && depth > 1
|
const isHovering = hoveringProperty === path.join('.')
|
||||||
|
|
||||||
const handleExpand = () => {
|
const handleExpand = () => {
|
||||||
setIsExpanded(!isExpanded)
|
setIsExpanded(!isExpanded)
|
||||||
@ -80,16 +83,16 @@ const SchemaNode: FC<SchemaNodeProps> = ({
|
|||||||
|
|
||||||
const handleMouseLeave = () => {
|
const handleMouseLeave = () => {
|
||||||
if (advancedEditing || isAddingNewField) return
|
if (advancedEditing || isAddingNewField) return
|
||||||
setHoveringPropertyDebounced('')
|
setHoveringPropertyDebounced(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<div className={classNames('relative z-10', indentPadding[depth])}>
|
<div className={classNames('relative z-10', indentPadding[depth])}>
|
||||||
{depth > 1 && hasChildren && (
|
{depth > 0 && hasChildren && (
|
||||||
<div className={classNames(
|
<div className={classNames(
|
||||||
'flex items-center absolute top-0 w-5 h-7 px-0.5 z-10 bg-background-section-burn',
|
'flex items-center absolute top-0 w-5 h-7 px-0.5 z-10 bg-background-section-burn',
|
||||||
indentLeft[depth],
|
indentLeft[depth - 1],
|
||||||
)}>
|
)}>
|
||||||
<button
|
<button
|
||||||
onClick={handleExpand}
|
onClick={handleExpand}
|
||||||
@ -108,7 +111,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({
|
|||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
>
|
>
|
||||||
{isHovering ? (
|
{(isHovering && depth > 0) ? (
|
||||||
<EditCard
|
<EditCard
|
||||||
fields={{
|
fields={{
|
||||||
name,
|
name,
|
||||||
@ -135,7 +138,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({
|
|||||||
<div className={classNames(
|
<div className={classNames(
|
||||||
'flex justify-center w-5 absolute top-7 z-0',
|
'flex justify-center w-5 absolute top-7 z-0',
|
||||||
schema.description ? 'h-[calc(100%-3rem)]' : 'h-[calc(100%-1.75rem)]',
|
schema.description ? 'h-[calc(100%-3rem)]' : 'h-[calc(100%-1.75rem)]',
|
||||||
indentLeft[depth + 1],
|
indentLeft[depth],
|
||||||
)}>
|
)}>
|
||||||
<Divider
|
<Divider
|
||||||
type='vertical'
|
type='vertical'
|
||||||
@ -180,7 +183,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{
|
{
|
||||||
depth === 1 && !isAddingNewField && (
|
depth === 0 && !isAddingNewField && (
|
||||||
<AddField />
|
<AddField />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,8 @@ import type { SchemaRoot } from '../../../types'
|
|||||||
import { VisualEditorContext } from './context'
|
import { VisualEditorContext } from './context'
|
||||||
|
|
||||||
type VisualEditorStore = {
|
type VisualEditorStore = {
|
||||||
hoveringProperty: string | ''
|
hoveringProperty: string | null
|
||||||
setHoveringProperty: (propertyPath: string) => void
|
setHoveringProperty: (propertyPath: string | null) => void
|
||||||
isAddingNewField: boolean
|
isAddingNewField: boolean
|
||||||
setIsAddingNewField: (isAdding: boolean) => void
|
setIsAddingNewField: (isAdding: boolean) => void
|
||||||
advancedEditing: boolean
|
advancedEditing: boolean
|
||||||
@ -15,8 +15,8 @@ type VisualEditorStore = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const createVisualEditorStore = () => createStore<VisualEditorStore>(set => ({
|
export const createVisualEditorStore = () => createStore<VisualEditorStore>(set => ({
|
||||||
hoveringProperty: '',
|
hoveringProperty: null,
|
||||||
setHoveringProperty: (propertyPath: string) => set({ hoveringProperty: propertyPath }),
|
setHoveringProperty: (propertyPath: string | null) => set({ hoveringProperty: propertyPath }),
|
||||||
isAddingNewField: false,
|
isAddingNewField: false,
|
||||||
setIsAddingNewField: (isAdding: boolean) => set({ isAddingNewField: isAdding }),
|
setIsAddingNewField: (isAdding: boolean) => set({ isAddingNewField: isAdding }),
|
||||||
advancedEditing: false,
|
advancedEditing: false,
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { ArrayType, Type } from './types'
|
import { ArrayType, Type } from './types'
|
||||||
import type { ArrayItems, Field, LLMNodeType } from './types'
|
import type { ArrayItems, Field, LLMNodeType } from './types'
|
||||||
import type { ErrorObject } from 'ajv'
|
import type { Schema, ValidationError } from 'jsonschema'
|
||||||
import { validateDraft07 } from '@/public/validate-esm.mjs'
|
import { Validator } from 'jsonschema'
|
||||||
import produce from 'immer'
|
import produce from 'immer'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
export const checkNodeValid = (payload: LLMNodeType) => {
|
export const checkNodeValid = (payload: LLMNodeType) => {
|
||||||
return true
|
return true
|
||||||
@ -58,22 +59,37 @@ export const jsonToSchema = (json: any): Field => {
|
|||||||
return schema
|
return schema
|
||||||
}
|
}
|
||||||
|
|
||||||
export const checkDepth = (json: any, currentDepth = 1) => {
|
export const checkJsonDepth = (json: any) => {
|
||||||
const type = inferType(json)
|
if (!json || typeof json !== 'object')
|
||||||
if (type !== Type.object && type !== Type.array)
|
return 0
|
||||||
return currentDepth
|
|
||||||
|
|
||||||
let maxDepth = currentDepth
|
let maxDepth = 0
|
||||||
if (type === Type.object) {
|
|
||||||
Object.keys(json).forEach((key) => {
|
if (Array.isArray(json) && json[0] && typeof json[0] === 'object') {
|
||||||
const depth = checkDepth(json[key], currentDepth + 1)
|
maxDepth = checkJsonDepth(json[0]) + 1
|
||||||
maxDepth = Math.max(maxDepth, depth)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
else if (type === Type.array && json.length > 0) {
|
else if (typeof json === 'object') {
|
||||||
const depth = checkDepth(json[0], currentDepth + 1)
|
const propertyDepths = Object.values(json).map(value => checkJsonDepth(value))
|
||||||
maxDepth = Math.max(maxDepth, depth)
|
maxDepth = propertyDepths.length ? Math.max(...propertyDepths) + 1 : 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return maxDepth
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkJsonSchemaDepth = (schema: Field) => {
|
||||||
|
if (!schema || typeof schema !== 'object')
|
||||||
|
return 0
|
||||||
|
|
||||||
|
let maxDepth = 0
|
||||||
|
|
||||||
|
if (schema.type === Type.object && schema.properties) {
|
||||||
|
const propertyDepths = Object.values(schema.properties).map(value => checkJsonSchemaDepth(value))
|
||||||
|
maxDepth = propertyDepths.length ? Math.max(...propertyDepths) + 1 : 1
|
||||||
|
}
|
||||||
|
else if (schema.type === Type.array && schema.items && schema.items.type === Type.object) {
|
||||||
|
maxDepth = checkJsonSchemaDepth(schema.items) + 1
|
||||||
|
}
|
||||||
|
|
||||||
return maxDepth
|
return maxDepth
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,6 +100,169 @@ export const findPropertyWithPath = (target: any, path: string[]) => {
|
|||||||
return current
|
return current
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const draft07MetaSchema = {
|
||||||
|
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||||
|
$id: 'http://json-schema.org/draft-07/schema#',
|
||||||
|
title: 'Core schema meta-schema',
|
||||||
|
definitions: {
|
||||||
|
schemaArray: {
|
||||||
|
type: 'array',
|
||||||
|
minItems: 1,
|
||||||
|
items: { $ref: '#' },
|
||||||
|
},
|
||||||
|
nonNegativeInteger: {
|
||||||
|
type: 'integer',
|
||||||
|
minimum: 0,
|
||||||
|
},
|
||||||
|
nonNegativeIntegerDefault0: {
|
||||||
|
allOf: [
|
||||||
|
{ $ref: '#/definitions/nonNegativeInteger' },
|
||||||
|
{ default: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
simpleTypes: {
|
||||||
|
enum: [
|
||||||
|
'array',
|
||||||
|
'boolean',
|
||||||
|
'integer',
|
||||||
|
'null',
|
||||||
|
'number',
|
||||||
|
'object',
|
||||||
|
'string',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
stringArray: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
uniqueItems: true,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: ['object', 'boolean'],
|
||||||
|
properties: {
|
||||||
|
$id: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'uri-reference',
|
||||||
|
},
|
||||||
|
$schema: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'uri',
|
||||||
|
},
|
||||||
|
$ref: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'uri-reference',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
default: true,
|
||||||
|
readOnly: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
examples: {
|
||||||
|
type: 'array',
|
||||||
|
items: true,
|
||||||
|
},
|
||||||
|
multipleOf: {
|
||||||
|
type: 'number',
|
||||||
|
exclusiveMinimum: 0,
|
||||||
|
},
|
||||||
|
maximum: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
exclusiveMaximum: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
minimum: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
exclusiveMinimum: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
maxLength: { $ref: '#/definitions/nonNegativeInteger' },
|
||||||
|
minLength: { $ref: '#/definitions/nonNegativeIntegerDefault0' },
|
||||||
|
pattern: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'regex',
|
||||||
|
},
|
||||||
|
additionalItems: { $ref: '#' },
|
||||||
|
items: {
|
||||||
|
anyOf: [
|
||||||
|
{ $ref: '#' },
|
||||||
|
{ $ref: '#/definitions/schemaArray' },
|
||||||
|
],
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
maxItems: { $ref: '#/definitions/nonNegativeInteger' },
|
||||||
|
minItems: { $ref: '#/definitions/nonNegativeIntegerDefault0' },
|
||||||
|
uniqueItems: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
contains: { $ref: '#' },
|
||||||
|
maxProperties: { $ref: '#/definitions/nonNegativeInteger' },
|
||||||
|
minProperties: { $ref: '#/definitions/nonNegativeIntegerDefault0' },
|
||||||
|
required: { $ref: '#/definitions/stringArray' },
|
||||||
|
additionalProperties: { $ref: '#' },
|
||||||
|
definitions: {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: { $ref: '#' },
|
||||||
|
default: {},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: { $ref: '#' },
|
||||||
|
default: {},
|
||||||
|
},
|
||||||
|
patternProperties: {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: { $ref: '#' },
|
||||||
|
propertyNames: { format: 'regex' },
|
||||||
|
default: {},
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: {
|
||||||
|
anyOf: [
|
||||||
|
{ $ref: '#' },
|
||||||
|
{ $ref: '#/definitions/stringArray' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
propertyNames: { $ref: '#' },
|
||||||
|
const: true,
|
||||||
|
enum: {
|
||||||
|
type: 'array',
|
||||||
|
items: true,
|
||||||
|
minItems: 1,
|
||||||
|
uniqueItems: true,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
anyOf: [
|
||||||
|
{ $ref: '#/definitions/simpleTypes' },
|
||||||
|
{
|
||||||
|
type: 'array',
|
||||||
|
items: { $ref: '#/definitions/simpleTypes' },
|
||||||
|
minItems: 1,
|
||||||
|
uniqueItems: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
format: { type: 'string' },
|
||||||
|
allOf: { $ref: '#/definitions/schemaArray' },
|
||||||
|
anyOf: { $ref: '#/definitions/schemaArray' },
|
||||||
|
oneOf: { $ref: '#/definitions/schemaArray' },
|
||||||
|
not: { $ref: '#' },
|
||||||
|
},
|
||||||
|
default: true,
|
||||||
|
} as unknown as Schema
|
||||||
|
|
||||||
|
const validator = new Validator()
|
||||||
|
|
||||||
export const validateSchemaAgainstDraft7 = (schemaToValidate: any) => {
|
export const validateSchemaAgainstDraft7 = (schemaToValidate: any) => {
|
||||||
const schema = produce(schemaToValidate, (draft: any) => {
|
const schema = produce(schemaToValidate, (draft: any) => {
|
||||||
// Make sure the schema has the $schema property for draft-07
|
// Make sure the schema has the $schema property for draft-07
|
||||||
@ -91,17 +270,20 @@ export const validateSchemaAgainstDraft7 = (schemaToValidate: any) => {
|
|||||||
draft.$schema = 'http://json-schema.org/draft-07/schema#'
|
draft.$schema = 'http://json-schema.org/draft-07/schema#'
|
||||||
})
|
})
|
||||||
|
|
||||||
const valid = validateDraft07(schema)
|
const result = validator.validate(schema, draft07MetaSchema, {
|
||||||
|
nestedErrors: true,
|
||||||
|
throwError: false,
|
||||||
|
})
|
||||||
|
|
||||||
// Access errors from the validation result
|
// Access errors from the validation result
|
||||||
const errors = valid ? [] : (validateDraft07 as any).errors || []
|
const errors = result.valid ? [] : result.errors || []
|
||||||
|
|
||||||
return errors
|
return errors
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getValidationErrorMessage = (errors: ErrorObject[]) => {
|
export const getValidationErrorMessage = (errors: ValidationError[]) => {
|
||||||
const message = errors.map((error) => {
|
const message = errors.map((error) => {
|
||||||
return `Error: ${error.instancePath} ${error.message} Details: ${JSON.stringify(error.params)}`
|
return `Error: ${error.path.join('.')} ${error.message} Details: ${JSON.stringify(error.stack)}`
|
||||||
}).join('; ')
|
}).join('; ')
|
||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
@ -125,3 +307,15 @@ export const convertBooleanToString = (schema: any) => {
|
|||||||
}
|
}
|
||||||
return schema
|
return schema
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const schemaRootObject = z.object({
|
||||||
|
type: z.literal('object'),
|
||||||
|
properties: z.record(z.string(), z.any()),
|
||||||
|
required: z.array(z.string()),
|
||||||
|
additionalProperties: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const preValidateSchema = (schema: any) => {
|
||||||
|
const result = schemaRootObject.safeParse(schema)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
const fs = require('node:fs')
|
|
||||||
const path = require('node:path')
|
|
||||||
const Ajv = require('ajv')
|
|
||||||
const standaloneCode = require('ajv/dist/standalone').default
|
|
||||||
|
|
||||||
const ajv = new Ajv({
|
|
||||||
allErrors: true,
|
|
||||||
verbose: true,
|
|
||||||
code: { source: true, esm: true },
|
|
||||||
})
|
|
||||||
|
|
||||||
const moduleCode = standaloneCode(ajv, {
|
|
||||||
validateDraft07: 'http://json-schema.org/draft-07/schema#',
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see {@link https://github.com/ajv-validator/ajv/issues/2209}
|
|
||||||
*/
|
|
||||||
const preamble = [
|
|
||||||
'"use strict";',
|
|
||||||
].join('')
|
|
||||||
const imports = new Set()
|
|
||||||
const requireRegex = /const (\S+)\s*=\s*require\((.+)\)\.(\S+);/g
|
|
||||||
const replaced = moduleCode
|
|
||||||
.replace(requireRegex, (_match, p1, p2, p3) => {
|
|
||||||
imports.add(`import { ${p3} as ${p1} } from ${p2};`)
|
|
||||||
return ''
|
|
||||||
})
|
|
||||||
.replace('"use strict";', '')
|
|
||||||
|
|
||||||
const uglyOutput = [preamble, Array.from(imports).join(''), replaced].join(
|
|
||||||
'',
|
|
||||||
)
|
|
||||||
|
|
||||||
fs.writeFileSync(path.join(__dirname, '../public/validate-esm.mjs'), uglyOutput)
|
|
@ -56,7 +56,6 @@
|
|||||||
"@tanstack/react-query": "^5.60.5",
|
"@tanstack/react-query": "^5.60.5",
|
||||||
"@tanstack/react-query-devtools": "^5.60.5",
|
"@tanstack/react-query-devtools": "^5.60.5",
|
||||||
"ahooks": "^3.8.4",
|
"ahooks": "^3.8.4",
|
||||||
"ajv": "^8.17.1",
|
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
@ -74,6 +73,7 @@
|
|||||||
"immer": "^9.0.19",
|
"immer": "^9.0.19",
|
||||||
"js-audio-recorder": "^1.0.7",
|
"js-audio-recorder": "^1.0.7",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
|
"jsonschema": "^1.5.0",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"katex": "^0.16.21",
|
"katex": "^0.16.21",
|
||||||
"ky": "^1.7.2",
|
"ky": "^1.7.2",
|
||||||
|
11
web/pnpm-lock.yaml
generated
11
web/pnpm-lock.yaml
generated
@ -103,9 +103,6 @@ importers:
|
|||||||
ahooks:
|
ahooks:
|
||||||
specifier: ^3.8.4
|
specifier: ^3.8.4
|
||||||
version: 3.8.4(react@19.0.0)
|
version: 3.8.4(react@19.0.0)
|
||||||
ajv:
|
|
||||||
specifier: ^8.17.1
|
|
||||||
version: 8.17.1
|
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.0
|
specifier: ^0.7.0
|
||||||
version: 0.7.0
|
version: 0.7.0
|
||||||
@ -157,6 +154,9 @@ importers:
|
|||||||
js-cookie:
|
js-cookie:
|
||||||
specifier: ^3.0.5
|
specifier: ^3.0.5
|
||||||
version: 3.0.5
|
version: 3.0.5
|
||||||
|
jsonschema:
|
||||||
|
specifier: ^1.5.0
|
||||||
|
version: 1.5.0
|
||||||
jwt-decode:
|
jwt-decode:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
@ -5854,6 +5854,9 @@ packages:
|
|||||||
jsonfile@6.1.0:
|
jsonfile@6.1.0:
|
||||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||||
|
|
||||||
|
jsonschema@1.5.0:
|
||||||
|
resolution: {integrity: sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==}
|
||||||
|
|
||||||
jsx-ast-utils@3.3.5:
|
jsx-ast-utils@3.3.5:
|
||||||
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
@ -15253,6 +15256,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
|
|
||||||
|
jsonschema@1.5.0: {}
|
||||||
|
|
||||||
jsx-ast-utils@3.3.5:
|
jsx-ast-utils@3.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
array-includes: 3.1.8
|
array-includes: 3.1.8
|
||||||
|
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user