Schema (prisma/schema.prisma):
- model LoginPairing met id (cuid), secret_hash + desktop_token_hash (beide NOT
NULL — scheiden mobiel- en desktop-bewijs), status (pending|approved|consumed
|cancelled), optionele user_id met onDelete: SetNull, desktop_ua VarChar(255),
desktop_ip VarChar(45) voor IPv6, created_at + expires_at + approved_at +
consumed_at, indexes op (expires_at) en (status, expires_at)
- back-relation login_pairings LoginPairing[] op User
Migratie (20260427200734_add_login_pairing):
- Prisma-gegenereerde DDL voor login_pairings + indexes + FK
- Toegevoegde notify_pairing_change() functie + login_pairings_notify trigger
op AFTER INSERT/UPDATE; emit pg_notify('scrum4me_pairing', payload) met
{ op: 'I'|'U', pairing_id, status }
- DELETE niet ondersteund — pairings gaan naar consumed/cancelled, niet weg
- Channel naam analoog aan scrum4me_changes uit ST-801
Verification: Node pg-client roundtrip-test via DATABASE_URL toonde notifies bij
INSERT (op=I) en UPDATE (op=U) met correcte payload-shape.
Bouwt voort op M8 LISTEN/NOTIFY-infra. SSE-route /api/auth/pair/stream/[id] in
ST-1004 abonneert hierop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
269 lines
7.5 KiB
Text
269 lines
7.5 KiB
Text
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
generator erd {
|
|
provider = "prisma-erd-generator"
|
|
output = "../docs/erd.svg"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
}
|
|
|
|
enum Role {
|
|
PRODUCT_OWNER
|
|
SCRUM_MASTER
|
|
DEVELOPER
|
|
}
|
|
|
|
enum StoryStatus {
|
|
OPEN
|
|
IN_SPRINT
|
|
DONE
|
|
}
|
|
|
|
enum TaskStatus {
|
|
TO_DO
|
|
IN_PROGRESS
|
|
REVIEW
|
|
DONE
|
|
}
|
|
|
|
enum LogType {
|
|
IMPLEMENTATION_PLAN
|
|
TEST_RESULT
|
|
COMMIT
|
|
}
|
|
|
|
enum TestStatus {
|
|
PASSED
|
|
FAILED
|
|
}
|
|
|
|
enum SprintStatus {
|
|
ACTIVE
|
|
COMPLETED
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
username String @unique
|
|
email String? @unique
|
|
password_hash String
|
|
is_demo Boolean @default(false)
|
|
bio String? @db.VarChar(160)
|
|
bio_detail String? @db.VarChar(2000)
|
|
avatar_data Bytes?
|
|
active_product_id String?
|
|
active_product Product? @relation("UserActiveProduct", fields: [active_product_id], references: [id], onDelete: SetNull)
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
roles UserRole[]
|
|
api_tokens ApiToken[]
|
|
products Product[]
|
|
todos Todo[]
|
|
product_members ProductMember[]
|
|
assigned_stories Story[] @relation("StoryAssignee")
|
|
login_pairings LoginPairing[]
|
|
|
|
@@index([active_product_id])
|
|
@@map("users")
|
|
}
|
|
|
|
model UserRole {
|
|
id String @id @default(cuid())
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
user_id String
|
|
role Role
|
|
|
|
@@unique([user_id, role])
|
|
@@map("user_roles")
|
|
}
|
|
|
|
model ApiToken {
|
|
id String @id @default(cuid())
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
user_id String
|
|
token_hash String @unique
|
|
label String?
|
|
created_at DateTime @default(now())
|
|
revoked_at DateTime?
|
|
|
|
@@index([token_hash])
|
|
@@map("api_tokens")
|
|
}
|
|
|
|
model Product {
|
|
id String @id @default(cuid())
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
user_id String
|
|
name String
|
|
code String? @db.VarChar(30)
|
|
description String?
|
|
repo_url String?
|
|
definition_of_done String
|
|
archived Boolean @default(false)
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
pbis Pbi[]
|
|
sprints Sprint[]
|
|
stories Story[]
|
|
todos Todo[]
|
|
members ProductMember[]
|
|
active_for_users User[] @relation("UserActiveProduct")
|
|
|
|
@@unique([user_id, name])
|
|
@@unique([user_id, code])
|
|
@@index([user_id, archived])
|
|
@@map("products")
|
|
}
|
|
|
|
model Pbi {
|
|
id String @id @default(cuid())
|
|
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
|
product_id String
|
|
code String? @db.VarChar(30)
|
|
title String
|
|
description String?
|
|
priority Int
|
|
sort_order Float
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
stories Story[]
|
|
|
|
@@unique([product_id, code])
|
|
@@index([product_id, priority, sort_order])
|
|
@@map("pbis")
|
|
}
|
|
|
|
model Story {
|
|
id String @id @default(cuid())
|
|
pbi Pbi @relation(fields: [pbi_id], references: [id], onDelete: Cascade)
|
|
pbi_id String
|
|
product Product @relation(fields: [product_id], references: [id])
|
|
product_id String
|
|
sprint Sprint? @relation(fields: [sprint_id], references: [id])
|
|
sprint_id String?
|
|
assignee User? @relation("StoryAssignee", fields: [assignee_id], references: [id], onDelete: SetNull)
|
|
assignee_id String?
|
|
code String? @db.VarChar(30)
|
|
title String
|
|
description String?
|
|
acceptance_criteria String?
|
|
priority Int
|
|
sort_order Float
|
|
status StoryStatus @default(OPEN)
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
logs StoryLog[]
|
|
tasks Task[]
|
|
|
|
@@unique([product_id, code])
|
|
@@index([pbi_id, priority, sort_order])
|
|
@@index([sprint_id, sort_order])
|
|
@@index([product_id, status])
|
|
@@index([sprint_id, assignee_id])
|
|
@@map("stories")
|
|
}
|
|
|
|
model StoryLog {
|
|
id String @id @default(cuid())
|
|
story Story @relation(fields: [story_id], references: [id], onDelete: Cascade)
|
|
story_id String
|
|
type LogType
|
|
content String
|
|
status TestStatus?
|
|
commit_hash String?
|
|
commit_message String?
|
|
metadata Json?
|
|
created_at DateTime @default(now())
|
|
|
|
@@index([story_id, created_at])
|
|
@@map("story_logs")
|
|
}
|
|
|
|
model Sprint {
|
|
id String @id @default(cuid())
|
|
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
|
product_id String
|
|
sprint_goal String
|
|
status SprintStatus @default(ACTIVE)
|
|
created_at DateTime @default(now())
|
|
completed_at DateTime?
|
|
stories Story[]
|
|
tasks Task[]
|
|
|
|
@@index([product_id, status])
|
|
@@map("sprints")
|
|
}
|
|
|
|
model Task {
|
|
id String @id @default(cuid())
|
|
story Story @relation(fields: [story_id], references: [id], onDelete: Cascade)
|
|
story_id String
|
|
sprint Sprint? @relation(fields: [sprint_id], references: [id])
|
|
sprint_id String?
|
|
title String
|
|
description String?
|
|
implementation_plan String?
|
|
priority Int
|
|
sort_order Float
|
|
status TaskStatus @default(TO_DO)
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
|
|
@@index([story_id, priority, sort_order])
|
|
@@index([sprint_id, status])
|
|
@@map("tasks")
|
|
}
|
|
|
|
model ProductMember {
|
|
id String @id @default(cuid())
|
|
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
|
product_id String
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
user_id String
|
|
created_at DateTime @default(now())
|
|
|
|
@@unique([product_id, user_id])
|
|
@@index([user_id])
|
|
@@map("product_members")
|
|
}
|
|
|
|
model Todo {
|
|
id String @id @default(cuid())
|
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
|
user_id String
|
|
product Product? @relation(fields: [product_id], references: [id], onDelete: SetNull)
|
|
product_id String?
|
|
title String
|
|
description String? @db.VarChar(2000)
|
|
done Boolean @default(false)
|
|
archived Boolean @default(false)
|
|
created_at DateTime @default(now())
|
|
updated_at DateTime @updatedAt
|
|
|
|
@@index([user_id, done, archived])
|
|
@@index([user_id, product_id])
|
|
@@map("todos")
|
|
}
|
|
|
|
model LoginPairing {
|
|
id String @id @default(cuid())
|
|
secret_hash String
|
|
desktop_token_hash String
|
|
status String
|
|
user_id String?
|
|
user User? @relation(fields: [user_id], references: [id], onDelete: SetNull)
|
|
desktop_ua String? @db.VarChar(255)
|
|
desktop_ip String? @db.VarChar(45)
|
|
created_at DateTime @default(now())
|
|
expires_at DateTime
|
|
approved_at DateTime?
|
|
consumed_at DateTime?
|
|
|
|
@@index([expires_at])
|
|
@@index([status, expires_at])
|
|
@@map("login_pairings")
|
|
}
|