Files
siop2/apps/api/prisma/schema.prisma
pr-daaif 266ffaaf1b feat(r1.1): socle backend du référentiel — modèle, contrat, API, seed, tests
- 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>
2026-07-16 12:27:48 +01:00

144 lines
4.7 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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[]
}