/** * Query Builder Tests * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 */ import { SafeQueryBuilder, createQueryBuilder, buildSelectQuery, buildCountQuery } from '../query-builder'; describe('Query Builder', () => { describe('SafeQueryBuilder', () => { let builder: SafeQueryBuilder; beforeEach(() => { builder = new SafeQueryBuilder(); }); describe('addParam', () => { it('should add params and return placeholders', () => { expect(builder.addParam('value1')).toBe('$1'); expect(builder.addParam('value2')).toBe('$2'); expect(builder.addParam('value3')).toBe('$3'); expect(builder.getParams()).toEqual(['value1', 'value2', 'value3']); }); it('should handle different types', () => { builder.addParam('string'); builder.addParam(123); builder.addParam(true); builder.addParam(null); expect(builder.getParams()).toEqual(['string', 123, true, null]); }); }); describe('getNextIndex', () => { it('should return the next parameter index', () => { expect(builder.getNextIndex()).toBe(1); builder.addParam('value'); expect(builder.getNextIndex()).toBe(2); }); }); describe('buildILike', () => { it('should build ILIKE condition with wildcards', () => { const condition = builder.buildILike('"name"', 'test'); expect(condition).toBe('"name" ILIKE $1'); expect(builder.getParams()).toEqual(['%test%']); }); it('should sanitize input', () => { const condition = builder.buildILike('"name"', ' test\0value '); expect(builder.getParams()).toEqual(['%testvalue%']); }); }); describe('buildILikeExact', () => { it('should build ILIKE condition without wildcards', () => { const condition = builder.buildILikeExact('"email"', 'test@example.com'); expect(condition).toBe('"email" ILIKE $1'); expect(builder.getParams()).toEqual(['test@example.com']); }); }); describe('buildILikePrefix', () => { it('should build ILIKE condition with trailing wildcard', () => { const condition = builder.buildILikePrefix('"title"', 'intro'); expect(condition).toBe('"title" ILIKE $1'); expect(builder.getParams()).toEqual(['intro%']); }); }); describe('buildIn', () => { it('should build IN clause using ANY', () => { const condition = builder.buildIn('"status"', ['active', 'pending']); expect(condition).toBe('"status" = ANY($1)'); expect(builder.getParams()).toEqual([['active', 'pending']]); }); it('should return FALSE for empty array', () => { const condition = builder.buildIn('"status"', []); expect(condition).toBe('FALSE'); expect(builder.getParams()).toEqual([]); }); }); describe('buildNotIn', () => { it('should build NOT IN clause using ALL', () => { const condition = builder.buildNotIn('"status"', ['deleted', 'archived']); expect(condition).toBe('"status" != ALL($1)'); }); it('should return TRUE for empty array', () => { const condition = builder.buildNotIn('"status"', []); expect(condition).toBe('TRUE'); }); }); describe('comparison operators', () => { it('should build equals condition', () => { expect(builder.buildEquals('"id"', 1)).toBe('"id" = $1'); }); it('should build not equals condition', () => { expect(builder.buildNotEquals('"id"', 1)).toBe('"id" != $1'); }); it('should build greater than condition', () => { expect(builder.buildGreaterThan('"count"', 10)).toBe('"count" > $1'); }); it('should build greater than or equals condition', () => { expect(builder.buildGreaterThanOrEquals('"count"', 10)).toBe('"count" >= $1'); }); it('should build less than condition', () => { expect(builder.buildLessThan('"count"', 10)).toBe('"count" < $1'); }); it('should build less than or equals condition', () => { expect(builder.buildLessThanOrEquals('"count"', 10)).toBe('"count" <= $1'); }); }); describe('buildBetween', () => { it('should build BETWEEN condition', () => { const condition = builder.buildBetween('"date"', '2024-01-01', '2024-12-31'); expect(condition).toBe('"date" BETWEEN $1 AND $2'); expect(builder.getParams()).toEqual(['2024-01-01', '2024-12-31']); }); }); describe('buildIsNull / buildIsNotNull', () => { it('should build IS NULL condition', () => { expect(builder.buildIsNull('"deletedAt"')).toBe('"deletedAt" IS NULL'); }); it('should build IS NOT NULL condition', () => { expect(builder.buildIsNotNull('"publishedAt"')).toBe('"publishedAt" IS NOT NULL'); }); }); describe('buildUUIDEquals', () => { it('should accept valid UUIDs', () => { const uuid = '550e8400-e29b-41d4-a716-446655440000'; const condition = builder.buildUUIDEquals('"userId"', uuid); expect(condition).toBe('"userId" = $1'); expect(builder.getParams()).toEqual([uuid]); }); it('should throw for invalid UUIDs', () => { expect(() => builder.buildUUIDEquals('"userId"', 'invalid')).toThrow('Invalid UUID'); }); }); describe('buildUUIDIn', () => { it('should accept array of valid UUIDs', () => { const uuids = [ '550e8400-e29b-41d4-a716-446655440000', '6ba7b810-9dad-11d1-80b4-00c04fd430c8' ]; const condition = builder.buildUUIDIn('"id"', uuids); expect(condition).toBe('"id" = ANY($1)'); }); it('should throw if any UUID is invalid', () => { const uuids = ['550e8400-e29b-41d4-a716-446655440000', 'invalid']; expect(() => builder.buildUUIDIn('"id"', uuids)).toThrow('Invalid UUID'); }); }); describe('conditions management', () => { it('should add and build WHERE clause', () => { builder.addCondition(builder.buildEquals('"status"', 'active')); builder.addCondition(builder.buildIsNull('"deletedAt"')); const where = builder.buildWhereClause(); expect(where).toBe('WHERE "status" = $1 AND "deletedAt" IS NULL'); }); it('should support custom separator', () => { builder.addCondition('"a" = 1'); builder.addCondition('"b" = 2'); const where = builder.buildWhereClause(' OR '); expect(where).toBe('WHERE "a" = 1 OR "b" = 2'); }); it('should return empty string for no conditions', () => { expect(builder.buildWhereClause()).toBe(''); }); it('should add condition only if value is truthy', () => { builder.addConditionIf('"a" = 1', 'value'); builder.addConditionIf('"b" = 2', undefined); builder.addConditionIf('"c" = 3', null); builder.addConditionIf('"d" = 4', ''); expect(builder.getConditions()).toEqual(['"a" = 1']); }); }); describe('reset', () => { it('should clear all state', () => { builder.addParam('value'); builder.addCondition('"a" = 1'); builder.reset(); expect(builder.getParams()).toEqual([]); expect(builder.getConditions()).toEqual([]); expect(builder.getNextIndex()).toBe(1); }); }); describe('clone', () => { it('should create independent copy', () => { builder.addParam('value1'); builder.addCondition('"a" = 1'); const clone = builder.clone(); clone.addParam('value2'); clone.addCondition('"b" = 2'); expect(builder.getParams()).toEqual(['value1']); expect(builder.getConditions()).toEqual(['"a" = 1']); expect(clone.getParams()).toEqual(['value1', 'value2']); expect(clone.getConditions()).toEqual(['"a" = 1', '"b" = 2']); }); }); }); describe('createQueryBuilder', () => { it('should create new builder instance', () => { const builder = createQueryBuilder(); expect(builder).toBeInstanceOf(SafeQueryBuilder); }); }); describe('buildSelectQuery', () => { it('should build basic SELECT query', () => { const builder = createQueryBuilder(); builder.addCondition(builder.buildIsNull('"deletedAt"')); const { query, params } = buildSelectQuery( 'documents', ['id', 'title', 'content'], builder ); expect(query).toBe('SELECT id, title, content FROM documents WHERE "deletedAt" IS NULL'); expect(params).toEqual([]); }); it('should add ORDER BY', () => { const builder = createQueryBuilder(); const { query } = buildSelectQuery( 'documents', ['*'], builder, { orderBy: '"createdAt"', orderDirection: 'DESC' } ); expect(query).toContain('ORDER BY "createdAt" DESC'); }); it('should add LIMIT and OFFSET', () => { const builder = createQueryBuilder(); const { query, params } = buildSelectQuery( 'documents', ['*'], builder, { limit: 10, offset: 20 } ); expect(query).toContain('LIMIT $1'); expect(query).toContain('OFFSET $2'); expect(params).toEqual([10, 20]); }); }); describe('buildCountQuery', () => { it('should build COUNT query', () => { const builder = createQueryBuilder(); builder.addCondition(builder.buildEquals('"status"', 'active')); const { query, params } = buildCountQuery('documents', builder); expect(query).toBe('SELECT COUNT(*) as count FROM documents WHERE "status" = $1'); expect(params).toEqual(['active']); }); it('should handle no conditions', () => { const builder = createQueryBuilder(); const { query } = buildCountQuery('documents', builder); expect(query).toBe('SELECT COUNT(*) as count FROM documents '); }); }); });