feat(PBI-96/T-1058): add ProductDoc + ProductDocLog schema + migration
Voegt twee enums (ProductDocFolder met 8 kern-folders + ProductDocLogType), een Product-uitbreiding (enabled_doc_folders array met alle 8 als default) en twee modellen toe (ProductDoc met @@unique(product_id, folder, slug) + ProductDocLog met denormalized actor_user_id en doc_id nullable + SetNull). Bestaande producten krijgen de 8-folder-default automatisch via de ALTER TABLE DEFAULT — geen backfill nodig (zie plan §A.4). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
73cb61d3a2
commit
667be61334
2 changed files with 209 additions and 86 deletions
|
|
@ -0,0 +1,64 @@
|
|||
-- CreateEnum
|
||||
CREATE TYPE "ProductDocFolder" AS ENUM ('ADR', 'ARCHITECTURE', 'PATTERNS', 'PLANS', 'RUNBOOKS', 'SPECS', 'MANUAL', 'API');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ProductDocLogType" AS ENUM ('CREATED', 'UPDATED', 'DELETED', 'FOLDER_ENABLED', 'FOLDER_DISABLED');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "products" ADD COLUMN "enabled_doc_folders" "ProductDocFolder"[] NOT NULL DEFAULT ARRAY['ADR', 'ARCHITECTURE', 'PATTERNS', 'PLANS', 'RUNBOOKS', 'SPECS', 'MANUAL', 'API']::"ProductDocFolder"[];
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "product_docs" (
|
||||
"id" TEXT NOT NULL,
|
||||
"product_id" TEXT NOT NULL,
|
||||
"folder" "ProductDocFolder" NOT NULL,
|
||||
"slug" VARCHAR(80) NOT NULL,
|
||||
"title" VARCHAR(200) NOT NULL,
|
||||
"content_md" TEXT NOT NULL,
|
||||
"status" VARCHAR(20) NOT NULL,
|
||||
"created_by" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "product_docs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "product_doc_logs" (
|
||||
"id" TEXT NOT NULL,
|
||||
"product_id" TEXT NOT NULL,
|
||||
"doc_id" TEXT,
|
||||
"actor_user_id" TEXT NOT NULL,
|
||||
"type" "ProductDocLogType" NOT NULL,
|
||||
"metadata" JSONB,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "product_doc_logs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "product_docs_product_id_folder_slug_key" ON "product_docs"("product_id", "folder", "slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "product_docs_product_id_folder_updated_at_idx" ON "product_docs"("product_id", "folder", "updated_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "product_docs_product_id_status_idx" ON "product_docs"("product_id", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "product_doc_logs_product_id_created_at_idx" ON "product_doc_logs"("product_id", "created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "product_doc_logs_doc_id_created_at_idx" ON "product_doc_logs"("doc_id", "created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "product_doc_logs_actor_user_id_created_at_idx" ON "product_doc_logs"("actor_user_id", "created_at");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "product_docs" ADD CONSTRAINT "product_docs_product_id_fkey" FOREIGN KEY ("product_id") REFERENCES "products"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "product_doc_logs" ADD CONSTRAINT "product_doc_logs_product_id_fkey" FOREIGN KEY ("product_id") REFERENCES "products"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "product_doc_logs" ADD CONSTRAINT "product_doc_logs_doc_id_fkey" FOREIGN KEY ("doc_id") REFERENCES "product_docs"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
|
@ -138,35 +138,54 @@ enum UserQuestionStatus {
|
|||
answered
|
||||
}
|
||||
|
||||
enum ProductDocFolder {
|
||||
ADR
|
||||
ARCHITECTURE
|
||||
PATTERNS
|
||||
PLANS
|
||||
RUNBOOKS
|
||||
SPECS
|
||||
MANUAL
|
||||
API
|
||||
}
|
||||
|
||||
enum ProductDocLogType {
|
||||
CREATED
|
||||
UPDATED
|
||||
DELETED
|
||||
FOLDER_ENABLED
|
||||
FOLDER_DISABLED
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
username String @unique
|
||||
email String? @unique
|
||||
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)
|
||||
must_reset_password Boolean @default(false)
|
||||
is_demo Boolean @default(false)
|
||||
bio String? @db.VarChar(160)
|
||||
bio_detail String? @db.VarChar(2000)
|
||||
must_reset_password Boolean @default(false)
|
||||
avatar_data Bytes?
|
||||
active_product_id String?
|
||||
active_product Product? @relation("UserActiveProduct", fields: [active_product_id], references: [id], onDelete: SetNull)
|
||||
idea_code_counter Int @default(0)
|
||||
min_quota_pct Int @default(20)
|
||||
settings Json @default("{}")
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
active_product Product? @relation("UserActiveProduct", fields: [active_product_id], references: [id], onDelete: SetNull)
|
||||
idea_code_counter Int @default(0)
|
||||
min_quota_pct Int @default(20)
|
||||
settings Json @default("{}")
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
roles UserRole[]
|
||||
api_tokens ApiToken[]
|
||||
products Product[]
|
||||
ideas Idea[]
|
||||
product_members ProductMember[]
|
||||
assigned_stories Story[] @relation("StoryAssignee")
|
||||
assigned_stories Story[] @relation("StoryAssignee")
|
||||
login_pairings LoginPairing[]
|
||||
asked_questions ClaudeQuestion[] @relation("ClaudeQuestionAsker")
|
||||
answered_questions ClaudeQuestion[] @relation("ClaudeQuestionAnswerer")
|
||||
asked_questions ClaudeQuestion[] @relation("ClaudeQuestionAsker")
|
||||
answered_questions ClaudeQuestion[] @relation("ClaudeQuestionAnswerer")
|
||||
claude_jobs ClaudeJob[]
|
||||
claude_workers ClaudeWorker[]
|
||||
started_sprint_runs SprintRun[] @relation("SprintRunStartedBy")
|
||||
started_sprint_runs SprintRun[] @relation("SprintRunStartedBy")
|
||||
push_subscriptions PushSubscription[]
|
||||
|
||||
@@index([active_product_id])
|
||||
|
|
@ -199,32 +218,35 @@ model ApiToken {
|
|||
}
|
||||
|
||||
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
|
||||
auto_pr Boolean @default(false)
|
||||
pr_strategy PrStrategy @default(SPRINT)
|
||||
preferred_model String?
|
||||
thinking_budget_default Int?
|
||||
preferred_permission_mode String?
|
||||
archived Boolean @default(false)
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
pbis Pbi[]
|
||||
sprints Sprint[]
|
||||
stories Story[]
|
||||
tasks Task[]
|
||||
members ProductMember[]
|
||||
active_for_users User[] @relation("UserActiveProduct")
|
||||
claude_questions ClaudeQuestion[]
|
||||
claude_jobs ClaudeJob[]
|
||||
ideas Idea[]
|
||||
idea_products IdeaProduct[]
|
||||
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
|
||||
auto_pr Boolean @default(false)
|
||||
pr_strategy PrStrategy @default(SPRINT)
|
||||
preferred_model String?
|
||||
thinking_budget_default Int?
|
||||
preferred_permission_mode String?
|
||||
archived Boolean @default(false)
|
||||
enabled_doc_folders ProductDocFolder[] @default([ADR, ARCHITECTURE, PATTERNS, PLANS, RUNBOOKS, SPECS, MANUAL, API])
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
pbis Pbi[]
|
||||
sprints Sprint[]
|
||||
stories Story[]
|
||||
tasks Task[]
|
||||
members ProductMember[]
|
||||
active_for_users User[] @relation("UserActiveProduct")
|
||||
claude_questions ClaudeQuestion[]
|
||||
claude_jobs ClaudeJob[]
|
||||
ideas Idea[]
|
||||
idea_products IdeaProduct[]
|
||||
docs ProductDoc[]
|
||||
doc_logs ProductDocLog[]
|
||||
|
||||
@@unique([user_id, name])
|
||||
@@unique([user_id, code])
|
||||
|
|
@ -388,47 +410,47 @@ model Task {
|
|||
}
|
||||
|
||||
model ClaudeJob {
|
||||
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: Cascade)
|
||||
product_id String
|
||||
task Task? @relation(fields: [task_id], references: [id], onDelete: Cascade)
|
||||
task_id String?
|
||||
idea Idea? @relation(fields: [idea_id], references: [id], onDelete: Cascade)
|
||||
idea_id String?
|
||||
sprint_run SprintRun? @relation(fields: [sprint_run_id], references: [id], onDelete: SetNull)
|
||||
sprint_run_id String?
|
||||
kind ClaudeJobKind @default(TASK_IMPLEMENTATION)
|
||||
status ClaudeJobStatus @default(QUEUED)
|
||||
claimed_by_token ApiToken? @relation(fields: [claimed_by_token_id], references: [id], onDelete: SetNull)
|
||||
claimed_by_token_id String?
|
||||
claimed_at DateTime?
|
||||
started_at DateTime?
|
||||
finished_at DateTime?
|
||||
pushed_at DateTime?
|
||||
verify_result VerifyResult?
|
||||
model_id String?
|
||||
input_tokens Int?
|
||||
output_tokens Int?
|
||||
cache_read_tokens Int?
|
||||
cache_write_tokens Int?
|
||||
requested_model String?
|
||||
requested_thinking_budget Int?
|
||||
requested_permission_mode String?
|
||||
actual_thinking_tokens Int?
|
||||
plan_snapshot String?
|
||||
base_sha String?
|
||||
head_sha String?
|
||||
branch String?
|
||||
pr_url String?
|
||||
summary String?
|
||||
error String?
|
||||
retry_count Int @default(0)
|
||||
lease_until DateTime?
|
||||
task_executions SprintTaskExecution[] @relation("SprintJobExecutions")
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
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: Cascade)
|
||||
product_id String
|
||||
task Task? @relation(fields: [task_id], references: [id], onDelete: Cascade)
|
||||
task_id String?
|
||||
idea Idea? @relation(fields: [idea_id], references: [id], onDelete: Cascade)
|
||||
idea_id String?
|
||||
sprint_run SprintRun? @relation(fields: [sprint_run_id], references: [id], onDelete: SetNull)
|
||||
sprint_run_id String?
|
||||
kind ClaudeJobKind @default(TASK_IMPLEMENTATION)
|
||||
status ClaudeJobStatus @default(QUEUED)
|
||||
claimed_by_token ApiToken? @relation(fields: [claimed_by_token_id], references: [id], onDelete: SetNull)
|
||||
claimed_by_token_id String?
|
||||
claimed_at DateTime?
|
||||
started_at DateTime?
|
||||
finished_at DateTime?
|
||||
pushed_at DateTime?
|
||||
verify_result VerifyResult?
|
||||
model_id String?
|
||||
input_tokens Int?
|
||||
output_tokens Int?
|
||||
cache_read_tokens Int?
|
||||
cache_write_tokens Int?
|
||||
requested_model String?
|
||||
requested_thinking_budget Int?
|
||||
requested_permission_mode String?
|
||||
actual_thinking_tokens Int?
|
||||
plan_snapshot String?
|
||||
base_sha String?
|
||||
head_sha String?
|
||||
branch String?
|
||||
pr_url String?
|
||||
summary String?
|
||||
error String?
|
||||
retry_count Int @default(0)
|
||||
lease_until DateTime?
|
||||
task_executions SprintTaskExecution[] @relation("SprintJobExecutions")
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
@@index([user_id, status])
|
||||
@@index([task_id, status])
|
||||
|
|
@ -515,6 +537,43 @@ model ProductMember {
|
|||
@@map("product_members")
|
||||
}
|
||||
|
||||
model ProductDoc {
|
||||
id String @id @default(cuid())
|
||||
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
||||
product_id String
|
||||
folder ProductDocFolder
|
||||
slug String @db.VarChar(80)
|
||||
title String @db.VarChar(200)
|
||||
content_md String @db.Text
|
||||
status String @db.VarChar(20)
|
||||
created_by String
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
logs ProductDocLog[]
|
||||
|
||||
@@unique([product_id, folder, slug])
|
||||
@@index([product_id, folder, updated_at])
|
||||
@@index([product_id, status])
|
||||
@@map("product_docs")
|
||||
}
|
||||
|
||||
model ProductDocLog {
|
||||
id String @id @default(cuid())
|
||||
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
||||
product_id String
|
||||
doc ProductDoc? @relation(fields: [doc_id], references: [id], onDelete: SetNull)
|
||||
doc_id String?
|
||||
actor_user_id String
|
||||
type ProductDocLogType
|
||||
metadata Json?
|
||||
created_at DateTime @default(now())
|
||||
|
||||
@@index([product_id, created_at])
|
||||
@@index([doc_id, created_at])
|
||||
@@index([actor_user_id, created_at])
|
||||
@@map("product_doc_logs")
|
||||
}
|
||||
|
||||
model Idea {
|
||||
id String @id @default(cuid())
|
||||
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
||||
|
|
@ -526,8 +585,8 @@ model Idea {
|
|||
description String? @db.VarChar(4000)
|
||||
grill_md String? @db.Text
|
||||
plan_md String? @db.Text
|
||||
plan_review_log Json? // ReviewLog from orchestrator (all rounds, convergence metrics, approval status)
|
||||
reviewed_at DateTime? // When last reviewed
|
||||
plan_review_log Json? // ReviewLog from orchestrator (all rounds, convergence metrics, approval status)
|
||||
reviewed_at DateTime? // When last reviewed
|
||||
pbi Pbi? @relation(fields: [pbi_id], references: [id], onDelete: SetNull)
|
||||
pbi_id String? @unique
|
||||
status IdeaStatus @default(DRAFT)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue