db(cards): baseline migration + drizzle-tracking bootstrap script
Some checks are pending
CI / validate (push) Waiting to run
Some checks are pending
CI / validate (push) Waiting to run
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) <noreply@anthropic.com>
This commit is contained in:
parent
5a29dd9a8c
commit
4bb1390180
7 changed files with 3523 additions and 5 deletions
|
|
@ -14,8 +14,10 @@
|
||||||
"lint": "echo 'lint configured later (eslint flat-config)'",
|
"lint": "echo 'lint configured later (eslint flat-config)'",
|
||||||
"clean": "rm -rf dist .turbo coverage",
|
"clean": "rm -rf dist .turbo coverage",
|
||||||
"drizzle:generate": "drizzle-kit generate",
|
"drizzle:generate": "drizzle-kit generate",
|
||||||
|
"drizzle:migrate": "drizzle-kit migrate",
|
||||||
"drizzle:push": "drizzle-kit push --force",
|
"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": {
|
"dependencies": {
|
||||||
"@cards/domain": "workspace:*",
|
"@cards/domain": "workspace:*",
|
||||||
|
|
|
||||||
90
apps/api/scripts/bootstrap-drizzle-tracking.ts
Normal file
90
apps/api/scripts/bootstrap-drizzle-tracking.ts
Normal file
|
|
@ -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 });
|
||||||
|
}
|
||||||
356
apps/api/src/db/migrations/0000_baseline.sql
Normal file
356
apps/api/src/db/migrations/0000_baseline.sql
Normal file
|
|
@ -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");
|
||||||
2896
apps/api/src/db/migrations/meta/0000_snapshot.json
Normal file
2896
apps/api/src/db/migrations/meta/0000_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
13
apps/api/src/db/migrations/meta/_journal.json
Normal file
13
apps/api/src/db/migrations/meta/_journal.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1778604624860,
|
||||||
|
"tag": "0000_baseline",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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.
|
E2E-Smoke"; ein `playwright.config.ts` existiert im Repo nicht.
|
||||||
Bei Live-App mit Marketplace-Forks die schmerzhafteste
|
Bei Live-App mit Marketplace-Forks die schmerzhafteste
|
||||||
Regressions-Quelle.
|
Regressions-Quelle.
|
||||||
- **Drizzle-Migrationen versionieren** — `drizzle.config.ts` zeigt
|
- **Drizzle-Migrationen versionieren — Repo-Setup gebaut 2026-05-12,
|
||||||
auf `out: './src/db/migrations'`, der Ordner existiert aber nicht
|
Prod-Bootstrap offen.** `0000_baseline.sql` (355 Zeilen, 25 Tabellen
|
||||||
(nur `db:push`). Mit Prod-Daten ein Risiko; Wechsel auf
|
+ 5 Enums) festgefrostet. `apps/api/scripts/bootstrap-drizzle-tracking.ts`
|
||||||
`drizzle-kit generate` + versionierte Migrationen.
|
+ `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-
|
- **Storybook / Histoire für `lib/components/`** — 20+ Svelte-
|
||||||
Komponenten ohne isolierte Vorschau; Lost-Pixel-Theming-Strategie
|
Komponenten ohne isolierte Vorschau; Lost-Pixel-Theming-Strategie
|
||||||
würde davon doppelt profitieren.
|
würde davon doppelt profitieren.
|
||||||
|
|
|
||||||
155
docs/playbooks/DRIZZLE_MIGRATIONS_BOOTSTRAP.md
Normal file
155
docs/playbooks/DRIZZLE_MIGRATIONS_BOOTSTRAP.md
Normal file
|
|
@ -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 `<dieser>` 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 <kurz_beschreibend>
|
||||||
|
```
|
||||||
|
3. **Inspiziere** das generierte SQL in `src/db/migrations/0001_<name>.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): <was>"
|
||||||
|
```
|
||||||
|
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.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue