mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
- migration r1_referentiel : Category (EQUIPMENT/COMPONENT_TYPE), Location (site → zone, lat/lng + colonne PostGIS générée geography(Point,4326) + index GIST), Asset (statut d'équipement), AssetComponent (organe sans emplacement PAR CONSTRUCTION), Team, invitation sur User ; migration autosuffisante (CREATE EXTENSION IF NOT EXISTS postgis) - contrat : 21 nouvelles opérations (26 total), générateur OpenAPI étendu aux paramètres de chemin ; spec + client web régénérés dans ce commit - API : modules categories/locations/assets/teams + gestion des personnes (liste, rôles, invitation lien 7 j à usage unique, activation publique qui connecte directement, mise à jour rôle/équipes) — tout sous @RequirePermission ; invariants en service (profondeur 2, kinds, catégorie jamais supprimée) - seed : parc de la maquette validée (5 sites + 8 zones, 8 appareils, organes A1/B2, 9 catégories, 2 équipes) — idempotent - 36 tests verts (couverture 96 % stmts / 85 % branches) : recette site→zone→appareil→organes, matrice vivante, invitation→activation ; smoke test sur build de prod - CI : postgres → postgis/postgis:18-3.6 (la migration R1 l'exige) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
144 lines
4.7 KiB
Plaintext
144 lines
4.7 KiB
Plaintext
// Modèle R0 — identité & permissions (docs/03-architecture/modele-donnees.md).
|
||
// La matrice rôles × objets × droits vit EN BASE ; le JWT ne porte jamais de droits.
|
||
|
||
generator client {
|
||
provider = "prisma-client-js"
|
||
// Cibles explicites : poste de dev (native) + conteneur node:24-slim
|
||
// (OpenSSL 3) en x64 (serveur partenaire) et arm64 (répétition locale).
|
||
binaryTargets = ["native", "debian-openssl-3.0.x", "linux-arm64-openssl-3.0.x"]
|
||
}
|
||
|
||
datasource db {
|
||
provider = "postgresql"
|
||
url = env("DATABASE_URL")
|
||
}
|
||
|
||
model Role {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
name String @unique // 7 rôles seedés — voir @siop/shared ROLE_NAMES
|
||
users User[]
|
||
permissions Permission[]
|
||
}
|
||
|
||
model Permission {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
roleId String @db.Uuid
|
||
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
||
objectCategory String // enum applicatif — voir @siop/shared OBJECT_CATEGORIES
|
||
canView Boolean @default(false)
|
||
canViewOther Boolean @default(false) // « voir autre » : au-delà de ses propres objets
|
||
canCreate Boolean @default(false)
|
||
canEdit Boolean @default(false)
|
||
canDelete Boolean @default(false)
|
||
|
||
@@unique([roleId, objectCategory])
|
||
}
|
||
|
||
model User {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
email String @unique
|
||
displayName String
|
||
passwordHash String? // null tant que le compte n'est pas activé (R1)
|
||
phone String?
|
||
roleId String @db.Uuid
|
||
role Role @relation(fields: [roleId], references: [id])
|
||
isActive Boolean @default(true)
|
||
isDemo Boolean @default(false) // seul un compte isDemo est empruntable (ADR-002)
|
||
// Invitation (R1) : lien d'activation 7 jours, usage unique.
|
||
// Statut dérivé : invité = passwordHash null && token présent.
|
||
activationToken String? @unique
|
||
activationExpiresAt DateTime?
|
||
teams Team[]
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([roleId])
|
||
}
|
||
|
||
// ————— R1 — Référentiel (docs/03-architecture/modele-donnees.md §R1) —————
|
||
|
||
enum CategoryKind {
|
||
EQUIPMENT
|
||
COMPONENT_TYPE
|
||
}
|
||
|
||
enum AssetStatus {
|
||
IN_SERVICE
|
||
OUT_OF_SERVICE
|
||
UNDER_MAINTENANCE
|
||
}
|
||
|
||
model Category {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
kind CategoryKind
|
||
name String
|
||
isActive Boolean @default(true) // désactivable, jamais supprimée si utilisée
|
||
assets Asset[]
|
||
components AssetComponent[]
|
||
|
||
@@unique([kind, name])
|
||
}
|
||
|
||
model Location {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
name String
|
||
parentId String? @db.Uuid // site (null) → zone ; profondeur max 2 (service)
|
||
parent Location? @relation("LocationTree", fields: [parentId], references: [id])
|
||
children Location[] @relation("LocationTree")
|
||
address String?
|
||
city String?
|
||
guardianName String?
|
||
guardianPhone String?
|
||
latitude Float?
|
||
longitude Float?
|
||
// + colonne PostGIS générée (voir migration r1_referentiel) :
|
||
// position geography(Point,4326) GENERATED ALWAYS AS (…) STORED
|
||
assets Asset[]
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([parentId])
|
||
}
|
||
|
||
model Asset {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
reference String @unique // « A1 » — imprimée sur l'étiquette QR
|
||
brand String
|
||
model String?
|
||
serialNumber String?
|
||
commissionedAt DateTime?
|
||
loadKg Int?
|
||
floors Int?
|
||
status AssetStatus @default(IN_SERVICE) // statut d'ÉQUIPEMENT ≠ statut d'OT
|
||
categoryId String @db.Uuid
|
||
category Category @relation(fields: [categoryId], references: [id])
|
||
locationId String @db.Uuid
|
||
location Location @relation(fields: [locationId], references: [id])
|
||
components AssetComponent[]
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([locationId])
|
||
@@index([categoryId])
|
||
}
|
||
|
||
// Organe : PAS de colonne emplacement — « un organe n'a pas d'emplacement
|
||
// propre » est garanti par construction (décision maquettes R1).
|
||
model AssetComponent {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
assetId String @db.Uuid
|
||
asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade)
|
||
typeId String @db.Uuid
|
||
type Category @relation(fields: [typeId], references: [id])
|
||
designation String?
|
||
|
||
@@index([assetId])
|
||
}
|
||
|
||
model Team {
|
||
id String @id @default(uuid()) @db.Uuid
|
||
name String @unique
|
||
description String?
|
||
members User[]
|
||
}
|