Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions e2e/principal-delete.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { createInMemoryGrantStore } from "@intx/authz";

import type { Memory } from "../src/memory.js";
import {
allow,
createTestDb,
createTestMemory,
seedPrincipal,
testDatabaseUrl,
type TestDb,
} from "./helpers.js";

describe.skipIf(testDatabaseUrl() === undefined)("deleting a principal", () => {
let db: TestDb;
let memory: Memory | undefined;

beforeAll(async () => {
db = await createTestDb();
await seedPrincipal(db, "acme", "alice");
memory = createTestMemory(
db,
createInMemoryGrantStore([allow("alice", "add")]),
);
});

afterAll(async () => {
await memory?.close();
await db?.close();
});

test("keeps the documents and versions they authored, with a null author", async () => {
const { documentId } = await memory!.add({
tenantId: "acme",
principalId: "alice",
content: {
title: "Outlives alice",
text: "kept after the author leaves",
},
});

await db.sql`DELETE FROM public.principal WHERE id = 'alice'`;

const rows = await db.sql<{ author: string | null }[]>`
SELECT v.created_by_principal_id AS author FROM memory.version v
JOIN memory.document d ON d.id = v.document_id
WHERE d.id = ${documentId}`;
expect(rows.map((r) => r.author)).toEqual([null]);
});
});
17 changes: 17 additions & 0 deletions migrations/0010_version_author_set_null.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- A removed principal must not delete the versions it authored: switch the
-- author foreign key from ON DELETE CASCADE to ON DELETE SET NULL. Replaced
-- only while it still cascades, so re-running is a no-op.

DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'version_created_by_principal_id_fkey' AND conrelid = '"memory"."version"'::regclass
AND confdeltype = 'c'
) THEN
ALTER TABLE "memory"."version"
DROP CONSTRAINT "version_created_by_principal_id_fkey",
ADD CONSTRAINT "version_created_by_principal_id_fkey"
FOREIGN KEY ("created_by_principal_id") REFERENCES "public"."principal"("id") ON DELETE SET NULL;
END IF;
END $$;
2 changes: 1 addition & 1 deletion src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const tenantRef = (column: string) =>
.references(() => hostTenant.id, { onDelete: "cascade" });
/** A removed principal must not vaporize the memory it created. */
const principalRef = (column: string) =>
text(column).references(() => hostPrincipal.id, { onDelete: "cascade" });
text(column).references(() => hostPrincipal.id, { onDelete: "set null" });

// No built-in `bytea` helper in drizzle-orm/pg-core; raw_capture.raw_bytes
// holds non-textual raw payloads (binary source formats) as a Buffer.
Expand Down
Loading