feat: implement JSON schema depth validation and update related components

This commit is contained in:
twwu 2025-03-18 15:09:08 +08:00
parent ffe08a35a4
commit 7a647cf18e
7 changed files with 65 additions and 32 deletions

View File

@ -6,6 +6,8 @@ import { RiClipboardLine, RiCloseLine, RiErrorWarningFill, RiIndentIncrease } fr
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import { Editor } from '@monaco-editor/react' import { Editor } from '@monaco-editor/react'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { checkDepth } from '../../utils'
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
type JsonImporterProps = { type JsonImporterProps = {
onSubmit: (schema: string) => void onSubmit: (schema: string) => void
@ -71,8 +73,17 @@ const JsonImporter: FC<JsonImporterProps> = ({
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {
try { try {
const parsedJSON = JSON.parse(json) const parsedJSON = JSON.parse(json)
const maxDepth = checkDepth(parsedJSON)
if (maxDepth > JSON_SCHEMA_MAX_DEPTH) {
setParseError({
type: 'error',
message: `Schema exceeds maximum depth of ${JSON_SCHEMA_MAX_DEPTH}.`,
})
return
}
onSubmit(parsedJSON) onSubmit(parsedJSON)
setParseError(null) setParseError(null)
setOpen(false)
} }
catch (e: any) { catch (e: any) {
if (e instanceof SyntaxError) if (e instanceof SyntaxError)

View File

@ -281,6 +281,7 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
}) })
useSubscribe('fieldChange', (params) => { useSubscribe('fieldChange', (params) => {
let samePropertyNameError = false
const { parentPath, oldFields, fields } = params as ChangeEventParams const { parentPath, oldFields, fields } = params as ChangeEventParams
const newSchema = produce(jsonSchema, (draft) => { const newSchema = produce(jsonSchema, (draft) => {
const parentSchema = findPropertyWithPath(draft, parentPath) as Field const parentSchema = findPropertyWithPath(draft, parentPath) as Field
@ -295,7 +296,7 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
type: 'error', type: 'error',
message: 'Property name already exists', message: 'Property name already exists',
}) })
return samePropertyNameError = true
} }
const newProperties = Object.entries(properties).reduce((acc, [key, value]) => { const newProperties = Object.entries(properties).reduce((acc, [key, value]) => {
@ -384,8 +385,7 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
type: 'error', type: 'error',
message: 'Property name already exists', message: 'Property name already exists',
}) })
emit('restorePropertyName') samePropertyNameError = true
return
} }
const newProperties = Object.entries(properties).reduce((acc, [key, value]) => { const newProperties = Object.entries(properties).reduce((acc, [key, value]) => {
@ -463,6 +463,7 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
schema.enum = fields.enum schema.enum = fields.enum
} }
}) })
if (samePropertyNameError) return
setJsonSchema(newSchema) setJsonSchema(newSchema)
emit('fieldChangeSuccess') emit('fieldChangeSuccess')
}) })

View File

