238 lines
8.1 KiB
JavaScript
238 lines
8.1 KiB
JavaScript
import { describe, expect, it } from 'vitest'
|
||
import {
|
||
advancedQueryFromAst,
|
||
astToSql,
|
||
buildAdvancedAst,
|
||
buildQuickAst,
|
||
defaultOperator,
|
||
emptyConditionValue,
|
||
hasConditionValue,
|
||
operatorsForField,
|
||
} from '@/components/fms-module-common/queryUtils'
|
||
|
||
const fields = new Map([
|
||
['name', { b_field: 'name', b_type: 'input' }],
|
||
['amount', { b_field: 'amount', b_type: 'money' }],
|
||
['created', { b_field: 'created', b_type: 'datetime' }],
|
||
['status', { b_field: 'status', b_type: 'select' }],
|
||
])
|
||
|
||
describe('查询条件 AST 构造', () => {
|
||
it('按字段类型给出默认操作符:文本 like、日期 between、其余 eq', () => {
|
||
expect(defaultOperator({ b_type: 'input' })).toBe('like')
|
||
expect(defaultOperator({ b_type: 'datetime' })).toBe('between')
|
||
expect(defaultOperator({ b_type: 'select' })).toBe('eq')
|
||
expect(operatorsForField({ b_type: 'money' })).toContain('between')
|
||
})
|
||
|
||
it('生成转义后的 like / 不等 / 空值条件', () => {
|
||
expect(
|
||
astToSql({ type: 'condition', field: 'name', operator: 'like', value: "O'Reilly" }, fields),
|
||
).toBe("[name] LIKE N'%O''Reilly%'")
|
||
expect(
|
||
astToSql({ type: 'condition', field: 'name', operator: 'not_like', value: 'a' }, fields),
|
||
).toBe("[name] NOT LIKE N'%a%'")
|
||
expect(
|
||
astToSql({ type: 'condition', field: 'status', operator: 'ne', value: 'done' }, fields),
|
||
).toBe("[status] <> N'done'")
|
||
expect(
|
||
astToSql({ type: 'condition', field: 'name', operator: 'is_null', value: '' }, fields),
|
||
).toBe('[name] IS NULL')
|
||
})
|
||
|
||
it('区间条件可退化为单边比较,日期单边取对应端点', () => {
|
||
expect(
|
||
astToSql(
|
||
{ type: 'condition', field: 'amount', operator: 'between', value: ['10', ''] },
|
||
fields,
|
||
),
|
||
).toBe('[amount] >= 10')
|
||
expect(
|
||
astToSql(
|
||
{ type: 'condition', field: 'created', operator: 'ge', value: ['2026-01-01', ''] },
|
||
fields,
|
||
),
|
||
).toBe("[created] >= N'2026-01-01 00:00:00'")
|
||
expect(
|
||
astToSql(
|
||
{ type: 'condition', field: 'created', operator: 'le', value: ['', '2026-01-31'] },
|
||
fields,
|
||
),
|
||
).toBe("[created] <= N'2026-01-31 23:59:59'")
|
||
})
|
||
|
||
it('嵌套条件组按各自逻辑加括号,空组不产生条件', () => {
|
||
const ast = {
|
||
type: 'group',
|
||
logic: 'and',
|
||
children: [
|
||
{ type: 'condition', field: 'status', operator: 'eq', value: 'new' },
|
||
{
|
||
type: 'group',
|
||
logic: 'or',
|
||
children: [
|
||
{ type: 'condition', field: 'name', operator: 'like', value: 'a' },
|
||
{ type: 'condition', field: 'name', operator: 'like', value: '' },
|
||
],
|
||
},
|
||
],
|
||
}
|
||
expect(astToSql(ast, fields)).toBe("([status] = N'new' AND [name] LIKE N'%a%')")
|
||
expect(astToSql({ type: 'group', logic: 'and', children: [] }, fields)).toBe('')
|
||
})
|
||
|
||
it('常用条件跳过空值,操作符取模块配置', () => {
|
||
const items = [
|
||
{ field: fields.get('name'), config: { b_operator: 'like' } },
|
||
{ field: fields.get('status'), config: { b_operator: 'eq' } },
|
||
]
|
||
const ast = buildQuickAst(items, { name: 'abc', status: '' })
|
||
expect(ast.children).toHaveLength(1)
|
||
expect(astToSql(ast, fields)).toBe("[name] LIKE N'%abc%'")
|
||
})
|
||
|
||
it('高级条件按每项 join 生成 AST,单条件的组不再套一层', () => {
|
||
const ast = buildAdvancedAst({
|
||
nodes: [
|
||
{ join: 'or', field: 'name', operator: 'like', value: 'a' },
|
||
{ join: 'or', field: 'name', operator: 'like', value: 'b' },
|
||
],
|
||
groups: [
|
||
{
|
||
id: 'g1',
|
||
join: 'or',
|
||
nodes: [{ join: 'and', field: 'status', operator: 'eq', value: 'new' }],
|
||
},
|
||
],
|
||
})
|
||
// join 决定每项与上一相邻项的连接;首项 join 忽略;单条件的组等价于一个普通条件
|
||
expect(astToSql(ast, fields)).toBe(
|
||
"([name] LIKE N'%a%' OR [name] LIKE N'%b%' OR [status] = N'new')",
|
||
)
|
||
})
|
||
|
||
it('支持 (A 且 B) 或 C:组行内且、组与前一行或', () => {
|
||
const ast = buildAdvancedAst({
|
||
nodes: [{ join: 'or', field: 'name', operator: 'eq', value: '张三' }],
|
||
groups: [
|
||
{
|
||
id: 'g1',
|
||
join: 'or',
|
||
nodes: [
|
||
{ join: 'and', field: 'amount', operator: 'gt', value: 100 },
|
||
{ join: 'and', field: 'amount', operator: 'gt', value: 1000 },
|
||
],
|
||
},
|
||
],
|
||
})
|
||
expect(astToSql(ast, fields)).toBe("([name] = N'张三' OR ([amount] > 100 AND [amount] > 1000))")
|
||
})
|
||
|
||
it('同一层可混用 且/或:(a 且 b) 或 c 从左到右加括号', () => {
|
||
const ast = buildAdvancedAst({
|
||
nodes: [
|
||
{ join: 'and', field: 'name', operator: 'like', value: 'a' },
|
||
{ join: 'and', field: 'name', operator: 'like', value: 'b' },
|
||
{ join: 'or', field: 'name', operator: 'like', value: 'c' },
|
||
],
|
||
})
|
||
expect(astToSql(ast, fields)).toBe(
|
||
"(([name] LIKE N'%a%' AND [name] LIKE N'%b%') OR [name] LIKE N'%c%')",
|
||
)
|
||
})
|
||
|
||
it('区间 / 多选只要有一项即算已填写,空值按操作符重置', () => {
|
||
expect(hasConditionValue(['', '20'])).toBe(true)
|
||
expect(hasConditionValue(['', ''])).toBe(false)
|
||
expect(hasConditionValue(undefined)).toBe(false)
|
||
expect(emptyConditionValue({ b_type: 'money' }, 'between')).toEqual(['', ''])
|
||
expect(emptyConditionValue({ b_type: 'select' }, 'eq')).toBeUndefined()
|
||
})
|
||
})
|
||
|
||
describe('高级条件 AST → 界面状态(advancedQueryFromAst)', () => {
|
||
let seq = 0
|
||
const idFactory = () => `n${(seq += 1)}`
|
||
|
||
it('空 AST / 空组都给空结构且不降级', () => {
|
||
expect(advancedQueryFromAst(null, { idFactory })).toEqual({
|
||
nodes: [],
|
||
groups: [],
|
||
degraded: false,
|
||
})
|
||
expect(advancedQueryFromAst({ type: 'group', children: [] }, { idFactory })).toEqual({
|
||
nodes: [],
|
||
groups: [],
|
||
degraded: false,
|
||
})
|
||
})
|
||
|
||
it('单层「行 + 组」往返:还原后再构造语义等价', () => {
|
||
const ast = buildAdvancedAst({
|
||
nodes: [
|
||
{ join: 'and', field: 'name', operator: 'like', value: 'a' },
|
||
{ join: 'or', field: 'name', operator: 'like', value: 'b' },
|
||
],
|
||
groups: [
|
||
{
|
||
join: 'and',
|
||
nodes: [
|
||
{ join: 'and', field: 'amount', operator: 'gt', value: 100 },
|
||
{ join: 'or', field: 'status', operator: 'eq', value: 'done' },
|
||
],
|
||
},
|
||
],
|
||
})
|
||
const state = advancedQueryFromAst(ast, { idFactory })
|
||
|
||
expect(state.degraded).toBe(false)
|
||
expect(state.nodes.map((node) => [node.field, node.join])).toEqual([
|
||
['name', 'and'],
|
||
['name', 'or'],
|
||
])
|
||
expect(state.groups).toHaveLength(1)
|
||
expect(state.groups[0].nodes.map((node) => [node.field, node.join])).toEqual([
|
||
['amount', 'and'],
|
||
['status', 'or'],
|
||
])
|
||
expect(astToSql(buildAdvancedAst(state), fields)).toBe(astToSql(ast, fields))
|
||
})
|
||
|
||
it('组内再嵌套组:拍平成单层并置 degraded(根层的组不算降级)', () => {
|
||
const ast = {
|
||
type: 'group',
|
||
children: [
|
||
{ type: 'condition', field: 'name', operator: 'like', value: 'a', join: 'and' },
|
||
{
|
||
type: 'group',
|
||
join: 'or',
|
||
children: [
|
||
{ type: 'condition', field: 'amount', operator: 'gt', value: 1, join: 'and' },
|
||
{
|
||
type: 'group',
|
||
join: 'and',
|
||
children: [{ type: 'condition', field: 'amount', operator: 'lt', value: 9 }],
|
||
},
|
||
],
|
||
},
|
||
],
|
||
}
|
||
const state = advancedQueryFromAst(ast, { idFactory })
|
||
|
||
expect(state.degraded).toBe(true)
|
||
expect(state.nodes.map((node) => node.field)).toEqual(['name'])
|
||
expect(state.groups).toHaveLength(1)
|
||
expect(state.groups[0].nodes.map((node) => node.field)).toEqual(['amount', 'amount'])
|
||
expect(state.groups[0].join).toBe('or')
|
||
})
|
||
|
||
it('单条件 AST(非 group)也能还原成一行', () => {
|
||
const state = advancedQueryFromAst(
|
||
{ type: 'condition', field: 'status', operator: 'eq', value: 'draft' },
|
||
{ idFactory },
|
||
)
|
||
expect(state.nodes).toHaveLength(1)
|
||
expect(state.nodes[0]).toMatchObject({ field: 'status', operator: 'eq', value: 'draft' })
|
||
})
|
||
})
|