feat: implement JSON schema validation and error handling in configuration modal

This commit is contained in:
twwu 2025-03-25 15:44:18 +08:00
parent 5efdd6e297
commit 9cac14b041
6 changed files with 309 additions and 54 deletions

View File

@ -9,7 +9,7 @@ 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 { jsonToSchema } from '../../utils' import { getValidationErrorMessage, jsonToSchema, 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'
@ -59,15 +59,15 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
try { try {
const schema = JSON.parse(json) const schema = JSON.parse(json)
setParseError(null) setParseError(null)
// const ajvError = validateSchemaAgainstDraft7(schema) const ajvError = validateSchemaAgainstDraft7(schema)
// if (ajvError.length > 0) { if (ajvError.length > 0) {
// setValidationError(getValidationErrorMessage(ajvError)) setValidationError(getValidationErrorMessage(ajvError))
// return return
// } }
// else { else {
setJsonSchema(schema) setJsonSchema(schema)
setValidationError('') setValidationError('')
// } }
} }
catch (error) { catch (error) {
setValidationError('') setValidationError('')
@ -117,15 +117,15 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
try { try {
schema = JSON.parse(json) schema = JSON.parse(json)
setParseError(null) setParseError(null)
// const ajvError = validateSchemaAgainstDraft7(schema) const ajvError = validateSchemaAgainstDraft7(schema)
// if (ajvError.length > 0) { if (ajvError.length > 0) {
// setValidationError(getValidationErrorMessage(ajvError)) setValidationError(getValidationErrorMessage(ajvError))
// return return
// } }
// else { else {
setJsonSchema(schema) setJsonSchema(schema)
setValidationError('') setValidationError('')
// } }
} }
catch (error) { catch (error) {
setValidationError('') setValidationError('')

View File

@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import CodeEditor from '../code-editor' import CodeEditor from '../code-editor'
import ErrorMessage from '../error-message' import ErrorMessage from '../error-message'
// import { getValidationErrorMessage, validateSchemaAgainstDraft7 } from '../../../utils' import { getValidationErrorMessage, validateSchemaAgainstDraft7 } from '../../../utils'
type GeneratedResultProps = { type GeneratedResultProps = {
schema: SchemaRoot schema: SchemaRoot
@ -44,14 +44,14 @@ 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 ajvError = validateSchemaAgainstDraft7(schema)
// if (ajvError.length > 0) { if (ajvError.length > 0) {
// setValidationError(getValidationErrorMessage(ajvError)) setValidationError(getValidationErrorMessage(ajvError))
// } }
// else { else {
onApply() onApply()
setValidationError('') setValidationError('')
// } }
}, [schema, onApply]) }, [schema, onApply])
return ( return (

View File

@ -55,7 +55,7 @@ const AdvancedOptions: FC<AdvancedOptionsProps> = ({
value={enumValue} value={enumValue}
onChange={handleEnumChange} onChange={handleEnumChange}
onBlur={handleEnumBlur} onBlur={handleEnumBlur}
placeholder={'\'abcd\', 1, 1.5, \'etc\''} placeholder={'abcd, 1, 1.5, etc.'}
/> />
</div> </div>
</div> </div>

View File

@ -1,8 +1,7 @@
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 Ajv, { type ErrorObject } from 'ajv' import Ajv, { type ErrorObject } from 'ajv'
// import draft7MetaSchema from 'ajv/dist/refs/json-schema-draft-07.json' import produce from 'immer'
// import produce from 'immer'
export const checkNodeValid = (payload: LLMNodeType) => { export const checkNodeValid = (payload: LLMNodeType) => {
return true return true
@ -83,29 +82,281 @@ export const findPropertyWithPath = (target: any, path: string[]) => {
return current return current
} }
// const ajv = new Ajv({ const draft7MetaSchema = {
// allErrors: true, $schema: 'http://json-schema.org/draft-07/schema#',
// verbose: true, $id: 'http://json-schema.org/draft-07/schema#',
// validateSchema: true, title: 'Core schema meta-schema',
// meta: false, definitions: {
// }) schemaArray: {
// ajv.addMetaSchema(draft7MetaSchema) 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',
},
$comment: {
type: 'string',
},
title: {
type: 'string',
},
description: {
type: 'string',
},
default: true,
readOnly: {
type: 'boolean',
default: false,
},
writeOnly: {
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: {
// export const validateSchemaAgainstDraft7 = (schemaToValidate: any) => { },
// const schema = produce(schemaToValidate, (draft: any) => { },
// // Make sure the schema has the $schema property for draft-07 properties: {
// if (!draft.$schema) type: 'object',
// draft.$schema = 'http://json-schema.org/draft-07/schema#' additionalProperties: {
// }) $ref: '#',
},
default: {
// const valid = ajv.validateSchema(schema) },
},
patternProperties: {
type: 'object',
additionalProperties: {
$ref: '#',
},
propertyNames: {
format: 'regex',
},
default: {
// return valid ? [] : ajv.errors || [] },
// } },
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',
},
contentMediaType: {
type: 'string',
},
contentEncoding: {
type: 'string',
},
if: {
$ref: '#',
},
then: {
$ref: '#',
},
else: {
$ref: '#',
},
allOf: {
$ref: '#/definitions/schemaArray',
},
anyOf: {
$ref: '#/definitions/schemaArray',
},
oneOf: {
$ref: '#/definitions/schemaArray',
},
not: {
$ref: '#',
},
},
default: true,
}
// export const getValidationErrorMessage = (errors: ErrorObject[]) => { const ajv = new Ajv({
// const message = errors.map((error) => { allErrors: true,
// return `Error: ${error.instancePath} ${error.message} Details: ${JSON.stringify(error.params)}` verbose: true,
// }).join('; ') validateSchema: true,
// return message meta: false,
// } })
ajv.addMetaSchema(draft7MetaSchema)
export const validateSchemaAgainstDraft7 = (schemaToValidate: any) => {
const schema = produce(schemaToValidate, (draft: any) => {
// Make sure the schema has the $schema property for draft-07
if (!draft.$schema)
draft.$schema = 'http://json-schema.org/draft-07/schema#'
})
const valid = ajv.validateSchema(schema)
return valid ? [] : ajv.errors || []
}
export const getValidationErrorMessage = (errors: ErrorObject[]) => {
const message = errors.map((error) => {
return `Error: ${error.instancePath} ${error.message} Details: ${JSON.stringify(error.params)}`
}).join('; ')
return message
}

View File

@ -55,6 +55,7 @@
"@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",

3
web/pnpm-lock.yaml generated
View File

@ -103,6 +103,9 @@ 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