From 3c3088e66bfe81a809e4f74ab7722bb0dcdaa785 Mon Sep 17 00:00:00 2001 From: Joachim Hummel Date: Fri, 17 Apr 2026 08:35:31 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20execute=20mit=20optionalen=20Params,=20r?= =?UTF-8?q?elease(error)=20bei=20reset-Fehler,=20Tests=20erg=C3=A4nzt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/server/db/tenant.test.ts | 23 ++++++++++++++++++++++- src/server/db/tenant.ts | 17 +++++++++++------ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/server/db/tenant.test.ts b/src/server/db/tenant.test.ts index eb00a67..1f1a9d9 100644 --- a/src/server/db/tenant.test.ts +++ b/src/server/db/tenant.test.ts @@ -62,6 +62,27 @@ describe('withTenant', () => { await withTenant('abc123', async (client) => { 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)) }) }) diff --git a/src/server/db/tenant.ts b/src/server/db/tenant.ts index a338642..fcfa779 100644 --- a/src/server/db/tenant.ts +++ b/src/server/db/tenant.ts @@ -2,7 +2,7 @@ import { type QueryResultRow } from 'pg' import { pool } from './client' interface TenantClient { - execute: (sql: string) => Promise + execute: (sql: string, params?: unknown[]) => Promise query: >(sql: string, params: unknown[]) => Promise } @@ -18,21 +18,26 @@ export async function withTenant( try { await client.query(`SET search_path = ${schema}, public`) const tenantClient: TenantClient = { - execute: async (sql) => { await client.query(sql) }, + execute: async (sql, params?) => { await client.query(sql, params) }, query: async >(sql: string, params: unknown[]) => client.query(sql, params).then((r) => r.rows), } return await fn(tenantClient) } finally { + let connectionBroken = false try { await client.query('SET search_path = public') } catch (resetErr) { + connectionBroken = true console.error({ msg: 'search_path reset failed', error: (resetErr as Error).message }) + client.release(resetErr as Error) } - try { - client.release() - } catch (releaseErr) { - console.error({ msg: 'Pool client release failed', error: (releaseErr as Error).message }) + if (!connectionBroken) { + try { + client.release() + } catch (releaseErr) { + console.error({ msg: 'Pool client release failed', error: (releaseErr as Error).message }) + } } } }