UNPKG

@mfissehaye/string-to-drizzle-orm-filters

Version:
343 lines (307 loc) 14 kB
import { describe, it, expect, vi, beforeEach } from "vitest"; import { SQL } from 'drizzle-orm' import { AnyColumn } from "drizzle-orm"; import { FilterGenerator, ColumnMap } from '../src/generator' import { Program } from "../src/ast"; import { ParserError } from "../src/parser"; const { mockAnd, mockEq, mockOr, mockLike, mockIlike, mockGt, mockGte, mockLt, mockLte, mockIsNull, mockIsNotNull, mockNot } = vi.hoisted(() => { // Mock Drizzle ORM function for testing const mockEq = vi.fn((col: AnyColumn | string, val: string | number) => `eq(${typeof col === 'object' ? (col as any).__name : col}, ${JSON.stringify(val)})`); const mockAnd = vi.fn((...args: any[]) => `and(${args.join(', ')})`); const mockOr = vi.fn((...args: any[]) => `or(${args.join(', ')})`); const mockLike = vi.fn((col: AnyColumn | string, val: string) => `like(${typeof col === 'object' ? (col as any).__name : col}, ${JSON.stringify(val)})`); const mockIlike = vi.fn((col: AnyColumn | string, val: string) => `ilike(${typeof col === 'object' ? (col as any).__name : col}, ${JSON.stringify(val)})`); const mockGt = vi.fn((col: AnyColumn | string, val: string | number) => `gt(${typeof col === 'object' ? (col as any).__name : col}, ${JSON.stringify(val)})`); const mockGte = vi.fn((col: AnyColumn | string, val: string | number) => `gte(${typeof col === 'object' ? (col as any).__name : col}, ${JSON.stringify(val)})`); const mockLt = vi.fn((col: AnyColumn | string, val: string | number) => `lt(${typeof col === 'object' ? (col as any).__name : col}, ${JSON.stringify(val)})`); const mockLte = vi.fn((col: AnyColumn | string, val: string | number) => `lte(${typeof col === 'object' ? (col as any).__name : col}, ${JSON.stringify(val)})`); const mockIsNull = vi.fn((col: AnyColumn | string) => `isNull(${typeof col === 'object' ? (col as any).__name : col})`); const mockIsNotNull = vi.fn((col: AnyColumn | string) => `isNotNull(${typeof col === 'object' ? (col as any).__name : col})`); const mockNot = vi.fn((filter: SQL) => `not(${filter})`); return { mockAnd, mockEq, mockOr, mockLike, mockIlike, mockGt, mockGte, mockLt, mockLte, mockIsNull, mockIsNotNull, mockNot }; }) // Create mock column objects for testing const mockUsersTable = { id: { __name: 'users.id' } as unknown as AnyColumn, name: { __name: 'users.name' } as unknown as AnyColumn, age: { __name: 'users.age' } as unknown as AnyColumn, price: { __name: 'users.price' } as unknown as AnyColumn, email: { __name: 'users.email' } as unknown as AnyColumn, }; const mockColumnMap: ColumnMap = { id: mockUsersTable.id, name: mockUsersTable.name, age: mockUsersTable.age, email: mockUsersTable.email, price: mockUsersTable.price, // For the example input: a: { __name: 'mock_table.a' } as unknown as AnyColumn, c: { __name: 'mock_table.c' } as unknown as AnyColumn, e: { __name: 'mock_table.e' } as unknown as AnyColumn, g: { __name: 'mock_table.g' } as unknown as AnyColumn, column: { __name: 'mock_table.column' } as unknown as AnyColumn, }; // Mock Drizzle ORM imports to use our spy functions vi.mock('drizzle-orm', async (importOriginal) => { const actual = await importOriginal<typeof import('drizzle-orm')>(); return { ...actual, eq: mockEq, and: mockAnd, or: mockOr, like: mockLike, ilike: mockIlike, gt: mockGt, gte: mockGte, lt: mockLt, lte: mockLte, isNull: mockIsNull, isNotNull: mockIsNotNull, not: mockNot, }; }); describe('FilterGenerator', () => { beforeEach(() => { // Reset all mocks before each test vi.clearAllMocks(); }); it('should generate a simple eq filter', () => { const ast: Program = { kind: 'Program', expression: { kind: 'CallExpression', functionName: 'eq', args: [ { kind: 'StringLiteral', value: 'name' }, { kind: 'StringLiteral', value: 'John Doe' }, ], }, }; const generator = new FilterGenerator(mockColumnMap); const result = generator.generate(ast); expect(mockEq).toHaveBeenCalledWith(mockUsersTable.name, 'John Doe'); expect(result).toBe('eq(users.name, "John Doe")'); }); it('should generate a simple logical AND filter', () => { const ast: Program = { kind: 'Program', expression: { kind: 'CallExpression', functionName: 'and', args: [ { kind: 'CallExpression', functionName: 'eq', args: [ { kind: 'StringLiteral', value: 'id' }, { kind: 'StringLiteral', value: '123' }, ], }, { kind: 'CallExpression', functionName: 'gt', args: [ { kind: 'StringLiteral', value: 'age' }, { kind: 'StringLiteral', value: '18' }, ], }, ], }, }; const generator = new FilterGenerator(mockColumnMap); const result = generator.generate(ast); expect(mockEq).toHaveBeenCalledWith(mockUsersTable.id, '123'); expect(mockGt).toHaveBeenCalledWith(mockUsersTable.age, '18'); expect(mockAnd).toHaveBeenCalledWith('eq(users.id, "123")', 'gt(users.age, "18")'); expect(result).toBe('and(eq(users.id, "123"), gt(users.age, "18"))'); }); it('should generate the example nested filter correctly', () => { const ast: Program = { kind: 'Program', expression: { kind: 'CallExpression', functionName: 'and', args: [ { kind: 'CallExpression', functionName: 'or', args: [ { kind: 'CallExpression', functionName: 'eq', args: [ { kind: 'StringLiteral', value: 'a' }, { kind: 'StringLiteral', value: 'b' }, ], }, { kind: 'CallExpression', functionName: 'like', args: [ { kind: 'StringLiteral', value: 'c' }, { kind: 'StringLiteral', value: 'd' }, ], }, ], }, { kind: 'CallExpression', functionName: 'and', args: [ { kind: 'CallExpression', functionName: 'gt', args: [ { kind: 'StringLiteral', value: 'e' }, { kind: 'StringLiteral', value: 'f' }, ], }, { kind: 'CallExpression', functionName: 'ilike', args: [ { kind: 'StringLiteral', value: 'g' }, { kind: 'StringLiteral', value: 'h' }, ], }, ], }, ], }, }; const generator = new FilterGenerator(mockColumnMap); const result = generator.generate(ast); // Verify individual calls expect(mockEq).toHaveBeenCalledWith(mockColumnMap['a'], 'b'); expect(mockLike).toHaveBeenCalledWith(mockColumnMap['c'], 'd'); expect(mockGt).toHaveBeenCalledWith(mockColumnMap['e'], 'f'); expect(mockIlike).toHaveBeenCalledWith(mockColumnMap['g'], 'h'); // Verify nested logical calls expect(mockOr).toHaveBeenCalledWith('eq(mock_table.a, "b")', 'like(mock_table.c, "d")'); expect(mockAnd).toHaveBeenCalledWith('gt(mock_table.e, "f")', 'ilike(mock_table.g, "h")'); // Verify top-level call expect(mockAnd).toHaveBeenCalledWith( 'or(eq(mock_table.a, "b"), like(mock_table.c, "d"))', 'and(gt(mock_table.e, "f"), ilike(mock_table.g, "h"))' ); expect(result).toBe( 'and(or(eq(mock_table.a, "b"), like(mock_table.c, "d")), and(gt(mock_table.e, "f"), ilike(mock_table.g, "h")))' ); }); it('should handle isNull operator', () => { const ast: Program = { kind: 'Program', expression: { kind: 'CallExpression', functionName: 'isNull', args: [ { kind: 'StringLiteral', value: 'email' }, ], }, }; const generator = new FilterGenerator(mockColumnMap); const result = generator.generate(ast); expect(mockIsNull).toHaveBeenCalledWith(mockUsersTable.email); expect(result).toBe('isNull(users.email)'); }); it('should handle not operator', () => { const ast: Program = { kind: 'Program', expression: { kind: 'CallExpression', functionName: 'not', args: [ { kind: 'CallExpression', functionName: 'eq', args: [ { kind: 'StringLiteral', value: 'id' }, { kind: 'StringLiteral', value: '123' }, ], }, ], }, }; const generator = new FilterGenerator(mockColumnMap); const result = generator.generate(ast); expect(mockEq).toHaveBeenCalledWith(mockUsersTable.id, '123'); expect(mockNot).toHaveBeenCalledWith('eq(users.id, "123")'); expect(result).toBe('not(eq(users.id, "123"))'); }); it('should throw ParserError for unsupported Drizzle ORM function', () => { const ast: Program = { kind: 'Program', expression: { kind: 'CallExpression', functionName: 'unsupportedFunction', args: [ { kind: 'StringLiteral', value: 'col' }, { kind: 'StringLiteral', value: 'val' }, ], }, }; const generator = new FilterGenerator(mockColumnMap); expect(() => generator.generate(ast)).toThrow(ParserError); expect(() => generator.generate(ast)).toThrow("Unsupported Drizzle ORM function: 'unsupportedFunction'"); }); it('should treat literal string not found in columnMap as value, not column', () => { const ast: Program = { kind: 'Program', expression: { kind: 'CallExpression', functionName: 'eq', args: [ { kind: 'StringLiteral', value: 'some_literal_string' }, // Not in columnMap { kind: 'StringLiteral', value: 'value' }, ], }, }; const generator = new FilterGenerator(mockColumnMap); const result = generator.generate(ast); expect(mockEq).toHaveBeenCalledWith('some_literal_string', 'value'); expect(result).toBe('eq(some_literal_string, "value")'); }); it('should handle empty program (though parser prevents this currently)', () => { const emptyAst: Program = { kind: 'Program', expression: null as any, // Simulate an empty expression, though parser enforces non-null }; const generator = new FilterGenerator(mockColumnMap); const result = generator.generate(emptyAst); expect(result).toBeUndefined(); // Or throw, based on desired behavior for malformed AST }); it('should generate an eq filter with an integer number literal', () => { const ast: Program = { kind: 'Program', expression: { kind: 'CallExpression', functionName: 'eq', args: [ { kind: 'StringLiteral', value: 'age' }, { kind: 'NumberLiteral', value: 30 }, ], }, }; const generator = new FilterGenerator(mockColumnMap); const result = generator.generate(ast); expect(mockEq).toHaveBeenCalledWith(mockUsersTable.age, 30); expect(result).toBe('eq(users.age, 30)'); }); it('should generate a gt filter with a floating-point number literal', () => { const ast: Program = { kind: 'Program', expression: { kind: 'CallExpression', functionName: 'gt', args: [ { kind: 'StringLiteral', value: 'price' }, { kind: 'NumberLiteral', value: 99.99 }, ], }, }; const generator = new FilterGenerator(mockColumnMap); const result = generator.generate(ast); expect(mockGt).toHaveBeenCalledWith(mockUsersTable.price, 99.99); expect(result).toBe('gt(users.price, 99.99)'); }); });