197 lines
5.5 KiB
JavaScript
197 lines
5.5 KiB
JavaScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
DEFAULT_OPERATOR_MAP,
|
|
mapOperators,
|
|
parseFilters,
|
|
splitMultiValue,
|
|
toTree,
|
|
} from '../src/index.js';
|
|
|
|
describe('mapOperators', () => {
|
|
it('maps known query codes', () => {
|
|
expect(mapOperators('EQ')).toBe('=');
|
|
expect(mapOperators('NOT_IN')).toBe('NOT IN');
|
|
});
|
|
|
|
it('allows custom overrides', () => {
|
|
expect(mapOperators('EQ', { EQ: 'equals' })).toBe('equals');
|
|
});
|
|
});
|
|
|
|
describe('splitMultiValue', () => {
|
|
it('splits IN values', () => {
|
|
expect(splitMultiValue('IN', 'a,b,c')).toEqual(['a', 'b', 'c']);
|
|
});
|
|
|
|
it('splits BETWEEN values', () => {
|
|
expect(splitMultiValue('BETWEEN', '2024-01-01,2024-12-31')).toEqual([
|
|
'2024-01-01',
|
|
'2024-12-31',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('parseFilters', () => {
|
|
const allowedFields = ['name', 'status', 'tags', 'created'];
|
|
|
|
it('requires allowedFields', () => {
|
|
expect(() => parseFilters([], {})).toThrow(/allowedFields/);
|
|
});
|
|
|
|
it('parses basic conditions with logic pass-through', () => {
|
|
const input = [
|
|
{ field: 'name', value: 'xx', query: 'EQ' },
|
|
{ field: 'tags', value: 'a,b', query: 'IN', logic: 'OR' },
|
|
];
|
|
const out = parseFilters(input, { allowedFields });
|
|
expect(out).toEqual([
|
|
{
|
|
field: 'name',
|
|
query: 'EQ',
|
|
op: '=',
|
|
value: 'xx',
|
|
},
|
|
{
|
|
field: 'tags',
|
|
query: 'IN',
|
|
op: 'IN',
|
|
value: 'a,b',
|
|
values: ['a', 'b'],
|
|
logic: 'OR',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('preserves empty markers by default', () => {
|
|
const out = parseFilters(
|
|
[{ field: 'name', value: null, query: 'EQ', empty: true }],
|
|
{ allowedFields },
|
|
);
|
|
expect(out[0].empty).toBe(true);
|
|
});
|
|
|
|
it('skips disabled and hidden by default', () => {
|
|
const out = parseFilters(
|
|
[
|
|
{ field: 'name', value: 'a', query: 'EQ', disabled: true },
|
|
{ field: 'status', value: 'b', query: 'EQ', hidden: true },
|
|
{ field: 'tags', value: 'c', query: 'EQ' },
|
|
],
|
|
{ allowedFields },
|
|
);
|
|
expect(out).toHaveLength(1);
|
|
expect(out[0].field).toBe('tags');
|
|
});
|
|
|
|
it('can skip empty items when skipEmpty is true', () => {
|
|
const out = parseFilters(
|
|
[{ field: 'name', value: null, query: 'EQ', empty: true }],
|
|
{ allowedFields, skipEmpty: true },
|
|
);
|
|
expect(out).toEqual([]);
|
|
});
|
|
|
|
it('throws on unknown field by default', () => {
|
|
expect(() =>
|
|
parseFilters([{ field: 'unknown', value: 'x', query: 'EQ' }], {
|
|
allowedFields,
|
|
}),
|
|
).toThrow(/unknown field/);
|
|
});
|
|
|
|
it('supports flat mode', () => {
|
|
const out = parseFilters(
|
|
[{ field: 'name', value: 'x', query: 'EQ', logic: 'AND' }],
|
|
{ allowedFields, mode: 'flat' },
|
|
);
|
|
expect(out[0].logic).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('toTree', () => {
|
|
it('nests flat filters', () => {
|
|
const flat = [
|
|
{ field: 'a', query: 'EQ', op: '=', value: '1' },
|
|
{ field: 'b', query: 'EQ', op: '=', value: '2', logic: 'OR' },
|
|
];
|
|
const tree = toTree(flat);
|
|
expect(tree[0]).toMatchObject({
|
|
logic: 'OR',
|
|
children: expect.any(Array),
|
|
});
|
|
expect(tree[0].children).toHaveLength(2);
|
|
});
|
|
|
|
it('merges adjacent same-logic leaves into one node', () => {
|
|
const flat = [
|
|
{ field: 'a', query: 'EQ', op: '=', value: '1' },
|
|
{ field: 'b', query: 'EQ', op: '=', value: '2', logic: 'AND' },
|
|
{ field: 'c', query: 'EQ', op: '=', value: '3', logic: 'AND' },
|
|
];
|
|
const tree = toTree(flat);
|
|
expect(tree).toHaveLength(1);
|
|
expect(tree[0].logic).toBe('AND');
|
|
expect(tree[0].children).toHaveLength(3);
|
|
});
|
|
|
|
it('left-associates mixed logic: A AND B OR C -> (A AND B) OR C', () => {
|
|
const flat = [
|
|
{ field: 'a', query: 'EQ', op: '=', value: '1' },
|
|
{ field: 'b', query: 'EQ', op: '=', value: '2', logic: 'AND' },
|
|
{ field: 'c', query: 'EQ', op: '=', value: '3', logic: 'OR' },
|
|
];
|
|
const tree = toTree(flat);
|
|
expect(tree).toHaveLength(1);
|
|
expect(tree[0].logic).toBe('OR');
|
|
expect(tree[0].children).toHaveLength(2);
|
|
expect(tree[0].children[0].logic).toBe('AND');
|
|
expect(tree[0].children[0].children).toHaveLength(2);
|
|
});
|
|
|
|
it('supports explicit nested group via children key', () => {
|
|
const flat = [
|
|
{ field: 'a', query: 'EQ', op: '=', value: '1' },
|
|
{
|
|
logic: 'OR',
|
|
children: [
|
|
{ field: 'b', query: 'EQ', op: '=', value: '2' },
|
|
{ field: 'c', query: 'EQ', op: '=', value: '3', logic: 'AND' },
|
|
],
|
|
},
|
|
];
|
|
const tree = toTree(flat);
|
|
// a OR (b AND c)
|
|
expect(tree[0].logic).toBe('OR');
|
|
expect(tree[0].children).toHaveLength(2);
|
|
const inner = tree[0].children[1].children;
|
|
expect(inner).toHaveLength(1);
|
|
expect(inner[0].logic).toBe('AND');
|
|
expect(inner[0].children).toHaveLength(2);
|
|
});
|
|
|
|
it('supports group key shorthand', () => {
|
|
const flat = [
|
|
{ field: 'a', query: 'EQ', op: '=', value: '1' },
|
|
{
|
|
logic: 'OR',
|
|
group: [{ field: 'b', query: 'EQ', op: '=', value: '2' }],
|
|
},
|
|
];
|
|
const tree = toTree(flat);
|
|
expect(tree[0].logic).toBe('OR');
|
|
expect(tree[0].children).toHaveLength(2);
|
|
expect(tree[0].children[1].children).toHaveLength(1);
|
|
});
|
|
|
|
it('returns empty array for empty input', () => {
|
|
expect(toTree([])).toEqual([]);
|
|
expect(toTree(null)).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('DEFAULT_OPERATOR_MAP', () => {
|
|
it('includes IS_NULL operators', () => {
|
|
expect(DEFAULT_OPERATOR_MAP.IS_NULL).toBe('IS NULL');
|
|
});
|
|
});
|