mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-11 00:02:13 +00:00
Sur une photo ou une vidéo, on ajoute les personnes de l'arbre qui y figurent et, sur une photo, on touche leur visage pour poser un point. Le souvenir apparaît aussi sur la fiche de chaque personne identifiée (« Avec Odette »), un prénom ouvre sa fiche et recentre l'arbre. L'administrateur enregistre directement. La famille propose, l'administrateur relit dans « Propositions à relire ». Suppression d'une personne ou d'un souvenir : identifications retirées. Table media_person_tags (migration additive), route PUT /trees/:id/media/:mediaId/people. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
494 lines
16 KiB
Plaintext
494 lines
16 KiB
Plaintext
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
|
|
life_event
|
|
// Personnes identifiées sur un souvenir (remplace la liste entière)
|
|
media_people
|
|
}
|
|
|
|
// 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
|
|
firstName String?
|
|
// Vide pour un compte né d'une connexion OIDC : il n'a pas de mot de passe Parenthèse
|
|
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[]
|
|
identities UserIdentity[]
|
|
lastOpenedTree Tree? @relation("UserLastOpenedTree", fields: [lastOpenedTreeId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([lastOpenedAt])
|
|
@@map("users")
|
|
}
|
|
|
|
// Compte d'un fournisseur OIDC relié à un compte Parenthèse : l'émetteur et le « sub » de l'ID token, jamais l'email,
|
|
// qui peut changer chez le fournisseur.
|
|
model UserIdentity {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
issuer String
|
|
subject String
|
|
createdAt DateTime @default(now())
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([issuer, subject])
|
|
@@index([userId])
|
|
@@map("user_identities")
|
|
}
|
|
|
|
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[]
|
|
mediaPersonTags MediaPersonTag[]
|
|
contributionSessions ContributionSession[]
|
|
auditLogs AuditLog[]
|
|
annotations Annotation[]
|
|
visits TreeVisit[]
|
|
lifeEvents LifeEvent[]
|
|
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?
|
|
// Décédé(e) sans date connue (issue #77) : vrai seulement quand deathDate est vide
|
|
isDeceased Boolean @default(false)
|
|
birthPlace String?
|
|
// Précision d'une date importée (« vers », « avant », « entre »…) et sa borne de fin pour « entre … et … »
|
|
birthDatePrecision String?
|
|
birthDateEnd DateTime?
|
|
deathDatePrecision String?
|
|
deathDateEnd DateTime?
|
|
deathPlace String?
|
|
nickname String?
|
|
profession String?
|
|
region String?
|
|
nationality String?
|
|
avatarPath String?
|
|
avatarMimeType String?
|
|
// Cadrage du portrait, appliqué à l'affichage sur l'original (jamais de fichier dérivé) :
|
|
// { "x": 0..1, "y": 0..1, "zoom": 1..4 }, centre du cadrage en proportion de l'image, zoom 1 = image entière
|
|
// recouvrant le cadre. Null = cadrage par défaut (centré, zoom 1).
|
|
avatarCrop Json?
|
|
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)
|
|
startPlace String?
|
|
startDatePrecision String?
|
|
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?
|
|
// Moment de l'onglet Vie auquel ce souvenir est rattaché (photo, vidéo, acte en PDF…)
|
|
lifeEventId String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
deletedAt DateTime?
|
|
tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([treeId, personId, displayOrder])
|
|
@@index([lifeEventId])
|
|
@@map("media_items")
|
|
}
|
|
|
|
// Personne identifiée sur un souvenir. Le souvenir reste à la personne qui le porte (media_items.personId) ;
|
|
// il apparaît aussi sur la fiche de chaque personne identifiée. x et y : point sur le visage, en proportion
|
|
// de l'image (0 à 1), facultatifs.
|
|
model MediaPersonTag {
|
|
id String @id @default(cuid())
|
|
treeId String
|
|
mediaId String
|
|
personId String
|
|
x Float?
|
|
y Float?
|
|
createdBy String?
|
|
createdAt DateTime @default(now())
|
|
tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([mediaId, personId])
|
|
@@index([treeId])
|
|
@@index([treeId, personId])
|
|
@@map("media_person_tags")
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
// Événements de vie d'une personne (baptême, résidence, métier, études, service militaire, inhumation…) ou
|
|
// d'un couple (divorce…). Naissance, décès et mariage gardent leur date et leur lieu sur la personne ou
|
|
// l'union ; leur ligne ici ne porte que les sources et les notes (type birth, death, marriage).
|
|
model LifeEvent {
|
|
id String @id @default(cuid())
|
|
treeId String
|
|
personId String?
|
|
unionId String?
|
|
type String
|
|
label String?
|
|
date DateTime?
|
|
dateEnd DateTime?
|
|
datePrecision String?
|
|
dateText String?
|
|
place String?
|
|
description String?
|
|
notes String?
|
|
sources String?
|
|
// Ancien rattachement d'une seule photo (17/10), remplacé par media_items.lifeEventId
|
|
mediaId String?
|
|
// Moment proposé par la famille : « pending » jusqu'à la relecture de l'administrateur
|
|
status String @default("approved")
|
|
contributionSessionId 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])
|
|
@@index([treeId, personId])
|
|
@@index([treeId, unionId])
|
|
@@map("life_events")
|
|
}
|