From 9cac14b04148ea399297f353a14004f4f95fac3c Mon Sep 17 00:00:00 2001 From: twwu Date: Tue, 25 Mar 2025 15:44:18 +0800 Subject: [PATCH] feat: implement JSON schema validation and error handling in configuration modal --- .../json-schema-config.tsx | 38 +-- .../generated-result.tsx | 18 +- .../edit-card/advanced-options.tsx | 2 +- .../components/workflow/nodes/llm/utils.ts | 301 ++++++++++++++++-- web/package.json | 1 + web/pnpm-lock.yaml | 3 + 6 files changed, 309 insertions(+), 54 deletions(-) diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx index b61579ada2..71cff8fe95 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import VisualEditor from './visual-editor' import SchemaEditor from './schema-editor' -import { jsonToSchema } from '../../utils' +import { getValidationErrorMessage, jsonToSchema, validateSchemaAgainstDraft7 } from '../../utils' import { MittProvider, VisualEditorContextProvider } from './visual-editor/context' import ErrorMessage from './error-message' @@ -59,15 +59,15 @@ const JsonSchemaConfig: FC = ({ try { const schema = JSON.parse(json) setParseError(null) - // const ajvError = validateSchemaAgainstDraft7(schema) - // if (ajvError.length > 0) { - // setValidationError(getValidationErrorMessage(ajvError)) - // return - // } - // else { - setJsonSchema(schema) - setValidationError('') - // } + const ajvError = validateSchemaAgainstDraft7(schema) + if (ajvError.length > 0) { + setValidationError(getValidationErrorMessage(ajvError)) + return + } + else { + setJsonSchema(schema) + setValidationError('') + } } catch (error) { setValidationError('') @@ -117,15 +117,15 @@ const JsonSchemaConfig: FC = ({ try { schema = JSON.parse(json) setParseError(null) - // const ajvError = validateSchemaAgainstDraft7(schema) - // if (ajvError.length > 0) { - // setValidationError(getValidationErrorMessage(ajvError)) - // return - // } - // else { - setJsonSchema(schema) - setValidationError('') - // } + const ajvError = validateSchemaAgainstDraft7(schema) + if (ajvError.length > 0) { + setValidationError(getValidationErrorMessage(ajvError)) + return + } + else { + setJsonSchema(schema) + setValidationError('') + } } catch (error) { setValidationError('') diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/generated-result.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/generated-result.tsx index 2c96702450..56a174c176 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/generated-result.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/generated-result.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import CodeEditor from '../code-editor' import ErrorMessage from '../error-message' -// import { getValidationErrorMessage, validateSchemaAgainstDraft7 } from '../../../utils' +import { getValidationErrorMessage, validateSchemaAgainstDraft7 } from '../../../utils' type GeneratedResultProps = { schema: SchemaRoot @@ -44,14 +44,14 @@ const GeneratedResult: FC = ({ const jsonSchema = useMemo(() => formatJSON(schema), [schema]) const handleApply = useCallback(() => { - // const ajvError = validateSchemaAgainstDraft7(schema) - // if (ajvError.length > 0) { - // setValidationError(getValidationErrorMessage(ajvError)) - // } - // else { - onApply() - setValidationError('') - // } + const ajvError = validateSchemaAgainstDraft7(schema) + if (ajvError.length > 0) { + setValidationError(getValidationErrorMessage(ajvError)) + } + else { + onApply() + setValidationError('') + } }, [schema, onApply]) return ( diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/advanced-options.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/advanced-options.tsx index a672fa4c02..4fff9dab75 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/advanced-options.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/advanced-options.tsx @@ -55,7 +55,7 @@ const AdvancedOptions: FC = ({ value={enumValue} onChange={handleEnumChange} onBlur={handleEnumBlur} - placeholder={'\'abcd\', 1, 1.5, \'etc\''} + placeholder={'abcd, 1, 1.5, etc.'} /> diff --git a/web/app/components/workflow/nodes/llm/utils.ts b/web/app/components/workflow/nodes/llm/utils.ts index 0daa3c4476..f0f2a69ef2 100644 --- a/web/app/components/workflow/nodes/llm/utils.ts +++ b/web/app/components/workflow/nodes/llm/utils.ts @@ -1,8 +1,7 @@ import { ArrayType, Type } from './types' import type { ArrayItems, Field, LLMNodeType } from './types' -// import Ajv, { type ErrorObject } from 'ajv' -// import draft7MetaSchema from 'ajv/dist/refs/json-schema-draft-07.json' -// import produce from 'immer' +import Ajv, { type ErrorObject } from 'ajv' +import produce from 'immer' export const checkNodeValid = (payload: LLMNodeType) => { return true @@ -83,29 +82,281 @@ export const findPropertyWithPath = (target: any, path: string[]) => { return current } -// const ajv = new Ajv({ -// allErrors: true, -// verbose: true, -// validateSchema: true, -// meta: false, -// }) -// ajv.addMetaSchema(draft7MetaSchema) +const draft7MetaSchema = { + $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', + }, + $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 -// if (!draft.$schema) -// draft.$schema = 'http://json-schema.org/draft-07/schema#' -// }) + }, + }, + properties: { + type: 'object', + 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 message = errors.map((error) => { -// return `Error: ${error.instancePath} ${error.message} Details: ${JSON.stringify(error.params)}` -// }).join('; ') -// return message -// } +const ajv = new Ajv({ + allErrors: true, + verbose: true, + validateSchema: true, + 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 +} diff --git a/web/package.json b/web/package.json index 2a55c558cf..915558b7e4 100644 --- a/web/package.json +++ b/web/package.json @@ -55,6 +55,7 @@ "@tanstack/react-query": "^5.60.5", "@tanstack/react-query-devtools": "^5.60.5", "ahooks": "^3.8.4", + "ajv": "^8.17.1", "class-variance-authority": "^0.7.0", "classnames": "^2.5.1", "copy-to-clipboard": "^3.3.3", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index eb31f688d8..9c4bc51460 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -103,6 +103,9 @@ importers: ahooks: specifier: ^3.8.4 version: 3.8.4(react@19.0.0) + ajv: + specifier: ^8.17.1 + version: 8.17.1 class-variance-authority: specifier: ^0.7.0 version: 0.7.0