Files
Vector/packages/db/prisma/schema.prisma
T
josh 7c0d422228
CI / Lint · Typecheck · Test · Build (push) Failing after 5m41s
CI / Playwright (smoke) (push) Has been skipped
chore: initial Vector 2.0 monorepo
Ground-up TypeScript rewrite of the Vector hardware parts inventory
system. Ships the full roadmap (Phases 0-8) in one initial commit:

- pnpm + Turbo monorepo: apps/{api,web,e2e}, packages/{db,shared,ui,config}
- Express 5 + Prisma 5 + zod validation + JWT w/ refresh-token rotation
- React 19 + Vite + shadcn/ui + TanStack Query/Table + nuqs URL state
- Repair/RMA, tags, bulk ops, saved views, CSV audit export
- Analytics dashboard on Recharts + EOL tracking
- Signed webhook subscriptions (HMAC-SHA256) with in-process emitter
- Vitest unit tests (shared schemas, api services/helpers) + Playwright skeleton
- Gitea Actions CI (lint, typecheck, test+coverage, build) + Renovate

Deferred follow-ups: Postgres cutover (data-migration script ready),
BullMQ worker for webhook delivery, @react-pdf PDF export, CSV import wizard.
2026-04-16 20:52:32 -04:00

243 lines
7.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[]
repairAssignments RepairJob[] @relation("RepairAssignee")
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
eolDate DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parts Part[]
}
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
mpn String
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)
replacementPartId String?
replacement Part? @relation("PartReplacement", fields: [replacementPartId], references: [id], onDelete: SetNull)
replacedBy Part[] @relation("PartReplacement")
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
events PartEvent[]
tags PartTag[]
repairs RepairJob[]
@@index([state])
@@index([binId])
@@index([manufacturerId])
@@index([mpn])
@@index([categoryId])
@@index([replacementPartId])
}
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())
name String @unique
location String?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
repairs RepairJob[]
}
model RepairJob {
id String @id @default(uuid())
partId String
part Part @relation(fields: [partId], references: [id], onDelete: Cascade)
hostId String?
host Host? @relation(fields: [hostId], references: [id], onDelete: SetNull)
assigneeId String?
assignee User? @relation("RepairAssignee", fields: [assigneeId], references: [id], onDelete: SetNull)
status String @default("PENDING")
openedAt DateTime @default(now())
closedAt DateTime?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([partId])
@@index([status])
@@index([hostId])
@@index([assigneeId])
@@index([status, openedAt(sort: Desc)])
}
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])
}