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'); }); });