Initial commit: digital reception monorepo (M1-M11 + demo extensions)
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "track_events" ADD COLUMN "face_bbox" JSONB;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Prisma migrate diff не умеет ADD VALUE для enum (известная багофича).
|
||||
-- Пишем руками. ADD VALUE не работает в транзакции, поэтому каждый в своём statement.
|
||||
ALTER TYPE "ZoneCode" ADD VALUE IF NOT EXISTS 'D';
|
||||
ALTER TYPE "ZoneCode" ADD VALUE IF NOT EXISTS 'E';
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
-- CreateExtension
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- CreateExtension
|
||||
CREATE EXTENSION IF NOT EXISTS "vector";
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Role" AS ENUM ('MANAGER', 'SENIOR_ADMIN', 'SECURITY', 'SYSADMIN');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ConsentAction" AS ENUM ('GRANTED', 'REVOKED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ZoneCode" AS ENUM ('A', 'B', 'C');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TrackStatus" AS ENUM ('UNMATCHED', 'MATCHED', 'ANONYMIZED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TrackEventType" AS ENUM ('arrived', 'waiting', 'service_started', 'service_ended', 'left_without_service');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ConsentRevocationStatus" AS ENUM ('PENDING', 'DONE');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "users" (
|
||||
"id" UUID NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"full_name" TEXT NOT NULL,
|
||||
"password_hash" TEXT NOT NULL,
|
||||
"role" "Role" NOT NULL,
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "refresh_tokens" (
|
||||
"id" UUID NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"token_hash" TEXT NOT NULL,
|
||||
"expires_at" TIMESTAMP(3) NOT NULL,
|
||||
"revoked_at" TIMESTAMP(3),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "refresh_tokens_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "patients" (
|
||||
"id" UUID NOT NULL,
|
||||
"polimed_patient_id" TEXT,
|
||||
"full_name" TEXT,
|
||||
"consent_received_at" TIMESTAMP(3),
|
||||
"consent_revoked_at" TIMESTAMP(3),
|
||||
"pending_deletion_at" TIMESTAMP(3),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "patients_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "patient_consents" (
|
||||
"id" UUID NOT NULL,
|
||||
"patient_id" UUID NOT NULL,
|
||||
"action" "ConsentAction" NOT NULL,
|
||||
"paper_ref" TEXT,
|
||||
"actor_user_id" UUID NOT NULL,
|
||||
"occurred_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "patient_consents_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "zones" (
|
||||
"id" UUID NOT NULL,
|
||||
"code" "ZoneCode" NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "zones_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "cameras" (
|
||||
"id" UUID NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"rtsp_url" TEXT,
|
||||
"zone_id" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "cameras_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "tracks" (
|
||||
"id" UUID NOT NULL,
|
||||
"patient_id" UUID,
|
||||
"status" "TrackStatus" NOT NULL DEFAULT 'UNMATCHED',
|
||||
"first_seen_at" TIMESTAMP(3) NOT NULL,
|
||||
"last_seen_at" TIMESTAMP(3) NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "tracks_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "track_events" (
|
||||
"id" UUID NOT NULL,
|
||||
"track_id" UUID NOT NULL,
|
||||
"type" "TrackEventType" NOT NULL,
|
||||
"camera_id" UUID NOT NULL,
|
||||
"zone_id" UUID NOT NULL,
|
||||
"occurred_at" TIMESTAMP(3) NOT NULL,
|
||||
"evidence_key" TEXT,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "track_events_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "face_embeddings" (
|
||||
"id" UUID NOT NULL,
|
||||
"embedding" vector(512) NOT NULL,
|
||||
"patient_id" UUID,
|
||||
"track_id" UUID,
|
||||
"camera_id" UUID NOT NULL,
|
||||
"quality" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"captured_at" TIMESTAMP(3) NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "face_embeddings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "visits" (
|
||||
"id" UUID NOT NULL,
|
||||
"patient_id" UUID NOT NULL,
|
||||
"polimed_appointment_id" TEXT,
|
||||
"arrived_at" TIMESTAMP(3) NOT NULL,
|
||||
"service_started_at" TIMESTAMP(3),
|
||||
"service_ended_at" TIMESTAMP(3),
|
||||
"left_without_service" BOOLEAN NOT NULL DEFAULT false,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "visits_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "biometry_access_log" (
|
||||
"id" UUID NOT NULL,
|
||||
"actor_user_id" UUID,
|
||||
"subject_patient_id" UUID,
|
||||
"action" TEXT NOT NULL,
|
||||
"request_path" TEXT,
|
||||
"occurred_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "biometry_access_log_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "consent_revocation_jobs" (
|
||||
"id" UUID NOT NULL,
|
||||
"patient_id" UUID NOT NULL,
|
||||
"revoked_at" TIMESTAMP(3) NOT NULL,
|
||||
"scheduled_for" TIMESTAMP(3) NOT NULL,
|
||||
"status" "ConsentRevocationStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"completed_at" TIMESTAMP(3),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "consent_revocation_jobs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "refresh_tokens_token_hash_key" ON "refresh_tokens"("token_hash");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "refresh_tokens_user_id_idx" ON "refresh_tokens"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "patients_polimed_patient_id_key" ON "patients"("polimed_patient_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "patient_consents_patient_id_idx" ON "patient_consents"("patient_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "zones_code_key" ON "zones"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "cameras_name_key" ON "cameras"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "tracks_status_first_seen_at_idx" ON "tracks"("status", "first_seen_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "tracks_patient_id_idx" ON "tracks"("patient_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "track_events_track_id_occurred_at_idx" ON "track_events"("track_id", "occurred_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "face_embeddings_track_id_idx" ON "face_embeddings"("track_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "face_embeddings_patient_id_idx" ON "face_embeddings"("patient_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "face_embeddings_captured_at_idx" ON "face_embeddings"("captured_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "visits_patient_id_arrived_at_idx" ON "visits"("patient_id", "arrived_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "biometry_access_log_occurred_at_idx" ON "biometry_access_log"("occurred_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "biometry_access_log_subject_patient_id_idx" ON "biometry_access_log"("subject_patient_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "consent_revocation_jobs_status_scheduled_for_idx" ON "consent_revocation_jobs"("status", "scheduled_for");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "refresh_tokens" ADD CONSTRAINT "refresh_tokens_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "patient_consents" ADD CONSTRAINT "patient_consents_patient_id_fkey" FOREIGN KEY ("patient_id") REFERENCES "patients"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "patient_consents" ADD CONSTRAINT "patient_consents_actor_user_id_fkey" FOREIGN KEY ("actor_user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "cameras" ADD CONSTRAINT "cameras_zone_id_fkey" FOREIGN KEY ("zone_id") REFERENCES "zones"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "tracks" ADD CONSTRAINT "tracks_patient_id_fkey" FOREIGN KEY ("patient_id") REFERENCES "patients"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "track_events" ADD CONSTRAINT "track_events_track_id_fkey" FOREIGN KEY ("track_id") REFERENCES "tracks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "track_events" ADD CONSTRAINT "track_events_camera_id_fkey" FOREIGN KEY ("camera_id") REFERENCES "cameras"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "track_events" ADD CONSTRAINT "track_events_zone_id_fkey" FOREIGN KEY ("zone_id") REFERENCES "zones"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "face_embeddings" ADD CONSTRAINT "face_embeddings_track_id_fkey" FOREIGN KEY ("track_id") REFERENCES "tracks"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "face_embeddings" ADD CONSTRAINT "face_embeddings_camera_id_fkey" FOREIGN KEY ("camera_id") REFERENCES "cameras"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "visits" ADD CONSTRAINT "visits_patient_id_fkey" FOREIGN KEY ("patient_id") REFERENCES "patients"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "biometry_access_log" ADD CONSTRAINT "biometry_access_log_actor_user_id_fkey" FOREIGN KEY ("actor_user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "consent_revocation_jobs" ADD CONSTRAINT "consent_revocation_jobs_patient_id_fkey" FOREIGN KEY ("patient_id") REFERENCES "patients"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["postgresqlExtensions"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
extensions = [vector, uuidOssp(map: "uuid-ossp")]
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// AUTH / RBAC
|
||||
// =========================================================================
|
||||
|
||||
enum Role {
|
||||
MANAGER
|
||||
SENIOR_ADMIN
|
||||
SECURITY
|
||||
SYSADMIN
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
email String @unique
|
||||
fullName String @map("full_name")
|
||||
passwordHash String @map("password_hash")
|
||||
role Role
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
refreshTokens RefreshToken[]
|
||||
consentActions PatientConsent[]
|
||||
biometryAccessLog BiometryAccessLog[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model RefreshToken {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
tokenHash String @unique @map("token_hash")
|
||||
expiresAt DateTime @map("expires_at")
|
||||
revokedAt DateTime? @map("revoked_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@map("refresh_tokens")
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// DOMAIN: PATIENT / CONSENT
|
||||
// =========================================================================
|
||||
|
||||
model Patient {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
polimedPatientId String? @unique @map("polimed_patient_id")
|
||||
fullName String? @map("full_name")
|
||||
consentReceivedAt DateTime? @map("consent_received_at")
|
||||
consentRevokedAt DateTime? @map("consent_revoked_at")
|
||||
pendingDeletionAt DateTime? @map("pending_deletion_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
consents PatientConsent[]
|
||||
tracks Track[]
|
||||
visits Visit[]
|
||||
consentRevocationJobs ConsentRevocationJob[]
|
||||
|
||||
@@map("patients")
|
||||
}
|
||||
|
||||
enum ConsentAction {
|
||||
GRANTED
|
||||
REVOKED
|
||||
}
|
||||
|
||||
model PatientConsent {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
patientId String @map("patient_id") @db.Uuid
|
||||
action ConsentAction
|
||||
paperRef String? @map("paper_ref")
|
||||
actorUserId String @map("actor_user_id") @db.Uuid
|
||||
occurredAt DateTime @default(now()) @map("occurred_at")
|
||||
|
||||
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
|
||||
actor User @relation(fields: [actorUserId], references: [id])
|
||||
|
||||
@@index([patientId])
|
||||
@@map("patient_consents")
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CAMERAS / ZONES
|
||||
// =========================================================================
|
||||
|
||||
enum ZoneCode {
|
||||
A
|
||||
B
|
||||
C
|
||||
D
|
||||
E
|
||||
}
|
||||
|
||||
model Zone {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
code ZoneCode @unique
|
||||
name String
|
||||
|
||||
cameras Camera[]
|
||||
trackEvents TrackEvent[]
|
||||
|
||||
@@map("zones")
|
||||
}
|
||||
|
||||
model Camera {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
name String @unique
|
||||
rtspUrl String? @map("rtsp_url")
|
||||
zoneId String @map("zone_id") @db.Uuid
|
||||
|
||||
zone Zone @relation(fields: [zoneId], references: [id])
|
||||
trackEvents TrackEvent[]
|
||||
faceEmbeddings FaceEmbedding[]
|
||||
|
||||
@@map("cameras")
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TRACKS / EVENTS / EMBEDDINGS
|
||||
// =========================================================================
|
||||
|
||||
enum TrackStatus {
|
||||
UNMATCHED
|
||||
MATCHED
|
||||
ANONYMIZED
|
||||
}
|
||||
|
||||
model Track {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
patientId String? @map("patient_id") @db.Uuid
|
||||
status TrackStatus @default(UNMATCHED)
|
||||
firstSeenAt DateTime @map("first_seen_at")
|
||||
lastSeenAt DateTime @map("last_seen_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
patient Patient? @relation(fields: [patientId], references: [id])
|
||||
events TrackEvent[]
|
||||
faceEmbeddings FaceEmbedding[]
|
||||
|
||||
@@index([status, firstSeenAt])
|
||||
@@index([patientId])
|
||||
@@map("tracks")
|
||||
}
|
||||
|
||||
enum TrackEventType {
|
||||
arrived
|
||||
waiting
|
||||
service_started
|
||||
service_ended
|
||||
left_without_service
|
||||
}
|
||||
|
||||
model TrackEvent {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
trackId String @map("track_id") @db.Uuid
|
||||
type TrackEventType
|
||||
cameraId String @map("camera_id") @db.Uuid
|
||||
zoneId String @map("zone_id") @db.Uuid
|
||||
occurredAt DateTime @map("occurred_at")
|
||||
evidenceKey String? @map("evidence_key")
|
||||
// [x1, y1, x2, y2] нормализовано 0..1 — bbox распознанного лица на кадре.
|
||||
faceBbox Json? @map("face_bbox") @db.JsonB
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
track Track @relation(fields: [trackId], references: [id], onDelete: Cascade)
|
||||
camera Camera @relation(fields: [cameraId], references: [id])
|
||||
zone Zone @relation(fields: [zoneId], references: [id])
|
||||
|
||||
@@index([trackId, occurredAt])
|
||||
@@map("track_events")
|
||||
}
|
||||
|
||||
// 512-d face embedding (InsightFace buffalo_l).
|
||||
// face-service пишет и читает напрямую через psycopg2 + pgvector.
|
||||
model FaceEmbedding {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
embedding Unsupported("vector(512)")
|
||||
patientId String? @map("patient_id") @db.Uuid
|
||||
trackId String? @map("track_id") @db.Uuid
|
||||
cameraId String @map("camera_id") @db.Uuid
|
||||
quality Float @default(0)
|
||||
capturedAt DateTime @map("captured_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
track Track? @relation(fields: [trackId], references: [id], onDelete: SetNull)
|
||||
camera Camera @relation(fields: [cameraId], references: [id])
|
||||
|
||||
@@index([trackId])
|
||||
@@index([patientId])
|
||||
@@index([capturedAt])
|
||||
@@map("face_embeddings")
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// VISITS
|
||||
// =========================================================================
|
||||
|
||||
model Visit {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
patientId String @map("patient_id") @db.Uuid
|
||||
polimedAppointmentId String? @map("polimed_appointment_id")
|
||||
arrivedAt DateTime @map("arrived_at")
|
||||
serviceStartedAt DateTime? @map("service_started_at")
|
||||
serviceEndedAt DateTime? @map("service_ended_at")
|
||||
leftWithoutService Boolean @default(false) @map("left_without_service")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([patientId, arrivedAt])
|
||||
@@map("visits")
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// AUDIT
|
||||
// =========================================================================
|
||||
|
||||
model BiometryAccessLog {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
actorUserId String? @map("actor_user_id") @db.Uuid
|
||||
subjectPatientId String? @map("subject_patient_id") @db.Uuid
|
||||
action String
|
||||
requestPath String? @map("request_path")
|
||||
occurredAt DateTime @default(now()) @map("occurred_at")
|
||||
|
||||
actor User? @relation(fields: [actorUserId], references: [id])
|
||||
|
||||
@@index([occurredAt])
|
||||
@@index([subjectPatientId])
|
||||
@@map("biometry_access_log")
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CONSENT REVOCATION JOBS (24h delete)
|
||||
// =========================================================================
|
||||
|
||||
enum ConsentRevocationStatus {
|
||||
PENDING
|
||||
DONE
|
||||
}
|
||||
|
||||
model ConsentRevocationJob {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
patientId String @map("patient_id") @db.Uuid
|
||||
revokedAt DateTime @map("revoked_at")
|
||||
scheduledFor DateTime @map("scheduled_for")
|
||||
status ConsentRevocationStatus @default(PENDING)
|
||||
completedAt DateTime? @map("completed_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([status, scheduledFor])
|
||||
@@map("consent_revocation_jobs")
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { PrismaClient, Role, ZoneCode } from '@prisma/client';
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const SEED_USERS: Array<{
|
||||
email: string;
|
||||
fullName: string;
|
||||
role: Role;
|
||||
passwordEnv: string;
|
||||
passwordFallback: string;
|
||||
}> = [
|
||||
{
|
||||
email: 'manager@local',
|
||||
fullName: 'Иван Управляющий',
|
||||
role: Role.MANAGER,
|
||||
passwordEnv: 'SEED_PASSWORD_MANAGER',
|
||||
passwordFallback: 'manager123',
|
||||
},
|
||||
{
|
||||
email: 'senior@local',
|
||||
fullName: 'Мария Старший Администратор',
|
||||
role: Role.SENIOR_ADMIN,
|
||||
passwordEnv: 'SEED_PASSWORD_SENIOR',
|
||||
passwordFallback: 'senior123',
|
||||
},
|
||||
{
|
||||
email: 'security@local',
|
||||
fullName: 'Пётр Безопасность',
|
||||
role: Role.SECURITY,
|
||||
passwordEnv: 'SEED_PASSWORD_SECURITY',
|
||||
passwordFallback: 'security123',
|
||||
},
|
||||
{
|
||||
email: 'admin@local',
|
||||
fullName: 'Анна Админ Системы',
|
||||
role: Role.SYSADMIN,
|
||||
passwordEnv: 'SEED_PASSWORD_SYSADMIN',
|
||||
passwordFallback: 'admin123',
|
||||
},
|
||||
];
|
||||
|
||||
const SEED_ZONES: Array<{ code: ZoneCode; name: string }> = [
|
||||
{ code: ZoneCode.A, name: 'Вход в клинику' },
|
||||
{ code: ZoneCode.B, name: 'Коридор / зона ожидания' },
|
||||
{ code: ZoneCode.C, name: 'Стойка рецепции' },
|
||||
{ code: ZoneCode.D, name: 'Перед кабинетом врача' },
|
||||
{ code: ZoneCode.E, name: 'В кабинете врача' },
|
||||
];
|
||||
|
||||
const SEED_CAMERAS: Array<{ name: string; zoneCode: ZoneCode }> = [
|
||||
{ name: 'cam-entrance', zoneCode: ZoneCode.A },
|
||||
{ name: 'cam-corridor', zoneCode: ZoneCode.B },
|
||||
// На рецепции 4 рабочих места — отдельная камера на каждое (С1…С4).
|
||||
{ name: 'cam-reception-1', zoneCode: ZoneCode.C },
|
||||
{ name: 'cam-reception-2', zoneCode: ZoneCode.C },
|
||||
{ name: 'cam-reception-3', zoneCode: ZoneCode.C },
|
||||
{ name: 'cam-reception-4', zoneCode: ZoneCode.C },
|
||||
{ name: 'cam-doctor-waiting', zoneCode: ZoneCode.D },
|
||||
{ name: 'cam-doctor-office', zoneCode: ZoneCode.E },
|
||||
];
|
||||
|
||||
async function main() {
|
||||
console.log('🌱 Seeding reception database...');
|
||||
|
||||
// Users
|
||||
for (const u of SEED_USERS) {
|
||||
const password = process.env[u.passwordEnv] ?? u.passwordFallback;
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
await prisma.user.upsert({
|
||||
where: { email: u.email },
|
||||
update: { fullName: u.fullName, role: u.role, passwordHash, isActive: true },
|
||||
create: { email: u.email, fullName: u.fullName, role: u.role, passwordHash, isActive: true },
|
||||
});
|
||||
console.log(` ✓ user ${u.email} (${u.role})`);
|
||||
}
|
||||
|
||||
// Zones
|
||||
for (const z of SEED_ZONES) {
|
||||
await prisma.zone.upsert({
|
||||
where: { code: z.code },
|
||||
update: { name: z.name },
|
||||
create: z,
|
||||
});
|
||||
console.log(` ✓ zone ${z.code} — ${z.name}`);
|
||||
}
|
||||
|
||||
// Cameras (bound to zones)
|
||||
for (const c of SEED_CAMERAS) {
|
||||
const zone = await prisma.zone.findUniqueOrThrow({ where: { code: c.zoneCode } });
|
||||
await prisma.camera.upsert({
|
||||
where: { name: c.name },
|
||||
update: { zoneId: zone.id },
|
||||
create: { name: c.name, zoneId: zone.id },
|
||||
});
|
||||
console.log(` ✓ camera ${c.name} → zone ${c.zoneCode}`);
|
||||
}
|
||||
|
||||
console.log('✅ Seed complete.');
|
||||
}
|
||||
|
||||
main()
|
||||
.then(async () => {
|
||||
await prisma.$disconnect();
|
||||
})
|
||||
.catch(async (e) => {
|
||||
console.error(e);
|
||||
await prisma.$disconnect();
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user