fix(communication): enforce room and project membership boundaries
This commit is contained in:
28
backend/src/modules/communication-access.ts
Normal file
28
backend/src/modules/communication-access.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { and, eq } from "drizzle-orm"
|
||||
import { authProfiles, projects } from "../../db/schema"
|
||||
|
||||
export function chatError(message: string, statusCode = 403): never {
|
||||
throw Object.assign(new Error(message), { statusCode })
|
||||
}
|
||||
|
||||
export function assertCustomRoomKey(key: string) {
|
||||
if (key === "allgemein" || /^(direct|project)_/.test(key)) {
|
||||
chatError("Dieser Raumschlüssel ist für Systemräume reserviert", 409)
|
||||
}
|
||||
}
|
||||
|
||||
export function managesProjects(req: any): boolean {
|
||||
return Boolean(req.user?.is_admin || req.hasPermission?.("organisation.projects.write"))
|
||||
}
|
||||
|
||||
export async function projectChatAccess(db: any, req: any, projectId?: number) {
|
||||
const tenantId = Number(req.user?.tenant_id)
|
||||
const conditions = [eq(projects.tenant, tenantId), eq(projects.archived, false)]
|
||||
if (projectId !== undefined) conditions.push(eq(projects.id, projectId))
|
||||
const rows = await db.select().from(projects).where(and(...conditions))
|
||||
if (managesProjects(req)) return rows
|
||||
const profiles = await db.select({ id: authProfiles.id }).from(authProfiles)
|
||||
.where(and(eq(authProfiles.tenant_id, tenantId), eq(authProfiles.user_id, req.user.user_id)))
|
||||
const profileIds = new Set(profiles.map((profile: any) => profile.id))
|
||||
return rows.filter((project: any) => (project.profiles || []).some((id: string) => profileIds.has(id)))
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
projects,
|
||||
} from "../../db/schema"
|
||||
import { NotificationService, UserDirectory } from "../modules/notification.service"
|
||||
import { assertCustomRoomKey, chatError, projectChatAccess } from "../modules/communication-access"
|
||||
|
||||
type ChatUser = {
|
||||
userId: string
|
||||
@@ -54,9 +55,12 @@ const normalizeRoomKey = (value: string) => value
|
||||
export default async function communicationRoutes(server: FastifyInstance) {
|
||||
const notifications = new NotificationService(server, getUserDirectory)
|
||||
|
||||
const requireTenant = (req: any) => {
|
||||
const requireTenant = async (req: any) => {
|
||||
const tenantId = Number(req.user.tenant_id)
|
||||
if (!tenantId) throw Object.assign(new Error("Kein aktiver Mandant"), { statusCode: 400 })
|
||||
const [membership] = await server.db.select({ userId: authTenantUsers.user_id }).from(authTenantUsers)
|
||||
.where(and(eq(authTenantUsers.tenant_id, tenantId), eq(authTenantUsers.user_id, req.user.user_id))).limit(1)
|
||||
if (!membership) chatError("Keine aktive Mandantenmitgliedschaft")
|
||||
return tenantId
|
||||
}
|
||||
|
||||
@@ -106,11 +110,12 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
||||
}
|
||||
}
|
||||
|
||||
if (room.type !== "general" || room.archived) chatError("Allgemeiner Chat ist nicht verfügbar", 409)
|
||||
await addMembers(room.id, (await tenantUsers(tenantId)).map((user) => user.userId))
|
||||
return room
|
||||
}
|
||||
|
||||
const requireRoom = async (tenantId: number, userId: string, roomKey: string) => {
|
||||
const requireRoom = async (req: any, tenantId: number, userId: string, roomKey: string) => {
|
||||
const [row] = await server.db.select({ room: communicationRooms }).from(communicationRooms)
|
||||
.innerJoin(communicationRoomMembers, eq(communicationRoomMembers.roomId, communicationRooms.id))
|
||||
.where(and(
|
||||
@@ -121,6 +126,9 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
||||
))
|
||||
.limit(1)
|
||||
if (!row) throw Object.assign(new Error("Chatraum nicht gefunden oder kein Zugriff"), { statusCode: 404 })
|
||||
if (row.room.type === "project" && !(await projectChatAccess(server.db, req, row.room.entityId)).length) {
|
||||
chatError("Chatraum nicht gefunden oder kein Zugriff", 404)
|
||||
}
|
||||
return row.room
|
||||
}
|
||||
|
||||
@@ -225,7 +233,7 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
||||
server.get("/communication/chat/status", async () => ({ ready: true, provider: "fedeo" }))
|
||||
|
||||
server.get("/communication/chat/users", async (req: any) => {
|
||||
const tenantId = requireTenant(req)
|
||||
const tenantId = await requireTenant(req)
|
||||
return { users: (await tenantUsers(tenantId)).map((user) => ({
|
||||
userId: user.userId,
|
||||
email: user.email,
|
||||
@@ -234,12 +242,13 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
||||
})
|
||||
|
||||
server.get("/communication/chat/rooms", async (req: any) => {
|
||||
const tenantId = requireTenant(req)
|
||||
const tenantId = await requireTenant(req)
|
||||
await ensureGeneralRoom(tenantId, req.user.user_id)
|
||||
const rows = await server.db.select({ room: communicationRooms }).from(communicationRooms)
|
||||
.innerJoin(communicationRoomMembers, eq(communicationRoomMembers.roomId, communicationRooms.id))
|
||||
.where(and(
|
||||
eq(communicationRooms.tenantId, tenantId),
|
||||
inArray(communicationRooms.type, ["general", "room"]),
|
||||
eq(communicationRooms.archived, false),
|
||||
eq(communicationRoomMembers.userId, req.user.user_id)
|
||||
)).orderBy(asc(communicationRooms.name))
|
||||
@@ -247,27 +256,32 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
||||
})
|
||||
|
||||
server.post("/communication/chat/rooms", async (req: any, reply) => {
|
||||
const tenantId = requireTenant(req)
|
||||
const tenantId = await requireTenant(req)
|
||||
const body = (req.body || {}) as { key?: string; name?: string; topic?: string }
|
||||
const name = body.name?.trim()
|
||||
const key = normalizeRoomKey(body.key?.trim() || name || "")
|
||||
if (!name || !key) return reply.code(400).send({ error: "Name und gültiger Raumschlüssel sind erforderlich" })
|
||||
const [created] = await server.db.insert(communicationRooms).values({
|
||||
tenantId, key, name, topic: body.topic?.trim() || null, type: "room", createdBy: req.user.user_id,
|
||||
}).onConflictDoNothing().returning()
|
||||
const room = created || (await server.db.select().from(communicationRooms)
|
||||
.where(and(eq(communicationRooms.tenantId, tenantId), eq(communicationRooms.key, key))).limit(1))[0]
|
||||
await addMembers(room.id, (await tenantUsers(tenantId)).map((user) => user.userId))
|
||||
return { ...roomToApi(room), alreadyExisted: !created }
|
||||
assertCustomRoomKey(key)
|
||||
const userIds = (await tenantUsers(tenantId)).map((user) => user.userId)
|
||||
const created = await server.db.transaction(async (tx) => {
|
||||
const [room] = await tx.insert(communicationRooms).values({
|
||||
tenantId, key, name, topic: body.topic?.trim() || null, type: "room", createdBy: req.user.user_id,
|
||||
}).onConflictDoNothing().returning()
|
||||
if (room) await tx.insert(communicationRoomMembers)
|
||||
.values([...new Set(userIds)].map((userId) => ({ roomId: room.id, userId }))).onConflictDoNothing()
|
||||
return room
|
||||
})
|
||||
if (!created) return reply.code(409).send({ error: "Dieser Raumschlüssel ist bereits vergeben" })
|
||||
return { ...roomToApi(created), alreadyExisted: false }
|
||||
})
|
||||
|
||||
server.get("/communication/chat/project-rooms", async (req: any) => {
|
||||
const tenantId = requireTenant(req)
|
||||
const tenantId = await requireTenant(req)
|
||||
const [roomRows, projectRows] = await Promise.all([
|
||||
server.db.select({ room: communicationRooms }).from(communicationRooms)
|
||||
.innerJoin(communicationRoomMembers, eq(communicationRoomMembers.roomId, communicationRooms.id))
|
||||
.where(and(eq(communicationRooms.tenantId, tenantId), eq(communicationRooms.type, "project"), eq(communicationRoomMembers.userId, req.user.user_id))),
|
||||
server.db.select().from(projects).where(and(eq(projects.tenant, tenantId), eq(projects.archived, false))),
|
||||
projectChatAccess(server.db, req),
|
||||
])
|
||||
const roomsByProject = new Map(roomRows.map(({ room }) => [room.entityId, room]))
|
||||
return { rooms: projectRows.map((project) => {
|
||||
@@ -283,10 +297,9 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
||||
})
|
||||
|
||||
server.post("/communication/chat/project-rooms/:projectId/provision", async (req: any, reply) => {
|
||||
const tenantId = requireTenant(req)
|
||||
const tenantId = await requireTenant(req)
|
||||
const projectId = Number(req.params.projectId)
|
||||
const [project] = await server.db.select().from(projects)
|
||||
.where(and(eq(projects.tenant, tenantId), eq(projects.id, projectId))).limit(1)
|
||||
const [project] = await projectChatAccess(server.db, req, projectId)
|
||||
if (!project) return reply.code(404).send({ error: "Projekt nicht gefunden" })
|
||||
const key = `project_${project.id}`
|
||||
let [room] = await server.db.select().from(communicationRooms)
|
||||
@@ -303,6 +316,9 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
||||
.where(and(eq(communicationRooms.tenantId, tenantId), eq(communicationRooms.key, key))).limit(1)
|
||||
}
|
||||
}
|
||||
if (room.type !== "project" || room.entityId !== projectId || room.archived) {
|
||||
chatError("Projektchat ist nicht verfügbar", 409)
|
||||
}
|
||||
const profileIds = (project.profiles || []) as string[]
|
||||
const memberProfiles = profileIds.length
|
||||
? await server.db.select({ userId: authProfiles.user_id }).from(authProfiles)
|
||||
@@ -314,7 +330,7 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
||||
})
|
||||
|
||||
server.get("/communication/chat/direct-rooms", async (req: any) => {
|
||||
const tenantId = requireTenant(req)
|
||||
const tenantId = await requireTenant(req)
|
||||
const users = (await tenantUsers(tenantId)).filter((user) => user.userId !== req.user.user_id)
|
||||
const existingRows = await server.db.select({ room: communicationRooms }).from(communicationRooms)
|
||||
.innerJoin(communicationRoomMembers, eq(communicationRoomMembers.roomId, communicationRooms.id))
|
||||
@@ -339,7 +355,7 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
||||
})
|
||||
|
||||
server.post("/communication/chat/direct-rooms/:userId/provision", async (req: any, reply) => {
|
||||
const tenantId = requireTenant(req)
|
||||
const tenantId = await requireTenant(req)
|
||||
const targetId = String(req.params.userId)
|
||||
const target = (await tenantUsers(tenantId)).find((user) => user.userId === targetId)
|
||||
if (!target || targetId === req.user.user_id) return reply.code(404).send({ error: "Benutzer nicht gefunden" })
|
||||
@@ -356,6 +372,7 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
||||
.where(and(eq(communicationRooms.tenantId, tenantId), eq(communicationRooms.key, key))).limit(1)
|
||||
}
|
||||
}
|
||||
if (room.type !== "direct" || room.archived) chatError("Direktchat ist nicht verfügbar", 409)
|
||||
await addMembers(room.id, [req.user.user_id, targetId])
|
||||
return {
|
||||
...roomToApi(room),
|
||||
@@ -368,17 +385,21 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
||||
})
|
||||
|
||||
server.get("/communication/chat/unread", async (req: any) => {
|
||||
const tenantId = requireTenant(req)
|
||||
const tenantId = await requireTenant(req)
|
||||
const memberships = await server.db.select({
|
||||
roomId: communicationRoomMembers.roomId,
|
||||
roomKey: communicationRooms.key,
|
||||
roomType: communicationRooms.type,
|
||||
projectId: communicationRooms.entityId,
|
||||
lastReadMessageId: communicationRoomReads.lastReadMessageId,
|
||||
}).from(communicationRoomMembers)
|
||||
.innerJoin(communicationRooms, eq(communicationRooms.id, communicationRoomMembers.roomId))
|
||||
.leftJoin(communicationRoomReads, and(eq(communicationRoomReads.roomId, communicationRoomMembers.roomId), eq(communicationRoomReads.userId, req.user.user_id)))
|
||||
.where(and(eq(communicationRoomMembers.userId, req.user.user_id), eq(communicationRooms.tenantId, tenantId)))
|
||||
.where(and(eq(communicationRoomMembers.userId, req.user.user_id), eq(communicationRooms.tenantId, tenantId), eq(communicationRooms.archived, false)))
|
||||
const allowedProjects = new Set((await projectChatAccess(server.db, req)).map((project: any) => project.id))
|
||||
const rooms: Record<string, { count: number; mentions: number }> = {}
|
||||
for (const membership of memberships) {
|
||||
if (membership.roomType === "project" && !allowedProjects.has(membership.projectId)) continue
|
||||
const [result] = await server.db.select({ count: sql<number>`count(*)::int` }).from(communicationMessages)
|
||||
.where(and(eq(communicationMessages.roomId, membership.roomId), gt(communicationMessages.id, membership.lastReadMessageId || 0), ne(communicationMessages.authorUserId, req.user.user_id)))
|
||||
rooms[membership.roomKey] = { count: result?.count || 0, mentions: 0 }
|
||||
@@ -387,22 +408,22 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
||||
})
|
||||
|
||||
server.get("/communication/chat/rooms/:roomKey/messages", async (req: any) => {
|
||||
const tenantId = requireTenant(req)
|
||||
const room = await requireRoom(tenantId, req.user.user_id, req.params.roomKey)
|
||||
const tenantId = await requireTenant(req)
|
||||
const room = await requireRoom(req, tenantId, req.user.user_id, req.params.roomKey)
|
||||
return { ...roomToApi(room), messages: await messagesForRoom(room.id, req.user.user_id, tenantId) }
|
||||
})
|
||||
|
||||
server.get("/communication/chat/rooms/:roomKey/sync", async (req: any) => {
|
||||
const tenantId = requireTenant(req)
|
||||
const room = await requireRoom(tenantId, req.user.user_id, req.params.roomKey)
|
||||
const tenantId = await requireTenant(req)
|
||||
const room = await requireRoom(req, tenantId, req.user.user_id, req.params.roomKey)
|
||||
const afterId = Math.max(Number(req.query?.afterId || 0), 0)
|
||||
const messages = await messagesForRoom(room.id, req.user.user_id, tenantId, afterId, 100)
|
||||
return { ...roomToApi(room), messages, nextId: messages[messages.length - 1]?.id || afterId }
|
||||
})
|
||||
|
||||
server.post("/communication/chat/rooms/:roomKey/messages", async (req: any, reply) => {
|
||||
const tenantId = requireTenant(req)
|
||||
const room = await requireRoom(tenantId, req.user.user_id, req.params.roomKey)
|
||||
const tenantId = await requireTenant(req)
|
||||
const room = await requireRoom(req, tenantId, req.user.user_id, req.params.roomKey)
|
||||
const body = String(req.body?.text || "").trim()
|
||||
if (!body) return reply.code(400).send({ error: "Nachricht darf nicht leer sein" })
|
||||
if (body.length > 10_000) return reply.code(400).send({ error: "Nachricht ist zu lang" })
|
||||
@@ -414,8 +435,8 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
||||
})
|
||||
|
||||
server.post("/communication/chat/rooms/:roomKey/read", async (req: any) => {
|
||||
const tenantId = requireTenant(req)
|
||||
const room = await requireRoom(tenantId, req.user.user_id, req.params.roomKey)
|
||||
const tenantId = await requireTenant(req)
|
||||
const room = await requireRoom(req, tenantId, req.user.user_id, req.params.roomKey)
|
||||
const requestedId = Math.max(Number(req.body?.messageId || 0), 0)
|
||||
const [latest] = await server.db.select({ id: communicationMessages.id }).from(communicationMessages)
|
||||
.where(eq(communicationMessages.roomId, room.id)).orderBy(desc(communicationMessages.id)).limit(1)
|
||||
@@ -434,8 +455,8 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
||||
})
|
||||
|
||||
server.get("/communication/chat/rooms/:roomKey/members", async (req: any) => {
|
||||
const tenantId = requireTenant(req)
|
||||
const room = await requireRoom(tenantId, req.user.user_id, req.params.roomKey)
|
||||
const tenantId = await requireTenant(req)
|
||||
const room = await requireRoom(req, tenantId, req.user.user_id, req.params.roomKey)
|
||||
const members = await server.db.select({
|
||||
userId: authUsers.id, email: authUsers.email,
|
||||
firstName: authProfiles.first_name, lastName: authProfiles.last_name, fullName: authProfiles.full_name,
|
||||
|
||||
50
backend/tests/communicationAccess.test.ts
Normal file
50
backend/tests/communicationAccess.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import assert from "node:assert/strict"
|
||||
import test from "node:test"
|
||||
import Fastify from "fastify"
|
||||
import communicationRoutes from "../src/routes/communication"
|
||||
import { assertCustomRoomKey } from "../src/modules/communication-access"
|
||||
import { collaborationDatabase, testDatabaseUrl } from "./helpers/collaboration-db"
|
||||
|
||||
test("custom room keys cannot impersonate system rooms", () => {
|
||||
for (const key of ["allgemein", "direct_abc", "project_7"]) assert.throws(() => assertCustomRoomKey(key))
|
||||
assert.doesNotThrow(() => assertCustomRoomKey("baustelle"))
|
||||
})
|
||||
|
||||
test("chat routes protect existing memberships and assigned projects", { skip: !testDatabaseUrl }, async () => {
|
||||
const fixture = await collaborationDatabase()
|
||||
const app = Fastify()
|
||||
const user = "00000000-0000-4000-8000-000000000001"
|
||||
const other = "00000000-0000-4000-8000-000000000002"
|
||||
const profile = "00000000-0000-4000-8000-000000000003"
|
||||
const room = "00000000-0000-4000-8000-000000000004"
|
||||
try {
|
||||
await fixture.pool.query(`INSERT INTO tenants VALUES (1, 'A', null), (2, 'B', null);
|
||||
INSERT INTO auth_users (id,email) VALUES ('${user}', 'a@example.test'), ('${other}', 'b@example.test');
|
||||
INSERT INTO auth_tenant_users (tenant_id,user_id) VALUES (1,'${user}'), (1,'${other}');
|
||||
INSERT INTO auth_profiles (id,tenant_id,user_id,first_name,last_name) VALUES ('${profile}',1,'${other}','B','Test');
|
||||
INSERT INTO projects (id,tenant,name,profiles,archived) VALUES (1,1,'Hidden',ARRAY['${profile}']::uuid[],false);
|
||||
INSERT INTO communication_rooms (id,tenant_id,key,name,type,archived) VALUES ('${room}',1,'private','Private','direct',false);
|
||||
INSERT INTO communication_room_members (room_id,user_id) VALUES ('${room}','${other}');`)
|
||||
app.decorate("db", fixture.db)
|
||||
app.addHook("preHandler", async (req: any) => {
|
||||
req.user = { tenant_id: 1, user_id: req.headers["x-test-user"] || user, is_admin: false }
|
||||
req.hasPermission = () => false
|
||||
})
|
||||
await app.register(communicationRoutes)
|
||||
const duplicate = await app.inject({ method: "POST", url: "/communication/chat/rooms", payload: { key: "private", name: "Open" } })
|
||||
assert.equal(duplicate.statusCode, 409)
|
||||
const memberships = await fixture.pool.query("SELECT user_id FROM communication_room_members WHERE room_id=$1", [room])
|
||||
assert.deepEqual(memberships.rows.map((row) => row.user_id), [other])
|
||||
const forbidden = await app.inject({ method: "POST", url: "/communication/chat/project-rooms/1/provision" })
|
||||
assert.equal(forbidden.statusCode, 404)
|
||||
const list = await app.inject("/communication/chat/project-rooms")
|
||||
assert.deepEqual(list.json().rooms, [])
|
||||
const assigned = await app.inject({ method: "POST", url: "/communication/chat/project-rooms/1/provision", headers: { "x-test-user": other } })
|
||||
assert.equal(assigned.statusCode, 200, assigned.body)
|
||||
await fixture.pool.query("UPDATE projects SET profiles='{}'::uuid[] WHERE id=1")
|
||||
const removed = await app.inject({ url: "/communication/chat/rooms/project_1/messages", headers: { "x-test-user": other } })
|
||||
assert.equal(removed.statusCode, 404)
|
||||
await fixture.pool.query("DELETE FROM auth_tenant_users WHERE user_id=$1", [user])
|
||||
assert.equal((await app.inject("/communication/chat/users")).statusCode, 403)
|
||||
} finally { await app.close(); await fixture.close() }
|
||||
})
|
||||
50
backend/tests/helpers/collaboration-db.ts
Normal file
50
backend/tests/helpers/collaboration-db.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { randomUUID } from "node:crypto"
|
||||
import { readFile } from "node:fs/promises"
|
||||
import { Pool } from "pg"
|
||||
import { drizzle } from "drizzle-orm/node-postgres"
|
||||
import { getTableColumns, getTableName } from "drizzle-orm"
|
||||
import * as schema from "../../db/schema"
|
||||
|
||||
export const testDatabaseUrl = process.env.COLLABORATION_TEST_DATABASE_URL
|
||||
|
||||
/** Always creates and drops a private schema in the explicitly designated disposable test database. */
|
||||
export async function collaborationDatabase(migrations: string[] = []) {
|
||||
if (!testDatabaseUrl || new URL(testDatabaseUrl).pathname !== "/collaboration_test") {
|
||||
throw new Error("Tests require COLLABORATION_TEST_DATABASE_URL ending in /collaboration_test")
|
||||
}
|
||||
const namespace = `collaboration_test_${randomUUID().replaceAll("-", "")}`
|
||||
const admin = new Pool({ connectionString: testDatabaseUrl, max: 1 })
|
||||
await admin.query(`CREATE SCHEMA "${namespace}"`)
|
||||
const pool = new Pool({ connectionString: testDatabaseUrl, options: `-c search_path=${namespace}`, max: 6 })
|
||||
const db = drizzle(pool, { schema })
|
||||
try {
|
||||
await pool.query("CREATE TYPE locked_tenant AS ENUM ('none', 'maintenance_tenant')")
|
||||
for (const table of [schema.authUsers, schema.authTenantUsers, schema.authProfiles, schema.authRoles,
|
||||
schema.authUserRoles, schema.authRolePermissions, schema.projects, schema.communicationRooms]) {
|
||||
const columns = Object.values(getTableColumns(table)).map((column: any) => {
|
||||
let suffix = column.primary ? " PRIMARY KEY" : ""
|
||||
if (column.name === "id" && column.getSQLType() === "uuid") suffix += " DEFAULT gen_random_uuid()"
|
||||
if (column.columnType === "PgTimestamp") suffix += " DEFAULT now()"
|
||||
if (column.getSQLType() === "boolean") suffix += " DEFAULT false"
|
||||
return `"${column.name}" ${column.getSQLType()}${suffix}`
|
||||
})
|
||||
await pool.query(`CREATE TABLE "${getTableName(table)}" (${columns.join(",")})`)
|
||||
}
|
||||
await pool.query(`CREATE TABLE tenants (id bigint PRIMARY KEY, name text, locked text)`)
|
||||
await pool.query(`CREATE UNIQUE INDEX ON communication_rooms (tenant_id, key)`)
|
||||
await pool.query(await readFile(new URL("../../db/migrations/0070_native_communication_chat.sql", import.meta.url), "utf8"))
|
||||
for (const name of migrations) {
|
||||
await pool.query(await readFile(new URL(`../../db/migrations/${name}`, import.meta.url), "utf8"))
|
||||
}
|
||||
return { db, pool, close: async () => {
|
||||
await pool.end()
|
||||
await admin.query(`DROP SCHEMA "${namespace}" CASCADE`)
|
||||
await admin.end()
|
||||
} }
|
||||
} catch (error) {
|
||||
await pool.end()
|
||||
await admin.query(`DROP SCHEMA "${namespace}" CASCADE`)
|
||||
await admin.end()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user