fix: execute mit optionalen Params, release(error) bei reset-Fehler, Tests ergänzt

This commit is contained in:
2026-04-17 08:35:31 +00:00
parent 4f739f7b1e
commit 3c3088e66b
2 changed files with 33 additions and 7 deletions

View File

@@ -62,6 +62,27 @@ describe('withTenant', () => {
await withTenant('abc123', async (client) => { await withTenant('abc123', async (client) => {
await client.execute('DELETE FROM sessions WHERE expired = true') await client.execute('DELETE FROM sessions WHERE expired = true')
}) })
expect(mockClient.query).toHaveBeenCalledWith('DELETE FROM sessions WHERE expired = true') expect(mockClient.query).toHaveBeenCalledWith('DELETE FROM sessions WHERE expired = true', undefined)
})
it('execute akzeptiert optionale Parameter', async () => {
await withTenant('abc123', async (client) => {
await client.execute('UPDATE t SET x = $1 WHERE id = $2', ['v', '1'])
})
expect(mockClient.query).toHaveBeenCalledWith('UPDATE t SET x = $1 WHERE id = $2', ['v', '1'])
})
it('zerstört Verbindung bei search_path-Reset-Fehler (kein Recycling)', async () => {
mockClient.query
.mockResolvedValueOnce({ rows: [] }) // SET search_path = tenant_...
.mockResolvedValueOnce({ rows: [] }) // fn-query
.mockRejectedValueOnce(new Error('reset-fehler')) // SET search_path = public schlägt fehl
await withTenant('abc123', async (client) => {
await client.query('SELECT 1', [])
})
// release() muss mit Error aufgerufen worden sein (Verbindung zerstört, nicht recycelt)
expect(mockClient.release).toHaveBeenCalledWith(expect.any(Error))
}) })
}) })

View File

@@ -2,7 +2,7 @@ import { type QueryResultRow } from 'pg'
import { pool } from './client' import { pool } from './client'
interface TenantClient { interface TenantClient {
execute: (sql: string) => Promise<void> execute: (sql: string, params?: unknown[]) => Promise<void>
query: <T extends QueryResultRow = Record<string, unknown>>(sql: string, params: unknown[]) => Promise<T[]> query: <T extends QueryResultRow = Record<string, unknown>>(sql: string, params: unknown[]) => Promise<T[]>
} }
@@ -18,17 +18,21 @@ export async function withTenant<T>(
try { try {
await client.query(`SET search_path = ${schema}, public`) await client.query(`SET search_path = ${schema}, public`)
const tenantClient: TenantClient = { const tenantClient: TenantClient = {
execute: async (sql) => { await client.query(sql) }, execute: async (sql, params?) => { await client.query(sql, params) },
query: async <T extends QueryResultRow = Record<string, unknown>>(sql: string, params: unknown[]) => query: async <T extends QueryResultRow = Record<string, unknown>>(sql: string, params: unknown[]) =>
client.query<T>(sql, params).then((r) => r.rows), client.query<T>(sql, params).then((r) => r.rows),
} }
return await fn(tenantClient) return await fn(tenantClient)
} finally { } finally {
let connectionBroken = false
try { try {
await client.query('SET search_path = public') await client.query('SET search_path = public')
} catch (resetErr) { } catch (resetErr) {
connectionBroken = true
console.error({ msg: 'search_path reset failed', error: (resetErr as Error).message }) console.error({ msg: 'search_path reset failed', error: (resetErr as Error).message })
client.release(resetErr as Error)
} }
if (!connectionBroken) {
try { try {
client.release() client.release()
} catch (releaseErr) { } catch (releaseErr) {
@@ -36,3 +40,4 @@ export async function withTenant<T>(
} }
} }
} }
}