Add safe database migration workflow and note history infrastructure.
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 5s

This introduces guarded migrations with automatic backups, fixes note creation after DB reset, and wires snapshot/restore history across notes surfaces.
This commit is contained in:
Sepehr Ramezani
2026-04-28 16:50:57 +02:00
parent 0fbb8aa599
commit ed807d3b2a
23 changed files with 8052 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
-- CreateTable
CREATE TABLE "NoteHistory" (
"id" TEXT NOT NULL,
"noteId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"version" INTEGER NOT NULL,
"reason" TEXT,
"title" TEXT,
"content" TEXT NOT NULL,
"color" TEXT NOT NULL,
"isPinned" BOOLEAN NOT NULL,
"isArchived" BOOLEAN NOT NULL,
"type" TEXT NOT NULL,
"checkItems" TEXT,
"labels" TEXT,
"images" TEXT,
"links" TEXT,
"isMarkdown" BOOLEAN NOT NULL,
"size" TEXT NOT NULL,
"notebookId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "NoteHistory_pkey" PRIMARY KEY ("id")
);
-- AlterTable
ALTER TABLE "UserAISettings"
ADD COLUMN "noteHistory" BOOLEAN NOT NULL DEFAULT false;
-- CreateIndex
CREATE UNIQUE INDEX "NoteHistory_noteId_version_key" ON "NoteHistory"("noteId", "version");
-- CreateIndex
CREATE INDEX "NoteHistory_noteId_createdAt_idx" ON "NoteHistory"("noteId", "createdAt" DESC);
-- CreateIndex
CREATE INDEX "NoteHistory_userId_noteId_createdAt_idx" ON "NoteHistory"("userId", "noteId", "createdAt" DESC);
-- AddForeignKey
ALTER TABLE "NoteHistory" ADD CONSTRAINT "NoteHistory_noteId_fkey" FOREIGN KEY ("noteId") REFERENCES "Note"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "NoteHistory" ADD CONSTRAINT "NoteHistory_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,409 @@
generator client {
provider = "prisma-client-js"
binaryTargets = ["debian-openssl-3.0.x", "native"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
name String?
email String @unique
emailVerified DateTime?
password String?
role String @default("USER")
image String?
theme String @default("light")
resetToken String? @unique
resetTokenExpiry DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
cardSizeMode String @default("variable")
accounts Account[]
agents Agent[]
aiFeedback AiFeedback[]
canvases Canvas[]
conversations Conversation[]
labels Label[]
memoryEchoInsights MemoryEchoInsight[]
notes Note[]
noteHistories NoteHistory[]
sentShares NoteShare[] @relation("SentShares")
receivedShares NoteShare[] @relation("ReceivedShares")
notebooks Notebook[]
sessions Session[]
aiSettings UserAISettings?
workflows Workflow[]
}
model Account {
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([provider, providerAccountId])
}
model Session {
sessionToken String @unique
userId String
expires DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model VerificationToken {
identifier String
token String
expires DateTime
@@id([identifier, token])
}
model Notebook {
id String @id @default(cuid())
name String
icon String?
color String?
order Int
userId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
agents Agent[]
conversations Conversation[]
labels Label[]
notes Note[]
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
workflows Workflow[]
@@index([userId, order])
@@index([userId])
}
model Label {
id String @id @default(cuid())
name String
color String @default("gray")
notebookId String?
userId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
notebook Notebook? @relation(fields: [notebookId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
notes Note[] @relation("LabelToNote")
@@unique([notebookId, name])
@@index([notebookId])
@@index([userId])
}
model Note {
id String @id @default(cuid())
title String?
content String
color String @default("default")
isPinned Boolean @default(false)
isArchived Boolean @default(false)
type String @default("text")
dismissedFromRecent Boolean @default(false)
checkItems String?
labels String?
images String?
links String?
reminder DateTime?
isReminderDone Boolean @default(false)
reminderRecurrence String?
reminderLocation String?
isMarkdown Boolean @default(false)
size String @default("small")
sharedWith String?
userId String?
order Int @default(0)
notebookId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
contentUpdatedAt DateTime @default(now())
autoGenerated Boolean?
aiProvider String?
aiConfidence Int?
language String?
languageConfidence Float?
lastAiAnalysis DateTime?
trashedAt DateTime?
aiFeedback AiFeedback[]
memoryEchoAsNote1 MemoryEchoInsight[] @relation("EchoNote1")
memoryEchoAsNote2 MemoryEchoInsight[] @relation("EchoNote2")
notebook Notebook? @relation(fields: [notebookId], references: [id])
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
noteEmbedding NoteEmbedding?
shares NoteShare[]
labelRelations Label[] @relation("LabelToNote")
historyEntries NoteHistory[]
@@index([isPinned])
@@index([isArchived])
@@index([trashedAt])
@@index([order])
@@index([reminder])
@@index([userId])
@@index([userId, notebookId])
}
model NoteHistory {
id String @id @default(cuid())
noteId String
userId String
version Int
reason String?
title String?
content String
color String
isPinned Boolean
isArchived Boolean
type String
checkItems String?
labels String?
images String?
links String?
isMarkdown Boolean
size String
notebookId String?
createdAt DateTime @default(now())
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([noteId, version])
@@index([noteId, createdAt(sort: Desc)])
@@index([userId, noteId, createdAt(sort: Desc)])
}
model NoteShare {
id String @id @default(cuid())
noteId String
userId String
sharedBy String
status String @default("pending")
permission String @default("view")
notifiedAt DateTime?
respondedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
sharer User @relation("SentShares", fields: [sharedBy], references: [id], onDelete: Cascade)
user User @relation("ReceivedShares", fields: [userId], references: [id], onDelete: Cascade)
@@unique([noteId, userId])
@@index([userId])
@@index([status])
@@index([sharedBy])
}
model SystemConfig {
key String @id
value String
}
model AiFeedback {
id String @id @default(cuid())
noteId String
userId String?
feedbackType String
feature String
originalContent String
correctedContent String?
metadata String?
createdAt DateTime @default(now())
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([noteId])
@@index([userId])
@@index([feature])
}
model MemoryEchoInsight {
id String @id @default(cuid())
userId String?
note1Id String
note2Id String
similarityScore Float
insight String
insightDate DateTime @default(now())
viewed Boolean @default(false)
feedback String?
dismissed Boolean @default(false)
note1 Note @relation("EchoNote1", fields: [note1Id], references: [id], onDelete: Cascade)
note2 Note @relation("EchoNote2", fields: [note2Id], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, insightDate])
@@index([userId, insightDate])
@@index([userId, dismissed])
}
model UserAISettings {
userId String @id
titleSuggestions Boolean @default(true)
semanticSearch Boolean @default(true)
paragraphRefactor Boolean @default(true)
memoryEcho Boolean @default(true)
memoryEchoFrequency String @default("daily")
aiProvider String @default("auto")
preferredLanguage String @default("auto")
fontSize String @default("medium")
demoMode Boolean @default(false)
showRecentNotes Boolean @default(true)
/// "masonry" = grille cartes Muuri ; "tabs" = onglets + panneau (type OneNote). Ancienne valeur "list" migrée vers "tabs" en lecture.
notesViewMode String @default("masonry")
emailNotifications Boolean @default(false)
desktopNotifications Boolean @default(false)
anonymousAnalytics Boolean @default(false)
autoLabeling Boolean @default(true)
noteHistory Boolean @default(false)
languageDetection Boolean @default(true)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([memoryEcho])
@@index([aiProvider])
@@index([memoryEchoFrequency])
@@index([preferredLanguage])
}
model NoteEmbedding {
id String @id @default(cuid())
noteId String @unique
embedding String
createdAt DateTime @default(now())
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
@@index([noteId])
}
model Agent {
id String @id @default(cuid())
name String
description String?
type String? @default("scraper")
role String
sourceUrls String?
frequency String @default("manual")
lastRun DateTime?
nextRun DateTime?
isEnabled Boolean @default(true)
targetNotebookId String?
sourceNotebookId String?
tools String? @default("[]")
maxSteps Int @default(10)
notifyEmail Boolean @default(false)
includeImages Boolean @default(false)
userId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
scheduledTime String? @default("08:00")
scheduledDay Int?
timezone String?
notebook Notebook? @relation(fields: [targetNotebookId], references: [id])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
actions AgentAction[]
@@index([userId])
@@index([isEnabled])
}
model AgentAction {
id String @id @default(cuid())
agentId String
status String @default("pending")
result String?
log String?
input String?
toolLog String?
tokensUsed Int?
createdAt DateTime @default(now())
agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
@@index([agentId])
}
model Conversation {
id String @id @default(cuid())
title String?
userId String
notebookId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
messages ChatMessage[]
notebook Notebook? @relation(fields: [notebookId], references: [id])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([notebookId])
}
model ChatMessage {
id String @id @default(cuid())
conversationId String
role String
content String
createdAt DateTime @default(now())
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
@@index([conversationId])
}
model Canvas {
id String @id @default(cuid())
name String
data String
userId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
}
model Workflow {
id String @id @default(cuid())
name String
description String?
graph String @default("{\"nodes\":[],\"edges\":[]}")
isEnabled Boolean @default(true)
userId String
notebookId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
notebook Notebook? @relation(fields: [notebookId], references: [id])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
runs WorkflowRun[]
@@index([userId])
@@index([isEnabled])
}
model WorkflowRun {
id String @id @default(cuid())
workflowId String
status String @default("running")
log String?
createdAt DateTime @default(now())
workflow Workflow @relation(fields: [workflowId], references: [id], onDelete: Cascade)
@@index([workflowId])
@@index([status])
}