From 506197623a41adaca172b465baf78686af7474c5 Mon Sep 17 00:00:00 2001 From: Joachim Hummel Date: Fri, 17 Apr 2026 08:20:25 +0000 Subject: [PATCH] feat: PostgreSQL withTenant-Helper und ClickHouse-Client Co-Authored-By: Claude Sonnet 4.6 --- src/server/clickhouse/client.ts | 8 ++++++++ src/server/db/client.ts | 9 +++++++++ src/server/db/tenant.test.ts | 36 +++++++++++++++++++++++++++++++++ src/server/db/tenant.ts | 11 ++++++++++ 4 files changed, 64 insertions(+) create mode 100644 src/server/clickhouse/client.ts create mode 100644 src/server/db/client.ts create mode 100644 src/server/db/tenant.test.ts create mode 100644 src/server/db/tenant.ts diff --git a/src/server/clickhouse/client.ts b/src/server/clickhouse/client.ts new file mode 100644 index 0000000..a8d8f43 --- /dev/null +++ b/src/server/clickhouse/client.ts @@ -0,0 +1,8 @@ +import { createClient } from '@clickhouse/client' + +export const clickhouse = createClient({ + url: process.env.CLICKHOUSE_URL, + username: process.env.CLICKHOUSE_USER, + password: process.env.CLICKHOUSE_PASSWORD, + database: 'newsletter', +}) diff --git a/src/server/db/client.ts b/src/server/db/client.ts new file mode 100644 index 0000000..ac0d831 --- /dev/null +++ b/src/server/db/client.ts @@ -0,0 +1,9 @@ +import { Pool } from 'pg' + +const pool = new Pool({ connectionString: process.env.DATABASE_URL }) + +export const db = { + execute: (sql: string) => pool.query(sql), + query: >(sql: string, params: unknown[]) => + pool.query(sql, params).then((r) => r.rows), +} diff --git a/src/server/db/tenant.test.ts b/src/server/db/tenant.test.ts new file mode 100644 index 0000000..e2e79e9 --- /dev/null +++ b/src/server/db/tenant.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +vi.mock('./client', () => ({ + db: { + execute: vi.fn(), + query: vi.fn().mockResolvedValue([{ id: '1' }]), + }, +})) + +import { withTenant } from './tenant' +import { db } from './client' + +describe('withTenant', () => { + beforeEach(() => vi.clearAllMocks()) + + it('setzt search_path auf tenant-Schema', async () => { + await withTenant('abc123', () => db.query('SELECT 1', [])) + expect(db.execute).toHaveBeenCalledWith('SET search_path = tenant_abc123, public') + }) + + it('setzt search_path zurück auf public nach Ausführung', async () => { + await withTenant('abc123', () => db.query('SELECT 1', [])) + expect(db.execute).toHaveBeenLastCalledWith('SET search_path = public') + }) + + it('setzt search_path zurück auch bei Fehler', async () => { + const fn = vi.fn().mockRejectedValue(new Error('DB-Fehler')) + await expect(withTenant('abc123', fn)).rejects.toThrow('DB-Fehler') + expect(db.execute).toHaveBeenLastCalledWith('SET search_path = public') + }) + + it('gibt Rückgabewert der Funktion zurück', async () => { + const result = await withTenant('abc123', async () => 'testdata') + expect(result).toBe('testdata') + }) +}) diff --git a/src/server/db/tenant.ts b/src/server/db/tenant.ts new file mode 100644 index 0000000..63554de --- /dev/null +++ b/src/server/db/tenant.ts @@ -0,0 +1,11 @@ +import { db } from './client' + +export async function withTenant(tenantId: string, fn: () => Promise): Promise { + const schema = `tenant_${tenantId}` + await db.execute(`SET search_path = ${schema}, public`) + try { + return await fn() + } finally { + await db.execute(`SET search_path = public`) + } +}