feat(db): Prisma-Datenmodell fuer Projekte, Notizen und Git-Verbindungen
Vier Tabellen in PostgreSQL: - Project: Stammdaten, Status, Prioritaet, Fortschritt, Tags, Akzentfarbe und die optionale Repo-Verknuepfung - Note: beliebig viele Notizen je Projekt, anpinnbar, Cascade-Delete - GitConnection: ein Eintrag je Git-System inkl. verschluesseltem Token - RepoSnapshot: zwischengespeicherter Repo-Stand, damit das Dashboard auch ohne Netz oder bei erreichtem Rate-Limit Daten zeigt ProjectStatus deckt den Lebenszyklus von der Idee bis zum Archiv ab. Branch-, Tag- und Commit-Listen liegen als JSONB, damit providerspezifische Felder nicht verloren gehen. Enthaelt die Initial-Migration (haendisch gepflegt, damit "prisma migrate deploy" im Container ohne Shadow-Datenbank durchlaeuft) und einen idempotenten Seed mit drei Beispielprojekten. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ProjectStatus" AS ENUM ('IDEA', 'OPEN', 'PLANNING', 'IN_DEVELOPMENT', 'DONE', 'ARCHIVED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "GitProvider" AS ENUM ('GITHUB', 'GITLAB', 'GITEA');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "GitConnection" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"provider" "GitProvider" NOT NULL,
|
||||
"baseUrl" TEXT NOT NULL,
|
||||
"username" TEXT,
|
||||
"tokenCipher" TEXT,
|
||||
"tokenHint" TEXT,
|
||||
"lastCheckedAt" TIMESTAMP(3),
|
||||
"lastCheckOk" BOOLEAN,
|
||||
"lastCheckMessage" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "GitConnection_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Project" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"summary" TEXT,
|
||||
"description" TEXT,
|
||||
"status" "ProjectStatus" NOT NULL DEFAULT 'IDEA',
|
||||
"priority" INTEGER NOT NULL DEFAULT 2,
|
||||
"progress" INTEGER NOT NULL DEFAULT 0,
|
||||
"accent" TEXT NOT NULL DEFAULT 'violet',
|
||||
"tags" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
"favorite" BOOLEAN NOT NULL DEFAULT false,
|
||||
"repoUrl" TEXT,
|
||||
"repoOwner" TEXT,
|
||||
"repoName" TEXT,
|
||||
"defaultBranch" TEXT,
|
||||
"connectionId" TEXT,
|
||||
"lastSyncedAt" TIMESTAMP(3),
|
||||
"syncError" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Project_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Note" (
|
||||
"id" TEXT NOT NULL,
|
||||
"projectId" TEXT NOT NULL,
|
||||
"title" TEXT,
|
||||
"content" TEXT NOT NULL,
|
||||
"pinned" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Note_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "RepoSnapshot" (
|
||||
"id" TEXT NOT NULL,
|
||||
"projectId" TEXT NOT NULL,
|
||||
"fullName" TEXT,
|
||||
"description" TEXT,
|
||||
"defaultBranch" TEXT,
|
||||
"htmlUrl" TEXT,
|
||||
"visibility" TEXT,
|
||||
"stars" INTEGER NOT NULL DEFAULT 0,
|
||||
"forks" INTEGER NOT NULL DEFAULT 0,
|
||||
"openIssues" INTEGER NOT NULL DEFAULT 0,
|
||||
"openPullRequests" INTEGER NOT NULL DEFAULT 0,
|
||||
"sizeKb" INTEGER NOT NULL DEFAULT 0,
|
||||
"lastCommitSha" TEXT,
|
||||
"lastCommitMessage" TEXT,
|
||||
"lastCommitAuthor" TEXT,
|
||||
"lastCommitAt" TIMESTAMP(3),
|
||||
"latestTag" TEXT,
|
||||
"latestRelease" TEXT,
|
||||
"latestReleaseAt" TIMESTAMP(3),
|
||||
"branchCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"tagCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"branches" JSONB NOT NULL DEFAULT '[]',
|
||||
"tags" JSONB NOT NULL DEFAULT '[]',
|
||||
"commits" JSONB NOT NULL DEFAULT '[]',
|
||||
"releases" JSONB NOT NULL DEFAULT '[]',
|
||||
"languages" JSONB NOT NULL DEFAULT '{}',
|
||||
"pushedAt" TIMESTAMP(3),
|
||||
"fetchedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "RepoSnapshot_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "GitConnection_name_key" ON "GitConnection"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "GitConnection_provider_idx" ON "GitConnection"("provider");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Project_slug_key" ON "Project"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Project_status_idx" ON "Project"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Project_connectionId_idx" ON "Project"("connectionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Project_updatedAt_idx" ON "Project"("updatedAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Note_projectId_idx" ON "Note"("projectId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Note_pinned_idx" ON "Note"("pinned");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "RepoSnapshot_projectId_key" ON "RepoSnapshot"("projectId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Project" ADD CONSTRAINT "Project_connectionId_fkey" FOREIGN KEY ("connectionId") REFERENCES "GitConnection"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Note" ADD CONSTRAINT "Note_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "RepoSnapshot" ADD CONSTRAINT "RepoSnapshot_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
@@ -0,0 +1,163 @@
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Nebula – Datenmodell
|
||||
// Datenbank: PostgreSQL (lokal oder via docker compose)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
// linux-musl-openssl-3.0.x wird fuer das Alpine-Docker-Image benoetigt.
|
||||
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
/// Lebenszyklus eines Projekts. IDEA ist der Startzustand fuer
|
||||
/// frisch notierte Einfaelle, ARCHIVED blendet Projekte aus dem
|
||||
/// Standard-Board aus, ohne sie zu loeschen.
|
||||
enum ProjectStatus {
|
||||
IDEA
|
||||
OPEN
|
||||
PLANNING
|
||||
IN_DEVELOPMENT
|
||||
DONE
|
||||
ARCHIVED
|
||||
}
|
||||
|
||||
/// Unterstuetzte Git-Systeme.
|
||||
enum GitProvider {
|
||||
GITHUB
|
||||
GITLAB
|
||||
GITEA
|
||||
}
|
||||
|
||||
/// Eine Verbindung zu einem Git-System inkl. verschluesseltem Token.
|
||||
model GitConnection {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
provider GitProvider
|
||||
/// Basis-URL der Instanz, z. B. https://github.com, https://gitlab.com,
|
||||
/// https://git.meine-domain.de
|
||||
baseUrl String
|
||||
/// Optionaler Benutzername – rein informativ fuer die Anzeige.
|
||||
username String?
|
||||
|
||||
/// Personal Access Token, AES-256-GCM-verschluesselt (siehe src/lib/crypto.ts).
|
||||
tokenCipher String?
|
||||
/// Gekuerzte Darstellung des Tokens fuer die UI, z. B. "ghp_…4f2a".
|
||||
tokenHint String?
|
||||
|
||||
lastCheckedAt DateTime?
|
||||
lastCheckOk Boolean?
|
||||
lastCheckMessage String?
|
||||
|
||||
projects Project[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([name])
|
||||
@@index([provider])
|
||||
}
|
||||
|
||||
/// Ein Vibe-Coding-Projekt – von der reinen Idee bis zum fertigen Repo.
|
||||
model Project {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
slug String @unique
|
||||
/// Einzeiler fuer die Karte im Dashboard.
|
||||
summary String?
|
||||
/// Ausfuehrliche Beschreibung / Ideenskizze (Markdown-freundlich).
|
||||
description String?
|
||||
status ProjectStatus @default(IDEA)
|
||||
/// 1 = niedrig, 2 = normal, 3 = hoch, 4 = kritisch
|
||||
priority Int @default(2)
|
||||
/// Fortschritt in Prozent (0–100)
|
||||
progress Int @default(0)
|
||||
/// Akzentfarbe der Karte: violet | cyan | emerald | amber | rose | blue
|
||||
accent String @default("violet")
|
||||
tags String[] @default([])
|
||||
favorite Boolean @default(false)
|
||||
|
||||
// ── Git-Anbindung ──────────────────────────────────────────
|
||||
repoUrl String?
|
||||
/// Owner bzw. Namespace/Gruppe (bei GitLab auch verschachtelt: "gruppe/subgruppe")
|
||||
repoOwner String?
|
||||
repoName String?
|
||||
defaultBranch String?
|
||||
connectionId String?
|
||||
connection GitConnection? @relation(fields: [connectionId], references: [id], onDelete: SetNull)
|
||||
|
||||
lastSyncedAt DateTime?
|
||||
syncError String?
|
||||
|
||||
notes Note[]
|
||||
snapshot RepoSnapshot?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([status])
|
||||
@@index([connectionId])
|
||||
@@index([updatedAt])
|
||||
}
|
||||
|
||||
/// Freie Notizen zu einem Projekt.
|
||||
model Note {
|
||||
id String @id @default(cuid())
|
||||
projectId String
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
title String?
|
||||
content String
|
||||
pinned Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([projectId])
|
||||
@@index([pinned])
|
||||
}
|
||||
|
||||
/// Zwischengespeicherter Stand des verknuepften Repositories. Wird beim
|
||||
/// Sync ueberschrieben, damit das Dashboard auch ohne Netz Daten zeigt.
|
||||
model RepoSnapshot {
|
||||
id String @id @default(cuid())
|
||||
projectId String @unique
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
|
||||
fullName String?
|
||||
description String?
|
||||
defaultBranch String?
|
||||
htmlUrl String?
|
||||
visibility String?
|
||||
|
||||
stars Int @default(0)
|
||||
forks Int @default(0)
|
||||
openIssues Int @default(0)
|
||||
openPullRequests Int @default(0)
|
||||
sizeKb Int @default(0)
|
||||
|
||||
lastCommitSha String?
|
||||
lastCommitMessage String?
|
||||
lastCommitAuthor String?
|
||||
lastCommitAt DateTime?
|
||||
|
||||
latestTag String?
|
||||
latestRelease String?
|
||||
latestReleaseAt DateTime?
|
||||
|
||||
branchCount Int @default(0)
|
||||
tagCount Int @default(0)
|
||||
|
||||
/// Rohdaten der Listen – bewusst als JSON, damit providerspezifische
|
||||
/// Zusatzfelder nicht verloren gehen.
|
||||
branches Json @default("[]")
|
||||
tags Json @default("[]")
|
||||
commits Json @default("[]")
|
||||
releases Json @default("[]")
|
||||
languages Json @default("{}")
|
||||
|
||||
pushedAt DateTime?
|
||||
fetchedAt DateTime @default(now())
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
import { PrismaClient, ProjectStatus } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
interface SampleNote {
|
||||
title?: string | null;
|
||||
content: string;
|
||||
pinned?: boolean;
|
||||
}
|
||||
|
||||
interface SampleProject {
|
||||
name: string;
|
||||
slug: string;
|
||||
summary: string;
|
||||
description: string;
|
||||
status: ProjectStatus;
|
||||
priority: number;
|
||||
progress: number;
|
||||
accent: string;
|
||||
tags: string[];
|
||||
favorite: boolean;
|
||||
notes: SampleNote[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Legt ein paar Beispielprojekte an, damit das Dashboard nach dem ersten
|
||||
* Start nicht leer ist. Idempotent: bereits vorhandene Slugs werden
|
||||
* uebersprungen.
|
||||
*/
|
||||
async function main() {
|
||||
const samples: SampleProject[] = [
|
||||
{
|
||||
name: "Nebula",
|
||||
slug: "nebula",
|
||||
summary: "Dieses Dashboard – Uebersicht ueber alle Vibe-Coding Projekte.",
|
||||
description:
|
||||
"Meta: Nebula verwaltet sich selbst. Notizen, Status und Git-Infos an einem Ort.",
|
||||
status: ProjectStatus.IN_DEVELOPMENT,
|
||||
priority: 3,
|
||||
progress: 65,
|
||||
accent: "violet",
|
||||
tags: ["next.js", "prisma", "dashboard"],
|
||||
favorite: true,
|
||||
notes: [
|
||||
{
|
||||
title: "Naechste Schritte",
|
||||
content:
|
||||
"- Kanban-Ansicht ergaenzen\n- Webhook-Endpoint fuer Push-Events\n- Export als Markdown",
|
||||
pinned: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Synthwave CLI",
|
||||
slug: "synthwave-cli",
|
||||
summary: "Terminal-Tool, das Projektgeruest inkl. CI in Sekunden erzeugt.",
|
||||
description:
|
||||
"Idee: ein `npx synthwave init` das Repo, Lint, Tests, Container und Pipeline vorkonfiguriert.",
|
||||
status: ProjectStatus.PLANNING,
|
||||
priority: 2,
|
||||
progress: 15,
|
||||
accent: "cyan",
|
||||
tags: ["cli", "typescript", "dx"],
|
||||
favorite: false,
|
||||
notes: [
|
||||
{
|
||||
title: null,
|
||||
content: "Konkurrenz pruefen: create-t3-app, degit, plop.",
|
||||
pinned: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Pixelgarten",
|
||||
slug: "pixelgarten",
|
||||
summary: "Browser-Spiel: ein Garten, der aus Commit-Aktivitaet waechst.",
|
||||
description:
|
||||
"Rohe Idee. Jeder Commit pflanzt eine Pflanze, Streaks lassen sie bluehen.",
|
||||
status: ProjectStatus.IDEA,
|
||||
priority: 1,
|
||||
progress: 0,
|
||||
accent: "emerald",
|
||||
tags: ["spiel", "canvas", "fun"],
|
||||
favorite: false,
|
||||
notes: [],
|
||||
},
|
||||
];
|
||||
|
||||
for (const sample of samples) {
|
||||
const existing = await prisma.project.findUnique({
|
||||
where: { slug: sample.slug },
|
||||
});
|
||||
if (existing) {
|
||||
console.log(` · uebersprungen (existiert): ${sample.slug}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const { notes, ...project } = sample;
|
||||
await prisma.project.create({
|
||||
data: {
|
||||
...project,
|
||||
notes: notes.length ? { create: notes } : undefined,
|
||||
},
|
||||
});
|
||||
console.log(` ✓ angelegt: ${sample.slug}`);
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.then(async () => {
|
||||
console.log("Seed abgeschlossen.");
|
||||
await prisma.$disconnect();
|
||||
})
|
||||
.catch(async (error) => {
|
||||
console.error("Seed fehlgeschlagen:", error);
|
||||
await prisma.$disconnect();
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user