This commit is contained in:
+351
@@ -0,0 +1,351 @@
|
||||
-- computer database schema
|
||||
-- Derived from computer_porting_guide.md section 2 and prisma/schema.prisma.
|
||||
-- IDs are text (CUID/UUID). CASCADE / SetNull per the guide.
|
||||
|
||||
-- Run against an EMPTY database. Never drops or recreates a database.
|
||||
BEGIN;
|
||||
|
||||
-- Trigram indexes back the page search substring predicate (review PERF-2).
|
||||
-- pg_trgm is a trusted extension, so the database owner can enable it.
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
|
||||
-- 2.1 legacy enum
|
||||
CREATE TYPE "KanbanStatus" AS ENUM ('INBOX','TODO','IN_PROGRESS','DONE','ARCHIVED');
|
||||
|
||||
-- 2.2 User
|
||||
CREATE TABLE "User" (
|
||||
id text PRIMARY KEY,
|
||||
name text,
|
||||
email text UNIQUE,
|
||||
"emailVerified" timestamptz,
|
||||
image text,
|
||||
password text,
|
||||
role text NOT NULL DEFAULT 'USER' CHECK (role IN ('USER','ADMIN')),
|
||||
"tokenVersion" integer NOT NULL DEFAULT 1,
|
||||
"createdAt" timestamptz NOT NULL DEFAULT now(),
|
||||
"updatedAt" timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- 2.3 Account, Session, VerificationToken
|
||||
CREATE TABLE "Account" (
|
||||
id text PRIMARY KEY,
|
||||
"userId" text NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
|
||||
type text NOT NULL,
|
||||
provider text NOT NULL,
|
||||
"providerAccountId" text NOT NULL,
|
||||
"refresh_token" text,
|
||||
"access_token" text,
|
||||
"expires_at" int,
|
||||
"token_type" text,
|
||||
scope text,
|
||||
"id_token" text,
|
||||
"session_state" text,
|
||||
UNIQUE(provider, "providerAccountId")
|
||||
);
|
||||
CREATE INDEX "Account_userId_idx" ON "Account"("userId");
|
||||
|
||||
CREATE TABLE "Session" (
|
||||
id text PRIMARY KEY,
|
||||
"sessionToken" text NOT NULL UNIQUE,
|
||||
"userId" text NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
|
||||
expires timestamptz NOT NULL
|
||||
);
|
||||
CREATE INDEX "Session_userId_idx" ON "Session"("userId");
|
||||
|
||||
CREATE TABLE "VerificationToken" (
|
||||
identifier text NOT NULL,
|
||||
token text NOT NULL UNIQUE,
|
||||
expires timestamptz NOT NULL,
|
||||
UNIQUE(identifier, token)
|
||||
);
|
||||
|
||||
-- 2.4 Bookmark
|
||||
CREATE TABLE "Bookmark" (
|
||||
id text PRIMARY KEY,
|
||||
url text NOT NULL,
|
||||
title text,
|
||||
description text,
|
||||
favicon text,
|
||||
"ogImage" text,
|
||||
"ogTitle" text,
|
||||
"ogDescription" text,
|
||||
notes text,
|
||||
"dueDate" timestamptz,
|
||||
"alertAt" timestamptz,
|
||||
"alertSent" boolean NOT NULL DEFAULT false,
|
||||
"kanbanStatus" "KanbanStatus" NOT NULL DEFAULT 'INBOX',
|
||||
"kanbanColumnId" text,
|
||||
-- Board ordering within a column. Server-normalized on every card move;
|
||||
-- legacy rows default to 0 and fall back to updatedAt-desc ordering.
|
||||
"kanbanPosition" integer NOT NULL DEFAULT 0,
|
||||
"imapWatchEnabled" boolean NOT NULL DEFAULT false,
|
||||
"imapQuery" text,
|
||||
"isPublic" boolean NOT NULL DEFAULT false,
|
||||
"ownerId" text NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
|
||||
"contactId" text,
|
||||
-- Optimistic concurrency revision (review CR-04): every owner-driven PUT
|
||||
-- (fields and tag replacement) requires the expected version and bumps it;
|
||||
-- the column-deletion move bumps it too. Sharing and server-driven flags
|
||||
-- (alertSent) are outside the contract — see API.md.
|
||||
version integer NOT NULL DEFAULT 0,
|
||||
"createdAt" timestamptz NOT NULL DEFAULT now(),
|
||||
"updatedAt" timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX "Bookmark_ownerId_idx" ON "Bookmark"("ownerId");
|
||||
CREATE INDEX "Bookmark_ownerId_updatedAt_id_idx" ON "Bookmark"("ownerId","updatedAt" DESC,id DESC);
|
||||
CREATE INDEX "Bookmark_contactId_idx" ON "Bookmark"("contactId");
|
||||
CREATE INDEX "Bookmark_kanbanStatus_idx" ON "Bookmark"("kanbanStatus");
|
||||
CREATE INDEX "Bookmark_kanbanColumnId_idx" ON "Bookmark"("kanbanColumnId");
|
||||
CREATE INDEX "Bookmark_ownerId_kanbanColumnId_kanbanPosition_idx" ON "Bookmark"("ownerId","kanbanColumnId","kanbanPosition",id);
|
||||
CREATE INDEX "Bookmark_dueDate_idx" ON "Bookmark"("dueDate");
|
||||
CREATE INDEX "Bookmark_alertAt_idx" ON "Bookmark"("alertAt");
|
||||
|
||||
-- 2.5 Tag
|
||||
CREATE TABLE "Tag" (
|
||||
id text PRIMARY KEY,
|
||||
name text NOT NULL,
|
||||
color text NOT NULL DEFAULT '#6366f1',
|
||||
"ownerId" text NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
|
||||
"createdAt" timestamptz NOT NULL DEFAULT now(),
|
||||
"updatedAt" timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE(name, "ownerId")
|
||||
);
|
||||
CREATE INDEX "Tag_ownerId_idx" ON "Tag"("ownerId");
|
||||
|
||||
-- 2.6 BookmarkTag
|
||||
CREATE TABLE "BookmarkTag" (
|
||||
"bookmarkId" text NOT NULL REFERENCES "Bookmark"(id) ON DELETE CASCADE,
|
||||
"tagId" text NOT NULL REFERENCES "Tag"(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY ("bookmarkId", "tagId")
|
||||
);
|
||||
CREATE INDEX "BookmarkTag_tagId_idx" ON "BookmarkTag"("tagId");
|
||||
|
||||
-- 2.7 BookmarkShare
|
||||
CREATE TABLE "BookmarkShare" (
|
||||
"bookmarkId" text NOT NULL REFERENCES "Bookmark"(id) ON DELETE CASCADE,
|
||||
"userId" text NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
|
||||
"createdAt" timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY ("bookmarkId", "userId")
|
||||
);
|
||||
CREATE INDEX "BookmarkShare_userId_idx" ON "BookmarkShare"("userId");
|
||||
|
||||
-- 2.8 TagShare
|
||||
CREATE TABLE "TagShare" (
|
||||
"tagId" text NOT NULL REFERENCES "Tag"(id) ON DELETE CASCADE,
|
||||
"userId" text NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
|
||||
"createdAt" timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY ("tagId", "userId")
|
||||
);
|
||||
CREATE INDEX "TagShare_userId_idx" ON "TagShare"("userId");
|
||||
|
||||
-- 2.9 Contact
|
||||
CREATE TABLE "Contact" (
|
||||
id text PRIMARY KEY,
|
||||
uid text NOT NULL,
|
||||
"displayName" text NOT NULL,
|
||||
email text,
|
||||
phone text,
|
||||
"cardDavUrl" text,
|
||||
-- CardDAV provenance (review CR-07): the address-book URL this contact was
|
||||
-- imported from. Deletion reconciliation is scoped to it, so manually
|
||||
-- created contacts (NULL) and other address books' imports are never
|
||||
-- touched by a sync.
|
||||
source text,
|
||||
"userId" text NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
|
||||
"createdAt" timestamptz NOT NULL DEFAULT now(),
|
||||
"updatedAt" timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE(uid, "userId")
|
||||
);
|
||||
CREATE INDEX "Contact_userId_idx" ON "Contact"("userId");
|
||||
ALTER TABLE "Bookmark" ADD CONSTRAINT "Bookmark_contactId_fkey"
|
||||
FOREIGN KEY ("contactId") REFERENCES "Contact"(id) ON DELETE SET NULL;
|
||||
|
||||
-- 2.10 ImapConfig
|
||||
CREATE TABLE "ImapConfig" (
|
||||
id text PRIMARY KEY,
|
||||
host text NOT NULL,
|
||||
port int NOT NULL DEFAULT 993,
|
||||
tls boolean NOT NULL DEFAULT true,
|
||||
username text NOT NULL,
|
||||
password text NOT NULL,
|
||||
folder text NOT NULL DEFAULT 'INBOX',
|
||||
"userId" text UNIQUE NOT NULL REFERENCES "User"(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 2.11 KanbanColumn
|
||||
CREATE TABLE "KanbanColumn" (
|
||||
id text PRIMARY KEY,
|
||||
label text NOT NULL,
|
||||
color text NOT NULL DEFAULT '#64748b',
|
||||
position int NOT NULL DEFAULT 0,
|
||||
"key" text,
|
||||
"userId" text NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
|
||||
"createdAt" timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX "KanbanColumn_userId_position_idx" ON "KanbanColumn"("userId", position);
|
||||
|
||||
-- Bookmark.kanbanColumnId -> KanbanColumn (SetNull)
|
||||
ALTER TABLE "Bookmark" ADD CONSTRAINT "Bookmark_kanbanColumnId_fkey"
|
||||
FOREIGN KEY ("kanbanColumnId") REFERENCES "KanbanColumn"(id) ON DELETE SET NULL;
|
||||
|
||||
-- 2.12 Page
|
||||
CREATE TABLE "Page" (
|
||||
id text PRIMARY KEY,
|
||||
title text NOT NULL,
|
||||
content text NOT NULL DEFAULT '',
|
||||
icon text,
|
||||
pinned boolean NOT NULL DEFAULT false,
|
||||
position int NOT NULL DEFAULT 0,
|
||||
-- Optimistic concurrency revision (review DATA-01): content saves bump it;
|
||||
-- a stale base version in PATCH is rejected with 409.
|
||||
version integer NOT NULL DEFAULT 1,
|
||||
"ownerId" text NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
|
||||
"createdAt" timestamptz NOT NULL DEFAULT now(),
|
||||
"updatedAt" timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX "Page_ownerId_position_idx" ON "Page"("ownerId", position);
|
||||
-- Index-backed substring search over the two searchable fields.
|
||||
CREATE INDEX "Page_title_trgm_idx" ON "Page" USING gin (title gin_trgm_ops);
|
||||
CREATE INDEX "Page_content_trgm_idx" ON "Page" USING gin (content gin_trgm_ops);
|
||||
|
||||
-- 2.13 PageBookmark
|
||||
CREATE TABLE "PageBookmark" (
|
||||
"pageId" text NOT NULL REFERENCES "Page"(id) ON DELETE CASCADE,
|
||||
"bookmarkId" text NOT NULL REFERENCES "Bookmark"(id) ON DELETE CASCADE,
|
||||
position int NOT NULL DEFAULT 0,
|
||||
"createdAt" timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY ("pageId", "bookmarkId")
|
||||
);
|
||||
CREATE INDEX "PageBookmark_bookmarkId_idx" ON "PageBookmark"("bookmarkId");
|
||||
|
||||
-- 2.14 SignalSource
|
||||
CREATE TABLE "SignalSource" (
|
||||
id text PRIMARY KEY,
|
||||
type text NOT NULL,
|
||||
config jsonb NOT NULL DEFAULT '{}',
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
"lastCheckedAt" timestamptz,
|
||||
"lastStatus" text,
|
||||
"bookmarkId" text NOT NULL REFERENCES "Bookmark"(id) ON DELETE CASCADE,
|
||||
"ownerId" text NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
|
||||
"createdAt" timestamptz NOT NULL DEFAULT now(),
|
||||
"updatedAt" timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX "SignalSource_bookmarkId_idx" ON "SignalSource"("bookmarkId");
|
||||
CREATE INDEX "SignalSource_ownerId_idx" ON "SignalSource"("ownerId");
|
||||
|
||||
-- 2.14b SignalCheckpoint (review SYNC-02): durable per-source mailbox
|
||||
-- ingestion state. A poll processes only UIDs newer than lastUid while
|
||||
-- uidValidity matches; the checkpoint advances only after the batch's
|
||||
-- signals are durably inserted. A uidValidity change resets ingestion.
|
||||
CREATE TABLE "SignalCheckpoint" (
|
||||
"sourceId" text PRIMARY KEY REFERENCES "SignalSource"(id) ON DELETE CASCADE,
|
||||
"uidValidity" bigint NOT NULL,
|
||||
"lastUid" bigint NOT NULL,
|
||||
"updatedAt" timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- 2.15 Signal
|
||||
CREATE TABLE "Signal" (
|
||||
id text PRIMARY KEY,
|
||||
"sourceType" text NOT NULL,
|
||||
title text NOT NULL,
|
||||
summary text,
|
||||
url text,
|
||||
"externalId" text,
|
||||
"occurredAt" timestamptz NOT NULL DEFAULT now(),
|
||||
read boolean NOT NULL DEFAULT false,
|
||||
"sourceId" text,
|
||||
"bookmarkId" text NOT NULL REFERENCES "Bookmark"(id) ON DELETE CASCADE,
|
||||
"ownerId" text NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
|
||||
"createdAt" timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE("sourceId", "externalId")
|
||||
);
|
||||
CREATE INDEX "Signal_bookmarkId_occurredAt_idx" ON "Signal"("bookmarkId", "occurredAt");
|
||||
CREATE INDEX "Signal_ownerId_occurredAt_idx" ON "Signal"("ownerId", "occurredAt");
|
||||
ALTER TABLE "Signal" ADD CONSTRAINT "Signal_sourceId_fkey"
|
||||
FOREIGN KEY ("sourceId") REFERENCES "SignalSource"(id) ON DELETE SET NULL;
|
||||
|
||||
-- 2.16 AppSetting: server-owned singleton settings rows. 'adminBootstrap'
|
||||
-- records that initial provisioning completed (review AUTH-06), so the
|
||||
-- absence of a qualifying administrator never re-opens first-signup
|
||||
-- promotion after a demotion or removal.
|
||||
CREATE TABLE "AppSetting" (
|
||||
key text PRIMARY KEY,
|
||||
value jsonb NOT NULL DEFAULT '{}',
|
||||
"updatedAt" timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- 2.16 SharedDate
|
||||
CREATE TABLE "SharedDate" (
|
||||
id text PRIMARY KEY,
|
||||
date date NOT NULL,
|
||||
note text,
|
||||
"creatorId" text NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
|
||||
"recipientId" text NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
|
||||
"createdAt" timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX "SharedDate_recipientId_idx" ON "SharedDate"("recipientId");
|
||||
CREATE INDEX "SharedDate_creatorId_idx" ON "SharedDate"("creatorId");
|
||||
CREATE INDEX "SharedDate_date_idx" ON "SharedDate"(date);
|
||||
CREATE INDEX "SharedDate_creatorId_recipientId_date_idx" ON "SharedDate"("creatorId","recipientId",date);
|
||||
CREATE INDEX "Bookmark_ownerId_alertAt_unsent_idx" ON "Bookmark"("ownerId","alertAt") WHERE NOT "alertSent";
|
||||
|
||||
-- 2.17 Collections (Notion-like upgrades phase 2; see 002_collections.sql for
|
||||
-- the design decisions). Contextual workflow state: the same bookmark can
|
||||
-- live in many collections with independent property values; membership
|
||||
-- never broadens access and never owns the bookmark.
|
||||
CREATE TABLE "Collection" (
|
||||
id text PRIMARY KEY,
|
||||
name text NOT NULL,
|
||||
description text,
|
||||
icon text,
|
||||
"ownerId" text NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
|
||||
version integer NOT NULL DEFAULT 1,
|
||||
"createdAt" timestamptz NOT NULL DEFAULT now(),
|
||||
"updatedAt" timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX "Collection_ownerId_idx" ON "Collection"("ownerId");
|
||||
|
||||
CREATE TABLE "CollectionProperty" (
|
||||
id text PRIMARY KEY,
|
||||
"collectionId" text NOT NULL REFERENCES "Collection"(id) ON DELETE CASCADE,
|
||||
name text NOT NULL,
|
||||
type text NOT NULL CHECK (type IN ('TEXT','NUMBER','SELECT','MULTI_SELECT','DATE','CHECKBOX','URL','RELATION')),
|
||||
position integer NOT NULL DEFAULT 0,
|
||||
config jsonb NOT NULL DEFAULT '{}',
|
||||
version integer NOT NULL DEFAULT 1,
|
||||
"createdAt" timestamptz NOT NULL DEFAULT now(),
|
||||
"updatedAt" timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE ("collectionId", name)
|
||||
);
|
||||
|
||||
CREATE TABLE "CollectionItem" (
|
||||
id text PRIMARY KEY,
|
||||
"collectionId" text NOT NULL REFERENCES "Collection"(id) ON DELETE CASCADE,
|
||||
"bookmarkId" text NOT NULL REFERENCES "Bookmark"(id) ON DELETE CASCADE,
|
||||
"propertyValues" jsonb NOT NULL DEFAULT '{}',
|
||||
position integer NOT NULL DEFAULT 0,
|
||||
version integer NOT NULL DEFAULT 1,
|
||||
"addedAt" timestamptz NOT NULL DEFAULT now(),
|
||||
"updatedAt" timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE ("collectionId", "bookmarkId")
|
||||
);
|
||||
CREATE INDEX "CollectionItem_bookmarkId_idx" ON "CollectionItem"("bookmarkId");
|
||||
|
||||
CREATE TABLE "CollectionView" (
|
||||
id text PRIMARY KEY,
|
||||
"collectionId" text NOT NULL REFERENCES "Collection"(id) ON DELETE CASCADE,
|
||||
name text NOT NULL,
|
||||
type text NOT NULL DEFAULT 'TABLE' CHECK (type IN ('TABLE','BOARD','CALENDAR','LIST')),
|
||||
position integer NOT NULL DEFAULT 0,
|
||||
filters jsonb NOT NULL DEFAULT '{"operator":"AND","clauses":[]}',
|
||||
sorts jsonb NOT NULL DEFAULT '[]',
|
||||
"groupByPropertyId" text REFERENCES "CollectionProperty"(id) ON DELETE SET NULL,
|
||||
"displaySettings" jsonb NOT NULL DEFAULT '{}',
|
||||
version integer NOT NULL DEFAULT 1,
|
||||
"createdAt" timestamptz NOT NULL DEFAULT now(),
|
||||
"updatedAt" timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX "CollectionView_collectionId_idx" ON "CollectionView"("collectionId");
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user