feat: PostgreSQL withTenant-Helper und ClickHouse-Client
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
8
src/server/clickhouse/client.ts
Normal file
8
src/server/clickhouse/client.ts
Normal file
@@ -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',
|
||||||
|
})
|
||||||
9
src/server/db/client.ts
Normal file
9
src/server/db/client.ts
Normal file
@@ -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: <T = Record<string, unknown>>(sql: string, params: unknown[]) =>
|
||||||
|
pool.query<T>(sql, params).then((r) => r.rows),
|
||||||
|
}
|
||||||
36
src/server/db/tenant.test.ts
Normal file
36
src/server/db/tenant.test.ts
Normal file
@@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
11
src/server/db/tenant.ts
Normal file
11
src/server/db/tenant.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { db } from './client'
|
||||||
|
|
||||||
|
export async function withTenant<T>(tenantId: string, fn: () => Promise<T>): Promise<T> {
|
||||||
|
const schema = `tenant_${tenantId}`
|
||||||
|
await db.execute(`SET search_path = ${schema}, public`)
|
||||||
|
try {
|
||||||
|
return await fn()
|
||||||
|
} finally {
|
||||||
|
await db.execute(`SET search_path = public`)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user