All files / src/trpc/routers initiative.ts

13.46% Statements 7/52
21.87% Branches 7/32
20% Functions 2/10
14.28% Lines 7/49

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154                    11x                 19x   19x                         19x             19x         19x                   19x                                                                                                                                                                                                  
/**
 * Initiative Router — create, list, get, update, merge config
 */
 
import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import type { ProcedureBuilder } from '../trpc.js';
import { requireInitiativeRepository, requireProjectRepository, requireTaskRepository } from './_helpers.js';
 
export function initiativeProcedures(publicProcedure: ProcedureBuilder) {
  return {
    createInitiative: publicProcedure
      .input(z.object({
        name: z.string().min(1),
        branch: z.string().nullable().optional(),
        projectIds: z.array(z.string().min(1)).min(1).optional(),
        executionMode: z.enum(['yolo', 'review_per_phase']).optional(),
      }))
      .mutation(async ({ ctx, input }) => {
        const repo = requireInitiativeRepository(ctx);
 
        Iif (input.projectIds && input.projectIds.length > 0) {
          const projectRepo = requireProjectRepository(ctx);
          for (const pid of input.projectIds) {
            const project = await projectRepo.findById(pid);
            if (!project) {
              throw new TRPCError({
                code: 'NOT_FOUND',
                message: `Project '${pid}' not found`,
              });
            }
          }
        }
 
        const initiative = await repo.create({
          name: input.name,
          status: 'active',
          ...(input.executionMode && { executionMode: input.executionMode }),
          ...(input.branch && { branch: input.branch }),
        });
 
        Iif (input.projectIds && input.projectIds.length > 0) {
          const projectRepo = requireProjectRepository(ctx);
          await projectRepo.setInitiativeProjects(initiative.id, input.projectIds);
        }
 
        Iif (ctx.pageRepository) {
          await ctx.pageRepository.create({
            initiativeId: initiative.id,
            parentPageId: null,
            title: input.name,
            content: null,
            sortOrder: 0,
          });
        }
 
        return initiative;
      }),
 
    listInitiatives: publicProcedure
      .input(z.object({
        status: z.enum(['active', 'completed', 'archived']).optional(),
      }).optional())
      .query(async ({ ctx, input }) => {
        const repo = requireInitiativeRepository(ctx);
        if (input?.status) {
          return repo.findByStatus(input.status);
        }
        return repo.findAll();
      }),
 
    getInitiative: publicProcedure
      .input(z.object({ id: z.string().min(1) }))
      .query(async ({ ctx, input }) => {
        const repo = requireInitiativeRepository(ctx);
        const initiative = await repo.findById(input.id);
        if (!initiative) {
          throw new TRPCError({
            code: 'NOT_FOUND',
            message: `Initiative '${input.id}' not found`,
          });
        }
 
        let projects: Array<{ id: string; name: string; url: string }> = [];
        if (ctx.projectRepository) {
          const fullProjects = await ctx.projectRepository.findProjectsByInitiativeId(input.id);
          projects = fullProjects.map((p) => ({ id: p.id, name: p.name, url: p.url }));
        }
 
        let branchLocked = false;
        if (ctx.taskRepository) {
          const tasks = await ctx.taskRepository.findByInitiativeId(input.id);
          branchLocked = tasks.some((t) => t.status !== 'pending');
        }
 
        return { ...initiative, projects, branchLocked };
      }),
 
    updateInitiative: publicProcedure
      .input(z.object({
        id: z.string().min(1),
        name: z.string().min(1).optional(),
        status: z.enum(['active', 'completed', 'archived']).optional(),
      }))
      .mutation(async ({ ctx, input }) => {
        const repo = requireInitiativeRepository(ctx);
        const { id, ...data } = input;
        return repo.update(id, data);
      }),
 
    deleteInitiative: publicProcedure
      .input(z.object({ id: z.string().min(1) }))
      .mutation(async ({ ctx, input }) => {
        const repo = requireInitiativeRepository(ctx);
        await repo.delete(input.id);
        return { success: true };
      }),
 
    updateInitiativeConfig: publicProcedure
      .input(z.object({
        initiativeId: z.string().min(1),
        mergeRequiresApproval: z.boolean().optional(),
        executionMode: z.enum(['yolo', 'review_per_phase']).optional(),
        branch: z.string().nullable().optional(),
      }))
      .mutation(async ({ ctx, input }) => {
        const repo = requireInitiativeRepository(ctx);
        const { initiativeId, ...data } = input;
 
        const existing = await repo.findById(initiativeId);
        if (!existing) {
          throw new TRPCError({
            code: 'NOT_FOUND',
            message: `Initiative '${initiativeId}' not found`,
          });
        }
 
        // Prevent branch changes once work has started
        if (data.branch !== undefined && ctx.taskRepository) {
          const tasks = await ctx.taskRepository.findByInitiativeId(initiativeId);
          const hasStarted = tasks.some((t) => t.status !== 'pending');
          if (hasStarted) {
            throw new TRPCError({
              code: 'PRECONDITION_FAILED',
              message: 'Cannot change branch after work has started',
            });
          }
        }
 
        return repo.update(initiativeId, data);
      }),
  };
}