From 0dcfe1b66bfe1c19561919e5fbeb6d2aa9f9d328 Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Sat, 17 Jan 2026 02:46:49 +0000 Subject: [PATCH] prevent overlapping sprints --- packages/backend/src/db/queries/sprints.ts | 17 ++++++++++++++- packages/backend/src/routes/sprint/create.ts | 23 +++++++++++++++----- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/db/queries/sprints.ts b/packages/backend/src/db/queries/sprints.ts index f15881d..3e2f496 100644 --- a/packages/backend/src/db/queries/sprints.ts +++ b/packages/backend/src/db/queries/sprints.ts @@ -1,5 +1,5 @@ import { Sprint } from "@sprint/shared"; -import { eq } from "drizzle-orm"; +import { and, eq, gte, lte } from "drizzle-orm"; import { db } from "../client"; export async function createSprint( @@ -25,3 +25,18 @@ export async function createSprint( export async function getSprintsByProject(projectId: number) { return await db.select().from(Sprint).where(eq(Sprint.projectId, projectId)); } + +export async function hasOverlappingSprints(projectId: number, startDate: Date, endDate: Date) { + const overlapping = await db + .select({ id: Sprint.id }) + .from(Sprint) + .where( + and( + eq(Sprint.projectId, projectId), + lte(Sprint.startDate, endDate), + gte(Sprint.endDate, startDate), + ), + ) + .limit(1); + return overlapping.length > 0; +} diff --git a/packages/backend/src/routes/sprint/create.ts b/packages/backend/src/routes/sprint/create.ts index 19a9390..9d535e8 100644 --- a/packages/backend/src/routes/sprint/create.ts +++ b/packages/backend/src/routes/sprint/create.ts @@ -1,6 +1,11 @@ import { SprintCreateRequestSchema } from "@sprint/shared"; import type { AuthedRequest } from "../../auth/middleware"; -import { createSprint, getOrganisationMemberRole, getProjectByID } from "../../db/queries"; +import { + createSprint, + getOrganisationMemberRole, + getProjectByID, + hasOverlappingSprints, +} from "../../db/queries"; import { errorResponse, parseJsonBody } from "../../validation"; export default async function sprintCreate(req: AuthedRequest) { @@ -11,19 +16,27 @@ export default async function sprintCreate(req: AuthedRequest) { const project = await getProjectByID(projectId); if (!project) { - return errorResponse(`project not found: ${projectId}`, "PROJECT_NOT_FOUND", 404); + return errorResponse(`Project not found: ${projectId}`, "PROJECT_NOT_FOUND", 404); } const membership = await getOrganisationMemberRole(project.organisationId, req.userId); if (!membership) { - return errorResponse("not a member of this organisation", "NOT_MEMBER", 403); + return errorResponse("Not a member of this organisation", "NOT_MEMBER", 403); } if (membership.role !== "owner" && membership.role !== "admin") { - return errorResponse("only owners and admins can create sprints", "PERMISSION_DENIED", 403); + return errorResponse("Only owners and admins can create sprints", "PERMISSION_DENIED", 403); } - const sprint = await createSprint(project.id, name, color, new Date(startDate), new Date(endDate)); + const start = new Date(startDate); + const end = new Date(endDate); + + const hasOverlap = await hasOverlappingSprints(project.id, start, end); + if (hasOverlap) { + return errorResponse("Sprint dates overlap with an existing sprint", "SPRINT_OVERLAP", 400); + } + + const sprint = await createSprint(project.id, name, color, start, end); return Response.json(sprint); }