From 4bb139018090fd27804b5007da0111b38c505218 Mon Sep 17 00:00:00 2001 From: Till JS Date: Tue, 12 May 2026 18:53:52 +0200 Subject: [PATCH] db(cards): baseline migration + drizzle-tracking bootstrap script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Schließt die Ops-Lücke „kein versioniertes Schema-Tracking" aus FEATURE_IDEAS.md. * apps/api/src/db/migrations/0000_baseline.sql — Drizzle-generierte Baseline-Migration, 355 Zeilen, 25 Tabellen + 5 Enums (cards- und marketplace-Schema). Eingefrostet auf den Live-Stand 2026-05-12. * apps/api/scripts/bootstrap-drizzle-tracking.ts — neues Script, markiert die Baseline in einer bestehenden DB als „bereits angewandt", ohne SQL erneut auszuführen. Verwendet sha256 wie drizzle-orm/migrator (Hash 312d67ba1aeb…), idempotent. * package.json: drizzle:migrate + drizzle:bootstrap-tracking npm-scripts. * docs/playbooks/DRIZZLE_MIGRATIONS_BOOTSTRAP.md — Hand-Over für Prod (Bootstrap einmalig, dann normaler Workflow: schema → generate → commit → migrate, kein push --force mehr). Lokal verifiziert: 17/104 Tests grün, bootstrap idempotent, drizzle-kit migrate erkennt die Baseline. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/api/package.json | 4 +- .../api/scripts/bootstrap-drizzle-tracking.ts | 90 + apps/api/src/db/migrations/0000_baseline.sql | 356 ++ .../src/db/migrations/meta/0000_snapshot.json | 2896 +++++++++++++++++ apps/api/src/db/migrations/meta/_journal.json | 13 + docs/FEATURE_IDEAS.md | 14 +- .../playbooks/DRIZZLE_MIGRATIONS_BOOTSTRAP.md | 155 + 7 files changed, 3523 insertions(+), 5 deletions(-) create mode 100644 apps/api/scripts/bootstrap-drizzle-tracking.ts create mode 100644 apps/api/src/db/migrations/0000_baseline.sql create mode 100644 apps/api/src/db/migrations/meta/0000_snapshot.json create mode 100644 apps/api/src/db/migrations/meta/_journal.json create mode 100644 docs/playbooks/DRIZZLE_MIGRATIONS_BOOTSTRAP.md diff --git a/apps/api/package.json b/apps/api/package.json index f8f2a0c..63b03b0 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -14,8 +14,10 @@ "lint": "echo 'lint configured later (eslint flat-config)'", "clean": "rm -rf dist .turbo coverage", "drizzle:generate": "drizzle-kit generate", + "drizzle:migrate": "drizzle-kit migrate", "drizzle:push": "drizzle-kit push --force", - "drizzle:studio": "drizzle-kit studio" + "drizzle:studio": "drizzle-kit studio", + "drizzle:bootstrap-tracking": "bun run scripts/bootstrap-drizzle-tracking.ts" }, "dependencies": { "@cards/domain": "workspace:*", diff --git a/apps/api/scripts/bootstrap-drizzle-tracking.ts b/apps/api/scripts/bootstrap-drizzle-tracking.ts new file mode 100644 index 0000000..3cc0648 --- /dev/null +++ b/apps/api/scripts/bootstrap-drizzle-tracking.ts @@ -0,0 +1,90 @@ +/** + * Bootstrap-Script für drizzle.__drizzle_migrations. + * + * Eine Cards-DB, die bisher über `drizzle-kit push` (Schema-Sync ohne + * Migrations-Tracking) gepflegt wurde, hat das Drizzle-Tracking-Schema + * nicht. Dieses Script holt das nach: + * + * 1. Erstellt `drizzle.__drizzle_migrations` (idempotent). + * 2. Liest `src/db/migrations/meta/_journal.json`. + * 3. Markiert jede dort gelistete Migration als „bereits angewandt", + * mit dem gleichen Hash, den `drizzle-orm/migrator` selbst + * berechnen würde — sha256(file_content) als hex. + * + * Nutzung: + * DATABASE_URL=postgresql://… pnpm bootstrap:drizzle + * + * Idempotent: ein zweiter Run macht nichts neu, sondern überspringt + * Migrations, deren Hash schon eingetragen ist. + * + * Nach dem Bootstrap wird `drizzle-kit migrate` jede Migration aus + * dem Journal als bekannt erkennen und keine SQL erneut ausführen. + * Künftige Schema-Änderungen ⇒ `drizzle-kit generate` ⇒ commit ⇒ + * `drizzle-kit migrate` führt nur die neuen Migrations aus. + */ + +import { createHash } from 'node:crypto'; +import { readFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import postgres from 'postgres'; + +const url = process.env.DATABASE_URL; +if (!url) { + console.error('DATABASE_URL not set'); + process.exit(1); +} + +const here = dirname(fileURLToPath(import.meta.url)); +const migrationsDir = resolve(here, '..', 'src', 'db', 'migrations'); +const journalPath = resolve(migrationsDir, 'meta', '_journal.json'); + +interface JournalEntry { + idx: number; + version: string; + when: number; + tag: string; + breakpoints: boolean; +} +interface Journal { + version: string; + dialect: string; + entries: JournalEntry[]; +} + +const journal: Journal = JSON.parse(readFileSync(journalPath, 'utf-8')); + +const sql = postgres(url, { max: 1 }); + +try { + await sql`CREATE SCHEMA IF NOT EXISTS drizzle`; + await sql`CREATE TABLE IF NOT EXISTS drizzle.__drizzle_migrations ( + id SERIAL PRIMARY KEY, + hash text NOT NULL, + created_at bigint + )`; + + const existing = await sql<{ hash: string }[]>`SELECT hash FROM drizzle.__drizzle_migrations`; + const existingHashes = new Set(existing.map((r) => r.hash)); + + for (const entry of journal.entries) { + const migrationPath = resolve(migrationsDir, `${entry.tag}.sql`); + const fileContent = readFileSync(migrationPath, 'utf-8'); + const hash = createHash('sha256').update(fileContent).digest('hex'); + + if (existingHashes.has(hash)) { + console.log(`SKIP ${entry.tag} (hash ${hash.slice(0, 12)}… already tracked)`); + continue; + } + + await sql`INSERT INTO drizzle.__drizzle_migrations ("hash", "created_at") + VALUES (${hash}, ${entry.when})`; + console.log(`MARKED ${entry.tag} (hash ${hash.slice(0, 12)}…)`); + } + + const final = await sql<{ count: number }[]>`SELECT COUNT(*)::int AS count + FROM drizzle.__drizzle_migrations`; + console.log(`\nTracking-Tabelle hat jetzt ${final[0]!.count} Eintrag/Einträge.`); +} finally { + await sql.end({ timeout: 5 }); +} diff --git a/apps/api/src/db/migrations/0000_baseline.sql b/apps/api/src/db/migrations/0000_baseline.sql new file mode 100644 index 0000000..ef6550b --- /dev/null +++ b/apps/api/src/db/migrations/0000_baseline.sql @@ -0,0 +1,356 @@ +CREATE SCHEMA "cards"; +--> statement-breakpoint +CREATE SCHEMA "marketplace"; +--> statement-breakpoint +CREATE TYPE "marketplace"."ai_mod_verdict" AS ENUM('pass', 'flag', 'block');--> statement-breakpoint +CREATE TYPE "marketplace"."card_type" AS ENUM('basic', 'basic-reverse', 'cloze', 'type-in', 'image-occlusion', 'audio', 'multiple-choice');--> statement-breakpoint +CREATE TYPE "marketplace"."pr_status" AS ENUM('open', 'merged', 'closed', 'rejected');--> statement-breakpoint +CREATE TYPE "marketplace"."report_category" AS ENUM('spam', 'copyright', 'nsfw', 'misinformation', 'hate', 'other');--> statement-breakpoint +CREATE TYPE "marketplace"."report_status" AS ENUM('open', 'dismissed', 'actioned');--> statement-breakpoint +CREATE TABLE "cards"."card_tags" ( + "card_id" text NOT NULL, + "tag_id" text NOT NULL, + CONSTRAINT "card_tags_card_id_tag_id_pk" PRIMARY KEY("card_id","tag_id") +); +--> statement-breakpoint +CREATE TABLE "cards"."cards" ( + "id" text PRIMARY KEY NOT NULL, + "deck_id" text NOT NULL, + "user_id" text NOT NULL, + "type" text NOT NULL, + "fields" jsonb NOT NULL, + "media_refs" jsonb DEFAULT '[]'::jsonb NOT NULL, + "content_hash" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "cards"."decks" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "name" text NOT NULL, + "description" text, + "color" text, + "category" text, + "visibility" text DEFAULT 'private' NOT NULL, + "fsrs_settings" jsonb DEFAULT '{}'::jsonb NOT NULL, + "content_hash" text, + "forked_from_marketplace_deck_id" text, + "forked_from_marketplace_version_id" text, + "archived_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "cards"."import_jobs" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "source" text NOT NULL, + "state" text DEFAULT 'queued' NOT NULL, + "meta" jsonb, + "error" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "finished_at" timestamp with time zone +); +--> statement-breakpoint +CREATE TABLE "marketplace"."ai_moderation_log" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "version_id" uuid NOT NULL, + "verdict" "marketplace"."ai_mod_verdict" NOT NULL, + "categories" text[], + "model" text, + "rationale" text, + "human_reviewed" boolean DEFAULT false NOT NULL, + "human_overrode" boolean DEFAULT false NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "marketplace"."author_follows" ( + "follower_user_id" text NOT NULL, + "author_user_id" text NOT NULL, + "since" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "marketplace"."author_payouts" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "author_user_id" text NOT NULL, + "source_purchase_id" uuid NOT NULL, + "credits_granted" integer NOT NULL, + "credits_grant_id" text, + "granted_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "marketplace"."authors" ( + "user_id" text PRIMARY KEY NOT NULL, + "slug" text NOT NULL, + "display_name" text NOT NULL, + "bio" text, + "avatar_url" text, + "joined_at" timestamp with time zone DEFAULT now() NOT NULL, + "pseudonym" boolean DEFAULT false NOT NULL, + "verified_mana" boolean DEFAULT false NOT NULL, + "verified_community" boolean DEFAULT false NOT NULL, + "banned_at" timestamp with time zone, + "banned_reason" text +); +--> statement-breakpoint +CREATE TABLE "marketplace"."card_discussions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "card_content_hash" text NOT NULL, + "deck_id" uuid NOT NULL, + "author_user_id" text NOT NULL, + "parent_id" uuid, + "body" text NOT NULL, + "hidden" boolean DEFAULT false NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "marketplace"."deck_forks" ( + "user_id" text NOT NULL, + "source_deck_id" uuid NOT NULL, + "source_version_id" uuid NOT NULL, + "forked_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "marketplace"."deck_pull_requests" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "deck_id" uuid NOT NULL, + "author_user_id" text NOT NULL, + "status" "marketplace"."pr_status" DEFAULT 'open' NOT NULL, + "title" text NOT NULL, + "body" text, + "diff" jsonb NOT NULL, + "merged_into_version_id" uuid, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "resolved_at" timestamp with time zone +); +--> statement-breakpoint +CREATE TABLE "marketplace"."deck_purchases" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "buyer_user_id" text NOT NULL, + "deck_id" uuid NOT NULL, + "version_id" uuid NOT NULL, + "price_credits" integer NOT NULL, + "author_share" integer NOT NULL, + "mana_share" integer NOT NULL, + "credits_transaction" text, + "purchased_at" timestamp with time zone DEFAULT now() NOT NULL, + "refunded_at" timestamp with time zone +); +--> statement-breakpoint +CREATE TABLE "marketplace"."deck_reports" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "deck_id" uuid NOT NULL, + "version_id" uuid, + "card_content_hash" text, + "reporter_user_id" text NOT NULL, + "category" "marketplace"."report_category" NOT NULL, + "body" text, + "status" "marketplace"."report_status" DEFAULT 'open' NOT NULL, + "resolved_by" text, + "resolved_at" timestamp with time zone, + "resolution_notes" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "marketplace"."deck_stars" ( + "user_id" text NOT NULL, + "deck_id" uuid NOT NULL, + "starred_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "marketplace"."deck_subscriptions" ( + "user_id" text NOT NULL, + "deck_id" uuid NOT NULL, + "current_version_id" uuid, + "subscribed_at" timestamp with time zone DEFAULT now() NOT NULL, + "notify_updates" boolean DEFAULT true NOT NULL +); +--> statement-breakpoint +CREATE TABLE "marketplace"."deck_tags" ( + "deck_id" uuid NOT NULL, + "tag_id" uuid NOT NULL +); +--> statement-breakpoint +CREATE TABLE "cards"."media_files" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "object_key" text NOT NULL, + "mime_type" text NOT NULL, + "original_filename" text, + "size_bytes" integer NOT NULL, + "kind" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "cards"."media_refs" ( + "id" text PRIMARY KEY NOT NULL, + "card_id" text NOT NULL, + "user_id" text NOT NULL, + "mana_media_object_id" text NOT NULL, + "kind" text NOT NULL, + "ord" integer DEFAULT 0 NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "marketplace"."deck_cards" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "version_id" uuid NOT NULL, + "type" "marketplace"."card_type" NOT NULL, + "fields" jsonb NOT NULL, + "ord" integer NOT NULL, + "content_hash" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE "marketplace"."deck_versions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "deck_id" uuid NOT NULL, + "semver" text NOT NULL, + "changelog" text, + "content_hash" text NOT NULL, + "card_count" integer NOT NULL, + "published_at" timestamp with time zone DEFAULT now() NOT NULL, + "deprecated_at" timestamp with time zone +); +--> statement-breakpoint +CREATE TABLE "marketplace"."decks" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "slug" text NOT NULL, + "title" text NOT NULL, + "description" text, + "language" text, + "category" text, + "license" text DEFAULT 'Cardecky-Personal-Use-1.0' NOT NULL, + "price_credits" integer DEFAULT 0 NOT NULL, + "owner_user_id" text NOT NULL, + "latest_version_id" uuid, + "is_featured" boolean DEFAULT false NOT NULL, + "is_takedown" boolean DEFAULT false NOT NULL, + "takedown_at" timestamp with time zone, + "takedown_reason" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "decks_price_requires_license" CHECK (price_credits = 0 OR license = 'Cardecky-Pro-Only-1.0') +); +--> statement-breakpoint +CREATE TABLE "cards"."reviews" ( + "card_id" text NOT NULL, + "sub_index" integer DEFAULT 0 NOT NULL, + "user_id" text NOT NULL, + "due" timestamp with time zone NOT NULL, + "stability" real NOT NULL, + "difficulty" real NOT NULL, + "elapsed_days" real DEFAULT 0 NOT NULL, + "scheduled_days" real DEFAULT 0 NOT NULL, + "learning_steps" integer DEFAULT 0 NOT NULL, + "reps" integer DEFAULT 0 NOT NULL, + "lapses" integer DEFAULT 0 NOT NULL, + "state" text DEFAULT 'new' NOT NULL, + "last_review" timestamp with time zone, + CONSTRAINT "reviews_card_id_sub_index_pk" PRIMARY KEY("card_id","sub_index") +); +--> statement-breakpoint +CREATE TABLE "cards"."study_sessions" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "deck_id" text NOT NULL, + "started_at" timestamp with time zone DEFAULT now() NOT NULL, + "finished_at" timestamp with time zone, + "cards_reviewed" integer DEFAULT 0 NOT NULL, + "cards_correct" integer DEFAULT 0 NOT NULL +); +--> statement-breakpoint +CREATE TABLE "marketplace"."tag_definitions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "slug" text NOT NULL, + "name" text NOT NULL, + "parent_id" uuid, + "description" text, + "curated" boolean DEFAULT false NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "cards"."tags" ( + "id" text PRIMARY KEY NOT NULL, + "deck_id" text NOT NULL, + "user_id" text NOT NULL, + "name" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "cards"."card_tags" ADD CONSTRAINT "card_tags_card_id_cards_id_fk" FOREIGN KEY ("card_id") REFERENCES "cards"."cards"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "cards"."cards" ADD CONSTRAINT "cards_deck_id_decks_id_fk" FOREIGN KEY ("deck_id") REFERENCES "cards"."decks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "marketplace"."ai_moderation_log" ADD CONSTRAINT "ai_moderation_log_version_id_deck_versions_id_fk" FOREIGN KEY ("version_id") REFERENCES "marketplace"."deck_versions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "marketplace"."author_follows" ADD CONSTRAINT "author_follows_author_user_id_authors_user_id_fk" FOREIGN KEY ("author_user_id") REFERENCES "marketplace"."authors"("user_id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "marketplace"."author_payouts" ADD CONSTRAINT "author_payouts_author_user_id_authors_user_id_fk" FOREIGN KEY ("author_user_id") REFERENCES "marketplace"."authors"("user_id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "marketplace"."author_payouts" ADD CONSTRAINT "author_payouts_source_purchase_id_deck_purchases_id_fk" FOREIGN KEY ("source_purchase_id") REFERENCES "marketplace"."deck_purchases"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "marketplace"."card_discussions" ADD CONSTRAINT "card_discussions_deck_id_decks_id_fk" FOREIGN KEY ("deck_id") REFERENCES "marketplace"."decks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "marketplace"."deck_forks" ADD CONSTRAINT "deck_forks_source_deck_id_decks_id_fk" FOREIGN KEY ("source_deck_id") REFERENCES "marketplace"."decks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "marketplace"."deck_forks" ADD CONSTRAINT "deck_forks_source_version_id_deck_versions_id_fk" FOREIGN KEY ("source_version_id") REFERENCES "marketplace"."deck_versions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "marketplace"."deck_pull_requests" ADD CONSTRAINT "deck_pull_requests_deck_id_decks_id_fk" FOREIGN KEY ("deck_id") REFERENCES "marketplace"."decks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "marketplace"."deck_pull_requests" ADD CONSTRAINT "deck_pull_requests_merged_into_version_id_deck_versions_id_fk" FOREIGN KEY ("merged_into_version_id") REFERENCES "marketplace"."deck_versions"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "marketplace"."deck_purchases" ADD CONSTRAINT "deck_purchases_deck_id_decks_id_fk" FOREIGN KEY ("deck_id") REFERENCES "marketplace"."decks"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "marketplace"."deck_purchases" ADD CONSTRAINT "deck_purchases_version_id_deck_versions_id_fk" FOREIGN KEY ("version_id") REFERENCES "marketplace"."deck_versions"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "marketplace"."deck_reports" ADD CONSTRAINT "deck_reports_deck_id_decks_id_fk" FOREIGN KEY ("deck_id") REFERENCES "marketplace"."decks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "marketplace"."deck_reports" ADD CONSTRAINT "deck_reports_version_id_deck_versions_id_fk" FOREIGN KEY ("version_id") REFERENCES "marketplace"."deck_versions"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "marketplace"."deck_stars" ADD CONSTRAINT "deck_stars_deck_id_decks_id_fk" FOREIGN KEY ("deck_id") REFERENCES "marketplace"."decks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "marketplace"."deck_subscriptions" ADD CONSTRAINT "deck_subscriptions_deck_id_decks_id_fk" FOREIGN KEY ("deck_id") REFERENCES "marketplace"."decks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "marketplace"."deck_subscriptions" ADD CONSTRAINT "deck_subscriptions_current_version_id_deck_versions_id_fk" FOREIGN KEY ("current_version_id") REFERENCES "marketplace"."deck_versions"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "marketplace"."deck_tags" ADD CONSTRAINT "deck_tags_deck_id_decks_id_fk" FOREIGN KEY ("deck_id") REFERENCES "marketplace"."decks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "marketplace"."deck_tags" ADD CONSTRAINT "deck_tags_tag_id_tag_definitions_id_fk" FOREIGN KEY ("tag_id") REFERENCES "marketplace"."tag_definitions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "cards"."media_refs" ADD CONSTRAINT "media_refs_card_id_cards_id_fk" FOREIGN KEY ("card_id") REFERENCES "cards"."cards"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "marketplace"."deck_cards" ADD CONSTRAINT "deck_cards_version_id_deck_versions_id_fk" FOREIGN KEY ("version_id") REFERENCES "marketplace"."deck_versions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "marketplace"."deck_versions" ADD CONSTRAINT "deck_versions_deck_id_decks_id_fk" FOREIGN KEY ("deck_id") REFERENCES "marketplace"."decks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "marketplace"."decks" ADD CONSTRAINT "decks_owner_user_id_authors_user_id_fk" FOREIGN KEY ("owner_user_id") REFERENCES "marketplace"."authors"("user_id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "cards"."reviews" ADD CONSTRAINT "reviews_card_id_cards_id_fk" FOREIGN KEY ("card_id") REFERENCES "cards"."cards"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "cards"."study_sessions" ADD CONSTRAINT "study_sessions_deck_id_decks_id_fk" FOREIGN KEY ("deck_id") REFERENCES "cards"."decks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "cards"."tags" ADD CONSTRAINT "tags_deck_id_decks_id_fk" FOREIGN KEY ("deck_id") REFERENCES "cards"."decks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "cards_deck_idx" ON "cards"."cards" USING btree ("deck_id");--> statement-breakpoint +CREATE INDEX "cards_user_idx" ON "cards"."cards" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "decks_user_idx" ON "cards"."decks" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "imports_user_idx" ON "cards"."import_jobs" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "imports_state_idx" ON "cards"."import_jobs" USING btree ("state");--> statement-breakpoint +CREATE INDEX "ai_moderation_log_version_idx" ON "marketplace"."ai_moderation_log" USING btree ("version_id");--> statement-breakpoint +CREATE INDEX "ai_moderation_log_verdict_idx" ON "marketplace"."ai_moderation_log" USING btree ("verdict");--> statement-breakpoint +CREATE UNIQUE INDEX "author_follows_pk" ON "marketplace"."author_follows" USING btree ("follower_user_id","author_user_id");--> statement-breakpoint +CREATE INDEX "author_follows_author_idx" ON "marketplace"."author_follows" USING btree ("author_user_id");--> statement-breakpoint +CREATE INDEX "author_follows_follower_idx" ON "marketplace"."author_follows" USING btree ("follower_user_id");--> statement-breakpoint +CREATE INDEX "author_payouts_author_idx" ON "marketplace"."author_payouts" USING btree ("author_user_id");--> statement-breakpoint +CREATE INDEX "author_payouts_purchase_idx" ON "marketplace"."author_payouts" USING btree ("source_purchase_id");--> statement-breakpoint +CREATE UNIQUE INDEX "authors_slug_idx" ON "marketplace"."authors" USING btree ("slug");--> statement-breakpoint +CREATE INDEX "authors_verified_idx" ON "marketplace"."authors" USING btree ("verified_mana","verified_community");--> statement-breakpoint +CREATE INDEX "card_discussions_hash_idx" ON "marketplace"."card_discussions" USING btree ("card_content_hash");--> statement-breakpoint +CREATE INDEX "card_discussions_deck_idx" ON "marketplace"."card_discussions" USING btree ("deck_id");--> statement-breakpoint +CREATE INDEX "card_discussions_parent_idx" ON "marketplace"."card_discussions" USING btree ("parent_id");--> statement-breakpoint +CREATE UNIQUE INDEX "deck_forks_pk" ON "marketplace"."deck_forks" USING btree ("user_id","source_deck_id","source_version_id");--> statement-breakpoint +CREATE INDEX "deck_forks_source_idx" ON "marketplace"."deck_forks" USING btree ("source_deck_id");--> statement-breakpoint +CREATE INDEX "deck_pull_requests_deck_idx" ON "marketplace"."deck_pull_requests" USING btree ("deck_id");--> statement-breakpoint +CREATE INDEX "deck_pull_requests_status_idx" ON "marketplace"."deck_pull_requests" USING btree ("deck_id","status");--> statement-breakpoint +CREATE INDEX "deck_pull_requests_author_idx" ON "marketplace"."deck_pull_requests" USING btree ("author_user_id");--> statement-breakpoint +CREATE UNIQUE INDEX "deck_purchases_buyer_deck_idx" ON "marketplace"."deck_purchases" USING btree ("buyer_user_id","deck_id");--> statement-breakpoint +CREATE INDEX "deck_purchases_buyer_idx" ON "marketplace"."deck_purchases" USING btree ("buyer_user_id");--> statement-breakpoint +CREATE INDEX "deck_purchases_deck_idx" ON "marketplace"."deck_purchases" USING btree ("deck_id");--> statement-breakpoint +CREATE INDEX "deck_reports_deck_idx" ON "marketplace"."deck_reports" USING btree ("deck_id");--> statement-breakpoint +CREATE INDEX "deck_reports_status_idx" ON "marketplace"."deck_reports" USING btree ("status");--> statement-breakpoint +CREATE UNIQUE INDEX "deck_stars_pk" ON "marketplace"."deck_stars" USING btree ("user_id","deck_id");--> statement-breakpoint +CREATE INDEX "deck_stars_deck_idx" ON "marketplace"."deck_stars" USING btree ("deck_id");--> statement-breakpoint +CREATE UNIQUE INDEX "deck_subscriptions_pk" ON "marketplace"."deck_subscriptions" USING btree ("user_id","deck_id");--> statement-breakpoint +CREATE INDEX "deck_subscriptions_deck_idx" ON "marketplace"."deck_subscriptions" USING btree ("deck_id");--> statement-breakpoint +CREATE INDEX "deck_subscriptions_user_idx" ON "marketplace"."deck_subscriptions" USING btree ("user_id");--> statement-breakpoint +CREATE UNIQUE INDEX "deck_tags_pk" ON "marketplace"."deck_tags" USING btree ("deck_id","tag_id");--> statement-breakpoint +CREATE INDEX "deck_tags_tag_idx" ON "marketplace"."deck_tags" USING btree ("tag_id");--> statement-breakpoint +CREATE INDEX "media_files_user_idx" ON "cards"."media_files" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "media_card_idx" ON "cards"."media_refs" USING btree ("card_id");--> statement-breakpoint +CREATE UNIQUE INDEX "deck_cards_version_ord_idx" ON "marketplace"."deck_cards" USING btree ("version_id","ord");--> statement-breakpoint +CREATE INDEX "deck_cards_hash_idx" ON "marketplace"."deck_cards" USING btree ("content_hash");--> statement-breakpoint +CREATE UNIQUE INDEX "deck_versions_deck_semver_idx" ON "marketplace"."deck_versions" USING btree ("deck_id","semver");--> statement-breakpoint +CREATE INDEX "deck_versions_deck_idx" ON "marketplace"."deck_versions" USING btree ("deck_id");--> statement-breakpoint +CREATE INDEX "deck_versions_hash_idx" ON "marketplace"."deck_versions" USING btree ("content_hash");--> statement-breakpoint +CREATE UNIQUE INDEX "decks_slug_idx" ON "marketplace"."decks" USING btree ("slug");--> statement-breakpoint +CREATE INDEX "decks_owner_idx" ON "marketplace"."decks" USING btree ("owner_user_id");--> statement-breakpoint +CREATE INDEX "decks_featured_idx" ON "marketplace"."decks" USING btree ("is_featured");--> statement-breakpoint +CREATE INDEX "reviews_user_due_idx" ON "cards"."reviews" USING btree ("user_id","due");--> statement-breakpoint +CREATE INDEX "sessions_user_started_idx" ON "cards"."study_sessions" USING btree ("user_id","started_at");--> statement-breakpoint +CREATE UNIQUE INDEX "tag_definitions_slug_idx" ON "marketplace"."tag_definitions" USING btree ("slug");--> statement-breakpoint +CREATE INDEX "tag_definitions_parent_idx" ON "marketplace"."tag_definitions" USING btree ("parent_id");--> statement-breakpoint +CREATE INDEX "tags_deck_idx" ON "cards"."tags" USING btree ("deck_id");--> statement-breakpoint +CREATE UNIQUE INDEX "tags_deck_name_uniq" ON "cards"."tags" USING btree ("deck_id","name"); \ No newline at end of file diff --git a/apps/api/src/db/migrations/meta/0000_snapshot.json b/apps/api/src/db/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..81bd2be --- /dev/null +++ b/apps/api/src/db/migrations/meta/0000_snapshot.json @@ -0,0 +1,2896 @@ +{ + "id": "132d3ee9-0062-4600-a7e4-e6fad6fcdb75", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "cards.card_tags": { + "name": "card_tags", + "schema": "cards", + "columns": { + "card_id": { + "name": "card_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "card_tags_card_id_cards_id_fk": { + "name": "card_tags_card_id_cards_id_fk", + "tableFrom": "card_tags", + "tableTo": "cards", + "schemaTo": "cards", + "columnsFrom": [ + "card_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "card_tags_card_id_tag_id_pk": { + "name": "card_tags_card_id_tag_id_pk", + "columns": [ + "card_id", + "tag_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cards.cards": { + "name": "cards", + "schema": "cards", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deck_id": { + "name": "deck_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "media_refs": { + "name": "media_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cards_deck_idx": { + "name": "cards_deck_idx", + "columns": [ + { + "expression": "deck_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cards_user_idx": { + "name": "cards_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cards_deck_id_decks_id_fk": { + "name": "cards_deck_id_decks_id_fk", + "tableFrom": "cards", + "tableTo": "decks", + "schemaTo": "cards", + "columnsFrom": [ + "deck_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cards.decks": { + "name": "decks", + "schema": "cards", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "fsrs_settings": { + "name": "fsrs_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "forked_from_marketplace_deck_id": { + "name": "forked_from_marketplace_deck_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "forked_from_marketplace_version_id": { + "name": "forked_from_marketplace_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "decks_user_idx": { + "name": "decks_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cards.import_jobs": { + "name": "import_jobs", + "schema": "cards", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "imports_user_idx": { + "name": "imports_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "imports_state_idx": { + "name": "imports_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "marketplace.ai_moderation_log": { + "name": "ai_moderation_log", + "schema": "marketplace", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "version_id": { + "name": "version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "ai_mod_verdict", + "typeSchema": "marketplace", + "primaryKey": false, + "notNull": true + }, + "categories": { + "name": "categories", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rationale": { + "name": "rationale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "human_reviewed": { + "name": "human_reviewed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "human_overrode": { + "name": "human_overrode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ai_moderation_log_version_idx": { + "name": "ai_moderation_log_version_idx", + "columns": [ + { + "expression": "version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_moderation_log_verdict_idx": { + "name": "ai_moderation_log_verdict_idx", + "columns": [ + { + "expression": "verdict", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_moderation_log_version_id_deck_versions_id_fk": { + "name": "ai_moderation_log_version_id_deck_versions_id_fk", + "tableFrom": "ai_moderation_log", + "tableTo": "deck_versions", + "schemaTo": "marketplace", + "columnsFrom": [ + "version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "marketplace.author_follows": { + "name": "author_follows", + "schema": "marketplace", + "columns": { + "follower_user_id": { + "name": "follower_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "since": { + "name": "since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "author_follows_pk": { + "name": "author_follows_pk", + "columns": [ + { + "expression": "follower_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "author_follows_author_idx": { + "name": "author_follows_author_idx", + "columns": [ + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "author_follows_follower_idx": { + "name": "author_follows_follower_idx", + "columns": [ + { + "expression": "follower_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "author_follows_author_user_id_authors_user_id_fk": { + "name": "author_follows_author_user_id_authors_user_id_fk", + "tableFrom": "author_follows", + "tableTo": "authors", + "schemaTo": "marketplace", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "marketplace.author_payouts": { + "name": "author_payouts", + "schema": "marketplace", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_purchase_id": { + "name": "source_purchase_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credits_granted": { + "name": "credits_granted", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credits_grant_id": { + "name": "credits_grant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "author_payouts_author_idx": { + "name": "author_payouts_author_idx", + "columns": [ + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "author_payouts_purchase_idx": { + "name": "author_payouts_purchase_idx", + "columns": [ + { + "expression": "source_purchase_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "author_payouts_author_user_id_authors_user_id_fk": { + "name": "author_payouts_author_user_id_authors_user_id_fk", + "tableFrom": "author_payouts", + "tableTo": "authors", + "schemaTo": "marketplace", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "author_payouts_source_purchase_id_deck_purchases_id_fk": { + "name": "author_payouts_source_purchase_id_deck_purchases_id_fk", + "tableFrom": "author_payouts", + "tableTo": "deck_purchases", + "schemaTo": "marketplace", + "columnsFrom": [ + "source_purchase_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "marketplace.authors": { + "name": "authors", + "schema": "marketplace", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "pseudonym": { + "name": "pseudonym", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "verified_mana": { + "name": "verified_mana", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "verified_community": { + "name": "verified_community", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "banned_at": { + "name": "banned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "banned_reason": { + "name": "banned_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "authors_slug_idx": { + "name": "authors_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "authors_verified_idx": { + "name": "authors_verified_idx", + "columns": [ + { + "expression": "verified_mana", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "verified_community", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "marketplace.card_discussions": { + "name": "card_discussions", + "schema": "marketplace", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "card_content_hash": { + "name": "card_content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deck_id": { + "name": "deck_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "card_discussions_hash_idx": { + "name": "card_discussions_hash_idx", + "columns": [ + { + "expression": "card_content_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "card_discussions_deck_idx": { + "name": "card_discussions_deck_idx", + "columns": [ + { + "expression": "deck_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "card_discussions_parent_idx": { + "name": "card_discussions_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "card_discussions_deck_id_decks_id_fk": { + "name": "card_discussions_deck_id_decks_id_fk", + "tableFrom": "card_discussions", + "tableTo": "decks", + "schemaTo": "marketplace", + "columnsFrom": [ + "deck_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "marketplace.deck_forks": { + "name": "deck_forks", + "schema": "marketplace", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_deck_id": { + "name": "source_deck_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_version_id": { + "name": "source_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "forked_at": { + "name": "forked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "deck_forks_pk": { + "name": "deck_forks_pk", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_deck_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deck_forks_source_idx": { + "name": "deck_forks_source_idx", + "columns": [ + { + "expression": "source_deck_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deck_forks_source_deck_id_decks_id_fk": { + "name": "deck_forks_source_deck_id_decks_id_fk", + "tableFrom": "deck_forks", + "tableTo": "decks", + "schemaTo": "marketplace", + "columnsFrom": [ + "source_deck_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deck_forks_source_version_id_deck_versions_id_fk": { + "name": "deck_forks_source_version_id_deck_versions_id_fk", + "tableFrom": "deck_forks", + "tableTo": "deck_versions", + "schemaTo": "marketplace", + "columnsFrom": [ + "source_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "marketplace.deck_pull_requests": { + "name": "deck_pull_requests", + "schema": "marketplace", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deck_id": { + "name": "deck_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "pr_status", + "typeSchema": "marketplace", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "diff": { + "name": "diff", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "merged_into_version_id": { + "name": "merged_into_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "deck_pull_requests_deck_idx": { + "name": "deck_pull_requests_deck_idx", + "columns": [ + { + "expression": "deck_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deck_pull_requests_status_idx": { + "name": "deck_pull_requests_status_idx", + "columns": [ + { + "expression": "deck_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deck_pull_requests_author_idx": { + "name": "deck_pull_requests_author_idx", + "columns": [ + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deck_pull_requests_deck_id_decks_id_fk": { + "name": "deck_pull_requests_deck_id_decks_id_fk", + "tableFrom": "deck_pull_requests", + "tableTo": "decks", + "schemaTo": "marketplace", + "columnsFrom": [ + "deck_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deck_pull_requests_merged_into_version_id_deck_versions_id_fk": { + "name": "deck_pull_requests_merged_into_version_id_deck_versions_id_fk", + "tableFrom": "deck_pull_requests", + "tableTo": "deck_versions", + "schemaTo": "marketplace", + "columnsFrom": [ + "merged_into_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "marketplace.deck_purchases": { + "name": "deck_purchases", + "schema": "marketplace", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "buyer_user_id": { + "name": "buyer_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deck_id": { + "name": "deck_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "price_credits": { + "name": "price_credits", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "author_share": { + "name": "author_share", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mana_share": { + "name": "mana_share", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credits_transaction": { + "name": "credits_transaction", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purchased_at": { + "name": "purchased_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "refunded_at": { + "name": "refunded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "deck_purchases_buyer_deck_idx": { + "name": "deck_purchases_buyer_deck_idx", + "columns": [ + { + "expression": "buyer_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deck_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deck_purchases_buyer_idx": { + "name": "deck_purchases_buyer_idx", + "columns": [ + { + "expression": "buyer_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deck_purchases_deck_idx": { + "name": "deck_purchases_deck_idx", + "columns": [ + { + "expression": "deck_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deck_purchases_deck_id_decks_id_fk": { + "name": "deck_purchases_deck_id_decks_id_fk", + "tableFrom": "deck_purchases", + "tableTo": "decks", + "schemaTo": "marketplace", + "columnsFrom": [ + "deck_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "deck_purchases_version_id_deck_versions_id_fk": { + "name": "deck_purchases_version_id_deck_versions_id_fk", + "tableFrom": "deck_purchases", + "tableTo": "deck_versions", + "schemaTo": "marketplace", + "columnsFrom": [ + "version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "marketplace.deck_reports": { + "name": "deck_reports", + "schema": "marketplace", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deck_id": { + "name": "deck_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "card_content_hash": { + "name": "card_content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reporter_user_id": { + "name": "reporter_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "report_category", + "typeSchema": "marketplace", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "report_status", + "typeSchema": "marketplace", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolution_notes": { + "name": "resolution_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "deck_reports_deck_idx": { + "name": "deck_reports_deck_idx", + "columns": [ + { + "expression": "deck_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deck_reports_status_idx": { + "name": "deck_reports_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deck_reports_deck_id_decks_id_fk": { + "name": "deck_reports_deck_id_decks_id_fk", + "tableFrom": "deck_reports", + "tableTo": "decks", + "schemaTo": "marketplace", + "columnsFrom": [ + "deck_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deck_reports_version_id_deck_versions_id_fk": { + "name": "deck_reports_version_id_deck_versions_id_fk", + "tableFrom": "deck_reports", + "tableTo": "deck_versions", + "schemaTo": "marketplace", + "columnsFrom": [ + "version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "marketplace.deck_stars": { + "name": "deck_stars", + "schema": "marketplace", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deck_id": { + "name": "deck_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "deck_stars_pk": { + "name": "deck_stars_pk", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deck_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deck_stars_deck_idx": { + "name": "deck_stars_deck_idx", + "columns": [ + { + "expression": "deck_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deck_stars_deck_id_decks_id_fk": { + "name": "deck_stars_deck_id_decks_id_fk", + "tableFrom": "deck_stars", + "tableTo": "decks", + "schemaTo": "marketplace", + "columnsFrom": [ + "deck_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "marketplace.deck_subscriptions": { + "name": "deck_subscriptions", + "schema": "marketplace", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deck_id": { + "name": "deck_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "current_version_id": { + "name": "current_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subscribed_at": { + "name": "subscribed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "notify_updates": { + "name": "notify_updates", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "deck_subscriptions_pk": { + "name": "deck_subscriptions_pk", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deck_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deck_subscriptions_deck_idx": { + "name": "deck_subscriptions_deck_idx", + "columns": [ + { + "expression": "deck_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deck_subscriptions_user_idx": { + "name": "deck_subscriptions_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deck_subscriptions_deck_id_decks_id_fk": { + "name": "deck_subscriptions_deck_id_decks_id_fk", + "tableFrom": "deck_subscriptions", + "tableTo": "decks", + "schemaTo": "marketplace", + "columnsFrom": [ + "deck_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deck_subscriptions_current_version_id_deck_versions_id_fk": { + "name": "deck_subscriptions_current_version_id_deck_versions_id_fk", + "tableFrom": "deck_subscriptions", + "tableTo": "deck_versions", + "schemaTo": "marketplace", + "columnsFrom": [ + "current_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "marketplace.deck_tags": { + "name": "deck_tags", + "schema": "marketplace", + "columns": { + "deck_id": { + "name": "deck_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "deck_tags_pk": { + "name": "deck_tags_pk", + "columns": [ + { + "expression": "deck_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deck_tags_tag_idx": { + "name": "deck_tags_tag_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deck_tags_deck_id_decks_id_fk": { + "name": "deck_tags_deck_id_decks_id_fk", + "tableFrom": "deck_tags", + "tableTo": "decks", + "schemaTo": "marketplace", + "columnsFrom": [ + "deck_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deck_tags_tag_id_tag_definitions_id_fk": { + "name": "deck_tags_tag_id_tag_definitions_id_fk", + "tableFrom": "deck_tags", + "tableTo": "tag_definitions", + "schemaTo": "marketplace", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cards.media_files": { + "name": "media_files", + "schema": "cards", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_filename": { + "name": "original_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "media_files_user_idx": { + "name": "media_files_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cards.media_refs": { + "name": "media_refs", + "schema": "cards", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "card_id": { + "name": "card_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mana_media_object_id": { + "name": "mana_media_object_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ord": { + "name": "ord", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "media_card_idx": { + "name": "media_card_idx", + "columns": [ + { + "expression": "card_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "media_refs_card_id_cards_id_fk": { + "name": "media_refs_card_id_cards_id_fk", + "tableFrom": "media_refs", + "tableTo": "cards", + "schemaTo": "cards", + "columnsFrom": [ + "card_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "marketplace.deck_cards": { + "name": "deck_cards", + "schema": "marketplace", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "version_id": { + "name": "version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "card_type", + "typeSchema": "marketplace", + "primaryKey": false, + "notNull": true + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "ord": { + "name": "ord", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "deck_cards_version_ord_idx": { + "name": "deck_cards_version_ord_idx", + "columns": [ + { + "expression": "version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ord", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deck_cards_hash_idx": { + "name": "deck_cards_hash_idx", + "columns": [ + { + "expression": "content_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deck_cards_version_id_deck_versions_id_fk": { + "name": "deck_cards_version_id_deck_versions_id_fk", + "tableFrom": "deck_cards", + "tableTo": "deck_versions", + "schemaTo": "marketplace", + "columnsFrom": [ + "version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "marketplace.deck_versions": { + "name": "deck_versions", + "schema": "marketplace", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deck_id": { + "name": "deck_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "semver": { + "name": "semver", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "changelog": { + "name": "changelog", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "card_count": { + "name": "card_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deprecated_at": { + "name": "deprecated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "deck_versions_deck_semver_idx": { + "name": "deck_versions_deck_semver_idx", + "columns": [ + { + "expression": "deck_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "semver", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deck_versions_deck_idx": { + "name": "deck_versions_deck_idx", + "columns": [ + { + "expression": "deck_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deck_versions_hash_idx": { + "name": "deck_versions_hash_idx", + "columns": [ + { + "expression": "content_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deck_versions_deck_id_decks_id_fk": { + "name": "deck_versions_deck_id_decks_id_fk", + "tableFrom": "deck_versions", + "tableTo": "decks", + "schemaTo": "marketplace", + "columnsFrom": [ + "deck_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "marketplace.decks": { + "name": "decks", + "schema": "marketplace", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "license": { + "name": "license", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Cardecky-Personal-Use-1.0'" + }, + "price_credits": { + "name": "price_credits", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "latest_version_id": { + "name": "latest_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_featured": { + "name": "is_featured", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_takedown": { + "name": "is_takedown", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "takedown_at": { + "name": "takedown_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "takedown_reason": { + "name": "takedown_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "decks_slug_idx": { + "name": "decks_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decks_owner_idx": { + "name": "decks_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decks_featured_idx": { + "name": "decks_featured_idx", + "columns": [ + { + "expression": "is_featured", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decks_owner_user_id_authors_user_id_fk": { + "name": "decks_owner_user_id_authors_user_id_fk", + "tableFrom": "decks", + "tableTo": "authors", + "schemaTo": "marketplace", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "decks_price_requires_license": { + "name": "decks_price_requires_license", + "value": "price_credits = 0 OR license = 'Cardecky-Pro-Only-1.0'" + } + }, + "isRLSEnabled": false + }, + "cards.reviews": { + "name": "reviews", + "schema": "cards", + "columns": { + "card_id": { + "name": "card_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sub_index": { + "name": "sub_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "due": { + "name": "due", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "stability": { + "name": "stability", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "difficulty": { + "name": "difficulty", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "elapsed_days": { + "name": "elapsed_days", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "scheduled_days": { + "name": "scheduled_days", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "learning_steps": { + "name": "learning_steps", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reps": { + "name": "reps", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lapses": { + "name": "lapses", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'new'" + }, + "last_review": { + "name": "last_review", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "reviews_user_due_idx": { + "name": "reviews_user_due_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "due", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reviews_card_id_cards_id_fk": { + "name": "reviews_card_id_cards_id_fk", + "tableFrom": "reviews", + "tableTo": "cards", + "schemaTo": "cards", + "columnsFrom": [ + "card_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "reviews_card_id_sub_index_pk": { + "name": "reviews_card_id_sub_index_pk", + "columns": [ + "card_id", + "sub_index" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cards.study_sessions": { + "name": "study_sessions", + "schema": "cards", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deck_id": { + "name": "deck_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cards_reviewed": { + "name": "cards_reviewed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cards_correct": { + "name": "cards_correct", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "sessions_user_started_idx": { + "name": "sessions_user_started_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "study_sessions_deck_id_decks_id_fk": { + "name": "study_sessions_deck_id_decks_id_fk", + "tableFrom": "study_sessions", + "tableTo": "decks", + "schemaTo": "cards", + "columnsFrom": [ + "deck_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "marketplace.tag_definitions": { + "name": "tag_definitions", + "schema": "marketplace", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "curated": { + "name": "curated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tag_definitions_slug_idx": { + "name": "tag_definitions_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tag_definitions_parent_idx": { + "name": "tag_definitions_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cards.tags": { + "name": "tags", + "schema": "cards", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deck_id": { + "name": "deck_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tags_deck_idx": { + "name": "tags_deck_idx", + "columns": [ + { + "expression": "deck_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tags_deck_name_uniq": { + "name": "tags_deck_name_uniq", + "columns": [ + { + "expression": "deck_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tags_deck_id_decks_id_fk": { + "name": "tags_deck_id_decks_id_fk", + "tableFrom": "tags", + "tableTo": "decks", + "schemaTo": "cards", + "columnsFrom": [ + "deck_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "marketplace.ai_mod_verdict": { + "name": "ai_mod_verdict", + "schema": "marketplace", + "values": [ + "pass", + "flag", + "block" + ] + }, + "marketplace.card_type": { + "name": "card_type", + "schema": "marketplace", + "values": [ + "basic", + "basic-reverse", + "cloze", + "type-in", + "image-occlusion", + "audio", + "multiple-choice" + ] + }, + "marketplace.pr_status": { + "name": "pr_status", + "schema": "marketplace", + "values": [ + "open", + "merged", + "closed", + "rejected" + ] + }, + "marketplace.report_category": { + "name": "report_category", + "schema": "marketplace", + "values": [ + "spam", + "copyright", + "nsfw", + "misinformation", + "hate", + "other" + ] + }, + "marketplace.report_status": { + "name": "report_status", + "schema": "marketplace", + "values": [ + "open", + "dismissed", + "actioned" + ] + } + }, + "schemas": { + "cards": "cards", + "marketplace": "marketplace" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/src/db/migrations/meta/_journal.json b/apps/api/src/db/migrations/meta/_journal.json new file mode 100644 index 0000000..61ddf1c --- /dev/null +++ b/apps/api/src/db/migrations/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1778604624860, + "tag": "0000_baseline", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/docs/FEATURE_IDEAS.md b/docs/FEATURE_IDEAS.md index 518efe5..808a8c4 100644 --- a/docs/FEATURE_IDEAS.md +++ b/docs/FEATURE_IDEAS.md @@ -363,10 +363,16 @@ Bisher gar nicht in dieser Liste behandelt, aber Cards ist live: E2E-Smoke"; ein `playwright.config.ts` existiert im Repo nicht. Bei Live-App mit Marketplace-Forks die schmerzhafteste Regressions-Quelle. -- **Drizzle-Migrationen versionieren** — `drizzle.config.ts` zeigt - auf `out: './src/db/migrations'`, der Ordner existiert aber nicht - (nur `db:push`). Mit Prod-Daten ein Risiko; Wechsel auf - `drizzle-kit generate` + versionierte Migrationen. +- **Drizzle-Migrationen versionieren — Repo-Setup gebaut 2026-05-12, + Prod-Bootstrap offen.** `0000_baseline.sql` (355 Zeilen, 25 Tabellen + + 5 Enums) festgefrostet. `apps/api/scripts/bootstrap-drizzle-tracking.ts` + + `pnpm drizzle:bootstrap-tracking` markiert die Baseline auf der + Live-DB als „bereits angewandt", ohne SQL re-auszuführen. Lokal + verifiziert: idempotent + `drizzle:migrate` erkennt die Migration + danach als bekannt. Playbook in + `docs/playbooks/DRIZZLE_MIGRATIONS_BOOTSTRAP.md`. Nächster Schritt + beim User: einmalig auf der Prod-Box ausführen, danach ist `push + --force` Tabu. - **Storybook / Histoire für `lib/components/`** — 20+ Svelte- Komponenten ohne isolierte Vorschau; Lost-Pixel-Theming-Strategie würde davon doppelt profitieren. diff --git a/docs/playbooks/DRIZZLE_MIGRATIONS_BOOTSTRAP.md b/docs/playbooks/DRIZZLE_MIGRATIONS_BOOTSTRAP.md new file mode 100644 index 0000000..b987409 --- /dev/null +++ b/docs/playbooks/DRIZZLE_MIGRATIONS_BOOTSTRAP.md @@ -0,0 +1,155 @@ +# Drizzle-Migrationen — Bootstrap und Workflow + +Stand: 2026-05-12. + +Cards hat bis 2026-05-12 alle Schema-Änderungen über +`drizzle-kit push --force` gefahren — Schema-Sync ohne versionierte +Migrationen. Live ist das gefährlich: kein Audit-Trail, keine +sichere Rollback-Story, schwer reviewable. + +Ab Commit `` gibt es eine **versionierte Migration-Welt** mit +einer Baseline (`0000_baseline.sql`), die das bestehende Schema +festfriert. + +--- + +## Was lokal schon passiert ist + +1. `pnpm drizzle:generate` → `apps/api/src/db/migrations/0000_baseline.sql` + (25 Tabellen, 5 Enums, alle FKs/Indizes — das gesamte cards- und + marketplace-Schema, eingefroren auf den Stand 2026-05-12). +2. `apps/api/scripts/bootstrap-drizzle-tracking.ts` (neues npm-script + `pnpm drizzle:bootstrap-tracking`) — markiert in einer existierenden + Live-DB die Baseline als „bereits angewandt", ohne SQL erneut + auszuführen. +3. Lokal verifiziert: Bootstrap ist idempotent, `pnpm drizzle:migrate` + erkennt die Migration danach als bekannt. + +--- + +## Was du auf der Prod-Box machen musst (einmalig) + +```bash +ssh mana-server +cd ~/projects/cards +git pull --ff-only origin main +``` + +### 1. Bootstrap auf der Live-DB ausführen + +```bash +# DATABASE_URL aus der env-Datei lesen, nicht hart codieren. +DB_PW=$(grep CARDS_DB_PASSWORD infrastructure/.env.production | cut -d= -f2-) +docker exec -e DATABASE_URL="postgresql://cards:${DB_PW}@cards-postgres:5432/cards" \ + cards-api node /app/apps/api/scripts/bootstrap-drizzle-tracking.ts +``` + +…**oder** wenn das im Container-Image-Setup nicht klappt (Script ist +TypeScript, der API-Container kommt mit Bun): + +```bash +# Alternative: vom Host aus über Container-Network ausführen. +# Mac Mini hat Bun installiert. +cd ~/projects/cards/apps/api +DATABASE_URL="postgresql://cards:${DB_PW}@127.0.0.1:5436/cards" \ + bun run scripts/bootstrap-drizzle-tracking.ts +``` + +**Erwartete Ausgabe:** + +``` +MARKED 0000_baseline (hash 312d67ba1aeb…) + +Tracking-Tabelle hat jetzt 1 Eintrag/Einträge. +``` + +Wenn statt `MARKED` schon `SKIP` kommt: jemand hat das Script bereits +gelaufen lassen. Idempotent → kein Problem. + +### 2. Verifizieren + +```bash +docker exec cards-postgres psql -U cards -d cards -c \ + "SELECT * FROM drizzle.__drizzle_migrations;" +``` + +Erwartete Antwort: eine Zeile mit Hash `312d67ba1aeb…` und +`created_at = 1778604624860`. + +### 3. Smoke-Test + +```bash +cd ~/projects/cards/apps/api +DATABASE_URL="postgresql://cards:${DB_PW}@127.0.0.1:5436/cards" \ + bun x drizzle-kit migrate +``` + +Erwartet: `[✓] migrations applied successfully!` ohne dass das +Schema wirklich angefasst wird (es kommt nur ein NOTICE +„relation '__drizzle_migrations' already exists, skipping"). + +--- + +## Künftiger Workflow + +### Neue Schema-Änderung + +1. **Editiere das Schema** in `apps/api/src/db/schema/**.ts`. +2. **Generiere die Migration** lokal: + ```bash + cd apps/api + pnpm drizzle:generate --name + ``` +3. **Inspiziere** das generierte SQL in `src/db/migrations/0001_.sql` + (oder höher) — bei destruktiven Änderungen (DROP COLUMN, ALTER + TYPE) ggf. von Hand patchen, damit Daten nicht verloren gehen. +4. **Commit** Schema-Files + Migration-Files + Journal zusammen: + ```bash + git add apps/api/src/db/schema apps/api/src/db/migrations + git commit -m "db(cards): " + ``` +5. **Test lokal**: + ```bash + DATABASE_URL="postgresql://cards:cards@localhost:5435/cards" \ + pnpm drizzle:migrate + ``` + +### Deploy + +1. `git push` auf Forgejo. +2. Auf der Box: `git pull --ff-only`. +3. **Migration anwenden** (kein push --force mehr!): + ```bash + cd ~/projects/cards/apps/api + DATABASE_URL="postgresql://cards:${DB_PW}@127.0.0.1:5436/cards" \ + bun x drizzle-kit migrate + ``` +4. Erst danach Container rebuilden + restarten, wenn der neue Code + auf das neue Schema angewiesen ist. + +--- + +## Tabu + +- **`drizzle-kit push --force` auf Prod nie wieder.** Das Script + bleibt in `package.json` als Dev-Convenience, aber für Prod ist + nur `drizzle-kit migrate` der Weg. +- **Migration-Files nicht nachträglich umschreiben**, sobald sie + in einem Prod-Pull-Stand sind — der Hash würde sich ändern und + Drizzle würde sie erneut anwenden wollen. +- **Wenn du das Schema lokal mit `push` schon „weiter" als die + Migration hast**, ist das ein Drift-Bug. Lösung: lokal die DB + rückbauen (`drop schema cards cascade; drop schema marketplace cascade`), + dann Migrationen frisch anwenden. Niemals diesen Drift in einen + Commit hineinwachsen lassen. + +--- + +## Bei Problemen + +- `pnpm drizzle:migrate` schmeißt „relation already exists" und + bricht ab? Bootstrap-Script ist nie gelaufen — Schritt 1 oben. +- Hash-Mismatch (Migration angeblich „neu" trotz Bootstrap)? Das + Migration-File wurde nach dem Bootstrap noch ediert. Entweder + Edit rückgängig oder den Tracking-Eintrag von Hand auf den neuen + Hash bringen — ausschließlich nach Code-Review.