import "dotenv/config"; /** * tenantTransaction(): atomic multi-step writes for one tenant. * Runs in the mode given by RLS_ENFORCED (owner path by default). The runner executes this * script once as-is; to verify the RLS path run: RLS_ENFORCED=true npx tsx scripts/test-tenant-transaction.ts * (requires craftvia_app LOGIN, see test-rls-enforcement.ts — otherwise that mode is skipped). * (1) commit: two writes inside → both visible afterwards * (2) rollback: two writes, then throw → nothing visible * (3) a write for another tenant inside the transaction → rejected, whole tx rolled back * (4) nested tenantTransaction joins the outer transaction (rollback covers both) */ let failures = 0; function check(name: string, cond: boolean, detail?: unknown) { if (cond) console.log(` ✓ ${name}`); else { failures++; console.log(` ✗ ${name}`, detail ?? ""); } } async function main() { const { prisma, dbForTenant, tenantTransaction, RLS_ENFORCED } = await import("../src/server/db"); console.log(`Modus: ${RLS_ENFORCED ? "RLS (craftvia_app)" : "Owner"}`); const stamp = Date.now(); const a = await prisma.tenant.create({ data: { name: "ZZ Tx A", slug: `zz-tx-a-${stamp}` } }); const b = await prisma.tenant.create({ data: { name: "ZZ Tx B", slug: `zz-tx-b-${stamp}` } }); const dbA = dbForTenant(a.id); const countA = () => prisma.numberSequence.count({ where: { tenantId: a.id } }); try { // (1) commit await tenantTransaction(dbA, a.id, async (tx) => { await tx.numberSequence.create({ data: { tenantId: a.id, key: "customer", prefix: "K-" } }); await tx.numberSequence.create({ data: { tenantId: a.id, key: "work_order", prefix: "A-" } }); }); check("(1) commit: both rows persisted", (await countA()) === 2, await countA()); // (2) rollback try { await tenantTransaction(dbA, a.id, async (tx) => { await tx.numberSequence.create({ data: { tenantId: a.id, key: "report", prefix: "B-" } }); await tx.numberSequence.create({ data: { tenantId: a.id, key: "emergency", prefix: "N-" } }); throw new Error("boom"); }); check("(2) rollback: error propagated", false); } catch (err) { check("(2) rollback: error propagated", (err as Error).message === "boom", (err as Error).message); } check("(2) rollback: no rows from failed tx", (await countA()) === 2, await countA()); // (3) foreign tenant inside the tx try { await tenantTransaction(dbA, a.id, async (tx) => { await tx.numberSequence.create({ data: { tenantId: a.id, key: "report", prefix: "B-" } }); const foreign = await prisma.numberSequence.create({ data: { tenantId: b.id, key: "customer", prefix: "K-" } }); await tx.numberSequence.update({ where: { id: foreign.id }, data: { prefix: "HACK-" } }); }); check("(3) foreign update rejected", false); } catch { check("(3) foreign update rejected", true); } const foreignRow = await prisma.numberSequence.findFirst({ where: { tenantId: b.id } }); check("(3) foreign row unchanged", foreignRow?.prefix === "K-", foreignRow?.prefix); check("(3) own write of failed tx rolled back", (await countA()) === 2, await countA()); // (4) nested join try { await tenantTransaction(dbA, a.id, async (tx) => { await tx.numberSequence.create({ data: { tenantId: a.id, key: "report", prefix: "B-" } }); await tenantTransaction(tx, a.id, async (inner) => { await inner.numberSequence.create({ data: { tenantId: a.id, key: "emergency", prefix: "N-" } }); }); throw new Error("outer boom"); }); } catch { /* expected */ } check("(4) nested writes rolled back with outer tx", (await countA()) === 2, await countA()); } finally { await prisma.numberSequence.deleteMany({ where: { tenantId: { in: [a.id, b.id] } } }); await prisma.auditLog.deleteMany({ where: { tenantId: { in: [a.id, b.id] } } }); await prisma.tenant.deleteMany({ where: { id: { in: [a.id, b.id] } } }); await prisma.$disconnect(); } console.log(failures ? `\n${failures} Fehler` : "\nOK"); process.exit(failures ? 1 : 0); } main().catch((err) => { const msg = String((err as Error)?.message ?? err); if (/craftvia_app|password authentication|permission denied for role/i.test(msg)) { console.log(`⚠ ÜBERSPRUNGEN: RLS-Verbindung nicht möglich (${msg.split("\n")[0]})`); process.exit(0); } console.error(err); process.exit(1); });