generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } enum MembershipRole { owner admin member } enum ContributionPolicy { direct pending } enum VisitorPolicy { read_only } enum ContributionSessionStatus { pending approved rejected } enum ContributionActorMode { user tree_access } enum ContributionEntityType { person union parent_child_link annotation media } // Souvenir envoyé par la famille : en attente jusqu'à la relecture du propriétaire enum MediaStatus { approved pending rejected } enum AnnotationType { drawing sticker text photo } enum ContributionAction { create update delete reorder } enum ConflictState { none needs_review } enum ContributionDecision { approved rejected } enum SharedAccessRole { visitor contributor } model User { id String @id @default(cuid()) email String @unique passwordHash String lastOpenedTreeId String? lastOpenedAccessMode String? lastOpenedRole String? lastOpenedAt DateTime? createdAt DateTime @default(now()) ownedTrees Tree[] @relation("TreeOwner") memberships TreeMembership[] sharedTreeAccesses UserTreeAccess[] submittedContributionSessions ContributionSession[] @relation("ContributionSubmittedBy") reviewedContributionSessions ContributionSession[] @relation("ContributionReviewedBy") treeVisits TreeVisit[] lastOpenedTree Tree? @relation("UserLastOpenedTree", fields: [lastOpenedTreeId], references: [id], onDelete: SetNull) @@index([lastOpenedAt]) @@map("users") } model Tree { id String @id @default(cuid()) ownerUserId String slug String @unique name String description String? rootPersonId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? owner User @relation("TreeOwner", fields: [ownerUserId], references: [id], onDelete: Restrict) accessPasswords TreeAccessPasswords? sharePassword TreeSharePassword? memberships TreeMembership[] sharedUserAccesses UserTreeAccess[] settings TreeSettings? immichConfig TreeImmichConfig? persons Person[] unions Union[] parentChildLinks ParentChildLink[] mediaItems MediaItem[] contributionSessions ContributionSession[] auditLogs AuditLog[] annotations Annotation[] visits TreeVisit[] lastOpenedByUsers User[] @relation("UserLastOpenedTree") @@index([ownerUserId]) @@map("trees") } model TreeAccessPasswords { treeId String @id visitorHash String contributorHash String updatedAt DateTime @updatedAt tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade) @@map("tree_access_passwords") } // Mot de passe de partage en clair chiffré, pour que le propriétaire voie toujours celui en vigueur. // Table à part : écrire dans tree_access_passwords changerait updatedAt, qui sert de version aux accès ouverts. model TreeSharePassword { treeId String @id passwordEnc String setAt DateTime @default(now()) tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade) @@map("tree_share_passwords") } model TreeMembership { id String @id @default(cuid()) treeId String userId String role MembershipRole contributionModeOverride ContributionPolicy? createdAt DateTime @default(now()) tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([treeId, userId]) @@index([userId]) @@map("tree_memberships") } model UserTreeAccess { id String @id @default(cuid()) treeId String userId String role SharedAccessRole createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([treeId, userId]) @@index([userId, updatedAt]) @@map("user_tree_accesses") } model TreeSettings { treeId String @id memberContributionPolicy ContributionPolicy @default(pending) visitorPolicy VisitorPolicy @default(read_only) contributorPolicy ContributionPolicy @default(pending) updatedAt DateTime @updatedAt tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade) @@map("tree_settings") } // Connexion Immich propre à chaque arbre (reconnaissance des visages) : l'adresse et la clé de // l'instance sont choisies par ses administrateurs, jamais héritées du serveur. La clé est // chiffrée au repos ; une ligne absente signifie « aucun accès Immich » pour cet arbre. model TreeImmichConfig { treeId String @id apiUrl String apiKeyEnc String updatedAt DateTime @updatedAt tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade) @@map("tree_immich_configs") } model Person { id String @id @default(cuid()) treeId String firstName String lastName String birthName String? birthDate DateTime? deathDate DateTime? birthPlace String? profession String? region String? nationality String? avatarPath String? avatarMimeType String? sex String? notes String? createdBy String? updatedBy String? // Personne Immich associée (reconnaissance des visages) : identifiant côté Immich, ou null. immichPersonId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade) @@index([treeId]) @@map("persons") } model Union { id String @id @default(cuid()) treeId String partner1PersonId String partner2PersonId String? unionType String? startDate DateTime? endDate DateTime? displayOrder Int @default(1) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade) @@index([treeId]) @@map("unions") } model ParentChildLink { id String @id @default(cuid()) treeId String parentPersonId String childPersonId String viaUnionId String? parentageType String? displayOrder Int @default(1) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade) @@index([treeId]) @@map("parent_child_links") } model MediaItem { id String @id @default(cuid()) treeId String personId String type String mimeType String filePath String thumbPath String? caption String? source String? sizeBytes Int displayOrder Int @default(1) isFeatured Boolean @default(false) uploadedBy String? status MediaStatus @default(approved) contributionSessionId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade) @@index([treeId, personId, displayOrder]) @@map("media_items") } model ContributionSession { id String @id @default(cuid()) treeId String submittedByUserId String? submittedByLabel String comment String? mode ContributionActorMode status ContributionSessionStatus @default(pending) title String createdAt DateTime @default(now()) reviewedAt DateTime? reviewedBy String? tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade) submittedByUser User? @relation("ContributionSubmittedBy", fields: [submittedByUserId], references: [id], onDelete: SetNull) reviewer User? @relation("ContributionReviewedBy", fields: [reviewedBy], references: [id], onDelete: SetNull) changes ContributionChange[] @@index([treeId, status, createdAt]) @@index([submittedByUserId]) @@map("contribution_sessions") } model ContributionChange { id String @id @default(cuid()) sessionId String entityType ContributionEntityType entityId String? action ContributionAction beforeJson Json? afterJson Json? conflictState ConflictState @default(none) decision ContributionDecision? createdAt DateTime @default(now()) session ContributionSession @relation(fields: [sessionId], references: [id], onDelete: Cascade) @@index([sessionId]) @@map("contribution_changes") } model Annotation { id String @id @default(cuid()) treeId String type AnnotationType x Float y Float content String style Json @default("{}") zIndex Int @default(0) createdBy String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade) @@index([treeId]) @@map("annotations") } model AuditLog { id String @id @default(cuid()) treeId String actorType String actorId String? action String entityType String entityId String payloadJson Json? createdAt DateTime @default(now()) tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade) @@index([treeId, createdAt]) @@map("audit_log") } // Journal des visites d'un arbre, lu par ses administrateurs (onglet Visites). // Pas d'adresse IP : visitorKey est une empreinte (arbre + IP + navigateur) qui sert à compter les visiteurs distincts. model TreeVisit { id String @id @default(cuid()) treeId String userId String? accessKind String visitorName String? visitorKey String device String createdAt DateTime @default(now()) tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade) user User? @relation(fields: [userId], references: [id], onDelete: SetNull) @@index([treeId, createdAt]) @@index([treeId, visitorKey, createdAt]) @@map("tree_visits") } model WaitlistEntry { id String @id @default(cuid()) email String @unique prenom String? source String @default("direct") createdAt DateTime @default(now()) @@map("waitlist_entries") }