hp-prod-tracker/scripts/migrate-to-dynamic-pipelines.ts
Leivur R. Djurhuus 40028b7ced feat: add pipeline stage resolver and organization access control
- Implemented `stage-resolver.ts` to unify old and new pipeline stage definitions.
- Created `org-scope.ts` for organization access verification and scoping queries.
- Added role-based permissions management in `permissions.ts` and `rbac-service.ts`.
- Introduced invitation management in `invitation-service.ts` with validation schemas.
- Developed custom field and notification rule services with respective validators.
- Established pipeline template CRUD operations in `pipeline-template-service.ts`.
- Added Zustand store for managing pipeline builder state in `pipeline-builder-store.ts`.
2026-03-14 22:43:43 -05:00

181 lines
5.4 KiB
TypeScript

/**
* Migrate to Dynamic Pipelines
*
* Creates a PipelineTemplate "HP CG Standard" for the dev org,
* copies stages + dependencies from PipelineStageTemplate,
* backfills stageDefinitionId on existing DeliverableStages,
* and sets pipelineTemplateId on existing projects.
*
* Run with: npx tsx scripts/migrate-to-dynamic-pipelines.ts
*/
import "dotenv/config";
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "../src/generated/prisma/client";
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! });
const prisma = new PrismaClient({ adapter });
const ORG_ID = "dev-org-001";
async function main() {
// 1. Check if already migrated
const existing = await prisma.pipelineTemplate.findFirst({
where: { organizationId: ORG_ID, name: "HP CG Standard" },
});
if (existing) {
console.log(`Pipeline template "HP CG Standard" already exists (${existing.id}). Skipping creation.`);
await backfillStages(existing.id);
await backfillProjects(existing.id);
console.log("Migration complete!");
return;
}
// 2. Fetch old global templates with dependencies
const oldTemplates = await prisma.pipelineStageTemplate.findMany({
include: { dependsOn: true },
orderBy: { order: "asc" },
});
if (oldTemplates.length === 0) {
console.log("No old templates found. Run seed first.");
return;
}
console.log(`Found ${oldTemplates.length} old pipeline stages. Creating dynamic template...`);
// 3. Create the new PipelineTemplate
const pipeline = await prisma.pipelineTemplate.create({
data: {
name: "HP CG Standard",
description: "Standard HP CG production pipeline with 10 stages",
organizationId: ORG_ID,
isDefault: true,
},
});
console.log(`Created PipelineTemplate: ${pipeline.id}`);
// 4. Create PipelineStageDefinitions from old templates
const oldToNewId = new Map<string, string>(); // old template ID → new definition ID
for (const old of oldTemplates) {
const def = await prisma.pipelineStageDefinition.create({
data: {
pipelineId: pipeline.id,
name: old.name,
slug: old.slug,
order: old.order,
isCriticalGate: old.isCriticalGate,
isOptional: old.isOptional,
description: old.description,
estimatedDays: old.estimatedDays,
},
});
oldToNewId.set(old.id, def.id);
console.log(` Stage: ${old.name}${def.id}`);
}
// 5. Create dependencies
let depCount = 0;
for (const old of oldTemplates) {
for (const dep of old.dependsOn) {
const newStageId = oldToNewId.get(dep.stageId);
const newPrereqId = oldToNewId.get(dep.prerequisiteId);
if (!newStageId || !newPrereqId) {
console.warn(` Skipping dependency: ${dep.stageId}${dep.prerequisiteId}`);
continue;
}
await prisma.pipelineStageDependencyV2.create({
data: { stageId: newStageId, prerequisiteId: newPrereqId },
});
depCount++;
}
}
console.log(`Created ${depCount} dependencies.`);
// 6. Backfill existing data
await backfillStages(pipeline.id);
await backfillProjects(pipeline.id);
console.log("Migration complete!");
}
async function backfillStages(pipelineId: string) {
// Get the stage definitions for this pipeline
const definitions = await prisma.pipelineStageDefinition.findMany({
where: { pipelineId },
});
// Get old templates to map slugs
const oldTemplates = await prisma.pipelineStageTemplate.findMany();
const slugToDefId = new Map(definitions.map((d) => [d.slug, d.id]));
const oldIdToSlug = new Map(oldTemplates.map((t) => [t.id, t.slug]));
// Find stages without stageDefinitionId
const stages = await prisma.deliverableStage.findMany({
where: { stageDefinitionId: null },
select: { id: true, templateId: true },
});
if (stages.length === 0) {
console.log("No stages need stageDefinitionId backfill.");
return;
}
console.log(`Backfilling stageDefinitionId on ${stages.length} stages...`);
let updated = 0;
const batchSize = 100;
for (let i = 0; i < stages.length; i += batchSize) {
const batch = stages.slice(i, i + batchSize);
await prisma.$transaction(
batch
.map((stage) => {
const slug = oldIdToSlug.get(stage.templateId);
const defId = slug ? slugToDefId.get(slug) : undefined;
if (!defId) return null;
return prisma.deliverableStage.update({
where: { id: stage.id },
data: { stageDefinitionId: defId },
});
})
.filter(Boolean) as any[]
);
updated += batch.length;
}
console.log(` Updated ${updated} stages.`);
}
async function backfillProjects(pipelineId: string) {
const projects = await prisma.project.findMany({
where: {
organizationId: ORG_ID,
pipelineTemplateId: null,
},
select: { id: true },
});
if (projects.length === 0) {
console.log("No projects need pipelineTemplateId backfill.");
return;
}
console.log(`Setting pipelineTemplateId on ${projects.length} projects...`);
await prisma.project.updateMany({
where: {
id: { in: projects.map((p) => p.id) },
},
data: { pipelineTemplateId: pipelineId },
});
console.log(` Updated ${projects.length} projects.`);
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});