{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "query-builder", "title": "Query Builder", "description": "Schema-driven nested query AST editor with immutable actions, draft state, validation, searchable fields, and custom value editors", "dependencies": ["@base-ui/react", "daisyui", "lucide-react"], "files": [ { "path": "registry/default/ui/query-builder/query-model.ts", "content": "export type QueryJsonPrimitive = string | number | boolean | null;\nexport type QueryJsonValue = QueryJsonPrimitive | QueryJsonValue[] | { [key: string]: QueryJsonValue };\n\nexport type QueryCombinator = 'and' | 'or';\nexport type QueryFieldKind =\n\t| 'string'\n\t| 'number'\n\t| 'integer'\n\t| 'boolean'\n\t| 'date'\n\t| 'datetime'\n\t| 'enum'\n\t| 'array'\n\t| 'unknown';\nexport type QueryOperatorCardinality = 'none' | 'single' | 'pair' | 'many';\n\nexport type QueryOption = {\n\tvalue: QueryJsonPrimitive;\n\tlabel: string;\n\tdisabled?: boolean;\n};\n\nexport type QueryField = {\n\tname: string;\n\tlabel: string;\n\tdescription?: string;\n\tkind: QueryFieldKind;\n\tgroup?: string;\n\trequired?: boolean;\n\toperators?: readonly string[];\n\tdefaultOperator?: string;\n\tdefaultValue?: QueryJsonValue;\n\toptions?: readonly QueryOption[];\n\titemKind?: QueryFieldKind;\n\teditor?: string;\n\tplaceholder?: string;\n\tmeta?: Record;\n};\n\nexport type QueryOperator = {\n\tname: string;\n\tlabel: string;\n\tcardinality: QueryOperatorCardinality;\n\tkinds?: readonly QueryFieldKind[];\n\teditor?: string;\n};\n\nexport type QueryRule = {\n\ttype: 'rule';\n\tid: string;\n\tfield: string;\n\toperator: string;\n\tvalue: QueryJsonValue;\n};\n\nexport type QueryGroup = {\n\ttype: 'group';\n\tid: string;\n\tcombinator: QueryCombinator;\n\tnegated: boolean;\n\tchildren: QueryNode[];\n};\n\nexport type QueryNode = QueryGroup | QueryRule;\nexport type QueryIdFactory = (kind: QueryNode['type']) => string;\n\nexport const defaultQueryOperators = [\n\t{ name: 'eq', label: '等于', cardinality: 'single' },\n\t{ name: 'ne', label: '不等于', cardinality: 'single' },\n\t{ name: 'contains', label: '包含', cardinality: 'single', kinds: ['string', 'array'] },\n\t{ name: 'notContains', label: '不包含', cardinality: 'single', kinds: ['string', 'array'] },\n\t{ name: 'startsWith', label: '开头为', cardinality: 'single', kinds: ['string'] },\n\t{ name: 'endsWith', label: '结尾为', cardinality: 'single', kinds: ['string'] },\n\t{ name: 'gt', label: '大于', cardinality: 'single', kinds: ['number', 'integer', 'date', 'datetime'] },\n\t{ name: 'gte', label: '大于等于', cardinality: 'single', kinds: ['number', 'integer', 'date', 'datetime'] },\n\t{ name: 'lt', label: '小于', cardinality: 'single', kinds: ['number', 'integer', 'date', 'datetime'] },\n\t{ name: 'lte', label: '小于等于', cardinality: 'single', kinds: ['number', 'integer', 'date', 'datetime'] },\n\t{ name: 'between', label: '介于', cardinality: 'pair', kinds: ['number', 'integer', 'date', 'datetime'] },\n\t{ name: 'in', label: '属于任一', cardinality: 'many' },\n\t{ name: 'notIn', label: '不属于任一', cardinality: 'many' },\n\t{ name: 'containsAny', label: '包含任一', cardinality: 'many', kinds: ['array'] },\n\t{ name: 'containsAll', label: '包含全部', cardinality: 'many', kinds: ['array'] },\n\t{ name: 'isNull', label: '为空', cardinality: 'none' },\n\t{ name: 'isNotNull', label: '不为空', cardinality: 'none' },\n] as const satisfies readonly QueryOperator[];\n\nconst defaultOperatorsByKind: Record = {\n\tstring: ['eq', 'ne', 'contains', 'notContains', 'startsWith', 'endsWith', 'in', 'notIn', 'isNull', 'isNotNull'],\n\tnumber: ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'between', 'in', 'notIn', 'isNull', 'isNotNull'],\n\tinteger: ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'between', 'in', 'notIn', 'isNull', 'isNotNull'],\n\tboolean: ['eq', 'ne', 'isNull', 'isNotNull'],\n\tdate: ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'between', 'isNull', 'isNotNull'],\n\tdatetime: ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'between', 'isNull', 'isNotNull'],\n\tenum: ['eq', 'ne', 'in', 'notIn', 'isNull', 'isNotNull'],\n\tarray: ['contains', 'notContains', 'containsAny', 'containsAll', 'isNull', 'isNotNull'],\n\tunknown: ['eq', 'ne', 'isNull', 'isNotNull'],\n};\n\nlet fallbackId = 0;\n\nexport function createQueryId(kind: QueryNode['type'] = 'rule') {\n\tif (typeof globalThis.crypto?.randomUUID === 'function') return `${kind}-${globalThis.crypto.randomUUID()}`;\n\tfallbackId += 1;\n\treturn `${kind}-${Date.now().toString(36)}-${fallbackId.toString(36)}`;\n}\n\nexport function createQueryIdFactory(prefix = 'query'): QueryIdFactory {\n\tlet sequence = 0;\n\treturn (kind) => {\n\t\tsequence += 1;\n\t\treturn `${prefix}-${kind}-${sequence}`;\n\t};\n}\n\nexport function createQueryGroup(\n\tid: string,\n\toptions: Partial> = {},\n): QueryGroup {\n\treturn {\n\t\ttype: 'group',\n\t\tid,\n\t\tcombinator: options.combinator ?? 'and',\n\t\tnegated: options.negated ?? false,\n\t\tchildren: options.children ? [...options.children] : [],\n\t};\n}\n\nexport function createQueryRule(id: string, field: string, operator: string, value: QueryJsonValue = null): QueryRule {\n\treturn { type: 'rule', id, field, operator, value };\n}\n\nexport function getQueryOperatorMap(operators: readonly QueryOperator[] = defaultQueryOperators) {\n\treturn new Map(operators.map((operator) => [operator.name, operator]));\n}\n\nexport function getOperatorsForField(\n\tfield: QueryField,\n\toperators: readonly QueryOperator[] = defaultQueryOperators,\n): QueryOperator[] {\n\tconst allowed = new Set(field.operators ?? defaultOperatorsByKind[field.kind]);\n\treturn operators.filter(\n\t\t(operator) => allowed.has(operator.name) && (!operator.kinds || operator.kinds.includes(field.kind)),\n\t);\n}\n\nexport function getDefaultOperatorForField(\n\tfield: QueryField,\n\toperators: readonly QueryOperator[] = defaultQueryOperators,\n) {\n\tconst available = getOperatorsForField(field, operators);\n\treturn available.find((operator) => operator.name === field.defaultOperator) ?? available[0];\n}\n\nexport function getDefaultValueForOperator(field: QueryField, operator?: QueryOperator): QueryJsonValue {\n\tif (!operator) return null;\n\tif (field.defaultValue !== undefined && operator.cardinality === 'single' && field.kind !== 'array')\n\t\treturn cloneQueryJsonValue(field.defaultValue);\n\tswitch (operator.cardinality) {\n\t\tcase 'none':\n\t\t\treturn null;\n\t\tcase 'pair':\n\t\t\treturn [null, null];\n\t\tcase 'many':\n\t\t\treturn [];\n\t\tcase 'single':\n\t\t\treturn field.kind === 'boolean' ? true : null;\n\t}\n}\n\nexport function createQueryRuleForField(\n\tfield: QueryField,\n\tid: string,\n\toperators: readonly QueryOperator[] = defaultQueryOperators,\n) {\n\tconst operator = getDefaultOperatorForField(field, operators);\n\treturn createQueryRule(id, field.name, operator?.name ?? '', getDefaultValueForOperator(field, operator));\n}\n\nexport function cloneQueryJsonValue(value: QueryJsonValue): QueryJsonValue {\n\tif (!isQueryJsonValue(value)) throw new TypeError('Cannot clone an invalid runtime JSON value.');\n\treturn cloneQueryJsonValueUnchecked(value, new WeakMap());\n}\n\nfunction cloneQueryJsonValueUnchecked(value: QueryJsonValue, clones: WeakMap): QueryJsonValue {\n\tif (value && typeof value === 'object') {\n\t\tconst existing = clones.get(value);\n\t\tif (existing !== undefined) return existing;\n\t}\n\tif (Array.isArray(value)) {\n\t\tconst output: QueryJsonValue[] = [];\n\t\tclones.set(value, output);\n\t\tfor (const item of value) output.push(cloneQueryJsonValueUnchecked(item, clones));\n\t\treturn output;\n\t}\n\tif (value && typeof value === 'object') {\n\t\tconst output = Object.create(Object.getPrototypeOf(value)) as Record;\n\t\tclones.set(value, output);\n\t\tfor (const [key, item] of Object.entries(value)) {\n\t\t\tObject.defineProperty(output, key, {\n\t\t\t\tvalue: cloneQueryJsonValueUnchecked(item, clones),\n\t\t\t\tenumerable: true,\n\t\t\t\tconfigurable: true,\n\t\t\t\twritable: true,\n\t\t\t});\n\t\t}\n\t\treturn output;\n\t}\n\treturn value;\n}\n\nexport function isQueryJsonValue(value: unknown): value is QueryJsonValue {\n\tconst ancestors = new WeakSet();\n\tconst visitedDepth = new WeakMap();\n\tconst stack: { value: unknown; depth: number; exiting?: boolean }[] = [{ value, depth: 0 }];\n\twhile (stack.length) {\n\t\tconst current = stack.pop() as { value: unknown; depth: number; exiting?: boolean };\n\t\tconst item = current.value;\n\t\tif (current.exiting) {\n\t\t\tancestors.delete(item as object);\n\t\t\tcontinue;\n\t\t}\n\t\tif (item === null || typeof item === 'string' || typeof item === 'boolean') continue;\n\t\tif (typeof item === 'number') {\n\t\t\tif (!Number.isFinite(item)) return false;\n\t\t\tcontinue;\n\t\t}\n\t\tif (!item || typeof item !== 'object') return false;\n\t\tif (!Array.isArray(item)) {\n\t\t\tconst prototype = Object.getPrototypeOf(item);\n\t\t\tif (prototype !== Object.prototype && prototype !== null) return false;\n\t\t}\n\t\tif (current.depth > 64 || ancestors.has(item)) return false;\n\t\tconst previousDepth = visitedDepth.get(item);\n\t\tif (previousDepth !== undefined && previousDepth >= current.depth) continue;\n\t\tvisitedDepth.set(item, current.depth);\n\t\tconst children: unknown[] = [];\n\t\tif (Array.isArray(item)) {\n\t\t\tfor (let index = 0; index < item.length; index += 1) {\n\t\t\t\tif (!(index in item)) return false;\n\t\t\t\tchildren.push(item[index]);\n\t\t\t}\n\t\t} else {\n\t\t\tchildren.push(...Object.values(item));\n\t\t}\n\t\tancestors.add(item);\n\t\tstack.push({ value: item, depth: current.depth, exiting: true });\n\t\tfor (let index = children.length - 1; index >= 0; index -= 1) {\n\t\t\tstack.push({ value: children[index], depth: current.depth + 1 });\n\t\t}\n\t}\n\treturn true;\n}\n", "type": "registry:lib", "target": "@components/query-builder/query-model.ts" }, { "path": "registry/default/ui/query-builder/query-state.ts", "content": "import type {\n\tQueryField,\n\tQueryGroup,\n\tQueryIdFactory,\n\tQueryJsonPrimitive,\n\tQueryJsonValue,\n\tQueryNode,\n\tQueryOperator,\n\tQueryRule,\n} from './query-model';\nimport {\n\tcloneQueryJsonValue,\n\tdefaultQueryOperators,\n\tgetOperatorsForField,\n\tgetQueryOperatorMap,\n\tisQueryJsonValue,\n} from './query-model';\n\nexport type QueryNodeLocation = {\n\tnode: QueryNode;\n\tparent: QueryGroup | null;\n\tindex: number;\n\tdepth: number;\n\tpath: number[];\n};\n\nexport type QueryAction =\n\t| { type: 'insert-node'; parentId: string; node: QueryNode; index?: number }\n\t| { type: 'update-rule'; id: string; patch: Partial> }\n\t| { type: 'update-group'; id: string; patch: Partial> }\n\t| { type: 'remove-node'; id: string }\n\t| { type: 'move-node'; id: string; direction: 'up' | 'down' }\n\t| { type: 'clear-group'; id: string }\n\t| { type: 'replace-query'; query: QueryGroup };\n\nexport type QueryReducerOptions = {\n\tmaxDepth?: number;\n};\n\nexport type QueryValidationIssueCode =\n\t| 'invalid-query'\n\t| 'duplicate-id'\n\t| 'max-depth'\n\t| 'unknown-field'\n\t| 'unknown-operator'\n\t| 'operator-not-allowed'\n\t| 'invalid-cardinality'\n\t| 'missing-value'\n\t| 'invalid-value';\n\nexport type QueryValidationIssue = {\n\tcode: QueryValidationIssueCode;\n\tnodeId: string;\n\tpath: number[];\n\tmessage: string;\n};\n\nexport type ValidateQueryOptions = {\n\tfields: readonly QueryField[];\n\toperators?: readonly QueryOperator[];\n\tmaxDepth?: number;\n};\n\nexport function walkQuery(query: QueryGroup, visitor: (location: QueryNodeLocation) => void) {\n\tconst stack: QueryNodeLocation[] = [{ node: query, parent: null, index: -1, depth: 0, path: [] }];\n\tconst seen = new WeakSet();\n\twhile (stack.length) {\n\t\tconst location = stack.pop() as QueryNodeLocation;\n\t\tconst runtimeNode: unknown = location.node;\n\t\tif (\n\t\t\tlocation.depth > 64 ||\n\t\t\t!isRecord(runtimeNode) ||\n\t\t\t(runtimeNode.type !== 'group' && runtimeNode.type !== 'rule') ||\n\t\t\ttypeof runtimeNode.id !== 'string' ||\n\t\t\tseen.has(runtimeNode)\n\t\t)\n\t\t\tcontinue;\n\t\tif (runtimeNode.type === 'group' && !Array.isArray(runtimeNode.children)) continue;\n\t\tseen.add(runtimeNode);\n\t\tvisitor(location);\n\t\tif (location.node.type !== 'group') continue;\n\t\tfor (let index = location.node.children.length - 1; index >= 0; index -= 1) {\n\t\t\tif (!(index in location.node.children)) continue;\n\t\t\tstack.push({\n\t\t\t\tnode: location.node.children[index],\n\t\t\t\tparent: location.node,\n\t\t\t\tindex,\n\t\t\t\tdepth: location.depth + 1,\n\t\t\t\tpath: [...location.path, index],\n\t\t\t});\n\t\t}\n\t}\n}\n\nexport function getQueryNodeLocation(query: QueryGroup, id: string) {\n\tlet found: QueryNodeLocation | undefined;\n\twalkQuery(query, (location) => {\n\t\tif (!found && location.node.id === id) found = location;\n\t});\n\treturn found;\n}\n\nexport function findQueryNode(query: QueryGroup, id: string) {\n\treturn getQueryNodeLocation(query, id)?.node;\n}\n\nexport function countQueryRules(node: QueryNode): number {\n\tconst runtimeNode: unknown = node;\n\tif (!isRecord(runtimeNode) || (runtimeNode.type !== 'rule' && runtimeNode.type !== 'group')) return 0;\n\tif (runtimeNode.type === 'rule') return isQueryNodeValue(runtimeNode) ? 1 : 0;\n\tlet count = 0;\n\twalkQuery(runtimeNode as QueryGroup, ({ node: current }) => {\n\t\tif (current.type === 'rule') count += 1;\n\t});\n\treturn count;\n}\n\nexport function getQueryDepth(node: QueryNode): number {\n\tconst runtimeNode: unknown = node;\n\tif (!isRecord(runtimeNode) || runtimeNode.type !== 'group') return 0;\n\tlet maxDepth = 0;\n\twalkQuery(runtimeNode as QueryGroup, ({ node: current, depth }) => {\n\t\tif (current.type === 'group') maxDepth = Math.max(maxDepth, depth);\n\t});\n\treturn maxDepth;\n}\n\nexport function cloneQueryNode(node: QueryNode, createId: QueryIdFactory): QueryNode {\n\tif (!isQueryNodeValue(node)) throw new TypeError('Cannot clone an invalid runtime query node.');\n\treturn cloneQueryNodeUnchecked(node, createId);\n}\n\nfunction cloneQueryNodeUnchecked(node: QueryNode, createId: QueryIdFactory): QueryNode {\n\tif (node.type === 'rule') {\n\t\treturn { ...node, id: createId('rule'), value: cloneQueryJsonValue(node.value) };\n\t}\n\treturn {\n\t\t...node,\n\t\tid: createId('group'),\n\t\tchildren: node.children.map((child) => cloneQueryNodeUnchecked(child, createId)),\n\t};\n}\n\nexport function cloneQuery(query: QueryGroup) {\n\tif (!isQueryGroupValue(query)) throw new TypeError('Cannot clone an invalid runtime query document.');\n\treturn cloneNodePreservingIds(query) as QueryGroup;\n}\n\nfunction cloneNodePreservingIds(node: QueryNode): QueryNode {\n\treturn node.type === 'rule'\n\t\t? { ...node, value: cloneQueryJsonValue(node.value) }\n\t\t: { ...node, children: node.children.map(cloneNodePreservingIds) };\n}\n\nexport function areQueriesEqual(left: QueryNode, right: QueryNode): boolean {\n\tif (!isQueryNodeValue(left) || !isQueryNodeValue(right)) return false;\n\treturn areQueryNodesEqualUnchecked(left, right, { pairs: new WeakMap(), remaining: 100_000 });\n}\n\ntype EqualityState = { pairs: WeakMap>; remaining: number };\n\nfunction rememberPair(left: object, right: object, state: EqualityState) {\n\tconst rights = state.pairs.get(left);\n\tif (rights?.has(right)) return true;\n\tif (rights) rights.add(right);\n\telse state.pairs.set(left, new WeakSet([right]));\n\treturn false;\n}\n\nfunction areQueryNodesEqualUnchecked(left: QueryNode, right: QueryNode, state: EqualityState): boolean {\n\tstate.remaining -= 1;\n\tif (state.remaining < 0) return false;\n\tif (rememberPair(left, right, state)) return true;\n\tif (left.type !== right.type || left.id !== right.id) return false;\n\tif (left.type === 'rule' && right.type === 'rule') {\n\t\treturn (\n\t\t\tleft.field === right.field && left.operator === right.operator && valuesEqual(left.value, right.value, state)\n\t\t);\n\t}\n\tif (left.type === 'group' && right.type === 'group') {\n\t\treturn (\n\t\t\tleft.combinator === right.combinator &&\n\t\t\tleft.negated === right.negated &&\n\t\t\tleft.children.length === right.children.length &&\n\t\t\tleft.children.every((child, index) => areQueryNodesEqualUnchecked(child, right.children[index], state))\n\t\t);\n\t}\n\treturn false;\n}\n\nfunction valuesEqual(\n\tleft: QueryJsonValue,\n\tright: QueryJsonValue,\n\tstate: EqualityState = { pairs: new WeakMap(), remaining: 100_000 },\n): boolean {\n\tstate.remaining -= 1;\n\tif (state.remaining < 0) return false;\n\tif (Object.is(left, right)) return true;\n\tif (Array.isArray(left) && Array.isArray(right)) {\n\t\tif (rememberPair(left, right, state)) return true;\n\t\treturn left.length === right.length && left.every((item, index) => valuesEqual(item, right[index], state));\n\t}\n\tif (\n\t\tleft &&\n\t\tright &&\n\t\ttypeof left === 'object' &&\n\t\ttypeof right === 'object' &&\n\t\t!Array.isArray(left) &&\n\t\t!Array.isArray(right)\n\t) {\n\t\tif (rememberPair(left, right, state)) return true;\n\t\tconst leftKeys = Object.keys(left);\n\t\tconst rightKeys = Object.keys(right);\n\t\treturn (\n\t\t\tleftKeys.length === rightKeys.length &&\n\t\t\tleftKeys.every((key) => Object.hasOwn(right, key) && valuesEqual(left[key], right[key], state))\n\t\t);\n\t}\n\treturn false;\n}\n\nexport function reduceQuery(query: QueryGroup, action: QueryAction, options: QueryReducerOptions = {}): QueryGroup {\n\tif (!isQueryGroupValue(query) || !isRecord(action) || typeof action.type !== 'string') return query;\n\tconst maxDepth = normalizeRuntimeMaxDepth(options.maxDepth, 4);\n\tswitch (action.type) {\n\t\tcase 'replace-query':\n\t\t\tif (!isQueryGroupValue(action.query, maxDepth) || areQueriesEqual(query, action.query)) return query;\n\t\t\treturn cloneQuery(action.query);\n\t\tcase 'insert-node': {\n\t\t\tif (typeof action.parentId !== 'string' || (action.index !== undefined && !Number.isInteger(action.index)))\n\t\t\t\treturn query;\n\t\t\tconst parent = getQueryNodeLocation(query, action.parentId);\n\t\t\tif (\n\t\t\t\t!parent ||\n\t\t\t\tparent.node.type !== 'group' ||\n\t\t\t\t!isNodeStructurallyValid(action.node, parent.depth + 1, maxDepth, new Set(), false, false) ||\n\t\t\t\thasIdIntersection(query, action.node)\n\t\t\t)\n\t\t\t\treturn query;\n\t\t\tconst index = Math.max(0, Math.min(action.index ?? parent.node.children.length, parent.node.children.length));\n\t\t\tconst node = cloneNodePreservingIds(action.node);\n\t\t\treturn updateGroup(query, action.parentId, (group) => ({\n\t\t\t\t...group,\n\t\t\t\tchildren: [...group.children.slice(0, index), node, ...group.children.slice(index)],\n\t\t\t}));\n\t\t}\n\t\tcase 'update-rule': {\n\t\t\tif (typeof action.id !== 'string' || !isRecord(action.patch)) return query;\n\t\t\tif (Object.hasOwn(action.patch, 'field') && typeof action.patch.field !== 'string') return query;\n\t\t\tif (Object.hasOwn(action.patch, 'operator') && typeof action.patch.operator !== 'string') return query;\n\t\t\tif (Object.hasOwn(action.patch, 'value') && !isQueryJsonValue(action.patch.value)) return query;\n\t\t\treturn updateNode(query, action.id, (node) => {\n\t\t\t\tif (node.type !== 'rule') return node;\n\t\t\t\tconst field = action.patch.field ?? node.field;\n\t\t\t\tconst operator = action.patch.operator ?? node.operator;\n\t\t\t\tconst value = Object.hasOwn(action.patch, 'value')\n\t\t\t\t\t? cloneQueryJsonValue(action.patch.value as QueryJsonValue)\n\t\t\t\t\t: node.value;\n\t\t\t\tif (field === node.field && operator === node.operator && valuesEqual(value, node.value)) return node;\n\t\t\t\treturn { ...node, field, operator, value };\n\t\t\t});\n\t\t}\n\t\tcase 'update-group':\n\t\t\tif (typeof action.id !== 'string' || !isRecord(action.patch)) return query;\n\t\t\tif (\n\t\t\t\t(Object.hasOwn(action.patch, 'combinator') &&\n\t\t\t\t\taction.patch.combinator !== 'and' &&\n\t\t\t\t\taction.patch.combinator !== 'or') ||\n\t\t\t\t(Object.hasOwn(action.patch, 'negated') && typeof action.patch.negated !== 'boolean')\n\t\t\t)\n\t\t\t\treturn query;\n\t\t\treturn updateGroup(query, action.id, (group) => {\n\t\t\t\tconst combinator = action.patch.combinator ?? group.combinator;\n\t\t\t\tconst negated = action.patch.negated ?? group.negated;\n\t\t\t\treturn combinator === group.combinator && negated === group.negated ? group : { ...group, combinator, negated };\n\t\t\t});\n\t\tcase 'remove-node':\n\t\t\tif (typeof action.id !== 'string') return query;\n\t\t\treturn action.id === query.id ? query : removeNode(query, action.id);\n\t\tcase 'clear-group':\n\t\t\tif (typeof action.id !== 'string') return query;\n\t\t\treturn updateGroup(query, action.id, (group) => (group.children.length ? { ...group, children: [] } : group));\n\t\tcase 'move-node':\n\t\t\tif (typeof action.id !== 'string' || (action.direction !== 'up' && action.direction !== 'down')) return query;\n\t\t\treturn moveSibling(query, action.id, action.direction);\n\t\tdefault:\n\t\t\treturn query;\n\t}\n}\n\nfunction updateNode(query: QueryGroup, id: string, updater: (node: QueryNode) => QueryNode): QueryGroup {\n\tif (query.id === id) {\n\t\tconst updated = updater(query);\n\t\treturn updated.type === 'group' ? updated : query;\n\t}\n\tlet changed = false;\n\tconst children = query.children.map((child) => {\n\t\tif (child.id === id) {\n\t\t\tconst updated = updater(child);\n\t\t\tchanged ||= updated !== child;\n\t\t\treturn updated;\n\t\t}\n\t\tif (child.type === 'group') {\n\t\t\tconst updated = updateNode(child, id, updater);\n\t\t\tchanged ||= updated !== child;\n\t\t\treturn updated;\n\t\t}\n\t\treturn child;\n\t});\n\treturn changed ? { ...query, children } : query;\n}\n\nfunction updateGroup(query: QueryGroup, id: string, updater: (group: QueryGroup) => QueryGroup) {\n\treturn updateNode(query, id, (node) => (node.type === 'group' ? updater(node) : node));\n}\n\nfunction removeNode(query: QueryGroup, id: string): QueryGroup {\n\tlet changed = false;\n\tconst children: QueryNode[] = [];\n\tfor (const child of query.children) {\n\t\tif (child.id === id) {\n\t\t\tchanged = true;\n\t\t\tcontinue;\n\t\t}\n\t\tif (child.type === 'group') {\n\t\t\tconst updated = removeNode(child, id);\n\t\t\tchanged ||= updated !== child;\n\t\t\tchildren.push(updated);\n\t\t} else {\n\t\t\tchildren.push(child);\n\t\t}\n\t}\n\treturn changed ? { ...query, children } : query;\n}\n\nfunction moveSibling(query: QueryGroup, id: string, direction: 'up' | 'down'): QueryGroup {\n\tconst index = query.children.findIndex((child) => child.id === id);\n\tif (index >= 0) {\n\t\tconst target = direction === 'up' ? index - 1 : index + 1;\n\t\tif (target < 0 || target >= query.children.length) return query;\n\t\tconst children = [...query.children];\n\t\t[children[index], children[target]] = [children[target], children[index]];\n\t\treturn { ...query, children };\n\t}\n\tlet changed = false;\n\tconst children = query.children.map((child) => {\n\t\tif (child.type !== 'group') return child;\n\t\tconst updated = moveSibling(child, id, direction);\n\t\tchanged ||= updated !== child;\n\t\treturn updated;\n\t});\n\treturn changed ? { ...query, children } : query;\n}\n\nfunction hasIdIntersection(query: QueryGroup, candidate: QueryNode) {\n\tconst existing = new Set();\n\twalkQuery(query, ({ node }) => existing.add(node.id));\n\tlet conflict = false;\n\tconst candidateIds = new Set();\n\tfunction visit(node: QueryNode) {\n\t\tif (typeof node.id !== 'string' || !node.id.trim() || existing.has(node.id) || candidateIds.has(node.id))\n\t\t\tconflict = true;\n\t\tcandidateIds.add(node.id);\n\t\tif (node.type === 'group') node.children.forEach(visit);\n\t}\n\tvisit(candidate);\n\treturn conflict;\n}\n\nexport function isQueryGroupValue(value: unknown, maxDepth = 64): value is QueryGroup {\n\treturn (\n\t\tisRecord(value) &&\n\t\tvalue.type === 'group' &&\n\t\tisNodeStructurallyValid(value, 0, normalizeRuntimeMaxDepth(maxDepth, 64), new Set(), false, false)\n\t);\n}\n\nexport function isQueryNodeValue(value: unknown, maxDepth = 64): value is QueryNode {\n\treturn isNodeStructurallyValid(value, 0, normalizeRuntimeMaxDepth(maxDepth, 64), new Set(), false, false);\n}\n\nfunction normalizeRuntimeMaxDepth(value: unknown, fallback: number) {\n\treturn Number.isInteger(value) && Number(value) >= 0 ? Math.min(Number(value), 64) : fallback;\n}\n\nfunction isQueryGroupShape(value: unknown): value is QueryGroup {\n\treturn isRecord(value) && value.type === 'group' && isNodeStructurallyValid(value, 0, 64, new Set(), true, true);\n}\n\nfunction isNodeStructurallyValid(\n\tnode: unknown,\n\tdepth: number,\n\tmaxDepth: number,\n\tids: Set,\n\tallowDuplicateIds: boolean,\n\tallowInvalidValues: boolean,\n): node is QueryNode {\n\tconst ancestors = new WeakSet();\n\tconst seenNodes = new WeakSet();\n\tconst stack: { value: unknown; depth: number; exiting?: boolean }[] = [{ value: node, depth }];\n\twhile (stack.length) {\n\t\tconst current = stack.pop() as { value: unknown; depth: number; exiting?: boolean };\n\t\tif (current.exiting) {\n\t\t\tancestors.delete(current.value as object);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!isRecord(current.value) || (current.value.type !== 'group' && current.value.type !== 'rule')) return false;\n\t\tconst currentNode = current.value;\n\t\tif (ancestors.has(currentNode) || seenNodes.has(currentNode)) return false;\n\t\tseenNodes.add(currentNode);\n\t\tif (typeof currentNode.id !== 'string' || !currentNode.id.trim() || (!allowDuplicateIds && ids.has(currentNode.id)))\n\t\t\treturn false;\n\t\tids.add(currentNode.id);\n\t\tif (currentNode.type === 'rule') {\n\t\t\tif (\n\t\t\t\ttypeof currentNode.field !== 'string' ||\n\t\t\t\ttypeof currentNode.operator !== 'string' ||\n\t\t\t\t(!allowInvalidValues && !isQueryJsonValue(currentNode.value))\n\t\t\t)\n\t\t\t\treturn false;\n\t\t\tcontinue;\n\t\t}\n\t\tif (\n\t\t\tcurrent.depth > maxDepth ||\n\t\t\t(currentNode.combinator !== 'and' && currentNode.combinator !== 'or') ||\n\t\t\ttypeof currentNode.negated !== 'boolean' ||\n\t\t\t!Array.isArray(currentNode.children)\n\t\t)\n\t\t\treturn false;\n\t\tancestors.add(currentNode);\n\t\tstack.push({ value: currentNode, depth: current.depth, exiting: true });\n\t\tfor (let index = currentNode.children.length - 1; index >= 0; index -= 1) {\n\t\t\tif (!(index in currentNode.children)) return false;\n\t\t\tstack.push({ value: currentNode.children[index], depth: current.depth + 1 });\n\t\t}\n\t}\n\treturn true;\n}\n\nfunction isRecord(value: unknown): value is Record {\n\tif (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n\tconst prototype = Object.getPrototypeOf(value);\n\treturn prototype === Object.prototype || prototype === null;\n}\n\nexport function validateQuery(query: QueryGroup, options: ValidateQueryOptions): QueryValidationIssue[] {\n\tconst runtimeQuery: unknown = query;\n\tif (!isQueryGroupShape(runtimeQuery)) {\n\t\tconst nodeId = isRecord(runtimeQuery) && typeof runtimeQuery.id === 'string' ? runtimeQuery.id : 'query-root';\n\t\treturn [issue('invalid-query', nodeId, [], '查询条件结构无效。')];\n\t}\n\tconst fields = new Map(options.fields.map((field) => [field.name, field]));\n\tconst operators = options.operators ?? defaultQueryOperators;\n\tconst operatorMap = getQueryOperatorMap(operators);\n\tconst maxDepth = normalizeRuntimeMaxDepth(options.maxDepth, 4);\n\tconst issues: QueryValidationIssue[] = [];\n\tconst ids = new Set();\n\twalkQuery(query, ({ node, depth, path }) => {\n\t\tif (ids.has(node.id)) issues.push(issue('duplicate-id', node.id, path, '节点 ID 必须唯一。'));\n\t\tids.add(node.id);\n\t\tif (node.type === 'group') {\n\t\t\tif (depth > maxDepth) issues.push(issue('max-depth', node.id, path, `分组最多可嵌套 ${maxDepth} 层。`));\n\t\t\treturn;\n\t\t}\n\t\tconst field = fields.get(node.field);\n\t\tif (!field) {\n\t\t\tissues.push(issue('unknown-field', node.id, path, '请选择可用字段。'));\n\t\t\treturn;\n\t\t}\n\t\tconst operator = operatorMap.get(node.operator);\n\t\tif (!operator) {\n\t\t\tissues.push(issue('unknown-operator', node.id, path, '请选择可用操作符。'));\n\t\t\treturn;\n\t\t}\n\t\tif (!getOperatorsForField(field, operators).some((candidate) => candidate.name === operator.name)) {\n\t\t\tissues.push(issue('operator-not-allowed', node.id, path, '当前字段不支持此操作符。'));\n\t\t\treturn;\n\t\t}\n\t\tvalidateRuleValue(node, field, operator, path, issues);\n\t});\n\treturn issues;\n}\n\nfunction validateRuleValue(\n\trule: QueryRule,\n\tfield: QueryField,\n\toperator: QueryOperator,\n\tpath: number[],\n\tissues: QueryValidationIssue[],\n) {\n\tif (!isQueryJsonValue(rule.value)) {\n\t\tissues.push(issue('invalid-value', rule.id, path, '值必须可序列化为 JSON。'));\n\t\treturn;\n\t}\n\tif (operator.cardinality === 'none') {\n\t\tif (rule.value !== null) issues.push(issue('invalid-cardinality', rule.id, path, '此操作符无需填写值。'));\n\t\treturn;\n\t}\n\tif (operator.cardinality === 'pair') {\n\t\tif (!Array.isArray(rule.value) || rule.value.length !== 2) {\n\t\t\tissues.push(issue('invalid-cardinality', rule.id, path, '此操作符需要两个值。'));\n\t\t\treturn;\n\t\t}\n\t\tif (rule.value.some(isMissing)) issues.push(issue('missing-value', rule.id, path, '请填写完整的范围值。'));\n\t\telse if (rule.value.some((value) => !isValidScalar(field, value))) {\n\t\t\tissues.push(issue('invalid-value', rule.id, path, '一个或多个范围值无效。'));\n\t\t}\n\t\treturn;\n\t}\n\tif (operator.cardinality === 'many') {\n\t\tif (!Array.isArray(rule.value)) {\n\t\t\tissues.push(issue('invalid-cardinality', rule.id, path, '此操作符需要一个值列表。'));\n\t\t\treturn;\n\t\t}\n\t\tif (rule.value.length === 0) issues.push(issue('missing-value', rule.id, path, '请至少添加一个值。'));\n\t\telse if (rule.value.some((value) => !isValidScalar(field, value))) {\n\t\t\tissues.push(issue('invalid-value', rule.id, path, '一个或多个列表值无效。'));\n\t\t}\n\t\treturn;\n\t}\n\tif (Array.isArray(rule.value) || isMissing(rule.value)) {\n\t\tissues.push(issue('missing-value', rule.id, path, '请输入值。'));\n\t} else if (!isValidScalar(field, rule.value)) {\n\t\tissues.push(issue('invalid-value', rule.id, path, '该值与所选字段类型不匹配。'));\n\t}\n}\n\nfunction isMissing(value: QueryJsonValue) {\n\treturn value === null || (typeof value === 'string' && value.trim().length === 0);\n}\n\nfunction isValidScalar(field: QueryField, value: QueryJsonValue) {\n\tif (value === null || Array.isArray(value) || typeof value === 'object') return false;\n\tconst kind = field.kind === 'array' ? (field.itemKind ?? 'unknown') : field.kind;\n\tif (kind === 'number' || kind === 'integer')\n\t\treturn typeof value === 'number' && Number.isFinite(value) && (kind !== 'integer' || Number.isInteger(value));\n\tif (kind === 'boolean') return typeof value === 'boolean';\n\tif (kind === 'date') return typeof value === 'string' && isValidDate(value);\n\tif (kind === 'datetime') return typeof value === 'string' && isValidDateTime(value);\n\tif (kind === 'string') return typeof value === 'string';\n\tif (kind === 'enum' && field.options)\n\t\treturn field.options.some((option) => Object.is(option.value, value as QueryJsonPrimitive));\n\treturn true;\n}\n\nfunction isValidDate(value: string) {\n\tconst match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(value);\n\tif (!match) return false;\n\tconst year = Number(match[1]);\n\tconst month = Number(match[2]);\n\tconst day = Number(match[3]);\n\tconst date = new Date(0);\n\tdate.setUTCFullYear(year, month - 1, day);\n\tdate.setUTCHours(0, 0, 0, 0);\n\treturn date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;\n}\n\nfunction isValidDateTime(value: string) {\n\tconst match = /^(\\d{4}-\\d{2}-\\d{2})T(\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d+)?)?(Z|([+-])(\\d{2}):(\\d{2}))$/.exec(value);\n\tif (!match || !isValidDate(match[1])) return false;\n\tconst hours = Number(match[2]);\n\tconst minutes = Number(match[3]);\n\tconst seconds = match[4] === undefined ? 0 : Number(match[4]);\n\tconst offsetHours = match[7] === undefined ? 0 : Number(match[7]);\n\tconst offsetMinutes = match[8] === undefined ? 0 : Number(match[8]);\n\treturn (\n\t\thours < 24 &&\n\t\tminutes < 60 &&\n\t\tseconds < 60 &&\n\t\toffsetHours < 24 &&\n\t\toffsetMinutes < 60 &&\n\t\tNumber.isFinite(Date.parse(value))\n\t);\n}\n\nfunction issue(code: QueryValidationIssueCode, nodeId: string, path: number[], message: string): QueryValidationIssue {\n\treturn { code, nodeId, path, message };\n}\n", "type": "registry:lib", "target": "@components/query-builder/query-state.ts" }, { "path": "registry/default/ui/query-builder/query-json-schema.ts", "content": "import type { QueryField, QueryFieldKind, QueryJsonPrimitive, QueryJsonValue, QueryOption } from './query-model';\nimport { isQueryJsonValue } from './query-model';\n\nexport type QueryJsonSchemaExtension = {\n\tlabel?: string;\n\teditor?: string;\n\toperators?: readonly string[];\n\thidden?: boolean;\n\tgroup?: string;\n\torder?: number;\n\tplaceholder?: string;\n\tfield?: string;\n\tleaf?: boolean;\n\tkind?: QueryFieldKind;\n\t[key: string]: unknown;\n};\n\nexport type QueryJsonSchema = {\n\t$ref?: string;\n\ttype?: string | readonly string[];\n\ttitle?: string;\n\tdescription?: string;\n\tformat?: string;\n\tdefault?: QueryJsonValue;\n\tconst?: QueryJsonPrimitive;\n\tenum?: readonly QueryJsonPrimitive[];\n\toneOf?: readonly QueryJsonSchema[];\n\tproperties?: Record;\n\trequired?: readonly string[];\n\titems?: QueryJsonSchema;\n\treadOnly?: boolean;\n\twriteOnly?: boolean;\n\t$defs?: Record;\n\tdefinitions?: Record;\n\t'x-enumNames'?: readonly string[];\n\t'x-query'?: QueryJsonSchemaExtension;\n};\n\nexport type QuerySchemaDiagnosticCode =\n\t| 'remote-ref'\n\t| 'unresolved-ref'\n\t| 'cyclic-ref'\n\t| 'max-depth'\n\t| 'duplicate-field'\n\t| 'invalid-extension'\n\t| 'unknown-extension'\n\t| 'invalid-schema';\n\nexport type QuerySchemaDiagnostic = {\n\tcode: QuerySchemaDiagnosticCode;\n\tseverity: 'warning' | 'error';\n\tpath: string;\n\tmessage: string;\n\tref?: string;\n};\n\nexport type QuerySchemaFieldContext = {\n\tpath: string;\n\tsegments: readonly string[];\n\tschema: QueryJsonSchema;\n\tfield: QueryField;\n};\n\nexport type QueryFieldsFromJsonSchemaOptions = {\n\tmaxDepth?: number;\n\tfieldOverrides?: Readonly>>;\n\tinclude?: (context: QuerySchemaFieldContext) => boolean;\n\tmapField?: (context: QuerySchemaFieldContext) => QueryField | null;\n};\n\nexport type QueryFieldsFromJsonSchemaResult = {\n\tfields: QueryField[];\n\tdiagnostics: QuerySchemaDiagnostic[];\n};\n\ntype FieldRecord = { field: QueryField; order: number; index: number };\n\nconst extensionKeys = new Set([\n\t'label',\n\t'editor',\n\t'operators',\n\t'hidden',\n\t'group',\n\t'order',\n\t'placeholder',\n\t'field',\n\t'leaf',\n\t'kind',\n]);\n\nexport function queryFieldsFromJsonSchema(\n\tschema: QueryJsonSchema,\n\toptions: QueryFieldsFromJsonSchemaOptions = {},\n): QueryFieldsFromJsonSchemaResult {\n\tconst diagnostics: QuerySchemaDiagnostic[] = [];\n\tconst records: FieldRecord[] = [];\n\tconst requestedMaxDepth = options.maxDepth;\n\tconst maxDepth =\n\t\tNumber.isInteger(requestedMaxDepth) && Number(requestedMaxDepth) >= 0 ? Math.min(Number(requestedMaxDepth), 64) : 8;\n\tlet index = 0;\n\n\tfunction visitObject(\n\t\tinput: QueryJsonSchema,\n\t\tsegments: string[],\n\t\tinheritedGroup: string | undefined,\n\t\tdepth: number,\n\t\trefStack: readonly string[],\n\t) {\n\t\tif (depth > maxDepth) {\n\t\t\tdiagnostics.push(\n\t\t\t\tdiagnostic('max-depth', 'error', toPointer(segments), `Schema nesting exceeds ${maxDepth} levels.`),\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tconst resolved = resolveSchema(input, schema, refStack, segments, diagnostics);\n\t\tif (!resolved) return;\n\t\tconst properties = resolved.schema.properties;\n\t\tif (properties === undefined) {\n\t\t\tdiagnostics.push(\n\t\t\t\tdiagnostic('invalid-schema', 'warning', toPointer(segments), 'Object schema has no properties.'),\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tif (!isRecord(properties)) {\n\t\t\tdiagnostics.push(\n\t\t\t\tdiagnostic('invalid-schema', 'error', toPointer(segments), 'Schema properties must be an object map.'),\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tconst required = new Set(stringArray(resolved.schema.required) ?? []);\n\t\tfor (const [propertyName, rawProperty] of Object.entries(properties)) {\n\t\t\tconst propertySegments = [...segments, propertyName];\n\t\t\tconst propertyPath = toPointer(propertySegments);\n\t\t\tconst property = resolveSchema(rawProperty, schema, resolved.stack, propertySegments, diagnostics);\n\t\t\tif (!property) continue;\n\t\t\tconst extension = readExtension(property.schema, propertyPath, diagnostics);\n\t\t\tif (extension.hidden) continue;\n\t\t\tconst objectLike = isObjectSchema(property.schema);\n\t\t\tconst explicitGroup = stringValue(extension.group);\n\t\t\tif (objectLike && !extension.leaf) {\n\t\t\t\tvisitObject(\n\t\t\t\t\tproperty.schema,\n\t\t\t\t\tpropertySegments,\n\t\t\t\t\texplicitGroup ?? property.schema.title ?? inheritedGroup,\n\t\t\t\t\tdepth + 1,\n\t\t\t\t\tproperty.stack,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet field = createField(\n\t\t\t\tproperty.schema,\n\t\t\t\tpropertyName,\n\t\t\t\tpropertyPath,\n\t\t\t\texplicitGroup ?? inheritedGroup,\n\t\t\t\trequired.has(propertyName),\n\t\t\t\textension,\n\t\t\t\tschema,\n\t\t\t\tproperty.stack,\n\t\t\t\tdiagnostics,\n\t\t\t);\n\t\t\tif (!field) continue;\n\t\t\tconst overrides = options.fieldOverrides;\n\t\t\tconst namedOverride = overrides && Object.hasOwn(overrides, field.name) ? overrides[field.name] : undefined;\n\t\t\tconst pathOverride = overrides && Object.hasOwn(overrides, propertyPath) ? overrides[propertyPath] : undefined;\n\t\t\tconst override = namedOverride ?? pathOverride;\n\t\t\tif (override) field = { ...field, ...override, name: override.name ?? field.name };\n\t\t\tconst context: QuerySchemaFieldContext = {\n\t\t\t\tpath: propertyPath,\n\t\t\t\tsegments: propertySegments,\n\t\t\t\tschema: property.schema,\n\t\t\t\tfield,\n\t\t\t};\n\t\t\tif (options.include && !options.include(context)) continue;\n\t\t\tif (options.mapField) {\n\t\t\t\tconst mapped = options.mapField(context);\n\t\t\t\tif (!mapped) continue;\n\t\t\t\tfield = mapped;\n\t\t\t}\n\t\t\trecords.push({ field, order: numberValue(extension.order) ?? Number.MAX_SAFE_INTEGER, index });\n\t\t\tindex += 1;\n\t\t}\n\t}\n\n\tconst root = resolveSchema(schema, schema, [], [], diagnostics);\n\tif (root) visitObject(root.schema, [], root.schema.title, 0, root.stack);\n\tconst seenFields = new Set();\n\tconst fields = records\n\t\t.toSorted((left, right) => left.order - right.order || left.index - right.index)\n\t\t.flatMap((record) => {\n\t\t\tif (seenFields.has(record.field.name)) {\n\t\t\t\tdiagnostics.push(\n\t\t\t\t\tdiagnostic(\n\t\t\t\t\t\t'duplicate-field',\n\t\t\t\t\t\t'error',\n\t\t\t\t\t\tString(record.field.meta?.schemaPath ?? record.field.name),\n\t\t\t\t\t\t`Duplicate query field name: ${record.field.name}.`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\tseenFields.add(record.field.name);\n\t\t\treturn [record.field];\n\t\t});\n\treturn {\n\t\tfields,\n\t\tdiagnostics,\n\t};\n}\n\nfunction createField(\n\tschema: QueryJsonSchema,\n\tpropertyName: string,\n\tpath: string,\n\tgroup: string | undefined,\n\trequired: boolean,\n\textension: QueryJsonSchemaExtension,\n\troot: QueryJsonSchema,\n\trefStack: readonly string[],\n\tdiagnostics: QuerySchemaDiagnostic[],\n): QueryField | undefined {\n\tconst pathSegments = decodePointer(path);\n\tconst optionResult = readOptions(schema, root, refStack, pathSegments, diagnostics);\n\tif (!optionResult.valid) return undefined;\n\tconst options = optionResult.options;\n\tconst kind = isFieldKind(extension.kind) ? extension.kind : inferKind(schema, options);\n\tconst item =\n\t\tschema.items !== undefined\n\t\t\t? resolveSchema(schema.items, root, refStack, [...pathSegments, 'items'], diagnostics)\n\t\t\t: undefined;\n\tif (schema.items !== undefined && !item) return undefined;\n\tconst itemOptionResult = item\n\t\t? readOptions(item.schema, root, item.stack, [...pathSegments, 'items'], diagnostics)\n\t\t: { valid: true as const, options: undefined };\n\tif (!itemOptionResult.valid) return undefined;\n\tconst itemOptions = itemOptionResult.options;\n\tconst itemKind = item ? inferKind(item.schema, itemOptions) : undefined;\n\tconst meta: Record = {};\n\tconst format = stringValue(schema.format);\n\tif (format) meta.format = format;\n\tmeta.schemaPath = path;\n\treturn {\n\t\tname: stringValue(extension.field) ?? path,\n\t\tlabel: stringValue(extension.label) ?? stringValue(schema.title) ?? humanize(propertyName),\n\t\tdescription: stringValue(schema.description),\n\t\tkind,\n\t\tgroup,\n\t\trequired,\n\t\toperators: stringArray(extension.operators),\n\t\tdefaultValue: schema.default !== undefined && isQueryJsonValue(schema.default) ? schema.default : undefined,\n\t\toptions: kind === 'array' ? itemOptions : options,\n\t\titemKind,\n\t\teditor: stringValue(extension.editor),\n\t\tplaceholder: stringValue(extension.placeholder),\n\t\tmeta,\n\t};\n}\n\nfunction resolveSchema(\n\tinput: unknown,\n\troot: QueryJsonSchema,\n\tstack: readonly string[],\n\tsegments: readonly string[],\n\tdiagnostics: QuerySchemaDiagnostic[],\n): { schema: QueryJsonSchema; stack: readonly string[] } | undefined {\n\tif (!isRecord(input)) {\n\t\tdiagnostics.push(diagnostic('invalid-schema', 'error', toPointer(segments), 'Schema nodes must be JSON objects.'));\n\t\treturn undefined;\n\t}\n\tif (stack.length > 64) {\n\t\tdiagnostics.push(\n\t\t\tdiagnostic('max-depth', 'error', toPointer(segments), 'Schema reference chain exceeds 64 levels.'),\n\t\t);\n\t\treturn undefined;\n\t}\n\tconst schema = input as QueryJsonSchema;\n\tif (segments.length && hasSchemaObjectCycle(schema)) {\n\t\tdiagnostics.push(\n\t\t\tdiagnostic('invalid-schema', 'error', toPointer(segments), 'Schema object cycles are not supported.'),\n\t\t);\n\t\treturn undefined;\n\t}\n\tif (!validateSchemaShape(schema, segments, diagnostics)) return undefined;\n\tif (schema.$ref === undefined) return { schema, stack };\n\tif (typeof schema.$ref !== 'string' || !schema.$ref) {\n\t\tdiagnostics.push(\n\t\t\tdiagnostic('invalid-schema', 'error', toPointer(segments), 'Schema $ref must be a non-empty string.'),\n\t\t);\n\t\treturn undefined;\n\t}\n\tconst ref = schema.$ref;\n\tif (!ref.startsWith('#')) {\n\t\tdiagnostics.push(\n\t\t\tdiagnostic('remote-ref', 'error', toPointer(segments), 'Only local JSON Pointer references are supported.', ref),\n\t\t);\n\t\treturn undefined;\n\t}\n\tconst parsed = parseLocalRef(ref);\n\tif (!parsed) {\n\t\tdiagnostics.push(\n\t\t\tdiagnostic(\n\t\t\t\t'unresolved-ref',\n\t\t\t\t'error',\n\t\t\t\ttoPointer(segments),\n\t\t\t\t'Local reference is not a valid URI-encoded JSON Pointer.',\n\t\t\t\tref,\n\t\t\t),\n\t\t);\n\t\treturn undefined;\n\t}\n\tif (stack.includes(parsed.canonical)) {\n\t\tdiagnostics.push(diagnostic('cyclic-ref', 'error', toPointer(segments), 'Cyclic schema reference rejected.', ref));\n\t\treturn undefined;\n\t}\n\tconst target = resolvePointer(root, parsed.segments);\n\tif (target === undefined) {\n\t\tdiagnostics.push(\n\t\t\tdiagnostic('unresolved-ref', 'error', toPointer(segments), 'Schema reference could not be resolved.', ref),\n\t\t);\n\t\treturn undefined;\n\t}\n\tif (!isRecord(target) || !validateSchemaShape(target as QueryJsonSchema, segments, diagnostics)) return undefined;\n\tconst { $ref: _ignored, ...siblings } = schema;\n\tconst nextStack = [...stack, parsed.canonical];\n\tconst merged = mergeReferencedSchema(target as QueryJsonSchema, siblings, root, nextStack, segments, diagnostics);\n\tif (!merged) return undefined;\n\treturn resolveSchema(merged, root, nextStack, segments, diagnostics);\n}\n\nfunction parseLocalRef(ref: string) {\n\ttry {\n\t\tconst fragment = decodeURIComponent(ref.slice(1));\n\t\tif (fragment !== '' && !fragment.startsWith('/')) return undefined;\n\t\tconst encodedSegments = fragment ? fragment.slice(1).split('/') : [];\n\t\tif (encodedSegments.some((segment) => /~(?:[^01]|$)/.test(segment))) return undefined;\n\t\tconst segments = encodedSegments.map(decodePointerSegment);\n\t\treturn { segments, canonical: `#${toPointer(segments)}` };\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction resolvePointer(root: QueryJsonSchema, segments: readonly string[]): unknown {\n\tlet value: unknown = root;\n\tfor (const segment of segments) {\n\t\tif (Array.isArray(value)) {\n\t\t\tif (!/^(?:0|[1-9]\\d*)$/.test(segment)) return undefined;\n\t\t\tconst index = Number(segment);\n\t\t\tif (!Number.isSafeInteger(index) || !Object.hasOwn(value, index)) return undefined;\n\t\t\tvalue = value[index];\n\t\t\tcontinue;\n\t\t}\n\t\tif (!isRecord(value) || !Object.hasOwn(value, segment)) return undefined;\n\t\tvalue = value[segment];\n\t}\n\treturn value;\n}\n\nfunction mergeReferencedSchema(\n\ttarget: QueryJsonSchema,\n\tsiblings: QueryJsonSchema,\n\troot: QueryJsonSchema,\n\tstack: readonly string[],\n\tsegments: readonly string[],\n\tdiagnostics: QuerySchemaDiagnostic[],\n) {\n\tif (hasSchemaObjectCycle(target) || hasSchemaObjectCycle(siblings)) {\n\t\tdiagnostics.push(\n\t\t\tdiagnostic('invalid-schema', 'error', toPointer(segments), 'Schema object cycles are not supported.'),\n\t\t);\n\t\treturn undefined;\n\t}\n\tconst normalizedTarget = normalizeFiniteOneOf(target, root, stack, segments, diagnostics);\n\tconst normalizedSibling = normalizeFiniteOneOf(siblings, root, stack, segments, diagnostics);\n\tif (!normalizedTarget || !normalizedSibling) return undefined;\n\treturn mergeSchemaFragments(normalizedTarget, normalizedSibling, segments, diagnostics);\n}\n\nfunction normalizeFiniteOneOf(\n\tschema: QueryJsonSchema,\n\troot: QueryJsonSchema,\n\tstack: readonly string[],\n\tsegments: readonly string[],\n\tdiagnostics: QuerySchemaDiagnostic[],\n\tdepth = 0,\n): QueryJsonSchema | undefined {\n\tif (depth > 64) {\n\t\tdiagnostics.push(diagnostic('max-depth', 'error', toPointer(segments), 'Schema normalization exceeds 64 levels.'));\n\t\treturn undefined;\n\t}\n\tif (!isRecord(schema)) {\n\t\tdiagnostics.push(diagnostic('invalid-schema', 'error', toPointer(segments), 'Schema nodes must be JSON objects.'));\n\t\treturn undefined;\n\t}\n\tlet normalized = schema;\n\tlet activeStack = stack;\n\tif (normalized.$ref) {\n\t\tconst resolved = resolveSchema(normalized, root, activeStack, segments, diagnostics);\n\t\tif (!resolved) return undefined;\n\t\tnormalized = resolved.schema;\n\t\tactiveStack = resolved.stack;\n\t}\n\tfor (const keyword of ['properties', '$defs', 'definitions'] as const) {\n\t\tconst source = normalized[keyword];\n\t\tif (!source) continue;\n\t\tconst output = Object.create(null) as Record;\n\t\tfor (const [key, child] of Object.entries(source)) {\n\t\t\tconst next = normalizeFiniteOneOf(child, root, activeStack, [...segments, keyword, key], diagnostics, depth + 1);\n\t\t\tif (!next) return undefined;\n\t\t\tObject.defineProperty(output, key, {\n\t\t\t\tvalue: next,\n\t\t\t\tenumerable: true,\n\t\t\t\tconfigurable: true,\n\t\t\t\twritable: true,\n\t\t\t});\n\t\t}\n\t\tnormalized = { ...normalized, [keyword]: output };\n\t}\n\tif (normalized.items) {\n\t\tconst items = normalizeFiniteOneOf(\n\t\t\tnormalized.items,\n\t\t\troot,\n\t\t\tactiveStack,\n\t\t\t[...segments, 'items'],\n\t\t\tdiagnostics,\n\t\t\tdepth + 1,\n\t\t);\n\t\tif (!items) return undefined;\n\t\tnormalized = { ...normalized, items };\n\t}\n\tif (normalized.oneOf === undefined) return normalized;\n\tif (!isSchemaArray(normalized.oneOf)) return undefined;\n\tconst options: QueryOption[] = [];\n\tfor (let index = 0; index < normalized.oneOf.length; index += 1) {\n\t\tconst resolved = resolveSchema(\n\t\t\tnormalized.oneOf[index],\n\t\t\troot,\n\t\t\tactiveStack,\n\t\t\t[...segments, 'oneOf', String(index)],\n\t\t\tdiagnostics,\n\t\t);\n\t\tif (!resolved) return undefined;\n\t\tconst value = resolved.schema.const;\n\t\tif (!isQueryJsonPrimitive(value)) {\n\t\t\tdiagnostics.push(\n\t\t\t\tdiagnostic(\n\t\t\t\t\t'invalid-schema',\n\t\t\t\t\t'error',\n\t\t\t\t\ttoPointer([...segments, 'oneOf', String(index)]),\n\t\t\t\t\t'Only finite const-based oneOf references can be combined.',\n\t\t\t\t),\n\t\t\t);\n\t\t\treturn undefined;\n\t\t}\n\t\tif (options.some((option) => Object.is(option.value, value))) {\n\t\t\tdiagnostics.push(\n\t\t\t\tdiagnostic(\n\t\t\t\t\t'invalid-schema',\n\t\t\t\t\t'error',\n\t\t\t\t\ttoPointer([...segments, 'oneOf', String(index)]),\n\t\t\t\t\t'oneOf constant options must be unique.',\n\t\t\t\t),\n\t\t\t);\n\t\t\treturn undefined;\n\t\t}\n\t\toptions.push({ value, label: stringValue(resolved.schema.title) ?? String(value) });\n\t}\n\tconst types = normalizedTypes(normalized.type);\n\tconst enumValues = isPrimitiveArray(normalized.enum) ? normalized.enum : undefined;\n\tconst hasConst = normalized.const !== undefined;\n\tconst values = options.filter(\n\t\t(option) =>\n\t\t\t(!types.length || types.some((type) => primitiveMatchesType(option.value, type))) &&\n\t\t\t(!enumValues || enumValues.some((value) => Object.is(value, option.value))) &&\n\t\t\t(!hasConst || Object.is(normalized.const, option.value)),\n\t);\n\tif (!values.length) {\n\t\tdiagnostics.push(\n\t\t\tdiagnostic('invalid-schema', 'error', toPointer(segments), 'Schema value constraints have no intersection.'),\n\t\t);\n\t\treturn undefined;\n\t}\n\tconst enumNames = stringArray(normalized['x-enumNames']);\n\tconst labels = values.map((option) => {\n\t\tconst enumIndex = enumValues?.findIndex((value) => Object.is(value, option.value)) ?? -1;\n\t\treturn enumIndex >= 0 ? (enumNames?.[enumIndex] ?? option.label) : option.label;\n\t});\n\tconst { oneOf: _oneOf, ...rest } = normalized;\n\treturn { ...rest, enum: values.map((option) => option.value), 'x-enumNames': labels };\n}\n\nfunction mergeSchemaFragments(\n\ttarget: QueryJsonSchema,\n\tsibling: QueryJsonSchema,\n\tsegments: readonly string[],\n\tdiagnostics: QuerySchemaDiagnostic[],\n): QueryJsonSchema | undefined {\n\tif (hasSchemaObjectCycle(target) || hasSchemaObjectCycle(sibling)) {\n\t\tdiagnostics.push(\n\t\t\tdiagnostic('invalid-schema', 'error', toPointer(segments), 'Schema object cycles are not supported.'),\n\t\t);\n\t\treturn undefined;\n\t}\n\tif (!validateSchemaShape(target, segments, diagnostics) || !validateSchemaShape(sibling, segments, diagnostics))\n\t\treturn undefined;\n\tconst targetTypes = normalizedTypes(target.type);\n\tconst siblingTypes = normalizedTypes(sibling.type);\n\tconst mergedTypes =\n\t\ttargetTypes.length && siblingTypes.length ? intersectSchemaTypes(targetTypes, siblingTypes) : undefined;\n\tif (mergedTypes?.length === 0) {\n\t\tdiagnostics.push(\n\t\t\tdiagnostic(\n\t\t\t\t'invalid-schema',\n\t\t\t\t'error',\n\t\t\t\ttoPointer(segments),\n\t\t\t\t'Reference siblings declare incompatible schema types.',\n\t\t\t),\n\t\t);\n\t\treturn undefined;\n\t}\n\tconst effectiveTypes = mergedTypes ?? (siblingTypes.length ? siblingTypes : targetTypes);\n\tconst targetHasValueConstraint =\n\t\ttarget.enum !== undefined || target.const !== undefined || target.oneOf !== undefined;\n\tconst siblingHasValueConstraint =\n\t\tsibling.enum !== undefined || sibling.const !== undefined || sibling.oneOf !== undefined;\n\tif (\n\t\t(target.oneOf && (sibling.type !== undefined || siblingHasValueConstraint)) ||\n\t\t(sibling.oneOf && (target.type !== undefined || targetHasValueConstraint))\n\t) {\n\t\tdiagnostics.push(\n\t\t\tdiagnostic(\n\t\t\t\t'invalid-schema',\n\t\t\t\t'error',\n\t\t\t\ttoPointer(segments),\n\t\t\t\t'Combining oneOf with sibling value or type constraints is not supported.',\n\t\t\t),\n\t\t);\n\t\treturn undefined;\n\t}\n\tconst targetEnum = isPrimitiveArray(target.enum) ? target.enum : undefined;\n\tconst siblingEnum = isPrimitiveArray(sibling.enum) ? sibling.enum : undefined;\n\tconst intersectedEnum =\n\t\ttargetEnum && siblingEnum\n\t\t\t? targetEnum.filter((value) => siblingEnum.some((candidate) => Object.is(candidate, value)))\n\t\t\t: (siblingEnum ?? targetEnum);\n\tconst mergedEnum = effectiveTypes.length\n\t\t? intersectedEnum?.filter((value) => effectiveTypes.some((type) => primitiveMatchesType(value, type)))\n\t\t: intersectedEnum;\n\tif (intersectedEnum && mergedEnum?.length === 0) {\n\t\tdiagnostics.push(\n\t\t\tdiagnostic(\n\t\t\t\t'invalid-schema',\n\t\t\t\t'error',\n\t\t\t\ttoPointer(segments),\n\t\t\t\t'Reference enum has no values allowed by the combined constraints.',\n\t\t\t),\n\t\t);\n\t\treturn undefined;\n\t}\n\tconst hasTargetConst = target.const !== undefined;\n\tconst hasSiblingConst = sibling.const !== undefined;\n\tif (hasTargetConst && hasSiblingConst && !Object.is(target.const, sibling.const)) {\n\t\tdiagnostics.push(\n\t\t\tdiagnostic(\n\t\t\t\t'invalid-schema',\n\t\t\t\t'error',\n\t\t\t\ttoPointer(segments),\n\t\t\t\t'Reference siblings declare incompatible const values.',\n\t\t\t),\n\t\t);\n\t\treturn undefined;\n\t}\n\tconst mergedConst = hasSiblingConst ? sibling.const : target.const;\n\tconst hasMergedConst = hasTargetConst || hasSiblingConst;\n\tif (\n\t\thasMergedConst &&\n\t\teffectiveTypes.length &&\n\t\t!effectiveTypes.some((type) => primitiveMatchesType(mergedConst as QueryJsonPrimitive, type))\n\t) {\n\t\tdiagnostics.push(\n\t\t\tdiagnostic('invalid-schema', 'error', toPointer(segments), 'Reference const does not match the combined type.'),\n\t\t);\n\t\treturn undefined;\n\t}\n\tif (hasMergedConst && mergedEnum && !mergedEnum.some((value) => Object.is(value, mergedConst))) {\n\t\tdiagnostics.push(\n\t\t\tdiagnostic(\n\t\t\t\t'invalid-schema',\n\t\t\t\t'error',\n\t\t\t\ttoPointer(segments),\n\t\t\t\t'Reference const is not allowed by the combined enum.',\n\t\t\t),\n\t\t);\n\t\treturn undefined;\n\t}\n\tconst mergedEnumNames = mergedEnum?.map(\n\t\t(value) => enumValueLabel(sibling, value) ?? enumValueLabel(target, value) ?? String(value),\n\t);\n\tlet valid = true;\n\tconst mergeMap = (\n\t\tleft: Record | undefined,\n\t\tright: Record | undefined,\n\t\tkeyword: 'properties' | '$defs' | 'definitions',\n\t) => {\n\t\tif (!left && !right) return undefined;\n\t\tconst output = { ...left, ...right };\n\t\tfor (const key of Object.keys(left ?? {})) {\n\t\t\tif (!left || !right || !Object.hasOwn(left, key) || !Object.hasOwn(right, key)) continue;\n\t\t\tconst merged = mergeSchemaFragments(left[key], right[key], [...segments, keyword, key], diagnostics);\n\t\t\tif (merged) {\n\t\t\t\tObject.defineProperty(output, key, {\n\t\t\t\t\tvalue: merged,\n\t\t\t\t\tenumerable: true,\n\t\t\t\t\tconfigurable: true,\n\t\t\t\t\twritable: true,\n\t\t\t\t});\n\t\t\t} else delete output[key];\n\t\t}\n\t\treturn output;\n\t};\n\tlet items = sibling.items ?? target.items;\n\tif (target.items && sibling.items) {\n\t\tconst mergedItems = mergeSchemaFragments(target.items, sibling.items, [...segments, 'items'], diagnostics);\n\t\tif (mergedItems) items = mergedItems;\n\t\telse valid = false;\n\t}\n\tconst properties = mergeMap(target.properties, sibling.properties, 'properties');\n\tconst required =\n\t\ttarget.required || sibling.required\n\t\t\t? [...new Set([...(target.required ?? []), ...(sibling.required ?? [])])].filter(\n\t\t\t\t\t(name) => !properties || Object.hasOwn(properties, name),\n\t\t\t\t)\n\t\t\t: undefined;\n\tconst merged = {\n\t\t...target,\n\t\t...sibling,\n\t\ttype: mergedTypes ? (mergedTypes.length === 1 ? mergedTypes[0] : mergedTypes) : (sibling.type ?? target.type),\n\t\tenum: mergedEnum,\n\t\tconst: mergedConst,\n\t\t'x-enumNames': mergedEnumNames,\n\t\tproperties,\n\t\t$defs: mergeMap(target.$defs, sibling.$defs, '$defs'),\n\t\tdefinitions: mergeMap(target.definitions, sibling.definitions, 'definitions'),\n\t\titems,\n\t\trequired,\n\t};\n\treturn valid ? merged : undefined;\n}\n\nfunction validateSchemaShape(\n\tschema: QueryJsonSchema,\n\tsegments: readonly string[],\n\tdiagnostics: QuerySchemaDiagnostic[],\n) {\n\tconst source = schema as Record;\n\tlet valid = true;\n\tconst reject = (message: string) => {\n\t\tvalid = false;\n\t\tdiagnostics.push(diagnostic('invalid-schema', 'error', toPointer(segments), message));\n\t};\n\tif (source.type !== undefined && !isSchemaType(source.type))\n\t\treject('Schema type must be a supported string or dense string array.');\n\tfor (const keyword of ['properties', '$defs', 'definitions'] as const) {\n\t\tif (source[keyword] !== undefined && !isRecord(source[keyword])) reject(`Schema ${keyword} must be an object map.`);\n\t}\n\tif (source.required !== undefined && !stringArray(source.required))\n\t\treject('Schema required must be a dense array of strings.');\n\tif (source.items !== undefined && !isRecord(source.items)) reject('Schema items must be an object schema.');\n\tif (source.enum !== undefined && !isPrimitiveArray(source.enum))\n\t\treject('Schema enum must be a non-empty dense array of JSON primitives.');\n\tif (source.oneOf !== undefined && !isSchemaArray(source.oneOf))\n\t\treject('Schema oneOf must be a non-empty dense array of object schemas.');\n\tif (source['x-enumNames'] !== undefined && !stringArray(source['x-enumNames']))\n\t\treject('Schema x-enumNames must be a dense array of strings.');\n\tfor (const keyword of ['title', 'description', 'format'] as const) {\n\t\tif (source[keyword] !== undefined && typeof source[keyword] !== 'string')\n\t\t\treject(`Schema ${keyword} must be a string.`);\n\t}\n\tfor (const keyword of ['readOnly', 'writeOnly'] as const) {\n\t\tif (source[keyword] !== undefined && typeof source[keyword] !== 'boolean')\n\t\t\treject(`Schema ${keyword} must be a boolean.`);\n\t}\n\tif (source.default !== undefined && !isQueryJsonValue(source.default))\n\t\treject('Schema default must be a dense JSON value.');\n\tif (source.const !== undefined && !isQueryJsonPrimitive(source.const))\n\t\treject('Schema const must be a finite JSON primitive.');\n\telse if (\n\t\tisQueryJsonPrimitive(source.const) &&\n\t\tnormalizedTypes(schema.type).length &&\n\t\t!normalizedTypes(schema.type).some((type) => primitiveMatchesType(source.const as QueryJsonPrimitive, type))\n\t)\n\t\treject('Schema const must match its declared type.');\n\tif (\n\t\tisQueryJsonPrimitive(source.const) &&\n\t\tisPrimitiveArray(source.enum) &&\n\t\t!source.enum.some((value) => Object.is(value, source.const))\n\t)\n\t\treject('Schema const must be allowed by its enum.');\n\treturn valid;\n}\n\nfunction readExtension(schema: QueryJsonSchema, path: string, diagnostics: QuerySchemaDiagnostic[]) {\n\tconst raw: unknown = schema['x-query'];\n\tif (raw === undefined) return {};\n\tif (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n\t\tdiagnostics.push(diagnostic('invalid-extension', 'warning', path, 'x-query must be an object.'));\n\t\treturn {};\n\t}\n\tconst source = raw as Record;\n\tconst extension: QueryJsonSchemaExtension = {};\n\tfor (const key of Object.keys(source)) {\n\t\tif (!extensionKeys.has(key))\n\t\t\tdiagnostics.push(diagnostic('unknown-extension', 'warning', path, `Unknown x-query key: ${key}.`));\n\t}\n\tfor (const key of ['label', 'editor', 'group', 'placeholder', 'field'] as const) {\n\t\tif (source[key] === undefined) continue;\n\t\tconst value = stringValue(source[key]);\n\t\tif (value) extension[key] = value;\n\t\telse\n\t\t\tdiagnostics.push(diagnostic('invalid-extension', 'warning', path, `x-query.${key} must be a non-empty string.`));\n\t}\n\tfor (const key of ['hidden', 'leaf'] as const) {\n\t\tif (source[key] === undefined) continue;\n\t\tif (typeof source[key] === 'boolean') extension[key] = source[key];\n\t\telse diagnostics.push(diagnostic('invalid-extension', 'warning', path, `x-query.${key} must be a boolean.`));\n\t}\n\tif (source.order !== undefined) {\n\t\tconst order = numberValue(source.order);\n\t\tif (order !== undefined) extension.order = order;\n\t\telse diagnostics.push(diagnostic('invalid-extension', 'warning', path, 'x-query.order must be a finite number.'));\n\t}\n\tif (source.operators !== undefined) {\n\t\tconst operators = stringArray(source.operators);\n\t\tif (operators) extension.operators = operators;\n\t\telse\n\t\t\tdiagnostics.push(\n\t\t\t\tdiagnostic('invalid-extension', 'warning', path, 'x-query.operators must be a dense array of strings.'),\n\t\t\t);\n\t}\n\tif (source.kind !== undefined) {\n\t\tif (isFieldKind(source.kind)) extension.kind = source.kind;\n\t\telse\n\t\t\tdiagnostics.push(diagnostic('invalid-extension', 'warning', path, 'x-query.kind is not a supported field kind.'));\n\t}\n\treturn extension;\n}\n\nfunction enumValueLabel(schema: QueryJsonSchema, value: QueryJsonPrimitive) {\n\tif (!isPrimitiveArray(schema.enum)) return undefined;\n\tconst index = schema.enum.findIndex((candidate) => Object.is(candidate, value));\n\treturn index < 0 ? undefined : stringArray(schema['x-enumNames'])?.[index];\n}\n\nfunction readOptions(\n\tschema: QueryJsonSchema,\n\troot: QueryJsonSchema,\n\tstack: readonly string[],\n\tsegments: readonly string[],\n\tdiagnostics: QuerySchemaDiagnostic[],\n): { valid: boolean; options?: QueryOption[] } {\n\tconst types = normalizedTypes(schema.type);\n\tconst sources: QueryOption[][] = [];\n\tif (isPrimitiveArray(schema.enum)) {\n\t\tconst names = stringArray(schema['x-enumNames']);\n\t\tconst options = schema.enum.flatMap((value, index) =>\n\t\t\ttypes.length && !types.some((type) => primitiveMatchesType(value, type))\n\t\t\t\t? []\n\t\t\t\t: [{ value, label: names?.[index] ?? String(value) }],\n\t\t);\n\t\tif (!options.length) {\n\t\t\tdiagnostics.push(\n\t\t\t\tdiagnostic('invalid-schema', 'error', toPointer(segments), 'Schema enum has no values allowed by its type.'),\n\t\t\t);\n\t\t\treturn { valid: false };\n\t\t}\n\t\tsources.push(options);\n\t}\n\tif (isQueryJsonPrimitive(schema.const)) {\n\t\tsources.push([{ value: schema.const, label: String(schema.const) }]);\n\t}\n\tif (isSchemaArray(schema.oneOf)) {\n\t\tconst options: QueryOption[] = [];\n\t\tlet finite = true;\n\t\tfor (let index = 0; index < schema.oneOf.length; index += 1) {\n\t\t\tconst resolved = resolveSchema(\n\t\t\t\tschema.oneOf[index],\n\t\t\t\troot,\n\t\t\t\tstack,\n\t\t\t\t[...segments, 'oneOf', String(index)],\n\t\t\t\tdiagnostics,\n\t\t\t);\n\t\t\tif (!resolved) return { valid: false };\n\t\t\tconst value = resolved.schema.const;\n\t\t\tif (!isQueryJsonPrimitive(value)) {\n\t\t\t\tfinite = false;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (options.some((option) => Object.is(option.value, value))) {\n\t\t\t\tdiagnostics.push(\n\t\t\t\t\tdiagnostic(\n\t\t\t\t\t\t'invalid-schema',\n\t\t\t\t\t\t'error',\n\t\t\t\t\t\ttoPointer([...segments, 'oneOf', String(index)]),\n\t\t\t\t\t\t'oneOf constant options must be unique.',\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\treturn { valid: false };\n\t\t\t}\n\t\t\tif (!types.length || types.some((type) => primitiveMatchesType(value, type))) {\n\t\t\t\toptions.push({\n\t\t\t\t\tvalue,\n\t\t\t\t\tlabel: stringValue(resolved.schema.title) ?? String(value),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tif (!finite) {\n\t\t\tdiagnostics.push(\n\t\t\t\tdiagnostic(\n\t\t\t\t\t'invalid-schema',\n\t\t\t\t\t'error',\n\t\t\t\t\ttoPointer(segments),\n\t\t\t\t\t'Only finite const-based oneOf schemas can become query options.',\n\t\t\t\t),\n\t\t\t);\n\t\t\treturn { valid: false };\n\t\t}\n\t\tif (finite) {\n\t\t\tif (!options.length) {\n\t\t\t\tdiagnostics.push(\n\t\t\t\t\tdiagnostic('invalid-schema', 'error', toPointer(segments), 'oneOf has no constants allowed by its type.'),\n\t\t\t\t);\n\t\t\t\treturn { valid: false };\n\t\t\t}\n\t\t\tsources.push(options);\n\t\t}\n\t}\n\tif (!sources.length) return { valid: true };\n\tconst options = sources[0].filter((option) =>\n\t\tsources.slice(1).every((source) => source.some((candidate) => Object.is(candidate.value, option.value))),\n\t);\n\tif (!options.length) {\n\t\tdiagnostics.push(\n\t\t\tdiagnostic('invalid-schema', 'error', toPointer(segments), 'Schema value constraints have no intersection.'),\n\t\t);\n\t\treturn { valid: false };\n\t}\n\treturn { valid: true, options };\n}\n\nfunction inferKind(schema: QueryJsonSchema, options?: readonly QueryOption[]): QueryFieldKind {\n\tif (normalizedTypes(schema.type).includes('array')) return 'array';\n\tif (options?.length) return 'enum';\n\tif (schema.format === 'date') return 'date';\n\tif (schema.format === 'date-time') return 'datetime';\n\tconst type = normalizedTypes(schema.type).find((candidate) => candidate !== 'null');\n\tif (type === 'string' || type === 'number' || type === 'integer' || type === 'boolean') return type;\n\treturn 'unknown';\n}\n\nfunction isObjectSchema(schema: QueryJsonSchema) {\n\treturn normalizedTypes(schema.type).includes('object') || Boolean(schema.properties);\n}\n\nfunction normalizedTypes(type: QueryJsonSchema['type']) {\n\tif (typeof type === 'string') return [type];\n\tif (!Array.isArray(type)) return [];\n\tconst output: string[] = [];\n\tfor (let index = 0; index < type.length; index += 1) {\n\t\tif (!(index in type) || typeof type[index] !== 'string') return [];\n\t\toutput.push(type[index]);\n\t}\n\treturn output;\n}\n\nfunction intersectSchemaTypes(left: readonly string[], right: readonly string[]) {\n\tconst output: string[] = [];\n\tfor (const leftType of left) {\n\t\tfor (const rightType of right) {\n\t\t\tconst type =\n\t\t\t\tleftType === rightType\n\t\t\t\t\t? leftType\n\t\t\t\t\t: (leftType === 'number' && rightType === 'integer') || (leftType === 'integer' && rightType === 'number')\n\t\t\t\t\t\t? 'integer'\n\t\t\t\t\t\t: undefined;\n\t\t\tif (type && !output.includes(type)) output.push(type);\n\t\t}\n\t}\n\treturn output;\n}\n\nfunction toPointer(segments: readonly string[]) {\n\treturn segments.length ? `/${segments.map(encodePointerSegment).join('/')}` : '';\n}\n\nfunction decodePointer(pointer: string) {\n\treturn pointer ? pointer.slice(1).split('/').map(decodePointerSegment) : [];\n}\n\nfunction encodePointerSegment(value: string) {\n\treturn value.replaceAll('~', '~0').replaceAll('/', '~1');\n}\n\nfunction decodePointerSegment(value: string) {\n\treturn value.replaceAll('~1', '/').replaceAll('~0', '~');\n}\n\nfunction humanize(value: string) {\n\tconst label = value\n\t\t.replace(/([a-z\\d])([A-Z])/g, '$1 $2')\n\t\t.replace(/[_-]+/g, ' ')\n\t\t.trim();\n\treturn label ? label[0].toUpperCase() + label.slice(1) : value;\n}\n\nfunction isRecord(value: unknown): value is Record {\n\tif (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n\tconst prototype = Object.getPrototypeOf(value);\n\treturn prototype === Object.prototype || prototype === null;\n}\n\nfunction schemaObjectChildren(value: Record) {\n\tconst children: unknown[] = [];\n\tfor (const keyword of ['properties', '$defs', 'definitions'] as const) {\n\t\tif (isRecord(value[keyword])) children.push(...Object.values(value[keyword]));\n\t}\n\tif (value.items !== undefined) children.push(value.items);\n\tif (Array.isArray(value.oneOf)) children.push(...value.oneOf);\n\treturn children;\n}\n\nfunction hasSchemaObjectCycle(value: unknown): boolean {\n\tif (!isRecord(value)) return false;\n\tconst ancestors = new WeakSet();\n\tconst visited = new WeakSet();\n\tconst stack: { node: Record; children: unknown[]; index: number }[] = [\n\t\t{ node: value, children: schemaObjectChildren(value), index: 0 },\n\t];\n\tancestors.add(value);\n\twhile (stack.length) {\n\t\tconst frame = stack[stack.length - 1];\n\t\tif (frame.index >= frame.children.length) {\n\t\t\tancestors.delete(frame.node);\n\t\t\tvisited.add(frame.node);\n\t\t\tstack.pop();\n\t\t\tcontinue;\n\t\t}\n\t\tconst child = frame.children[frame.index];\n\t\tframe.index += 1;\n\t\tif (!isRecord(child)) continue;\n\t\tif (ancestors.has(child)) return true;\n\t\tif (visited.has(child)) continue;\n\t\tancestors.add(child);\n\t\tstack.push({ node: child, children: schemaObjectChildren(child), index: 0 });\n\t}\n\treturn false;\n}\n\nfunction isSchemaType(value: unknown) {\n\tconst allowed = new Set(['null', 'object', 'array', 'string', 'number', 'integer', 'boolean']);\n\tif (typeof value === 'string') return allowed.has(value);\n\tif (!Array.isArray(value) || value.length === 0) return false;\n\tfor (let index = 0; index < value.length; index += 1) {\n\t\tif (!(index in value) || typeof value[index] !== 'string' || !allowed.has(value[index])) return false;\n\t}\n\treturn true;\n}\n\nfunction isQueryJsonPrimitive(value: unknown): value is QueryJsonPrimitive {\n\treturn (\n\t\tvalue === null ||\n\t\ttypeof value === 'string' ||\n\t\ttypeof value === 'boolean' ||\n\t\t(typeof value === 'number' && Number.isFinite(value))\n\t);\n}\n\nfunction primitiveMatchesType(value: QueryJsonPrimitive, type: string) {\n\tswitch (type) {\n\t\tcase 'null':\n\t\t\treturn value === null;\n\t\tcase 'string':\n\t\t\treturn typeof value === 'string';\n\t\tcase 'boolean':\n\t\t\treturn typeof value === 'boolean';\n\t\tcase 'number':\n\t\t\treturn typeof value === 'number' && Number.isFinite(value);\n\t\tcase 'integer':\n\t\t\treturn typeof value === 'number' && Number.isInteger(value);\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\nfunction isPrimitiveArray(value: unknown): value is readonly QueryJsonPrimitive[] {\n\tif (!Array.isArray(value) || value.length === 0) return false;\n\tfor (let index = 0; index < value.length; index += 1) {\n\t\tif (!(index in value) || !isQueryJsonPrimitive(value[index])) return false;\n\t}\n\treturn true;\n}\n\nfunction isSchemaArray(value: unknown): value is readonly QueryJsonSchema[] {\n\tif (!Array.isArray(value) || value.length === 0) return false;\n\tfor (let index = 0; index < value.length; index += 1) {\n\t\tif (!(index in value) || !isRecord(value[index])) return false;\n\t}\n\treturn true;\n}\n\nfunction stringValue(value: unknown) {\n\treturn typeof value === 'string' && value.trim() ? value : undefined;\n}\n\nfunction numberValue(value: unknown) {\n\treturn typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction stringArray(value: unknown) {\n\tif (!Array.isArray(value)) return undefined;\n\tfor (let index = 0; index < value.length; index += 1) {\n\t\tif (!(index in value) || typeof value[index] !== 'string') return undefined;\n\t}\n\treturn value as string[];\n}\n\nfunction isFieldKind(value: unknown): value is QueryFieldKind {\n\treturn (\n\t\ttypeof value === 'string' &&\n\t\t['string', 'number', 'integer', 'boolean', 'date', 'datetime', 'enum', 'array', 'unknown'].includes(value)\n\t);\n}\n\nfunction diagnostic(\n\tcode: QuerySchemaDiagnosticCode,\n\tseverity: QuerySchemaDiagnostic['severity'],\n\tpath: string,\n\tmessage: string,\n\tref?: string,\n): QuerySchemaDiagnostic {\n\treturn { code, severity, path, message, ref };\n}\n", "type": "registry:lib", "target": "@components/query-builder/query-json-schema.ts" }, { "path": "registry/default/ui/query-builder/query-messages.ts", "content": "import type { QueryValidationIssue } from './query-state';\n\nexport type QueryBuilderMessages = {\n\ttitle: string;\n\tmatch: string;\n\tall: string;\n\tany: string;\n\tnot: string;\n\taddCondition: string;\n\taddGroup: string;\n\tsearchFields: string;\n\tsearchValues: string;\n\tnoFields: string;\n\tchooseField: string;\n\tchooseOperator: string;\n\tunavailableOperator: (label: string) => string;\n\tchooseValue: string;\n\tselectedCount: (count: number) => string;\n\tselectedOnly: string;\n\tclearValues: string;\n\ttrueLabel: string;\n\tfalseLabel: string;\n\tnoValue: string;\n\taddValue: string;\n\tremoveValue: string;\n\trangeStart: string;\n\trangeEnd: string;\n\tcollapseGroup: string;\n\texpandGroup: string;\n\tduplicate: string;\n\tremove: string;\n\tmoveUp: string;\n\tmoveDown: string;\n\temptyGroup: string;\n\tinvalidQuery: string;\n\tqueryCondition: string;\n\tunknownField: string;\n\tunknownOperator: string;\n\tconditionLabel: (label: string) => string;\n\tgroupLabel: (count: number) => string;\n\tactionsLabel: (label: string) => string;\n\tactionLabel: (action: string, target: string) => string;\n\tvalidationIssue: (issue: QueryValidationIssue) => string;\n\tconditionCount: (count: number) => string;\n\tvalidationCount: (count: number) => string;\n};\n\nexport const defaultQueryBuilderMessages: QueryBuilderMessages = {\n\ttitle: '筛选条件',\n\tmatch: '匹配',\n\tall: '全部',\n\tany: '任一',\n\tnot: '排除',\n\taddCondition: '添加条件',\n\taddGroup: '添加分组',\n\tsearchFields: '搜索字段',\n\tsearchValues: '搜索值',\n\tnoFields: '没有匹配字段',\n\tchooseField: '选择字段',\n\tchooseOperator: '选择操作符',\n\tunavailableOperator: (label) => `${label}(不可用)`,\n\tchooseValue: '选择值',\n\tselectedCount: (count) => `已选 ${count} 项`,\n\tselectedOnly: '仅看已选',\n\tclearValues: '清空',\n\ttrueLabel: '是',\n\tfalseLabel: '否',\n\tnoValue: '无需填写值',\n\taddValue: '添加值',\n\tremoveValue: '移除值',\n\trangeStart: '最小值',\n\trangeEnd: '最大值',\n\tcollapseGroup: '收起分组',\n\texpandGroup: '展开分组',\n\tduplicate: '复制',\n\tremove: '删除',\n\tmoveUp: '上移',\n\tmoveDown: '下移',\n\temptyGroup: '暂无条件',\n\tinvalidQuery: '查询条件结构无效。',\n\tqueryCondition: '查询条件',\n\tunknownField: '未知字段',\n\tunknownOperator: '未知操作符',\n\tconditionLabel: (label) => `${label}条件`,\n\tgroupLabel: (count) => `包含 ${count} 个条件的分组`,\n\tactionsLabel: (label) => `${label}操作`,\n\tactionLabel: (action, target) => `${action}:${target}`,\n\tvalidationIssue: (issue) => issue.message,\n\tconditionCount: (count) => `${count} 个条件`,\n\tvalidationCount: (count) => `${count} 个问题`,\n};\n", "type": "registry:lib", "target": "@components/query-builder/query-messages.ts" }, { "path": "registry/default/ui/query-builder/use-query-builder.ts", "content": "import { useCallback, useEffect, useRef, useState } from 'react';\nimport type { QueryGroup } from './query-model';\nimport { createQueryGroup } from './query-model';\nimport type { QueryAction, QueryReducerOptions } from './query-state';\nimport { areQueriesEqual, cloneQuery, isQueryGroupValue, reduceQuery } from './query-state';\n\nexport type QueryChangeHandler = (query: QueryGroup, action: QueryAction) => void;\n\nexport type UseQueryBuilderControllerOptions = {\n\tquery: QueryGroup;\n\tonQueryChange: QueryChangeHandler;\n\treducerOptions?: QueryReducerOptions;\n};\n\nexport function useQueryBuilderController({ query, onQueryChange, reducerOptions }: UseQueryBuilderControllerOptions) {\n\tconst dispatch = useCallback(\n\t\t(action: QueryAction) => {\n\t\t\tconst next = reduceQuery(query, action, reducerOptions);\n\t\t\tif (next === query) return query;\n\t\t\tonQueryChange(next, action);\n\t\t\treturn next;\n\t\t},\n\t\t[onQueryChange, query, reducerOptions],\n\t);\n\treturn { query, dispatch };\n}\n\nexport type QueryDraftApplyOptions = {\n\tforce?: boolean;\n};\n\nexport type UseQueryBuilderDraftOptions = {\n\tvalue: QueryGroup;\n\tonApply?: (query: QueryGroup) => void;\n\tvalidate?: (query: QueryGroup) => readonly TIssue[];\n\treducerOptions?: QueryReducerOptions;\n};\n\nexport type QueryDraftSetter = (query: QueryGroup | ((current: QueryGroup) => QueryGroup)) => void;\n\nfunction prepareExternalQuery(value: unknown) {\n\treturn isQueryGroupValue(value)\n\t\t? { query: cloneQuery(value), valid: true }\n\t\t: { query: createQueryGroup('query-invalid-external'), valid: false };\n}\n\nexport function useQueryBuilderDraft({\n\tvalue,\n\tonApply,\n\tvalidate,\n\treducerOptions,\n}: UseQueryBuilderDraftOptions) {\n\tconst initialRef = useRef | null>(null);\n\tif (!initialRef.current) initialRef.current = prepareExternalQuery(value);\n\tconst initial = initialRef.current;\n\tconst baselineRef = useRef(initial.query);\n\tconst latestExternalRef = useRef(initial.query);\n\tconst seenExternalRef = useRef(initial.query);\n\tconst draftRef = useRef(initial.query);\n\tconst [draft, setDraftState] = useState(initial.query);\n\tconst [stale, setStale] = useState(false);\n\tconst [invalidExternal, setInvalidExternal] = useState(!initial.valid);\n\tconst staleRef = useRef(false);\n\n\tconst setDraft = useCallback((next) => {\n\t\tsetDraftState((current) => {\n\t\t\tconst resolved = typeof next === 'function' ? next(current) : next;\n\t\t\tif (!isQueryGroupValue(resolved)) return current;\n\t\t\tif (areQueriesEqual(current, resolved)) return current;\n\t\t\tconst owned = cloneQuery(resolved);\n\t\t\tdraftRef.current = owned;\n\t\t\treturn owned;\n\t\t});\n\t}, []);\n\n\tuseEffect(() => {\n\t\tif (!isQueryGroupValue(value)) {\n\t\t\tsetInvalidExternal(true);\n\t\t\treturn;\n\t\t}\n\t\tsetInvalidExternal(false);\n\t\tconst incoming = cloneQuery(value);\n\t\tif (areQueriesEqual(seenExternalRef.current, incoming)) return;\n\t\tseenExternalRef.current = incoming;\n\t\tlatestExternalRef.current = incoming;\n\t\tconst draftMatchesIncoming = areQueriesEqual(draftRef.current, incoming);\n\t\tconst draftMatchesBaseline = areQueriesEqual(draftRef.current, baselineRef.current);\n\t\tconst baselineMatchesIncoming = areQueriesEqual(baselineRef.current, incoming);\n\t\tif (draftMatchesIncoming || draftMatchesBaseline) {\n\t\t\tbaselineRef.current = incoming;\n\t\t\tdraftRef.current = incoming;\n\t\t\tsetDraftState(incoming);\n\t\t\tstaleRef.current = false;\n\t\t\tsetStale(false);\n\t\t} else if (baselineMatchesIncoming) {\n\t\t\tstaleRef.current = false;\n\t\t\tsetStale(false);\n\t\t} else {\n\t\t\tstaleRef.current = true;\n\t\t\tsetStale(true);\n\t\t}\n\t}, [value]);\n\n\tuseEffect(() => {\n\t\tif (!staleRef.current || !areQueriesEqual(draft, latestExternalRef.current)) return;\n\t\tbaselineRef.current = cloneQuery(latestExternalRef.current);\n\t\tstaleRef.current = false;\n\t\tsetStale(false);\n\t}, [draft]);\n\n\tconst dispatch = useCallback(\n\t\t(action: QueryAction) => {\n\t\t\tconst current = draftRef.current;\n\t\t\tconst next = reduceQuery(current, action, reducerOptions);\n\t\t\tif (next === current) return current;\n\t\t\tdraftRef.current = next;\n\t\t\tsetDraftState(next);\n\t\t\treturn next;\n\t\t},\n\t\t[reducerOptions],\n\t);\n\n\tconst reset = useCallback(() => {\n\t\tconst next = cloneQuery(latestExternalRef.current);\n\t\tbaselineRef.current = next;\n\t\tdraftRef.current = next;\n\t\tsetDraftState(next);\n\t\tstaleRef.current = false;\n\t\tsetStale(false);\n\t\treturn next;\n\t}, []);\n\n\tconst apply = useCallback(\n\t\t(options: QueryDraftApplyOptions = {}) => {\n\t\t\tif (staleRef.current && !options.force) return undefined;\n\t\t\tconst next = cloneQuery(draftRef.current);\n\t\t\tonApply?.(cloneQuery(next));\n\t\t\treturn next;\n\t\t},\n\t\t[onApply],\n\t);\n\tconst overwrite = useCallback(() => apply({ force: true }), [apply]);\n\n\treturn {\n\t\tdraft,\n\t\tdispatch,\n\t\tsetDraft,\n\t\tonQueryChange: setDraft,\n\t\tdirty: !areQueriesEqual(draft, baselineRef.current),\n\t\tstale,\n\t\tcanApply: !stale,\n\t\tinvalidExternal,\n\t\tissues: validate?.(draft) ?? [],\n\t\tapply,\n\t\toverwrite,\n\t\treset,\n\t};\n}\n", "type": "registry:hook", "target": "@components/query-builder/use-query-builder.ts" }, { "path": "registry/default/ui/query-builder/query-editors.tsx", "content": "import { Popover as BasePopover } from '@base-ui/react/popover';\nimport { Check, ChevronDown, Plus, Search, X } from 'lucide-react';\nimport type { ComponentType, KeyboardEvent as ReactKeyboardEvent, ReactNode } from 'react';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { cn } from '@/lib/utils';\nimport type { QueryBuilderMessages } from './query-messages';\nimport type {\n\tQueryField,\n\tQueryFieldKind,\n\tQueryJsonPrimitive,\n\tQueryJsonValue,\n\tQueryOperator,\n\tQueryRule,\n} from './query-model';\n\nexport type QueryFieldPickerProps = {\n\tfields: readonly QueryField[];\n\tlabel: ReactNode;\n\tsearchLabel: string;\n\temptyLabel: string;\n\tdisabled?: boolean;\n\tinvalid?: boolean;\n\tariaDescribedBy?: string;\n\tclassName?: string;\n\tonSelect: (field: QueryField) => void;\n};\n\nexport function QueryFieldPicker({\n\tfields,\n\tlabel,\n\tsearchLabel,\n\temptyLabel,\n\tdisabled,\n\tinvalid,\n\tariaDescribedBy,\n\tclassName,\n\tonSelect,\n}: QueryFieldPickerProps) {\n\tconst [open, setOpen] = useState(false);\n\tconst [search, setSearch] = useState('');\n\tconst optionsRef = useRef(null);\n\tconst filtered = useMemo(() => {\n\t\tconst needle = search.trim().toLocaleLowerCase();\n\t\treturn needle\n\t\t\t? fields.filter((field) =>\n\t\t\t\t\t`${field.label} ${field.name} ${field.group ?? ''}`.toLocaleLowerCase().includes(needle),\n\t\t\t\t)\n\t\t\t: [...fields];\n\t}, [fields, search]);\n\tconst groups = useMemo(() => groupFields(filtered), [filtered]);\n\treturn (\n\t\t {\n\t\t\t\tsetOpen(nextOpen);\n\t\t\t\tif (!nextOpen) setSearch('');\n\t\t\t}}\n\t\t>\n\t\t\t\n\t\t\t\t}\n\t\t\t>\n\t\t\t\t{label}\n\t\t\t\t