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.
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"username" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"passwordHash" TEXT NOT NULL,
|
||||
"role" TEXT NOT NULL DEFAULT 'TECHNICIAN',
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Manufacturer" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Site" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Room" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"siteId" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "Room_siteId_fkey" FOREIGN KEY ("siteId") REFERENCES "Site" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Bin" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"roomId" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "Bin_roomId_fkey" FOREIGN KEY ("roomId") REFERENCES "Room" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Part" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"serialNumber" TEXT NOT NULL,
|
||||
"mpn" TEXT NOT NULL,
|
||||
"manufacturerId" TEXT NOT NULL,
|
||||
"price" REAL,
|
||||
"state" TEXT NOT NULL DEFAULT 'SPARE',
|
||||
"binId" TEXT,
|
||||
"notes" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "Part_manufacturerId_fkey" FOREIGN KEY ("manufacturerId") REFERENCES "Manufacturer" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "Part_binId_fkey" FOREIGN KEY ("binId") REFERENCES "Bin" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PartEvent" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"partId" TEXT NOT NULL,
|
||||
"userId" TEXT,
|
||||
"type" TEXT NOT NULL,
|
||||
"field" TEXT,
|
||||
"oldValue" TEXT,
|
||||
"newValue" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "PartEvent_partId_fkey" FOREIGN KEY ("partId") REFERENCES "Part" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "PartEvent_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Manufacturer_name_key" ON "Manufacturer"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Site_name_key" ON "Site"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Room_siteId_idx" ON "Room"("siteId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Room_siteId_name_key" ON "Room"("siteId", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Bin_roomId_idx" ON "Bin"("roomId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Bin_roomId_name_key" ON "Bin"("roomId", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Part_serialNumber_key" ON "Part"("serialNumber");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Part_state_idx" ON "Part"("state");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Part_binId_idx" ON "Part"("binId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Part_manufacturerId_idx" ON "Part"("manufacturerId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Part_mpn_idx" ON "Part"("mpn");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "PartEvent_partId_createdAt_idx" ON "PartEvent"("partId", "createdAt" DESC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "PartEvent_userId_idx" ON "PartEvent"("userId");
|
||||
@@ -0,0 +1,20 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "RefreshToken" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"userId" TEXT NOT NULL,
|
||||
"tokenHash" TEXT NOT NULL,
|
||||
"expiresAt" DATETIME NOT NULL,
|
||||
"revokedAt" DATETIME,
|
||||
"replacedBy" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "RefreshToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "RefreshToken_tokenHash_key" ON "RefreshToken"("tokenHash");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "RefreshToken_userId_idx" ON "RefreshToken"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "RefreshToken_expiresAt_idx" ON "RefreshToken"("expiresAt");
|
||||
@@ -0,0 +1,173 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Manufacturer" ADD COLUMN "eolDate" DATETIME;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Category" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Tag" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"color" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PartTag" (
|
||||
"partId" TEXT NOT NULL,
|
||||
"tagId" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
PRIMARY KEY ("partId", "tagId"),
|
||||
CONSTRAINT "PartTag_partId_fkey" FOREIGN KEY ("partId") REFERENCES "Part" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "PartTag_tagId_fkey" FOREIGN KEY ("tagId") REFERENCES "Tag" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Host" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"location" TEXT,
|
||||
"notes" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "RepairJob" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"partId" TEXT NOT NULL,
|
||||
"hostId" TEXT,
|
||||
"assigneeId" TEXT,
|
||||
"status" TEXT NOT NULL DEFAULT 'PENDING',
|
||||
"openedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"closedAt" DATETIME,
|
||||
"notes" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "RepairJob_partId_fkey" FOREIGN KEY ("partId") REFERENCES "Part" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "RepairJob_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
CONSTRAINT "RepairJob_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WebhookSubscription" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"url" TEXT NOT NULL,
|
||||
"secret" TEXT NOT NULL,
|
||||
"events" TEXT NOT NULL,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SavedView" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"userId" TEXT NOT NULL,
|
||||
"resource" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"filterJson" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "SavedView_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CsvImportJob" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"userId" TEXT,
|
||||
"resource" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'PENDING',
|
||||
"filename" TEXT NOT NULL,
|
||||
"stagedRows" INTEGER NOT NULL DEFAULT 0,
|
||||
"errors" TEXT,
|
||||
"startedAt" DATETIME,
|
||||
"finishedAt" DATETIME,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "CsvImportJob_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_Part" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"serialNumber" TEXT NOT NULL,
|
||||
"mpn" TEXT NOT NULL,
|
||||
"manufacturerId" TEXT NOT NULL,
|
||||
"price" REAL,
|
||||
"state" TEXT NOT NULL DEFAULT 'SPARE',
|
||||
"binId" TEXT,
|
||||
"categoryId" TEXT,
|
||||
"replacementPartId" TEXT,
|
||||
"notes" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "Part_manufacturerId_fkey" FOREIGN KEY ("manufacturerId") REFERENCES "Manufacturer" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "Part_binId_fkey" FOREIGN KEY ("binId") REFERENCES "Bin" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
CONSTRAINT "Part_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
CONSTRAINT "Part_replacementPartId_fkey" FOREIGN KEY ("replacementPartId") REFERENCES "Part" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_Part" ("binId", "createdAt", "id", "manufacturerId", "mpn", "notes", "price", "serialNumber", "state", "updatedAt") SELECT "binId", "createdAt", "id", "manufacturerId", "mpn", "notes", "price", "serialNumber", "state", "updatedAt" FROM "Part";
|
||||
DROP TABLE "Part";
|
||||
ALTER TABLE "new_Part" RENAME TO "Part";
|
||||
CREATE UNIQUE INDEX "Part_serialNumber_key" ON "Part"("serialNumber");
|
||||
CREATE INDEX "Part_state_idx" ON "Part"("state");
|
||||
CREATE INDEX "Part_binId_idx" ON "Part"("binId");
|
||||
CREATE INDEX "Part_manufacturerId_idx" ON "Part"("manufacturerId");
|
||||
CREATE INDEX "Part_mpn_idx" ON "Part"("mpn");
|
||||
CREATE INDEX "Part_categoryId_idx" ON "Part"("categoryId");
|
||||
CREATE INDEX "Part_replacementPartId_idx" ON "Part"("replacementPartId");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Category_name_key" ON "Category"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Tag_name_key" ON "Tag"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "PartTag_tagId_idx" ON "PartTag"("tagId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Host_name_key" ON "Host"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "RepairJob_partId_idx" ON "RepairJob"("partId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "RepairJob_status_idx" ON "RepairJob"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "RepairJob_hostId_idx" ON "RepairJob"("hostId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "RepairJob_assigneeId_idx" ON "RepairJob"("assigneeId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "RepairJob_status_openedAt_idx" ON "RepairJob"("status", "openedAt" DESC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "WebhookSubscription_active_idx" ON "WebhookSubscription"("active");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "SavedView_userId_idx" ON "SavedView"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "SavedView_userId_resource_name_key" ON "SavedView"("userId", "resource", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "CsvImportJob_userId_idx" ON "CsvImportJob"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "CsvImportJob_status_idx" ON "CsvImportJob"("status");
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "sqlite"
|
||||
@@ -0,0 +1,242 @@
|
||||
// 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])
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { prisma } from '../src/client.js';
|
||||
|
||||
async function main() {
|
||||
const passwordHash = await bcrypt.hash('admin', 12);
|
||||
|
||||
const admin = await prisma.user.upsert({
|
||||
where: { username: 'admin' },
|
||||
update: {},
|
||||
create: {
|
||||
username: 'admin',
|
||||
email: 'admin@vector.local',
|
||||
passwordHash,
|
||||
role: 'ADMIN',
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Seeded admin user: ${admin.username} (${admin.email})`);
|
||||
console.log('Default password: admin — change this immediately!');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => prisma.$disconnect());
|
||||
Reference in New Issue
Block a user