Files
Vector/packages/db/prisma/schema.prisma
T
josh 3d77f2846d
CI / Lint · Typecheck · Test · Build (push) Successful in 44s
CI / Playwright (smoke) (push) Has been skipped
CI / Build & push images (push) Successful in 1m0s
feat: split Repairs into FM, Repair, and Custody workflows
The old Repairs module had grown ticketing-system features (status lifecycle,
comments, assignee, notes) that duplicate what the external ticketing tool
already owns. Vector only needs to track whether maintenance is open or closed.

- Rename RepairJob -> Fm (OPEN/CLOSED only), drop RepairComment, assignee, notes
- New Repair table: persistent log of physical part swaps, with ingest on
  unknown broken MPN via partModels.upsertByMpn
- New custody model: PENDING_DROP_IN_CUSTODY / PENDING_DESTRUCTION_IN_CUSTODY
  states + Part.custodianId, with a "My Custody" page for drop-off
- PartModel.destroyOnFail routes broken parts to the destruction path
- Host lookup on /fms and /repairs accepts hostId XOR assetId
- Wire the dormant webhook emitter: fm.opened, fm.closed, repair.logged
- Single fresh Prisma migration (dev DB was wiped, no backfill)

Tests: 60 passing (custody transitions in parts.test.ts; new fms.test.ts,
repairs.test.ts, custody.test.ts covering happy paths, validation failures,
webhook emissions, and ingest-on-unknown-MPN).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 12:22:56 -04:00

302 lines
9.1 KiB
Plaintext

// NOTE: provider is temporarily set to "sqlite" for Phase 1 local verification.
// Flip to "postgresql" once Docker + docker-compose Postgres is available.
// All cascade rules and indexes below are portable between providers.
//
// Postgres-only additions applied post-cutover (see packages/db/POSTGRES_FTS.md):
// * Generated tsvector column on Part(serial, mpn, notes) + GIN index (Phase 3 scope).
// * WebhookSubscription.events and SavedView.filterJson currently stored as String (JSON
// text) for SQLite portability; on Postgres these become String[] and Jsonb respectively.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
username String @unique
email String @unique
passwordHash String
role String @default("TECHNICIAN")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
partEvents PartEvent[]
refreshTokens RefreshToken[]
custodyParts Part[] @relation("Custody")
repairs Repair[]
savedViews SavedView[]
csvImportJobs CsvImportJob[]
}
model RefreshToken {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
tokenHash String @unique
expiresAt DateTime
revokedAt DateTime?
replacedBy String?
createdAt DateTime @default(now())
@@index([userId])
@@index([expiresAt])
}
model Manufacturer {
id String @id @default(uuid())
name String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parts Part[]
partModels PartModel[]
}
model PartModel {
id String @id @default(uuid())
manufacturerId String
manufacturer Manufacturer @relation(fields: [manufacturerId], references: [id], onDelete: Restrict)
mpn String
eolDate DateTime?
destroyOnFail Boolean @default(false)
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parts Part[]
@@unique([manufacturerId, mpn])
@@index([manufacturerId])
@@index([eolDate])
}
model Site {
id String @id @default(uuid())
name String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
rooms Room[]
}
model Room {
id String @id @default(uuid())
name String
siteId String
site Site @relation(fields: [siteId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
bins Bin[]
@@unique([siteId, name])
@@index([siteId])
}
model Bin {
id String @id @default(uuid())
name String
roomId String
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parts Part[]
@@unique([roomId, name])
@@index([roomId])
}
model Category {
id String @id @default(uuid())
name String @unique
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parts Part[]
}
model Part {
id String @id @default(uuid())
serialNumber String @unique
partModelId String
partModel PartModel @relation(fields: [partModelId], references: [id], onDelete: Restrict)
manufacturerId String
manufacturer Manufacturer @relation(fields: [manufacturerId], references: [id], onDelete: Restrict)
price Float?
state String @default("SPARE")
binId String?
bin Bin? @relation(fields: [binId], references: [id], onDelete: SetNull)
categoryId String?
category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
hostId String?
host Host? @relation(fields: [hostId], references: [id], onDelete: SetNull)
custodianId String?
custodian User? @relation("Custody", fields: [custodianId], references: [id], onDelete: SetNull)
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
events PartEvent[]
tags PartTag[]
problemInFms FmPart[]
brokenRepairs Repair[] @relation("BrokenRepairs")
replacementRepairs Repair[] @relation("ReplacementRepairs")
@@index([state])
@@index([binId])
@@index([manufacturerId])
@@index([partModelId])
@@index([categoryId])
@@index([hostId])
@@index([custodianId])
}
model PartEvent {
id String @id @default(uuid())
partId String
part Part @relation(fields: [partId], references: [id], onDelete: Cascade)
userId String?
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
type String
field String?
oldValue String?
newValue String?
createdAt DateTime @default(now())
@@index([partId, createdAt(sort: Desc)])
@@index([userId])
}
model Tag {
id String @id @default(uuid())
name String @unique
color String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parts PartTag[]
}
model PartTag {
partId String
tagId String
part Part @relation(fields: [partId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@id([partId, tagId])
@@index([tagId])
}
model Host {
id String @id @default(uuid())
assetId String @unique
name String @unique
location String?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parts Part[]
fms Fm[]
repairs Repair[]
}
model Fm {
id String @id @default(uuid())
hostId String
host Host @relation(fields: [hostId], references: [id], onDelete: Restrict)
status String @default("OPEN")
problem String
openedAt DateTime @default(now())
closedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
problemParts FmPart[]
repairs Repair[]
@@index([status])
@@index([hostId])
@@index([status, openedAt(sort: Desc)])
@@map("fms")
}
model FmPart {
fmId String
partId String
fm Fm @relation(fields: [fmId], references: [id], onDelete: Cascade)
part Part @relation(fields: [partId], references: [id], onDelete: Restrict)
createdAt DateTime @default(now())
@@id([fmId, partId])
@@index([partId])
@@map("fm_parts")
}
model Repair {
id String @id @default(uuid())
hostId String
host Host @relation(fields: [hostId], references: [id], onDelete: Restrict)
brokenPartId String
brokenPart Part @relation("BrokenRepairs", fields: [brokenPartId], references: [id], onDelete: Restrict)
replacementPartId String
replacement Part @relation("ReplacementRepairs", fields: [replacementPartId], references: [id], onDelete: Restrict)
performedById String
performedBy User @relation(fields: [performedById], references: [id], onDelete: Restrict)
performedAt DateTime @default(now())
fmId String?
fm Fm? @relation(fields: [fmId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([hostId, performedAt(sort: Desc)])
@@index([fmId])
@@index([performedById, performedAt(sort: Desc)])
@@index([brokenPartId])
@@index([replacementPartId])
@@map("repairs")
}
model WebhookSubscription {
id String @id @default(uuid())
url String
secret String
// JSON array of WebhookEventName values. Becomes String[] on Postgres cutover.
events String
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([active])
}
model SavedView {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
resource String
name String
// JSON blob describing filters/sort/columns. Becomes Jsonb on Postgres cutover.
filterJson String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, resource, name])
@@index([userId])
}
model CsvImportJob {
id String @id @default(uuid())
userId String?
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
resource String
status String @default("PENDING")
filename String
stagedRows Int @default(0)
// JSON array of CsvImportRowError. Nullable until validation runs.
errors String?
startedAt DateTime?
finishedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([status])
}