Scrum4Me/scripts/insert-milestone.ts
Madhura68 7c82a736f5 feat(codes): server actions + seed/scripts gebruiken code overal
- actions/tasks.ts: saveTask + createTaskAction (legacy form) gebruiken
  createWithCodeRetry + generateNextTaskCode; persisten product_id
  denormalisatie. P2002 op user-supplied code wordt 422 met fieldError
- actions/pbis.ts + stories.ts: insert-helpers nemen verplichte string;
  update laat code-veld weg uit data wanneer null (kan niet meer leeg
  worden gemaakt nu DB NOT NULL is)
- actions/todos.ts: promoteTodoToPbi/Story genereren expliciet een code
  voor het transactiestart (kan niet binnen $transaction array retryen)
- prisma/seed.ts: per-product task counter geeft elke task een T-N code
- scripts/insert-milestone.ts: createMany berekent maxN voor product
  en assigneert T-{maxN+i+1} per nieuwe task

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 08:36:41 +02:00

208 lines
6.7 KiB
TypeScript

// Idempotent insert van één milestone (PBI + stories + tasks) uit de backlog.
//
// Gebruik:
// npm run db:insert-milestone -- M8 # standaard product SCRUM4ME
// npm run db:insert-milestone -- M8 --product SCRUM4ME # idem
// npm run db:insert-milestone -- M8 --dry-run # niets schrijven, alleen tonen
//
// Bron: docs/scrum4me-backlog.md (gecommit, single source of truth).
// Tweede keer draaien is veilig: bestaande PBIs/stories/tasks worden niet
// gedupliceerd. Sprint-toewijzing doe je apart via Sprint Planning in de UI.
import { PrismaClient } from '@prisma/client'
import * as dotenv from 'dotenv'
import * as path from 'path'
import { Pool } from 'pg'
import { PrismaPg } from '@prisma/adapter-pg'
import { loadBacklog } from '../prisma/seed-data/parse-backlog'
const root = path.resolve(__dirname, '..')
dotenv.config({ path: path.join(root, '.env.local'), override: true })
dotenv.config({ path: path.join(root, '.env') })
interface Args {
milestoneKey: string
productCode: string
dryRun: boolean
}
function parseArgs(argv: string[]): Args {
const positional: string[] = []
let productCode = 'SCRUM4ME'
let dryRun = false
for (let i = 0; i < argv.length; i++) {
const a = argv[i]
if (a === '--product') productCode = argv[++i] ?? ''
else if (a === '--dry-run') dryRun = true
else if (a.startsWith('--')) throw new Error(`Unknown flag: ${a}`)
else positional.push(a)
}
if (positional.length !== 1) {
throw new Error('Usage: insert-milestone <milestone-key> [--product CODE] [--dry-run]')
}
const milestoneKey = positional[0]
if (!/^M[\d.]+$/.test(milestoneKey)) {
throw new Error(`Invalid milestone key: ${milestoneKey} (expected M0, M3.5, M8, ...)`)
}
if (!productCode) throw new Error('--product cannot be empty')
return { milestoneKey, productCode, dryRun }
}
async function main() {
const args = parseArgs(process.argv.slice(2))
const url = process.env.DATABASE_URL
if (!url) throw new Error('DATABASE_URL is not set. Check .env.local')
const pool = new Pool({ connectionString: url })
const adapter = new PrismaPg(pool)
const prisma = new PrismaClient({ adapter })
try {
const milestones = await loadBacklog(root, { strict: false })
const ms = milestones.find((m) => m.key === args.milestoneKey)
if (!ms) {
throw new Error(
`Milestone ${args.milestoneKey} not found in docs/scrum4me-backlog.md (parsed: ${milestones.map((m) => m.key).join(', ')})`,
)
}
const product = await prisma.product.findFirst({
where: { code: args.productCode, archived: false },
select: { id: true, name: true, code: true },
})
if (!product) throw new Error(`Product with code "${args.productCode}" not found`)
console.log(
`Inserting ${ms.key} (${ms.title}) into product ${product.code}${ms.stories.length} stories${args.dryRun ? ' [DRY RUN]' : ''}`,
)
let pbiCreated = 0
let storiesCreated = 0
let storiesUpdated = 0
let tasksCreated = 0
// PBI upsert
const existingPbi = await prisma.pbi.findFirst({
where: { product_id: product.id, code: ms.key },
select: { id: true },
})
let pbiId: string
if (existingPbi) {
pbiId = existingPbi.id
} else if (args.dryRun) {
pbiId = '<new-pbi-id>'
pbiCreated = 1
} else {
const maxPbi = await prisma.pbi.aggregate({
where: { product_id: product.id },
_max: { sort_order: true },
})
const nextSort = (maxPbi._max.sort_order ?? 0) + 1
const created = await prisma.pbi.create({
data: {
product_id: product.id,
code: ms.key,
title: ms.title,
description: ms.goal,
priority: ms.priority,
sort_order: nextSort,
},
select: { id: true },
})
pbiId = created.id
pbiCreated = 1
}
// Stories + tasks
for (const s of ms.stories) {
const existingStory = await prisma.story.findFirst({
where: { product_id: product.id, code: s.ref },
select: { id: true, _count: { select: { tasks: true } } },
})
const storyData = {
pbi_id: pbiId,
product_id: product.id,
code: s.ref,
title: s.title,
description: s.tasks.map((t) => t.title).join('; '),
acceptance_criteria: s.acceptance_criteria,
priority: ms.priority,
sort_order: s.sort_order,
status: s.status === 'DONE' ? 'DONE' as const : 'OPEN' as const,
}
let storyId: string
let hadTasks = false
if (existingStory) {
hadTasks = existingStory._count.tasks > 0
storyId = existingStory.id
if (!args.dryRun) {
await prisma.story.update({
where: { id: existingStory.id },
data: storyData,
})
}
storiesUpdated++
} else if (args.dryRun) {
storyId = `<new-story-${s.ref}>`
storiesCreated++
} else {
const created = await prisma.story.create({
data: storyData,
select: { id: true },
})
storyId = created.id
storiesCreated++
}
// Tasks: alleen als de story op dit moment 0 tasks had
if (!hadTasks && s.tasks.length > 0) {
if (!args.dryRun) {
const allTasks = await prisma.task.findMany({
where: { product_id: product.id },
select: { code: true },
})
const maxN = allTasks.reduce((m, t) => {
const match = /^T-(\d+)$/.exec(t.code)
return match ? Math.max(m, Number(match[1])) : m
}, 0)
await prisma.task.createMany({
data: s.tasks.map((t, i) => ({
story_id: storyId,
product_id: product.id,
code: `T-${maxN + i + 1}`,
title: t.title,
description: t.description || null,
priority: ms.priority,
sort_order: t.sort_order,
status: s.status === 'DONE' ? 'DONE' as const : 'TO_DO' as const,
})),
})
}
tasksCreated += s.tasks.length
}
}
console.log(
`Result: PBI ${pbiCreated ? 'created' : 'reused'}, stories ${storiesCreated} created / ${storiesUpdated} updated, tasks ${tasksCreated} created${args.dryRun ? ' [DRY RUN — niets geschreven]' : ''}`,
)
if (storiesCreated > 0 || pbiCreated > 0) {
console.log(
`Tip: stories staan nog niet in een sprint. Plan ze via Sprint Planning in de UI als je ze in de Solo Paneel wil zien.`,
)
}
} finally {
await prisma.$disconnect()
await pool.end()
}
}
main().catch((err) => {
console.error('insert-milestone failed:', err.message)
process.exit(1)
})