release gate / check (push) Failing after 6m36s
Implement Notion-like upgrade phase 4: append-only page revisions with restore-as-a-new-version; recoverable bookmark/page/collection deletions whose restores never resurrect public flags or shares; owner-scoped JSON export; and explicit, revocable read-only collection sharing with owner-only mutations and no transitive third-party bookmark leaks. Add migration 009, API/docs, integration coverage, and page/collections/trash UI.
49 lines
2.1 KiB
SQL
49 lines
2.1 KiB
SQL
-- 009_trust.sql — Notion-like upgrades phase 4 (trust).
|
|
--
|
|
-- Server-side page revision history, recoverable deletions, and explicit
|
|
-- collection sharing.
|
|
--
|
|
-- Trust rules:
|
|
-- * Every committed page content version is snapshotted. Restoring a
|
|
-- revision writes a NEW version (history is append-only) and the restore
|
|
-- itself is recorded.
|
|
-- * Trash is a per-owner snapshot taken in the same transaction as the
|
|
-- delete. It restores intrinsic data and owner-scoped relationships, but
|
|
-- never access grants (bookmark public flags, bookmark/tag shares,
|
|
-- collection shares). A granted access revoked while an item was in the
|
|
-- trash cannot come back with the item.
|
|
-- * Collection sharing is read-only and revocable. Recipients see member
|
|
-- bookmarks through the narrowed shared projection; mutations stay
|
|
-- owner-only. BookmarkVisible gains the explicit collection-share edge.
|
|
|
|
CREATE TABLE IF NOT EXISTS "PageRevision" (
|
|
id text PRIMARY KEY,
|
|
"pageId" text NOT NULL REFERENCES "Page"(id) ON DELETE CASCADE,
|
|
version integer NOT NULL,
|
|
title text NOT NULL,
|
|
content text NOT NULL,
|
|
icon text,
|
|
"createdAt" timestamptz NOT NULL DEFAULT now(),
|
|
UNIQUE ("pageId", version)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS "PageRevision_pageId_version_idx" ON "PageRevision"("pageId", version DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS "Trash" (
|
|
id text PRIMARY KEY,
|
|
"ownerId" text NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
|
|
type text NOT NULL CHECK (type IN ('bookmark','page','collection')),
|
|
"originalId" text NOT NULL,
|
|
data jsonb NOT NULL,
|
|
"deletedAt" timestamptz NOT NULL DEFAULT now(),
|
|
UNIQUE (type, "originalId")
|
|
);
|
|
CREATE INDEX IF NOT EXISTS "Trash_ownerId_deletedAt_idx" ON "Trash"("ownerId", "deletedAt" DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS "CollectionShare" (
|
|
"collectionId" text NOT NULL REFERENCES "Collection"(id) ON DELETE CASCADE,
|
|
"userId" text NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
|
|
"createdAt" timestamptz NOT NULL DEFAULT now(),
|
|
PRIMARY KEY ("collectionId", "userId")
|
|
);
|
|
CREATE INDEX IF NOT EXISTS "CollectionShare_userId_idx" ON "CollectionShare"("userId");
|