refactor: Restructure monorepo to apps/server/ and apps/web/ layout
Move src/ → apps/server/ and packages/web/ → apps/web/ to adopt standard monorepo conventions (apps/ for runnable apps, packages/ for reusable libraries). Update all config files, shared package imports, test fixtures, and documentation to reflect new paths. Key fixes: - Update workspace config to ["apps/*", "packages/*"] - Update tsconfig.json rootDir/include for apps/server/ - Add apps/web/** to vitest exclude list - Update drizzle.config.ts schema path - Fix ensure-schema.ts migration path detection (3 levels up in dev, 2 levels up in dist) - Fix tests/integration/cli-server.test.ts import paths - Update packages/shared imports to apps/server/ paths - Update all docs/ files with new paths
This commit is contained in:
157
apps/server/trpc/routers/project.ts
Normal file
157
apps/server/trpc/routers/project.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Project Router — register, list, get, delete, initiative associations
|
||||
*/
|
||||
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { z } from 'zod';
|
||||
import { join } from 'node:path';
|
||||
import { rm } from 'node:fs/promises';
|
||||
import type { ProcedureBuilder } from '../trpc.js';
|
||||
import { requireProjectRepository } from './_helpers.js';
|
||||
import { cloneProject } from '../../git/clone.js';
|
||||
import { getProjectCloneDir } from '../../git/project-clones.js';
|
||||
|
||||
export function projectProcedures(publicProcedure: ProcedureBuilder) {
|
||||
return {
|
||||
registerProject: publicProcedure
|
||||
.input(z.object({
|
||||
name: z.string().min(1),
|
||||
url: z.string().min(1),
|
||||
defaultBranch: z.string().min(1).optional(),
|
||||
}))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const repo = requireProjectRepository(ctx);
|
||||
|
||||
let project;
|
||||
try {
|
||||
project = await repo.create({
|
||||
name: input.name,
|
||||
url: input.url,
|
||||
...(input.defaultBranch && { defaultBranch: input.defaultBranch }),
|
||||
});
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
if (msg.includes('UNIQUE') || msg.includes('unique')) {
|
||||
throw new TRPCError({
|
||||
code: 'CONFLICT',
|
||||
message: `A project with that name or URL already exists`,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (ctx.workspaceRoot) {
|
||||
const clonePath = join(ctx.workspaceRoot, getProjectCloneDir(input.name, project.id));
|
||||
|
||||
try {
|
||||
await cloneProject(input.url, clonePath);
|
||||
} catch (cloneError) {
|
||||
await repo.delete(project.id);
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: `Failed to clone repository: ${(cloneError as Error).message}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Validate that the specified default branch exists in the cloned repo
|
||||
const branchToValidate = input.defaultBranch ?? 'main';
|
||||
if (ctx.branchManager) {
|
||||
const exists = await ctx.branchManager.remoteBranchExists(clonePath, branchToValidate);
|
||||
if (!exists) {
|
||||
// Clean up: remove project and clone
|
||||
await rm(clonePath, { recursive: true, force: true }).catch(() => {});
|
||||
await repo.delete(project.id);
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: `Branch '${branchToValidate}' does not exist in the repository`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return project;
|
||||
}),
|
||||
|
||||
listProjects: publicProcedure
|
||||
.query(async ({ ctx }) => {
|
||||
const repo = requireProjectRepository(ctx);
|
||||
return repo.findAll();
|
||||
}),
|
||||
|
||||
getProject: publicProcedure
|
||||
.input(z.object({ id: z.string().min(1) }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const repo = requireProjectRepository(ctx);
|
||||
const project = await repo.findById(input.id);
|
||||
if (!project) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: `Project '${input.id}' not found`,
|
||||
});
|
||||
}
|
||||
return project;
|
||||
}),
|
||||
|
||||
deleteProject: publicProcedure
|
||||
.input(z.object({ id: z.string().min(1) }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const repo = requireProjectRepository(ctx);
|
||||
const project = await repo.findById(input.id);
|
||||
if (project && ctx.workspaceRoot) {
|
||||
const clonePath = join(ctx.workspaceRoot, getProjectCloneDir(project.name, project.id));
|
||||
await rm(clonePath, { recursive: true, force: true }).catch(() => {});
|
||||
}
|
||||
await repo.delete(input.id);
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
updateProject: publicProcedure
|
||||
.input(z.object({
|
||||
id: z.string().min(1),
|
||||
defaultBranch: z.string().min(1).optional(),
|
||||
}))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const repo = requireProjectRepository(ctx);
|
||||
const { id, ...data } = input;
|
||||
const existing = await repo.findById(id);
|
||||
if (!existing) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: `Project '${id}' not found`,
|
||||
});
|
||||
}
|
||||
|
||||
// Validate that the new default branch exists in the repo
|
||||
if (data.defaultBranch && ctx.workspaceRoot && ctx.branchManager) {
|
||||
const clonePath = join(ctx.workspaceRoot, getProjectCloneDir(existing.name, existing.id));
|
||||
const exists = await ctx.branchManager.remoteBranchExists(clonePath, data.defaultBranch);
|
||||
if (!exists) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: `Branch '${data.defaultBranch}' does not exist in the repository`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return repo.update(id, data);
|
||||
}),
|
||||
|
||||
getInitiativeProjects: publicProcedure
|
||||
.input(z.object({ initiativeId: z.string().min(1) }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const repo = requireProjectRepository(ctx);
|
||||
return repo.findProjectsByInitiativeId(input.initiativeId);
|
||||
}),
|
||||
|
||||
updateInitiativeProjects: publicProcedure
|
||||
.input(z.object({
|
||||
initiativeId: z.string().min(1),
|
||||
projectIds: z.array(z.string().min(1)).min(1),
|
||||
}))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const repo = requireProjectRepository(ctx);
|
||||
await repo.setInitiativeProjects(input.initiativeId, input.projectIds);
|
||||
return { success: true };
|
||||
}),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user