initial commit on Federted servers
release gate / check (push) Failing after 8m35s

This commit is contained in:
Andrew S Erwin
2026-10-19 16:23:55 -05:00
commit b9721ff13f
217 changed files with 31534 additions and 0 deletions
+68
View File
@@ -0,0 +1,68 @@
-- 001_legacy_additive.sql — additive upgrades historically shipped as
-- db/upgrade.sql, adopted into the ordered ledger (review MIG-01).
-- Idempotent: every statement is guarded by an existence check, so first
-- ledger adoption on an already-upgraded database is a no-op. From ledger
-- adoption onward recorded migrations are immutable; new changes go into
-- new numbered files. The runner (db/migrate.sh) applies each file in its
-- own transaction with ON_ERROR_STOP.
-- Review MIG-02: report orphaned bookmark->contact references with an
-- actionable error before adding the foreign key, instead of failing with a
-- bare constraint violation. Never erase relationships silently — repair is
-- a deliberate operator decision.
DO $$
DECLARE orphans bigint;
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname='Bookmark_contactId_fkey' AND conrelid='"Bookmark"'::regclass) THEN
SELECT count(*) INTO orphans FROM "Bookmark" b
WHERE b."contactId" IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM "Contact" c WHERE c.id=b."contactId");
IF orphans > 0 THEN
RAISE EXCEPTION 'migration 001: % bookmark(s) reference missing Contact rows; repair them deliberately (e.g. UPDATE "Bookmark" SET "contactId"=NULL WHERE "contactId" IS NOT NULL AND NOT EXISTS (SELECT 1 FROM "Contact" c WHERE c.id="Bookmark"."contactId")) and re-run', orphans;
END IF;
ALTER TABLE "Bookmark" ADD CONSTRAINT "Bookmark_contactId_fkey" FOREIGN KEY ("contactId") REFERENCES "Contact"(id) ON DELETE SET NULL;
END IF;
END $$;
-- Bring pre-port mailbox switches into the background signal scheduler.
-- Existing sources (including paused watches) and modern watchers are preserved.
INSERT INTO "SignalSource"(id,type,config,enabled,"bookmarkId","ownerId")
SELECT 'legacy-'||b.id,'email',json_build_object('subject',COALESCE(b."imapQuery",''),'legacy',true),true,b.id,b."ownerId"
FROM "Bookmark" b
WHERE b."imapWatchEnabled" AND NOT EXISTS (
SELECT 1 FROM "SignalSource" s WHERE s."bookmarkId"=b.id AND s."ownerId"=b."ownerId" AND s.config->>'legacy'='true'
)
ON CONFLICT(id) DO NOTHING;
CREATE INDEX IF NOT EXISTS "Bookmark_ownerId_updatedAt_id_idx" ON "Bookmark"("ownerId","updatedAt" DESC,id DESC);
-- Revocable sessions: cookie JWTs embed this version; signout bumps it.
ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "tokenVersion" integer NOT NULL DEFAULT 1;
-- Query-pattern indexes: shared-date visibility join and the alerts poll.
CREATE INDEX IF NOT EXISTS "SharedDate_creatorId_recipientId_date_idx" ON "SharedDate"("creatorId","recipientId",date);
CREATE INDEX IF NOT EXISTS "Bookmark_ownerId_alertAt_unsent_idx" ON "Bookmark"("ownerId","alertAt") WHERE NOT "alertSent";
-- Prisma's Json maps to jsonb; enables GIN indexing and dedups keys.
-- Re-running on an already-jsonb column is a validated no-op.
ALTER TABLE "SignalSource" ALTER COLUMN config TYPE jsonb USING config::jsonb;
-- Optimistic concurrency for page saves (review DATA-01): content saves bump
-- this revision and a stale base version in PATCH is rejected with 409.
ALTER TABLE "Page" ADD COLUMN IF NOT EXISTS version integer NOT NULL DEFAULT 1;
-- Durable per-source mailbox ingestion checkpoints (review SYNC-02): polls
-- process only UIDs newer than lastUid and advance only after durable inserts.
CREATE TABLE IF NOT EXISTS "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()
);
-- One-shot administrator bootstrap state (review AUTH-06): once recorded,
-- signup never auto-promotes again; recovery is operator-driven.
CREATE TABLE IF NOT EXISTS "AppSetting" (
key text PRIMARY KEY,
value jsonb NOT NULL DEFAULT '{}',
"updatedAt" timestamptz NOT NULL DEFAULT now()
);
-- Installations that already have a qualifying administrator completed
-- bootstrap before this ledger existed; record it so demoting that
-- administrator cannot re-open first-signup promotion.
INSERT INTO "AppSetting"(key,value)
SELECT 'adminBootstrap',jsonb_build_object('completed',true)
WHERE EXISTS (SELECT 1 FROM "User" WHERE role='ADMIN' AND email NOT LIKE '%@example.com')
ON CONFLICT(key) DO NOTHING;
+78
View File
@@ -0,0 +1,78 @@
-- 002_collections.sql — collections, typed properties, memberships and saved
-- views (Notion-like upgrades phase 2: N-01/N-02/N-03 data layer).
--
-- Design decisions (computer_notion_upgrades.md §4, locked before shipping):
-- * Membership is contextual: the same bookmark can belong to many
-- collections with independent property values; removing a membership or a
-- whole collection never touches the bookmark (N-01).
-- * Membership never broadens access: item reads re-check BookmarkVisible at
-- query time, and creation validates visibility (decision #4, N-08).
-- * Views are queries, not copies: filters/sorts/grouping live on the view
-- and never duplicate record data (decision #3).
-- * Deviation from the proposal draft, per its own API rule ("PATCH must
-- require the expected version", the DATA-05 lesson): CollectionProperty,
-- CollectionItem and CollectionView carry a version column bumped by every
-- mutating PATCH; a stale base is a 409. Property rows also gained
-- updatedAt for the same reason.
-- * Property types are a CHECK constraint, not a Postgres enum (easier to
-- evolve). RELATION is reserved in the constraint but the API rejects it
-- until cross-collection access checks are designed (draft §6 open issue).
-- * Date semantics (decision #7): DATE properties declare config.mode
-- 'date' (YYYY-MM-DD, calendar-day stable across timezones) or 'datetime'
-- (RFC3339, normalized to UTC on write). Values live in CollectionItem.
-- propertyValues (JSONB keyed by property id) and are validated server-side
-- against CollectionProperty on every write.
CREATE TABLE IF NOT EXISTS "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 IF NOT EXISTS "Collection_ownerId_idx" ON "Collection"("ownerId");
CREATE TABLE IF NOT EXISTS "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 IF NOT EXISTS "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 IF NOT EXISTS "CollectionItem_bookmarkId_idx" ON "CollectionItem"("bookmarkId");
CREATE TABLE IF NOT EXISTS "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 IF NOT EXISTS "CollectionView_collectionId_idx" ON "CollectionView"("collectionId");
@@ -0,0 +1,19 @@
-- 003_collections_propertyvalues_case.sql — repair a quoting mistake in 002.
-- 002 created CollectionItem.propertyValues unquoted, so PostgreSQL folded it
-- to lowercase "propertyvalues" on databases where the migration's own DDL
-- materialized the table (existing installs upgraded through the ledger).
-- schema.sql and the API contract use the quoted camelCase name. 002 is
-- recorded and immutable, so the repair ships as this guarded rename; on
-- databases built from the corrected schema.sql the column already has the
-- right name and this is a no-op.
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM pg_attribute
WHERE attrelid='"CollectionItem"'::regclass
AND attname='propertyvalues'
AND NOT attisdropped
) THEN
ALTER TABLE "CollectionItem" RENAME COLUMN propertyvalues TO "propertyValues";
END IF;
END $$;
+7
View File
@@ -0,0 +1,7 @@
-- 004_bookmark_version.sql — optimistic concurrency for bookmark updates
-- (review CR-04). Owner-driven PUT /api/bookmarks/{id} (fields and tag
-- replacement) now requires the expected version and bumps it; the
-- column-deletion move bumps it too, so a client holding a stale snapshot
-- gets a 409 instead of silently overwriting a concurrent edit. Sharing and
-- server-driven flags (alertSent) stay outside the contract.
ALTER TABLE "Bookmark" ADD COLUMN IF NOT EXISTS version integer NOT NULL DEFAULT 0;
+8
View File
@@ -0,0 +1,8 @@
-- 005_contact_source.sql — CardDAV provenance for deletion reconciliation
-- (review CR-07). Sync mirrors the configured address book, scoped by this
-- column: only contacts previously imported from the SAME source URL are
-- candidates for remote-deletion removal; manually created contacts (NULL)
-- and other address books' imports are never touched. Existing rows keep
-- NULL and are adopted (stamped with the source) the next time the server
-- returns them.
ALTER TABLE "Contact" ADD COLUMN IF NOT EXISTS source text;
@@ -0,0 +1,8 @@
-- 006_bookmark_kanban_position.sql — persisted card ordering on kanban
-- boards. Every card move normalizes the target column's positions so a
-- drag inside a column survives reloads and concurrent moves stay
-- deterministic. Legacy rows default to 0 and keep their existing
-- updatedAt-descending order until the column is first arranged.
ALTER TABLE "Bookmark" ADD COLUMN IF NOT EXISTS "kanbanPosition" integer NOT NULL DEFAULT 0;
CREATE INDEX IF NOT EXISTS "Bookmark_ownerId_kanbanColumnId_kanbanPosition_idx"
ON "Bookmark"("ownerId","kanbanColumnId","kanbanPosition",id);
+10
View File
@@ -0,0 +1,10 @@
-- 007_page_search_index.sql — make page search index-backed and literal.
--
-- The page list searched title/content with an unescaped ILIKE pattern: "%"
-- and "_" behaved as wildcards and every request scanned the owner's pages.
-- The handler now escapes those characters; these trigram indexes let
-- Postgres serve the substring predicate without a sequential scan.
-- pg_trgm is a trusted extension, so the database owner can enable it.
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX IF NOT EXISTS "Page_title_trgm_idx" ON "Page" USING gin (title gin_trgm_ops);
CREATE INDEX IF NOT EXISTS "Page_content_trgm_idx" ON "Page" USING gin (content gin_trgm_ops);