// 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 [--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 = '' 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 = `` 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) })