docs: sync data-model, glossary en specs met huidig schema (#164)
Brengt de docs gelijk met de werkelijkheid na PBI-46/47/50/58/59/61/63 en M12. Belangrijkste fixes: - data-model.md herschreven naar prisma/schema.prisma: nieuwe entiteiten (Idea, IdeaLog, IdeaProduct, UserQuestion, ClaudeQuestion, ClaudeJob, SprintRun, SprintTaskExecution, ClaudeWorker, LoginPairing, PushSubscription, ModelPrice, ProductMember), nieuwe enums (FAILED/EXCLUDED, OPEN/CLOSED/ARCHIVED, ADMIN, etc.) en codes (PBI/ST/T/SP-N) toegevoegd; verwijderde todos-tabel verwijderd. - glossary.md: Sprint zonder "max 1 actief" (PBI-63), Story/Task incl. FAILED/EXCLUDED, Todo verwijderd, Idea/SprintRun/ClaudeJob/ verify_result toegevoegd. - project-structure.md: app/(app)/todos vervangen door ideas/insights/jobs/manual/admin/solo; api-tree volledig. - overview.md: "geen realtime in v1" en Docker-rationale herschreven — Postgres LISTEN/NOTIFY + SSE, claude_jobs als queue, opt-in Docker-deploy-flow. - functional.md: F-08 Todo-lijst -> Ideeen-laag, F-09 multi-sprint, F-10 Task-status incl. FAILED/EXCLUDED, F-11 endpoint-lijst, navigatiestructuur, datamodel-schets en Flow 3 bijgewerkt. - README.md API-tabel: /api/todos weg, ideas/jobs/users/profile/health toegevoegd, kort over realtime/auth-pair/internal/cron. - patterns + mcp-integration runbook: Todo-/ACTIVE-references vervangen door Idea/OPEN; create_todo MCP-tool note over verwijdering. Linkcheck groen (105 files), INDEX hergegenereerd (98 docs). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3842c05ae9
commit
f7464db837
12 changed files with 544 additions and 424 deletions
|
|
@ -3,27 +3,34 @@ title: "Data Model & Prisma Schema"
|
|||
status: active
|
||||
audience: [maintainer, contributor]
|
||||
language: nl
|
||||
last_updated: 2026-05-03
|
||||
last_updated: 2026-05-08
|
||||
related: [auth-and-sessions.md](./auth-and-sessions.md)
|
||||
---
|
||||
|
||||
## Datamodel
|
||||
|
||||
> Bron van waarheid is [`prisma/schema.prisma`](../../prisma/schema.prisma); dit document samenvat de tabellen en sleutelinvarianten. Bij twijfel wint het schema.
|
||||
|
||||
### `users`
|
||||
|
||||
| Kolom | Type | Constraints | Noten |
|
||||
|---|---|---|---|
|
||||
| id | String (cuid) | PK | Gegenereerd door Prisma |
|
||||
| id | String (cuid) | PK | |
|
||||
| username | String | unique, not null, min 3 | Inlognaam |
|
||||
| email | String? | unique | Optioneel; gebruikt voor wachtwoord-reset-flows |
|
||||
| password_hash | String | not null | bcrypt hash (cost factor 12) |
|
||||
| is_demo | Boolean | default false | Demo-gebruiker heeft read-only rechten |
|
||||
| bio | String | nullable, max 160 | Korte profielomschrijving |
|
||||
| bio_detail | String | nullable, max 2000 | Uitgebreide profielbeschrijving |
|
||||
| avatar_data | Bytes | nullable | Profielfoto als WebP bytea (max 700×700) |
|
||||
| bio | String? | max 160 | Korte profielomschrijving |
|
||||
| bio_detail | String? | max 2000 | Uitgebreide profielbeschrijving |
|
||||
| must_reset_password | Boolean | default false | Forceert wachtwoord-reset bij volgende login |
|
||||
| avatar_data | Bytes? | | Profielfoto als WebP bytea (max 700×700) |
|
||||
| active_product_id | String? | FK → products (SetNull) | Persistente actieve PB-keuze (M9) |
|
||||
| idea_code_counter | Int | default 0 | Sequentiële teller voor user-scoped idea-codes |
|
||||
| min_quota_pct | Int | default 20 | Worker stand-by-drempel (M13 quota-check) |
|
||||
| created_at | DateTime | default now() | |
|
||||
| updated_at | DateTime | auto-update | Gebruikt als cache-buster voor avatar-URL |
|
||||
| updated_at | DateTime | auto-update | Cache-buster voor avatar-URL |
|
||||
|
||||
**Indexes:** `username` (unique lookup bij inloggen)
|
||||
**Indexes:** `username` (unique), `email` (unique), `active_product_id`
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -32,10 +39,9 @@ related: [auth-and-sessions.md](./auth-and-sessions.md)
|
|||
| Kolom | Type | Constraints | Noten |
|
||||
|---|---|---|---|
|
||||
| id | String (cuid) | PK | |
|
||||
| user_id | String | FK → users, not null | |
|
||||
| role | Enum | PRODUCT_OWNER \| SCRUM_MASTER \| DEVELOPER | |
|
||||
| user_id | String | FK → users (Cascade) | |
|
||||
| role | Enum | `PRODUCT_OWNER \| SCRUM_MASTER \| DEVELOPER \| ADMIN` | |
|
||||
|
||||
**Indexes:** `(user_id)` — meerdere rollen per gebruiker
|
||||
**Constraint:** unique `(user_id, role)`
|
||||
|
||||
---
|
||||
|
|
@ -45,13 +51,13 @@ related: [auth-and-sessions.md](./auth-and-sessions.md)
|
|||
| Kolom | Type | Constraints | Noten |
|
||||
|---|---|---|---|
|
||||
| id | String (cuid) | PK | |
|
||||
| user_id | String | FK → users, not null | |
|
||||
| token_hash | String | not null | SHA-256 hash van het token |
|
||||
| label | String | nullable | Bijv. "Claude Code — laptop" |
|
||||
| user_id | String | FK → users (Cascade) | |
|
||||
| token_hash | String | unique, not null | SHA-256 hash van het token |
|
||||
| label | String? | | Bijv. "Claude Code — laptop" |
|
||||
| created_at | DateTime | default now() | |
|
||||
| revoked_at | DateTime | nullable | Null = actief |
|
||||
| revoked_at | DateTime? | | Null = actief |
|
||||
|
||||
**Indexes:** `token_hash` (lookup bij elke API-aanroep — moet snel zijn)
|
||||
**Indexes:** `token_hash` (unique). Eén token kan max. één `claude_workers`-record hebben.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -60,17 +66,20 @@ related: [auth-and-sessions.md](./auth-and-sessions.md)
|
|||
| Kolom | Type | Constraints | Noten |
|
||||
|---|---|---|---|
|
||||
| id | String (cuid) | PK | |
|
||||
| user_id | String | FK → users, not null | |
|
||||
| name | String | not null, max 200 | Uniek per gebruiker |
|
||||
| description | String | nullable, max 1000 | |
|
||||
| repo_url | String | nullable | Gevalideerde URL |
|
||||
| definition_of_done | String | not null, max 500 | Vaste instelling per product |
|
||||
| user_id | String | FK → users (Cascade) | Eigenaar |
|
||||
| name | String | not null | Uniek per gebruiker |
|
||||
| code | String? | max 30 | Optionele afkorting; uniek per gebruiker als gezet |
|
||||
| description | String? | | |
|
||||
| repo_url | String? | | Gevalideerde URL |
|
||||
| definition_of_done | String | not null | Vaste instelling per product |
|
||||
| auto_pr | Boolean | default false | Automatische PR creëren na sprint-completion |
|
||||
| pr_strategy | Enum | `SPRINT \| STORY \| SPRINT_BATCH`, default `SPRINT` | Granulariteit voor auto-PR |
|
||||
| archived | Boolean | default false | |
|
||||
| created_at | DateTime | default now() | |
|
||||
| updated_at | DateTime | auto-update | |
|
||||
|
||||
**Indexes:** `(user_id, archived)` — standaard query filtert op actieve producten
|
||||
**Constraint:** unique `(user_id, name)`
|
||||
**Constraints:** unique `(user_id, name)`, unique `(user_id, code)`
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -79,19 +88,22 @@ related: [auth-and-sessions.md](./auth-and-sessions.md)
|
|||
| Kolom | Type | Constraints | Noten |
|
||||
|---|---|---|---|
|
||||
| id | String (cuid) | PK | |
|
||||
| product_id | String | FK → products (cascade delete) | |
|
||||
| code | String | nullable, max 30 | Auto-gegenereerd of handmatig |
|
||||
| title | String | not null, max 200 | |
|
||||
| description | String | nullable, max 2000 | |
|
||||
| priority | Int | 1–4, not null | 1 = Kritiek, 4 = Laag |
|
||||
| product_id | String | FK → products (Cascade) | |
|
||||
| code | String | max 30, not null | Verplicht; auto-gegenereerd of handmatig |
|
||||
| title | String | not null | |
|
||||
| description | String? | | |
|
||||
| priority | Int | 1–4 | 1 = Kritiek, 4 = Laag |
|
||||
| sort_order | Float | not null | Float voor volgorde tussen items zonder renummering |
|
||||
| status | Enum | READY \| BLOCKED \| DONE, default READY | Auto-promotie naar DONE bij sprint-close (zie hieronder) |
|
||||
| status | Enum | `READY \| BLOCKED \| FAILED \| DONE`, default `READY` | Auto-promotie naar DONE bij sprint-close |
|
||||
| pr_url | String? | | URL van de PR die deze PBI dekt (PBI-strategie) |
|
||||
| pr_merged_at | DateTime? | | Gezet wanneer de PR daadwerkelijk gemerged is |
|
||||
| created_at | DateTime | default now() | |
|
||||
| updated_at | DateTime | auto-update | |
|
||||
|
||||
**Indexes:** `(product_id, priority, sort_order)` — standaard query voor het gesplitste scherm; `(product_id, status)` — voor het statusfilter op de Product Backlog
|
||||
**Indexes:** `(product_id, priority, sort_order)`, `(product_id, status)`
|
||||
**Constraint:** unique `(product_id, code)`
|
||||
|
||||
**Cascade-regel (sprint-close):** wanneer een Sprint wordt afgerond via `completeSprintAction` en alle stories van een PBI eindigen op DONE (na toepassing van de afsluitbeslissingen), zet diezelfde transactie de PBI-status op DONE. Promotie alléén — een PBI op DONE wordt nooit automatisch teruggezet. Stories die niet in deze Sprint zaten worden meegerekend op hun huidige DB-status. Een PBI zonder stories blijft READY.
|
||||
**Cascade-regel (sprint-close):** wanneer een Sprint wordt afgerond via `completeSprintAction` en alle stories van een PBI eindigen op DONE, zet diezelfde transactie de PBI-status op DONE. Promotie alléén — een DONE-PBI wordt nooit automatisch teruggezet. Stories die niet in deze Sprint zaten worden meegerekend op hun huidige DB-status. Een PBI zonder stories blijft READY.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -100,21 +112,24 @@ related: [auth-and-sessions.md](./auth-and-sessions.md)
|
|||
| Kolom | Type | Constraints | Noten |
|
||||
|---|---|---|---|
|
||||
| id | String (cuid) | PK | |
|
||||
| pbi_id | String | FK → pbis (cascade delete) | |
|
||||
| pbi_id | String | FK → pbis (Cascade) | |
|
||||
| product_id | String | FK → products | Denormalisatie voor snellere queries |
|
||||
| sprint_id | String | FK → sprints, nullable | Null = in Product Backlog |
|
||||
| title | String | not null, max 200 | |
|
||||
| description | String | nullable, max 2000 | |
|
||||
| acceptance_criteria | String | nullable, max 2000 | |
|
||||
| priority | Int | 1–4, not null | |
|
||||
| sprint_id | String? | FK → sprints | Null = in Product Backlog |
|
||||
| assignee_id | String? | FK → users (SetNull) | Story-claim op het Solo-bord |
|
||||
| code | String | max 30, not null | Auto-gegenereerd of handmatig |
|
||||
| title | String | not null | |
|
||||
| description | String? | | |
|
||||
| acceptance_criteria | String? | | |
|
||||
| priority | Int | 1–4 | |
|
||||
| sort_order | Float | not null | |
|
||||
| status | Enum | OPEN \| IN_SPRINT \| DONE | |
|
||||
| status | Enum | `OPEN \| IN_SPRINT \| DONE \| FAILED`, default `OPEN` | |
|
||||
| created_at | DateTime | default now() | |
|
||||
| updated_at | DateTime | auto-update | |
|
||||
|
||||
**Indexes:** `(pbi_id, priority, sort_order)`, `(sprint_id, sort_order)`, `(product_id, status)`
|
||||
**Indexes:** `(pbi_id, priority, sort_order)`, `(sprint_id, sort_order)`, `(product_id, status)`, `(sprint_id, assignee_id)`
|
||||
**Constraint:** unique `(product_id, code)`
|
||||
|
||||
**Auto-promotie/demotie via task-status:** zodra alle tasks van een story op `DONE` staan en de huidige story-status nog niet `DONE` is, promoot dezelfde transactie de story naar `DONE`. Wordt een task van een `DONE`-story uit `DONE` getrokken (heropening), dan demoot de story terug naar `IN_SPRINT` — niet naar `OPEN`, want `OPEN` betekent "terug in productbacklog" en is een sprint-management-actie. De logica zit in [lib/tasks-status-update.ts](../../lib/tasks-status-update.ts) en wordt aangeroepen door alle drie de task-status-write-paden (`updateTaskStatusAction`, `saveTask` edit-mode, REST `PATCH /api/tasks/[id]`).
|
||||
**Auto-promotie/demotie via task-status:** zodra alle tasks van een story op `DONE` staan en de story-status nog niet `DONE` is, promoot dezelfde transactie de story naar `DONE`. Wordt een task van een `DONE`-story heropend, dan demoot de story terug naar `IN_SPRINT` — niet naar `OPEN` (dat zou "terug in productbacklog" betekenen, een sprint-management-actie). Logica zit in [lib/tasks-status-update.ts](../../lib/tasks-status-update.ts) en wordt aangeroepen door alle drie de task-status-write-paden (`updateTaskStatusAction`, `saveTask` edit-mode, REST `PATCH /api/tasks/[id]`).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -123,15 +138,16 @@ related: [auth-and-sessions.md](./auth-and-sessions.md)
|
|||
| Kolom | Type | Constraints | Noten |
|
||||
|---|---|---|---|
|
||||
| id | String (cuid) | PK | |
|
||||
| story_id | String | FK → stories (cascade delete) | |
|
||||
| type | Enum | IMPLEMENTATION_PLAN \| TEST_RESULT \| COMMIT | |
|
||||
| content | String | not null | Tekst van plan of testuitvoer |
|
||||
| status | Enum | PASSED \| FAILED, nullable | Alleen bij type TEST_RESULT |
|
||||
| commit_hash | String | nullable | Alleen bij type COMMIT |
|
||||
| commit_message | String | nullable | Alleen bij type COMMIT |
|
||||
| story_id | String | FK → stories (Cascade) | |
|
||||
| type | Enum | `IMPLEMENTATION_PLAN \| TEST_RESULT \| COMMIT` | |
|
||||
| content | String | not null | |
|
||||
| status | Enum | `PASSED \| FAILED`? | Alleen bij type `TEST_RESULT` |
|
||||
| commit_hash | String? | | Alleen bij type `COMMIT` |
|
||||
| commit_message | String? | | Alleen bij type `COMMIT` |
|
||||
| metadata | Json? | | Vrije bag voor bron-info (bv. agent, model_id) |
|
||||
| created_at | DateTime | default now() | |
|
||||
|
||||
**Indexes:** `(story_id, created_at)` — chronologische weergave in de UI
|
||||
**Indexes:** `(story_id, created_at)`
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -140,14 +156,43 @@ related: [auth-and-sessions.md](./auth-and-sessions.md)
|
|||
| Kolom | Type | Constraints | Noten |
|
||||
|---|---|---|---|
|
||||
| id | String (cuid) | PK | |
|
||||
| product_id | String | FK → products (cascade delete) | |
|
||||
| sprint_goal | String | not null, max 500 | |
|
||||
| status | Enum | ACTIVE \| COMPLETED | |
|
||||
| product_id | String | FK → products (Cascade) | |
|
||||
| code | String | max 30, not null | Auto-gegenereerd `SP-N` per product (PBI-59) |
|
||||
| sprint_goal | String | not null | |
|
||||
| status | Enum | `OPEN \| CLOSED \| ARCHIVED \| FAILED`, default `OPEN` | |
|
||||
| start_date | Date? | | Optionele planningsmetadata |
|
||||
| end_date | Date? | | Optionele planningsmetadata |
|
||||
| created_at | DateTime | default now() | |
|
||||
| completed_at | DateTime | nullable | |
|
||||
| completed_at | DateTime? | | Wordt gezet bij overgang naar CLOSED |
|
||||
|
||||
**Indexes:** `(product_id, status)` — query voor actieve Sprint per product
|
||||
**Constraint:** Max. 1 actieve Sprint per product (gehandhaafd in applicatielaag)
|
||||
**Indexes:** `(product_id, status)`
|
||||
**Constraint:** unique `(product_id, code)`
|
||||
|
||||
**Eén product, meerdere sprints (PBI-63):** een product kan tegelijk meer dan één sprint hebben. `OPEN` is geen exclusieve status; de sprint-switcher in de product-header laat de gebruiker tussen sprints kiezen. Stories in `IN_SPRINT` linken via `sprint_id` naar één specifieke sprint.
|
||||
|
||||
---
|
||||
|
||||
### `sprint_runs`
|
||||
|
||||
Eén `sprint_runs`-record per uitvoering van de SPRINT_IMPLEMENTATION-flow (PBI-46/47/50). Houdt status, branch en chained retries bij wanneer een run is gepauzeerd of mislukt.
|
||||
|
||||
| Kolom | Type | Constraints | Noten |
|
||||
|---|---|---|---|
|
||||
| id | String (cuid) | PK | |
|
||||
| sprint_id | String | FK → sprints (Cascade) | |
|
||||
| started_by_id | String | FK → users | Wie de run startte |
|
||||
| status | Enum | `QUEUED \| RUNNING \| PAUSED \| DONE \| FAILED \| CANCELLED` | |
|
||||
| pr_strategy | Enum | `SPRINT \| STORY \| SPRINT_BATCH` | Snapshot van de strategie bij start |
|
||||
| branch | String? | | Werkbranch voor de run |
|
||||
| pr_url | String? | | |
|
||||
| started_at / finished_at | DateTime? | | |
|
||||
| failure_reason | String? | | Vrij tekstveld bij FAILED/CANCELLED |
|
||||
| failed_task_id | String? | FK → tasks (SetNull) | Eerste task die de run brak (cascade-FAIL) |
|
||||
| pause_context | Json? | | Gevalideerd door Zod (`lib/sprint-run/pause-context.ts`) |
|
||||
| previous_run_id | String? | unique, FK → sprint_runs (SetNull) | Chain naar een eerdere run |
|
||||
| created_at / updated_at | DateTime | | |
|
||||
|
||||
**Indexes:** `(sprint_id, status)`, `(started_by_id, status)`
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -156,51 +201,269 @@ related: [auth-and-sessions.md](./auth-and-sessions.md)
|
|||
| Kolom | Type | Constraints | Noten |
|
||||
|---|---|---|---|
|
||||
| id | String (cuid) | PK | |
|
||||
| story_id | String | FK → stories (cascade delete) | |
|
||||
| sprint_id | String | FK → sprints, nullable | Denormalisatie voor snellere queries |
|
||||
| title | String | not null, max 200 | |
|
||||
| description | String | nullable, max 1000 | |
|
||||
| implementation_plan | String | nullable | Opgeslagen door Claude Code MCP via `PATCH /api/tasks/:id` |
|
||||
| priority | Int | 1–4, not null | |
|
||||
| story_id | String | FK → stories (Cascade) | |
|
||||
| product_id | String | FK → products (Cascade) | Denormalisatie voor product-scoped queries |
|
||||
| sprint_id | String? | FK → sprints | Denormalisatie; geërfd van story bij sprint-toevoeging |
|
||||
| code | String | max 30, not null | Auto-gegenereerd `T-N` per product |
|
||||
| title | String | not null | |
|
||||
| description | String? | | |
|
||||
| implementation_plan | String? | | Opgeslagen via Server Action of `PATCH /api/tasks/:id` |
|
||||
| priority | Int | 1–4 | |
|
||||
| sort_order | Float | not null | |
|
||||
| status | Enum | TO_DO \| IN_PROGRESS \| REVIEW \| DONE | |
|
||||
| status | Enum | `TO_DO \| IN_PROGRESS \| REVIEW \| DONE \| FAILED \| EXCLUDED`, default `TO_DO` | `EXCLUDED` slaat verify-skip op tijdens een sprint-run |
|
||||
| verify_only | Boolean | default false | Run-mode: alleen verifiëren, niet implementeren |
|
||||
| verify_required | Enum | `ALIGNED \| ALIGNED_OR_PARTIAL \| ANY`, default `ALIGNED_OR_PARTIAL` | Drempel waarop een job's verify_result als acceptabel telt |
|
||||
| repo_url | String? | | Optionele override van `product.repo_url` voor tasks die in een andere repo wonen |
|
||||
| created_at | DateTime | default now() | |
|
||||
| updated_at | DateTime | auto-update | |
|
||||
|
||||
**Indexes:** `(story_id, priority, sort_order)`, `(sprint_id, status)`
|
||||
**Indexes:** `(story_id, priority, sort_order)`, `(sprint_id, status)`, `(product_id)`
|
||||
**Constraint:** unique `(product_id, code)` — `code` blijft stabiel bij re-parenting (Jira-stijl)
|
||||
|
||||
---
|
||||
|
||||
### `todos`
|
||||
### `claude_jobs`
|
||||
|
||||
Job-queue waarop `wait_for_job` (MCP) atomisch claimt via `FOR UPDATE SKIP LOCKED`. Eén rij per task-implementatie, idea-grill, idea-make-plan, plan-chat of sprint-run.
|
||||
|
||||
| Kolom | Type | Constraints | Noten |
|
||||
|---|---|---|---|
|
||||
| id | String (cuid) | PK | |
|
||||
| user_id | String | FK → users, not null | |
|
||||
| product_id | String? | FK → products, nullable | Optioneel in UI; SetNull bij verwijderen product |
|
||||
| title | String | not null | |
|
||||
| done | Boolean | default false | |
|
||||
| archived | Boolean | default false | |
|
||||
| created_at | DateTime | default now() | |
|
||||
| updated_at | DateTime | auto-update | |
|
||||
| user_id | String | FK → users (Cascade) | |
|
||||
| product_id | String | FK → products (Cascade) | |
|
||||
| task_id | String? | FK → tasks (Cascade) | Bij `TASK_IMPLEMENTATION` of `SPRINT_IMPLEMENTATION` |
|
||||
| idea_id | String? | FK → ideas (Cascade) | Bij `IDEA_GRILL` / `IDEA_MAKE_PLAN` |
|
||||
| sprint_run_id | String? | FK → sprint_runs (SetNull) | Koppel naar de bovenliggende run |
|
||||
| kind | Enum | `TASK_IMPLEMENTATION \| IDEA_GRILL \| IDEA_MAKE_PLAN \| PLAN_CHAT \| SPRINT_IMPLEMENTATION` | |
|
||||
| status | Enum | `QUEUED \| CLAIMED \| RUNNING \| DONE \| FAILED \| CANCELLED \| SKIPPED` | |
|
||||
| claimed_by_token_id | String? | FK → api_tokens (SetNull) | Auth-koppel voor `update_job_status` |
|
||||
| claimed_at / started_at / finished_at / pushed_at | DateTime? | | Lifecycle-stempels |
|
||||
| verify_result | Enum? | `ALIGNED \| PARTIAL \| EMPTY \| DIVERGENT` | Alleen voor task-/sprint-jobs |
|
||||
| model_id | String? | | Anthropic model dat de agent rapporteerde |
|
||||
| input_tokens / output_tokens / cache_read_tokens / cache_write_tokens | Int? | | Token-usage voor billing-overzicht |
|
||||
| plan_snapshot | String? | | Bevroren plan op claim-moment |
|
||||
| base_sha / head_sha / branch / pr_url | String? | | Git-context |
|
||||
| summary | String? | | Vrije agent-samenvatting bij DONE |
|
||||
| error | String? | | Reden bij FAILED |
|
||||
| retry_count | Int | default 0 | |
|
||||
| lease_until | DateTime? | | Stale-CLAIMED → terug naar QUEUED na 30 min |
|
||||
| created_at / updated_at | DateTime | | |
|
||||
|
||||
**Indexes:** `(user_id, done, archived)` — standaard weergave filtert op actieve todo's; `(user_id, product_id)` — filteren per product
|
||||
**Indexes:** `(user_id, status)`, `(task_id, status)`, `(idea_id, status)`, `(sprint_run_id, status)`, `(status, claimed_at)`, `(status, finished_at)`, `(status, lease_until)`
|
||||
|
||||
---
|
||||
|
||||
### `sprint_task_executions`
|
||||
|
||||
Bevroren scope-snapshot per `SPRINT_IMPLEMENTATION`-claim (PBI-50). Bij claim wordt voor elke `TO_DO`-task in scope één `PENDING`-record gemaakt met `implementation_plan` + `verify_required` gesnapshot. Worker en gate werken uitsluitend op deze rows; latere wijzigingen aan Task hebben geen invloed op de lopende batch.
|
||||
|
||||
| Kolom | Type | Constraints | Noten |
|
||||
|---|---|---|---|
|
||||
| id | String (cuid) | PK | |
|
||||
| sprint_job_id | String | FK → claude_jobs (Cascade) | De parent SPRINT_IMPLEMENTATION-job |
|
||||
| task_id | String | FK → tasks (Cascade) | |
|
||||
| order | Int | not null | Volgorde binnen de batch |
|
||||
| plan_snapshot | String (Text) | not null | Het bevroren implementation_plan |
|
||||
| verify_required_snapshot | Enum | `ALIGNED \| ALIGNED_OR_PARTIAL \| ANY` | |
|
||||
| verify_only_snapshot | Boolean | default false | |
|
||||
| base_sha / head_sha | String? | | |
|
||||
| status | Enum | `PENDING \| RUNNING \| DONE \| FAILED \| SKIPPED` | |
|
||||
| verify_result | Enum? | `ALIGNED \| PARTIAL \| EMPTY \| DIVERGENT` | |
|
||||
| verify_summary / skip_reason | String? (Text) | | |
|
||||
| started_at / finished_at | DateTime? | | |
|
||||
| created_at / updated_at | DateTime | | |
|
||||
|
||||
**Constraint:** unique `(sprint_job_id, task_id)`
|
||||
**Indexes:** `(sprint_job_id, order)`
|
||||
|
||||
---
|
||||
|
||||
### `model_prices`
|
||||
|
||||
Prijslookup voor Anthropic-modellen, gebruikt door de jobs-pagina om kosten te berekenen.
|
||||
|
||||
| Kolom | Type | Constraints | Noten |
|
||||
|---|---|---|---|
|
||||
| id | String (cuid) | PK | |
|
||||
| model_id | String | unique | Anthropic model-id (bv. `claude-opus-4-7`) |
|
||||
| input_price_per_1m | Decimal(12,6) | | USD per 1M tokens (default) |
|
||||
| output_price_per_1m | Decimal(12,6) | | |
|
||||
| cache_read_price_per_1m | Decimal(12,6) | | |
|
||||
| cache_write_price_per_1m | Decimal(12,6) | | |
|
||||
| currency | String | default `USD` | |
|
||||
| created_at / updated_at | DateTime | | |
|
||||
|
||||
---
|
||||
|
||||
### `claude_workers`
|
||||
|
||||
Live-presence-record per actieve agent worker. Ingevoegd bij MCP-startup, geüpdatet via `worker_heartbeat` (5s), opgeruimd bij SIGTERM. NavBar telt actieve workers op `last_seen_at < now() - 15s`.
|
||||
|
||||
| Kolom | Type | Constraints | Noten |
|
||||
|---|---|---|---|
|
||||
| id | String (cuid) | PK | |
|
||||
| user_id | String | FK → users (Cascade) | |
|
||||
| token_id | String | unique, FK → api_tokens (Cascade) | Eén worker per token |
|
||||
| product_id | String? | | Optioneel; gerapporteerd door de agent |
|
||||
| started_at / last_seen_at | DateTime | default now() | |
|
||||
| last_quota_pct | Int? | | M13 pre-flight quota-check |
|
||||
| last_quota_check_at | DateTime? | | |
|
||||
|
||||
**Indexes:** `(user_id, last_seen_at)`
|
||||
|
||||
---
|
||||
|
||||
### `product_members`
|
||||
|
||||
Koppelt Developer-gebruikers aan een product backlog. De eigenaar (`products.user_id`) heeft altijd volledige toegang; via `product_members` kunnen aanvullende Developers leesrechten en schrijfrechten op stories, taken en sprints van dat product krijgen. Rollen worden niet hier opgeslagen — dat doet `user_roles`. Een gebruiker kan alleen worden toegevoegd als hij/zij de rol `DEVELOPER` heeft.
|
||||
|
||||
| Kolom | Type | Constraints | Noten |
|
||||
|---|---|---|---|
|
||||
| id | String (cuid) | PK | |
|
||||
| product_id | String | FK → products (cascade delete) | |
|
||||
| user_id | String | FK → users (cascade delete) | |
|
||||
| product_id | String | FK → products (Cascade) | |
|
||||
| user_id | String | FK → users (Cascade) | |
|
||||
| created_at | DateTime | default now() | |
|
||||
|
||||
**Indexes:** `(user_id)` — opzoeken van producten waarbij een gebruiker lid is
|
||||
**Constraint:** unique `(product_id, user_id)` — één lidmaatschap per gebruiker per product
|
||||
**Constraint:** unique `(product_id, user_id)`
|
||||
**Indexes:** `(user_id)`
|
||||
|
||||
Koppelt Developer-gebruikers aan een product backlog. De eigenaar (`products.user_id`) heeft altijd volledige toegang; via `product_members` kunnen aanvullende Developers leesrechten en schrijfrechten op stories, taken en sprints van dat product krijgen. Rollen worden niet opgeslagen in deze tabel — dat doet `user_roles`. Een gebruiker kan alleen worden toegevoegd als hij/zij de rol `DEVELOPER` heeft.
|
||||
---
|
||||
|
||||
### `ideas`
|
||||
|
||||
Idea-entity (M12) tussen losse notitie en PBI. Een idea wordt eerst _gegrilled_ (interactieve Q&A → `grill_md`), daarna gemateriaaliseerd tot een plan (`plan_md`) dat deterministisch geparseerd wordt naar PBI + stories + tasks. Vervangt de oude `todos`-tabel volledig (atomische migratie ST-1239 — todos zijn gedropt).
|
||||
|
||||
| Kolom | Type | Constraints | Noten |
|
||||
|---|---|---|---|
|
||||
| id | String (cuid) | PK | |
|
||||
| user_id | String | FK → users (Cascade) | |
|
||||
| product_id | String? | FK → products (SetNull) | Primaire scope; null = unscoped capture |
|
||||
| code | String | max 30, not null | Sequentieel per gebruiker via `idea_code_counter` |
|
||||
| title | String | not null | |
|
||||
| description | String? | max 4000 | Initiële tekst |
|
||||
| grill_md | String? (Text) | | Output van IDEA_GRILL |
|
||||
| plan_md | String? (Text) | | Output van IDEA_MAKE_PLAN |
|
||||
| pbi_id | String? | unique, FK → pbis (SetNull) | Wordt gevuld na materialisatie |
|
||||
| status | Enum | `DRAFT \| GRILLING \| GRILL_FAILED \| GRILLED \| PLANNING \| PLAN_FAILED \| PLAN_READY \| PLANNED`, default `DRAFT` | |
|
||||
| archived | Boolean | default false | |
|
||||
| created_at / updated_at | DateTime | | |
|
||||
|
||||
**Constraint:** unique `(user_id, code)`
|
||||
**Indexes:** `(user_id, archived, status)`, `(user_id, product_id)`
|
||||
|
||||
---
|
||||
|
||||
### `idea_products`
|
||||
|
||||
Optionele secundaire producten waar een idea ook impact heeft.
|
||||
|
||||
| Kolom | Type | Constraints | Noten |
|
||||
|---|---|---|---|
|
||||
| id | String (cuid) | PK | |
|
||||
| idea_id | String | FK → ideas (Cascade) | |
|
||||
| product_id | String | FK → products (Cascade) | |
|
||||
| created_at | DateTime | default now() | |
|
||||
|
||||
**Constraint:** unique `(idea_id, product_id)`
|
||||
**Indexes:** `(product_id)`
|
||||
|
||||
---
|
||||
|
||||
### `idea_logs`
|
||||
|
||||
Activiteitenlog per idea — soortgelijke rol als `story_logs` voor stories.
|
||||
|
||||
| Kolom | Type | Constraints | Noten |
|
||||
|---|---|---|---|
|
||||
| id | String (cuid) | PK | |
|
||||
| idea_id | String | FK → ideas (Cascade) | |
|
||||
| type | Enum | `DECISION \| NOTE \| GRILL_RESULT \| PLAN_RESULT \| STATUS_CHANGE \| JOB_EVENT` | |
|
||||
| content | String (Text) | not null | |
|
||||
| metadata | Json? | | |
|
||||
| created_at | DateTime | default now() | |
|
||||
|
||||
**Indexes:** `(idea_id, created_at)`
|
||||
|
||||
---
|
||||
|
||||
### `user_questions`
|
||||
|
||||
Vrije vragen die een gebruiker aan zichzelf stelt op een idea (M12). Wordt door de Idea-detail-UI ingelezen om te helpen bij het grillen.
|
||||
|
||||
| Kolom | Type | Constraints | Noten |
|
||||
|---|---|---|---|
|
||||
| id | String (cuid) | PK | |
|
||||
| idea_id | String | FK → ideas (Cascade) | |
|
||||
| user_id | String | not null | Eigenaar |
|
||||
| question | String (Text) | not null | |
|
||||
| answer | String? (Text) | | |
|
||||
| status | Enum (lowercase) | `pending \| answered`, default `pending` | |
|
||||
| created_at / updated_at | DateTime | | |
|
||||
|
||||
**Indexes:** `(idea_id, status)`, `(user_id)`
|
||||
|
||||
---
|
||||
|
||||
### `claude_questions`
|
||||
|
||||
Persistent vraag-antwoord-kanaal van een agent (via `mcp__scrum4me__ask_user_question`) richting de actieve gebruiker (M11). LISTEN/NOTIFY pusht het antwoord terug naar de wachtende agent. Zie [architecture/claude-question-channel.md](./claude-question-channel.md).
|
||||
|
||||
| Kolom | Type | Constraints | Noten |
|
||||
|---|---|---|---|
|
||||
| id | String (cuid) | PK | |
|
||||
| story_id | String? | FK → stories (Cascade) | Eén van story/task/idea is verplicht |
|
||||
| task_id | String? | FK → tasks (SetNull) | |
|
||||
| idea_id | String? | FK → ideas (Cascade) | |
|
||||
| product_id | String | FK → products (Cascade) | Gedenormaliseerd voor het SSE-filter |
|
||||
| asked_by | String | FK → users | Token-houder = Claude |
|
||||
| question | String (Text) | not null | |
|
||||
| options | Json? | | `string[]` voor multi-choice; null voor free-text |
|
||||
| status | String | not null | `open \| answered \| cancelled \| expired` |
|
||||
| answer | String? (Text) | | |
|
||||
| answered_by | String? | FK → users | |
|
||||
| answered_at | DateTime? | | |
|
||||
| created_at | DateTime | default now() | |
|
||||
| expires_at | DateTime | not null | Default `now() + 24h`, ingesteld door MCP-tool |
|
||||
|
||||
**Indexes:** `(story_id, status)`, `(idea_id, status)`, `(product_id, status)`, `(status, expires_at)`
|
||||
|
||||
---
|
||||
|
||||
### `login_pairings`
|
||||
|
||||
QR-pairing-flow (M10). Desktop start een pairing, telefoon scant en bevestigt; daarna kan de desktop ruilen voor een sessie.
|
||||
|
||||
| Kolom | Type | Constraints | Noten |
|
||||
|---|---|---|---|
|
||||
| id | String (cuid) | PK | |
|
||||
| secret_hash | String | not null | Hash van het pairing-secret |
|
||||
| desktop_token_hash | String | not null | Hash van het pre-auth desktop-token |
|
||||
| status | String | not null | `pending \| approved \| consumed \| expired` |
|
||||
| user_id | String? | FK → users (SetNull) | Gezet bij approval |
|
||||
| desktop_ua | String? | max 255 | UA-string van de desktop-aanvraag |
|
||||
| desktop_ip | String? | max 45 | IP voor audit |
|
||||
| created_at | DateTime | default now() | |
|
||||
| expires_at | DateTime | not null | TTL voor afhandeling |
|
||||
| approved_at / consumed_at | DateTime? | | |
|
||||
|
||||
**Indexes:** `(expires_at)`, `(status, expires_at)`
|
||||
|
||||
---
|
||||
|
||||
### `push_subscriptions`
|
||||
|
||||
Web Push subscriptions per gebruiker, voor notificaties.
|
||||
|
||||
| Kolom | Type | Constraints | Noten |
|
||||
|---|---|---|---|
|
||||
| id | String (cuid) | PK | |
|
||||
| user_id | String | FK → users (Cascade) | |
|
||||
| endpoint | String | unique, not null | Push-service endpoint |
|
||||
| p256dh / auth | String | not null | VAPID keys |
|
||||
| user_agent | String? | | UA bij subscribe |
|
||||
| created_at / last_used_at | DateTime | default now() | |
|
||||
|
||||
**Indexes:** `(user_id)`
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -211,7 +474,7 @@ Producttoegang is centraal gedefinieerd als:
|
|||
- eigenaar: `products.user_id === gebruiker.id`
|
||||
- teamlid: `product_members` bevat `(product_id, user_id)`
|
||||
|
||||
Code gebruikt hiervoor `productAccessFilter(userId)` uit `lib/product-access.ts`. Route Handlers en Server Actions mogen geen eigenaar-only filter (`user_id`) gebruiken voor product-scoped resources tenzij het expliciet om eigenaarsbeheer gaat, zoals archiveren of teamleden beheren.
|
||||
Code gebruikt hiervoor `productAccessFilter(userId)` uit `lib/product-access.ts`. Route Handlers en Server Actions mogen geen eigenaar-only filter (`user_id`) gebruiken voor product-scoped resources tenzij het expliciet om eigenaarsbeheer gaat (archiveren, teamleden beheren).
|
||||
|
||||
Schrijfoperaties volgen deze invarianten:
|
||||
|
||||
|
|
@ -225,236 +488,32 @@ Schrijfoperaties volgen deze invarianten:
|
|||
|
||||
---
|
||||
|
||||
## Prisma Schema (excerpt)
|
||||
## Enums (overzicht)
|
||||
|
||||
```prisma
|
||||
// prisma/schema.prisma
|
||||
| Enum | Waarden |
|
||||
|---|---|
|
||||
| `Role` | `PRODUCT_OWNER`, `SCRUM_MASTER`, `DEVELOPER`, `ADMIN` |
|
||||
| `PbiStatus` | `READY`, `BLOCKED`, `FAILED`, `DONE` |
|
||||
| `StoryStatus` | `OPEN`, `IN_SPRINT`, `DONE`, `FAILED` |
|
||||
| `TaskStatus` | `TO_DO`, `IN_PROGRESS`, `REVIEW`, `DONE`, `FAILED`, `EXCLUDED` |
|
||||
| `SprintStatus` | `OPEN`, `CLOSED`, `ARCHIVED`, `FAILED` |
|
||||
| `SprintRunStatus` | `QUEUED`, `RUNNING`, `PAUSED`, `DONE`, `FAILED`, `CANCELLED` |
|
||||
| `PrStrategy` | `SPRINT`, `STORY`, `SPRINT_BATCH` |
|
||||
| `LogType` | `IMPLEMENTATION_PLAN`, `TEST_RESULT`, `COMMIT` |
|
||||
| `TestStatus` | `PASSED`, `FAILED` |
|
||||
| `ClaudeJobStatus` | `QUEUED`, `CLAIMED`, `RUNNING`, `DONE`, `FAILED`, `CANCELLED`, `SKIPPED` |
|
||||
| `ClaudeJobKind` | `TASK_IMPLEMENTATION`, `IDEA_GRILL`, `IDEA_MAKE_PLAN`, `PLAN_CHAT`, `SPRINT_IMPLEMENTATION` |
|
||||
| `VerifyResult` | `ALIGNED`, `PARTIAL`, `EMPTY`, `DIVERGENT` |
|
||||
| `VerifyRequired` | `ALIGNED`, `ALIGNED_OR_PARTIAL`, `ANY` |
|
||||
| `SprintTaskExecutionStatus` | `PENDING`, `RUNNING`, `DONE`, `FAILED`, `SKIPPED` |
|
||||
| `IdeaStatus` | `DRAFT`, `GRILLING`, `GRILL_FAILED`, `GRILLED`, `PLANNING`, `PLAN_FAILED`, `PLAN_READY`, `PLANNED` |
|
||||
| `IdeaLogType` | `DECISION`, `NOTE`, `GRILL_RESULT`, `PLAN_RESULT`, `STATUS_CHANGE`, `JOB_EVENT` |
|
||||
| `UserQuestionStatus` | `pending`, `answered` (lowercase, niet UPPER_SNAKE) |
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
// Database wordt bepaald via prisma.config.ts — niet hier
|
||||
|
||||
enum Role {
|
||||
PRODUCT_OWNER
|
||||
SCRUM_MASTER
|
||||
DEVELOPER
|
||||
}
|
||||
|
||||
enum StoryStatus {
|
||||
OPEN
|
||||
IN_SPRINT
|
||||
DONE
|
||||
}
|
||||
|
||||
enum PbiStatus {
|
||||
READY
|
||||
BLOCKED
|
||||
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
|
||||
password_hash String
|
||||
is_demo Boolean @default(false)
|
||||
bio String? @db.VarChar(160)
|
||||
bio_detail String? @db.VarChar(2000)
|
||||
avatar_data Bytes?
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
roles UserRole[]
|
||||
api_tokens ApiToken[]
|
||||
products Product[]
|
||||
todos Todo[]
|
||||
product_members ProductMember[]
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
model Product {
|
||||
id String @id @default(cuid())
|
||||
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
||||
user_id String
|
||||
name String
|
||||
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[]
|
||||
|
||||
@@unique([user_id, name])
|
||||
@@index([user_id, archived])
|
||||
}
|
||||
|
||||
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
|
||||
status PbiStatus @default(READY)
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
stories Story[]
|
||||
|
||||
@@unique([product_id, code])
|
||||
@@index([product_id, priority, sort_order])
|
||||
@@index([product_id, status])
|
||||
}
|
||||
|
||||
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?
|
||||
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[]
|
||||
|
||||
@@index([pbi_id, priority, sort_order])
|
||||
@@index([sprint_id, sort_order])
|
||||
@@index([product_id, status])
|
||||
}
|
||||
|
||||
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?
|
||||
created_at DateTime @default(now())
|
||||
|
||||
@@index([story_id, created_at])
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
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
|
||||
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])
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
```
|
||||
> API-grens: `TaskStatus` en `StoryStatus` worden tussen DB (UPPER_SNAKE) en API (lowercase) vertaald via `lib/task-status.ts` (zie [ADR-0004](../adr/0004-status-enum-mapping.md)).
|
||||
|
||||
---
|
||||
|
||||
## Prisma Schema
|
||||
|
||||
De volledige, levende definitie staat in [`prisma/schema.prisma`](../../prisma/schema.prisma). Genereer de ERD lokaal met `npm run db:erd` (zie [README — Database](../../README.md#database)). Het ERD-diagram zelf staat in [docs/assets/erd.svg](../assets/erd.svg).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue