import { beforeEach, describe, expect, it, vi } from 'vitest'; import { AppError } from '../../src/utils/errors'; const { poolQueryMock, connectMock, clientQueryMock, releaseMock } = vi.hoisted(() => ({ poolQueryMock: vi.fn(), connectMock: vi.fn(), clientQueryMock: vi.fn(), releaseMock: vi.fn(), })); vi.mock('../../src/infra/database/database.manager', () => ({ DatabaseManager: { getInstance: vi.fn(() => ({ getPool: vi.fn(() => ({ query: poolQueryMock, connect: connectMock, })), })), }, })); import { AdminRecordService } from '../../src/services/database/admin-record.service'; describe('AdminRecordService', () => { beforeEach(() => { vi.clearAllMocks(); connectMock.mockResolvedValue({ query: clientQueryMock, release: releaseMock, }); }); it('reads protected-schema records through direct SQL with search and sorting', async () => { clientQueryMock.mockImplementation(async (sql: string) => { if (sql.includes('FROM information_schema.columns')) { return { rows: [ { column_name: 'id', data_type: 'uuid', is_nullable: 'NO', udt_name: 'uuid' }, { column_name: 'email', data_type: 'text', is_nullable: 'NO', udt_name: 'text' }, ], }; } if (sql.includes('COUNT(*)::text AS total')) { return { rows: [{ total: '2' }] }; } if (sql.includes('SELECT * FROM "auth"."users"')) { return { rows: [ { id: '1', email: 'demo-1@example.com' }, { id: '2', email: 'demo-2@example.com' }, ], }; } return { rows: [] }; }); const service = AdminRecordService.getInstance(); const result = await service.listRecords('auth', 'users', { limit: 10, offset: 0, search: 'demo', sort: [{ columnName: 'email', direction: 'asc' }], }); expect(result.total).toBe(2); expect(result.records).toHaveLength(2); const sqlCalls = clientQueryMock.mock.calls.map(([sql]) => sql as string); const beginIndex = sqlCalls.indexOf('BEGIN'); const setRoleIndex = sqlCalls.indexOf('SET LOCAL ROLE project_admin'); const dataQueryIndex = sqlCalls.findIndex((sql) => sql.includes('ORDER BY "email" ASC LIMIT $2 OFFSET $3') ); const resetRoleIndex = sqlCalls.indexOf('RESET ROLE'); const commitIndex = sqlCalls.indexOf('COMMIT'); expect(beginIndex).toBe(0); expect(setRoleIndex).toBeGreaterThan(-1); expect(dataQueryIndex).toBeGreaterThan(setRoleIndex); expect(resetRoleIndex).toBeGreaterThan(dataQueryIndex); expect(commitIndex).toBeGreaterThan(resetRoleIndex); expect(clientQueryMock.mock.calls[dataQueryIndex]?.[1]).toEqual(['%demo%', 10, 0]); // Read paths must not pay for the primary-key metadata query. expect(sqlCalls.some((sql) => sql.includes('information_schema.table_constraints'))).toBe( false ); }); it('delegates protected-schema writes to project_admin privileges', async () => { clientQueryMock .mockResolvedValueOnce({}) // BEGIN .mockResolvedValueOnce({}) // SET LOCAL ROLE .mockResolvedValueOnce({}) // set request.jwt.claims .mockResolvedValueOnce({ rows: [ { column_name: 'id', data_type: 'uuid', is_nullable: 'NO', udt_name: 'uuid' }, { column_name: 'email', data_type: 'text', is_nullable: 'NO', udt_name: 'text' }, ], }) // metadata .mockResolvedValueOnce({ rows: [{ id: 'u1', email: 'demo@example.com' }], }) // INSERT .mockResolvedValueOnce({}) // RESET ROLE .mockResolvedValueOnce({}) // reset config .mockResolvedValueOnce({}); // COMMIT const service = AdminRecordService.getInstance(); const result = await service.createRecords('auth', 'users', [{ email: 'demo@example.com' }]); expect(result).toEqual([{ id: 'u1', email: 'demo@example.com' }]); const sqlCalls = clientQueryMock.mock.calls.map(([sql]) => sql as string); const setRoleIndex = sqlCalls.indexOf('SET LOCAL ROLE project_admin'); const insertIndex = sqlCalls.findIndex((sql) => sql.includes('INSERT INTO "auth"."users"')); const commitIndex = sqlCalls.indexOf('COMMIT'); expect(sqlCalls[0]).toBe('BEGIN'); expect(setRoleIndex).toBeGreaterThan(-1); expect(insertIndex).toBeGreaterThan(setRoleIndex); expect(commitIndex).toBeGreaterThan(insertIndex); }); it('updates public records and converts blank nullable uuid values to null', async () => { clientQueryMock.mockImplementation(async (sql: string) => { if (sql.includes('FROM information_schema.columns')) { return { rows: [ { column_name: 'id', data_type: 'uuid', is_nullable: 'NO', udt_name: 'uuid' }, { column_name: 'owner_id', data_type: 'uuid', is_nullable: 'YES', udt_name: 'uuid' }, { column_name: 'name', data_type: 'text', is_nullable: 'NO', udt_name: 'text' }, ], }; } if (sql.includes('information_schema.table_constraints')) { return { rows: [{ column_name: 'id' }] }; } if (sql.includes('UPDATE "public"."projects"')) { return { rows: [{ id: 'p1', owner_id: null, name: 'Renamed project' }], }; } return { rows: [] }; }); const service = AdminRecordService.getInstance(); const record = await service.updateRecord( 'public', 'projects', { id: 'p1' }, { owner_id: '', name: 'Renamed project', } ); expect(record).toEqual({ id: 'p1', owner_id: null, name: 'Renamed project' }); const updateCall = clientQueryMock.mock.calls.find( ([sql]) => typeof sql === 'string' && sql.includes('UPDATE "public"."projects"') ); expect(updateCall?.[0]).toContain( 'SET "owner_id" = $1, "name" = $2 WHERE "id" = $3 RETURNING *' ); expect(updateCall?.[1]).toEqual([null, 'Renamed project', 'p1']); const sqlCalls = clientQueryMock.mock.calls.map(([sql]) => sql as string); const setRoleIndex = sqlCalls.indexOf('SET LOCAL ROLE project_admin'); const updateIndex = sqlCalls.findIndex((sql) => sql.includes('UPDATE "public"."projects"')); const commitIndex = sqlCalls.indexOf('COMMIT'); expect(sqlCalls[0]).toBe('BEGIN'); expect(setRoleIndex).toBeGreaterThan(-1); expect(updateIndex).toBeGreaterThan(setRoleIndex); expect(commitIndex).toBeGreaterThan(updateIndex); }); it('preserves empty strings for character varying inserts', async () => { clientQueryMock .mockResolvedValueOnce({}) // BEGIN .mockResolvedValueOnce({}) // SET LOCAL ROLE .mockResolvedValueOnce({}) // set_config .mockResolvedValueOnce({ rows: [ { column_name: 'id', data_type: 'uuid', is_nullable: 'NO', udt_name: 'uuid' }, { column_name: 'name', data_type: 'character varying', is_nullable: 'YES', udt_name: 'varchar', }, ], }) // metadata .mockResolvedValueOnce({ rows: [{ id: 'r1', name: '' }], }) // INSERT .mockResolvedValueOnce({}) // RESET ROLE .mockResolvedValueOnce({}) // reset config .mockResolvedValueOnce({}); // COMMIT const service = AdminRecordService.getInstance(); const result = await service.createRecords('public', 'projects', [{ name: '' }]); expect(result).toEqual([{ id: 'r1', name: '' }]); const insertCall = clientQueryMock.mock.calls.find( ([sql]) => typeof sql === 'string' && sql.includes('INSERT INTO "public"."projects"') ); expect(insertCall?.[1]).toEqual(['']); }); it('converts blank updates on nullable non-text columns to null', async () => { clientQueryMock.mockImplementation(async (sql: string) => { if (sql.includes('FROM information_schema.columns')) { return { rows: [ { column_name: 'id', data_type: 'uuid', is_nullable: 'NO', udt_name: 'uuid' }, { column_name: 'priority', data_type: 'integer', is_nullable: 'YES', udt_name: 'int4', }, ], }; } if (sql.includes('information_schema.table_constraints')) { return { rows: [{ column_name: 'id' }] }; } if (sql.includes('UPDATE "public"."projects"')) { return { rows: [{ id: 'p1', priority: null }], }; } return { rows: [] }; }); const service = AdminRecordService.getInstance(); const record = await service.updateRecord( 'public', 'projects', { id: 'p1' }, { priority: '', } ); expect(record).toEqual({ id: 'p1', priority: null }); const updateCall = clientQueryMock.mock.calls.find( ([sql]) => typeof sql === 'string' && sql.includes('UPDATE "public"."projects"') ); expect(updateCall?.[1]).toEqual([null, 'p1']); }); it('rejects blank updates on required non-text columns with a 400', async () => { clientQueryMock.mockImplementation(async (sql: string) => { if (sql.includes('FROM information_schema.columns')) { return { rows: [ { column_name: 'id', data_type: 'uuid', is_nullable: 'NO', udt_name: 'uuid' }, { column_name: 'priority', data_type: 'integer', is_nullable: 'NO', udt_name: 'int4', }, ], }; } return { rows: [] }; }); const service = AdminRecordService.getInstance(); await expect( service.updateRecord( 'public', 'projects', { id: 'p1' }, { priority: '', } ) ).rejects.toBeInstanceOf(AppError); const sqlCalls = clientQueryMock.mock.calls.map(([sql]) => sql as string); expect(sqlCalls).toContain('ROLLBACK'); }); it('deletes records inside a project_admin transaction', async () => { clientQueryMock.mockImplementation(async (sql: string) => { if (sql.includes('FROM information_schema.columns')) { return { rows: [ { column_name: 'id', data_type: 'uuid', is_nullable: 'NO', udt_name: 'uuid' }, { column_name: 'name', data_type: 'text', is_nullable: 'NO', udt_name: 'text' }, ], }; } if (sql.includes('information_schema.table_constraints')) { return { rows: [{ column_name: 'id' }] }; } if (sql.includes('DELETE FROM "public"."projects"')) { return { rowCount: 2, rows: [] }; } return { rows: [], rowCount: 0 }; }); const service = AdminRecordService.getInstance(); const deletedCount = await service.deleteRecords('public', 'projects', [ { id: 'p1' }, { id: 'p2' }, ]); expect(deletedCount).toBe(2); const deleteCall = clientQueryMock.mock.calls.find( ([sql]) => typeof sql === 'string' && sql.includes('DELETE FROM "public"."projects"') ); // Each selected single-column key is matched as its own AND-group, OR'd together. expect(deleteCall?.[0]).toContain('WHERE ("id" = $1) OR ("id" = $2)'); expect(deleteCall?.[1]).toEqual(['p1', 'p2']); const sqlCalls = clientQueryMock.mock.calls.map(([sql]) => sql as string); const setRoleIndex = sqlCalls.indexOf('SET LOCAL ROLE project_admin'); const deleteIndex = sqlCalls.findIndex((sql) => sql.includes('DELETE FROM "public"."projects"') ); const commitIndex = sqlCalls.indexOf('COMMIT'); expect(sqlCalls[0]).toBe('BEGIN'); expect(deleteIndex).toBeGreaterThan(setRoleIndex); expect(commitIndex).toBeGreaterThan(deleteIndex); }); it('updates a composite-key row using the full primary-key tuple', async () => { clientQueryMock.mockImplementation(async (sql: string) => { if (sql.includes('FROM information_schema.columns')) { return { rows: [ { column_name: 'tenant_id', data_type: 'text', is_nullable: 'NO', udt_name: 'text' }, { column_name: 'item_id', data_type: 'text', is_nullable: 'NO', udt_name: 'text' }, { column_name: 'label', data_type: 'text', is_nullable: 'YES', udt_name: 'text' }, ], }; } if (sql.includes('information_schema.table_constraints')) { return { rows: [{ column_name: 'tenant_id' }, { column_name: 'item_id' }] }; } if (sql.includes('UPDATE "public"."composite_pk_test"')) { return { rows: [{ tenant_id: 'tenant_a', item_id: 'item_2', label: 'second-updated' }], }; } return { rows: [] }; }); const service = AdminRecordService.getInstance(); const record = await service.updateRecord( 'public', 'composite_pk_test', { tenant_id: 'tenant_a', item_id: 'item_2' }, { label: 'second-updated' } ); expect(record).toEqual({ tenant_id: 'tenant_a', item_id: 'item_2', label: 'second-updated' }); const updateCall = clientQueryMock.mock.calls.find( ([sql]) => typeof sql === 'string' && sql.includes('UPDATE "public"."composite_pk_test"') ); // The WHERE clause must match both key columns, not just the (duplicated) first one. expect(updateCall?.[0]).toContain( 'SET "label" = $1 WHERE "tenant_id" = $2 AND "item_id" = $3 RETURNING *' ); expect(updateCall?.[1]).toEqual(['second-updated', 'tenant_a', 'item_2']); }); it('deletes only the selected composite-key tuples', async () => { clientQueryMock.mockImplementation(async (sql: string) => { if (sql.includes('FROM information_schema.columns')) { return { rows: [ { column_name: 'tenant_id', data_type: 'text', is_nullable: 'NO', udt_name: 'text' }, { column_name: 'item_id', data_type: 'text', is_nullable: 'NO', udt_name: 'text' }, { column_name: 'label', data_type: 'text', is_nullable: 'YES', udt_name: 'text' }, ], }; } if (sql.includes('information_schema.table_constraints')) { return { rows: [{ column_name: 'tenant_id' }, { column_name: 'item_id' }] }; } if (sql.includes('DELETE FROM "public"."composite_pk_test"')) { return { rowCount: 1, rows: [] }; } return { rows: [], rowCount: 0 }; }); const service = AdminRecordService.getInstance(); // tenant_a is duplicated across rows; deleting must target the exact tuple only. const deletedCount = await service.deleteRecords('public', 'composite_pk_test', [ { tenant_id: 'tenant_a', item_id: 'item_2' }, ]); expect(deletedCount).toBe(1); const deleteCall = clientQueryMock.mock.calls.find( ([sql]) => typeof sql === 'string' && sql.includes('DELETE FROM "public"."composite_pk_test"') ); expect(deleteCall?.[0]).toContain('WHERE ("tenant_id" = $1 AND "item_id" = $2)'); expect(deleteCall?.[1]).toEqual(['tenant_a', 'item_2']); }); it('matches each selected composite tuple independently when deleting many rows', async () => { clientQueryMock.mockImplementation(async (sql: string) => { if (sql.includes('FROM information_schema.columns')) { return { rows: [ { column_name: 'tenant_id', data_type: 'text', is_nullable: 'NO', udt_name: 'text' }, { column_name: 'item_id', data_type: 'text', is_nullable: 'NO', udt_name: 'text' }, ], }; } if (sql.includes('information_schema.table_constraints')) { return { rows: [{ column_name: 'tenant_id' }, { column_name: 'item_id' }] }; } if (sql.includes('DELETE FROM "public"."composite_pk_test"')) { return { rowCount: 2, rows: [] }; } return { rows: [], rowCount: 0 }; }); const service = AdminRecordService.getInstance(); const deletedCount = await service.deleteRecords('public', 'composite_pk_test', [ { tenant_id: 'tenant_a', item_id: 'item_1' }, { tenant_id: 'tenant_a', item_id: 'item_2' }, ]); expect(deletedCount).toBe(2); const deleteCall = clientQueryMock.mock.calls.find( ([sql]) => typeof sql === 'string' && sql.includes('DELETE FROM "public"."composite_pk_test"') ); expect(deleteCall?.[0]).toContain( 'WHERE ("tenant_id" = $1 AND "item_id" = $2) OR ("tenant_id" = $3 AND "item_id" = $4)' ); expect(deleteCall?.[1]).toEqual(['tenant_a', 'item_1', 'tenant_a', 'item_2']); }); it('rejects a partial composite key instead of mutating unintended rows', async () => { clientQueryMock.mockImplementation(async (sql: string) => { if (sql.includes('FROM information_schema.columns')) { return { rows: [ { column_name: 'tenant_id', data_type: 'text', is_nullable: 'NO', udt_name: 'text' }, { column_name: 'item_id', data_type: 'text', is_nullable: 'NO', udt_name: 'text' }, { column_name: 'label', data_type: 'text', is_nullable: 'YES', udt_name: 'text' }, ], }; } if (sql.includes('information_schema.table_constraints')) { return { rows: [{ column_name: 'tenant_id' }, { column_name: 'item_id' }] }; } return { rows: [], rowCount: 0 }; }); const service = AdminRecordService.getInstance(); // Supplying only the first PK column of a composite key must be rejected, not // turned into a broad DELETE that removes every row sharing that value. await expect( service.deleteRecords('public', 'composite_pk_test', [{ tenant_id: 'tenant_a' }]) ).rejects.toBeInstanceOf(AppError); const sqlCalls = clientQueryMock.mock.calls.map(([sql]) => sql as string); expect(sqlCalls.some((sql) => sql.includes('DELETE FROM'))).toBe(false); expect(sqlCalls).toContain('ROLLBACK'); // The same partial key must also be rejected on update. await expect( service.updateRecord('public', 'composite_pk_test', { tenant_id: 'tenant_a' }, { label: 'x' }) ).rejects.toBeInstanceOf(AppError); expect( clientQueryMock.mock.calls.some( ([sql]) => typeof sql === 'string' && sql.includes('UPDATE "public"."composite_pk_test"') ) ).toBe(false); }); it('falls back to the caller-provided key when the table has no primary key', async () => { clientQueryMock.mockImplementation(async (sql: string) => { if (sql.includes('FROM information_schema.columns')) { return { rows: [ { column_name: 'id', data_type: 'uuid', is_nullable: 'NO', udt_name: 'uuid' }, { column_name: 'name', data_type: 'text', is_nullable: 'YES', udt_name: 'text' }, ], }; } if (sql.includes('information_schema.table_constraints')) { return { rows: [] }; // table has no primary key } if (sql.includes('UPDATE "public"."pkless"')) { return { rows: [{ id: 'p1', name: 'Renamed' }] }; } if (sql.includes('DELETE FROM "public"."pkless"')) { return { rowCount: 1, rows: [] }; } return { rows: [], rowCount: 0 }; }); const service = AdminRecordService.getInstance(); // With no detectable primary key, the supplied key columns are used as-is. const updated = await service.updateRecord( 'public', 'pkless', { id: 'p1' }, { name: 'Renamed' } ); expect(updated).toEqual({ id: 'p1', name: 'Renamed' }); const updateCall = clientQueryMock.mock.calls.find( ([sql]) => typeof sql === 'string' && sql.includes('UPDATE "public"."pkless"') ); expect(updateCall?.[0]).toContain('WHERE "id" = $2 RETURNING *'); expect(updateCall?.[1]).toEqual(['Renamed', 'p1']); const deletedCount = await service.deleteRecords('public', 'pkless', [{ id: 'p1' }]); expect(deletedCount).toBe(1); const deleteCall = clientQueryMock.mock.calls.find( ([sql]) => typeof sql === 'string' && sql.includes('DELETE FROM "public"."pkless"') ); expect(deleteCall?.[0]).toContain('WHERE ("id" = $1)'); expect(deleteCall?.[1]).toEqual(['p1']); }); it('matches a null key column with IS NULL on a keyless table', async () => { clientQueryMock.mockImplementation(async (sql: string) => { if (sql.includes('FROM information_schema.columns')) { return { rows: [ { column_name: 'name', data_type: 'text', is_nullable: 'YES', udt_name: 'text' }, { column_name: 'email', data_type: 'text', is_nullable: 'YES', udt_name: 'text' }, ], }; } if (sql.includes('information_schema.table_constraints')) { return { rows: [] }; // table has no primary key } if (sql.includes('UPDATE "public"."pkless"')) { return { rows: [{ name: 'alice', email: 'fixed@x.com' }] }; } if (sql.includes('DELETE FROM "public"."pkless"')) { return { rowCount: 1, rows: [] }; } return { rows: [], rowCount: 0 }; }); const service = AdminRecordService.getInstance(); // A genuinely-null column in the all-columns fallback key must match the row // with `IS NULL`, not `= ''` (which would silently match nothing). const updated = await service.updateRecord( 'public', 'pkless', { name: 'alice', email: null }, { email: 'fixed@x.com' } ); expect(updated).toEqual({ name: 'alice', email: 'fixed@x.com' }); const updateCall = clientQueryMock.mock.calls.find( ([sql]) => typeof sql === 'string' && sql.includes('UPDATE "public"."pkless"') ); // The null component becomes `IS NULL` and binds no parameter for it. expect(updateCall?.[0]).toContain('WHERE "name" = $2 AND "email" IS NULL RETURNING *'); expect(updateCall?.[1]).toEqual(['fixed@x.com', 'alice']); const deletedCount = await service.deleteRecords('public', 'pkless', [ { name: 'alice', email: null }, ]); expect(deletedCount).toBe(1); const deleteCall = clientQueryMock.mock.calls.find( ([sql]) => typeof sql === 'string' && sql.includes('DELETE FROM "public"."pkless"') ); expect(deleteCall?.[0]).toContain('WHERE ("name" = $1 AND "email" IS NULL)'); expect(deleteCall?.[1]).toEqual(['alice']); }); it('discards the pooled client when admin transaction cleanup fails', async () => { const resetError = new Error('reset failed'); clientQueryMock.mockImplementation(async (sql: string) => { if (sql.includes('FROM information_schema.columns')) { return { rows: [ { column_name: 'id', data_type: 'uuid', is_nullable: 'NO', udt_name: 'uuid' }, { column_name: 'name', data_type: 'text', is_nullable: 'NO', udt_name: 'text' }, ], }; } if (sql.includes('information_schema.table_constraints')) { return { rows: [{ column_name: 'id' }] }; } if (sql.includes('UPDATE "public"."projects"')) { return { rows: [{ id: 'p1', name: 'Renamed project' }] }; } if (sql === 'RESET ROLE') { throw resetError; } return { rows: [], rowCount: 0 }; }); const service = AdminRecordService.getInstance(); await expect( service.updateRecord( 'public', 'projects', { id: 'p1' }, { name: 'Renamed project', } ) ).rejects.toBe(resetError); expect(clientQueryMock.mock.calls.map(([sql]) => sql as string)).toContain('ROLLBACK'); expect(releaseMock).toHaveBeenCalledWith(resetError); }); });