@ -14,6 +14,7 @@ import { useJsonSchemaConfigStore } from '../../store'
import { useMittContext } from '../../context' import { useMittContext } from '../../context'
import produce from 'immer' import produce from 'immer'
import { useUnmount } from 'ahooks' import { useUnmount } from 'ahooks'
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
export type EditData = { export type EditData = {
name: string name: string
@ -46,8 +47,6 @@ const TYPE_OPTIONS = [
{ value: ArrayType.object, text: 'array[object]' }, { value: ArrayType.object, text: 'array[object]' },
] ]
const DEPTH_LIMIT = 10
const EditCard: FC<EditCardProps> = ({ const EditCard: FC<EditCardProps> = ({
fields, fields,
depth, depth,
@ -64,7 +63,7 @@ const EditCard: FC<EditCardProps> = ({
const { emit, useSubscribe } = useMittContext() const { emit, useSubscribe } = useMittContext()
const blurWithActions = useRef(false) const blurWithActions = useRef(false)
const disableAddBtn = fields.type !== Type.object && fields.type !== ArrayType.object && depth < DEPTH_LIMIT const disableAddBtn = depth >= JSON_SCHEMA_MAX_DEPTH || (fields.type !== Type.object && fields.type !== ArrayType.object)
const hasAdvancedOptions = fields.type === Type.string || fields.type === Type.number const hasAdvancedOptions = fields.type === Type.string || fields.type === Type.number
const isAdvancedEditing = advancedEditing || isAddingNewField const isAdvancedEditing = advancedEditing || isAddingNewField

View File

@ -16,7 +16,7 @@ const VisualEditor: FC<VisualEditorProps> = ({
schema={schema} schema={schema}
required={false} required={false}
path={[]} path={[]}
depth={0} depth={1}
/> />
</div> </div>
) )

View File

@ -10,6 +10,7 @@ import Card from './card'
import { useJsonSchemaConfigStore } from '../store' import { useJsonSchemaConfigStore } from '../store'
import { useDebounceFn } from 'ahooks' import { useDebounceFn } from 'ahooks'
import AddField from './add-field' import AddField from './add-field'
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
type SchemaNodeProps = { type SchemaNodeProps = {
name: string name: string
@ -22,28 +23,28 @@ type SchemaNodeProps = {
// Support 10 levels of indentation // Support 10 levels of indentation
const indentPadding: Record<number, string> = { const indentPadding: Record<number, string> = {
0: 'pl-0', 1: 'pl-0',
1: 'pl-[20px]', 2: 'pl-[20px]',
2: 'pl-[40px]', 3: 'pl-[40px]',
3: 'pl-[60px]', 4: 'pl-[60px]',
4: 'pl-[80px]', 5: 'pl-[80px]',
5: 'pl-[100px]', 6: 'pl-[100px]',
6: 'pl-[120px]', 7: 'pl-[120px]',
7: 'pl-[140px]', 8: 'pl-[140px]',
8: 'pl-[160px]', 9: 'pl-[160px]',
9: 'pl-[180px]', 10: 'pl-[180px]',
} }
const indentLeft: Record<number, string> = { const indentLeft: Record<number, string> = {
1: 'left-0', 2: 'left-0',
2: 'left-[20px]', 3: 'left-[20px]',
3: 'left-[40px]', 4: 'left-[40px]',
4: 'left-[60px]', 5: 'left-[60px]',
5: 'left-[80px]', 6: 'left-[80px]',
6: 'left-[100px]', 7: 'left-[100px]',
7: 'left-[120px]', 8: 'left-[120px]',
8: 'left-[140px]', 9: 'left-[140px]',
9: 'left-[160px]', 10: 'left-[160px]',
} }
const SchemaNode: FC<SchemaNodeProps> = ({ const SchemaNode: FC<SchemaNodeProps> = ({
@ -66,7 +67,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({
const hasChildren = getHasChildren(schema) const hasChildren = getHasChildren(schema)
const type = getFieldType(schema) const type = getFieldType(schema)
const isHovering = hoveringProperty === path.join('.') && depth > 0 const isHovering = hoveringProperty === path.join('.') && depth > 1
const handleExpand = () => { const handleExpand = () => {
setIsExpanded(!isExpanded) setIsExpanded(!isExpanded)
@ -85,7 +86,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({
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 > 0 && hasChildren && ( {depth > 1 && 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],
@ -139,7 +140,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({
<Divider type='vertical' className='bg-divider-subtle mx-0' /> <Divider type='vertical' className='bg-divider-subtle mx-0' />
</div> </div>
{isExpanded && hasChildren && ( {isExpanded && hasChildren && depth < JSON_SCHEMA_MAX_DEPTH && (
<> <>
{schema.type === Type.object && schema.properties && ( {schema.type === Type.object && schema.properties && (
Object.entries(schema.properties).map(([key, childSchema]) => ( Object.entries(schema.properties).map(([key, childSchema]) => (
@ -176,7 +177,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({
)} )}
{ {
depth === 0 && !isAddingNewField && ( depth === 1 && !isAddingNewField && (
<AddField /> <AddField />
) )
} }

View File

@ -32,12 +32,12 @@ export const inferType = (value: any): Type => {
return Type.string return Type.string
} }
export function jsonToSchema(json: any): Field { export const jsonToSchema = (json: any): Field => {
const schema: Field = { const schema: Field = {
type: inferType(json), type: inferType(json),
} }
if (schema.type === 'object') { if (schema.type === Type.object) {
schema.properties = {} schema.properties = {}
schema.required = [] schema.required = []
schema.additionalProperties = false schema.additionalProperties = false
@ -53,3 +53,22 @@ export function jsonToSchema(json: any): Field {
return schema return schema
} }
export const checkDepth = (json: any, currentDepth = 1) => {
const type = inferType(json)
if (type !== Type.object && type !== Type.array)
return currentDepth
let maxDepth = currentDepth
if (type === Type.object) {
Object.keys(json).forEach((key) => {
const depth = checkDepth(json[key], currentDepth + 1)
maxDepth = Math.max(maxDepth, depth)
})
}
else if (type === Type.array && json.length > 0) {
const depth = checkDepth(json[0], currentDepth + 1)
maxDepth = Math.max(maxDepth, depth)
}
return maxDepth
}

View File

@ -276,3 +276,5 @@ export const GITHUB_ACCESS_TOKEN = process.env.NEXT_PUBLIC_GITHUB_ACCESS_TOKEN |
export const SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS = '.difypkg,.difybndl' export const SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS = '.difypkg,.difybndl'
export const FULL_DOC_PREVIEW_LENGTH = 50 export const FULL_DOC_PREVIEW_LENGTH = 50
export const JSON_SCHEMA_MAX_DEPTH = 10