route + query updates for "assignees" update

This commit is contained in:
Oliver Bryan
2026-01-16 22:44:15 +00:00
parent 6ffb05eb3b
commit 2842d21ed5
4 changed files with 86 additions and 28 deletions

View File

@@ -1,4 +1,4 @@
import { Issue, User } from "@sprint/shared";
import { Issue, IssueAssignee, type IssueResponse, User, type UserRecord } from "@sprint/shared";
import { aliasedTable, and, eq, inArray, sql } from "drizzle-orm";
import { db } from "../client";
@@ -8,7 +8,7 @@ export async function createIssue(
description: string,
creatorId: number,
sprintId?: number,
assigneeId?: number,
assigneeIds?: number[],
status?: string,
) {
// prevents two issues with the same unique number
@@ -22,7 +22,6 @@ export async function createIssue(
const nextNumber = (lastIssue?.max || 0) + 1;
// 2. create new issue
const [newIssue] = await tx
.insert(Issue)
.values({
@@ -32,11 +31,23 @@ export async function createIssue(
number: nextNumber,
creatorId,
sprintId,
assigneeId,
...(status && { status }),
})
.returning();
if (!newIssue) {
throw new Error("failed to create issue");
}
if (assigneeIds && assigneeIds.length > 0) {
await tx.insert(IssueAssignee).values(
assigneeIds.map((userId) => ({
issueId: newIssue.id,
userId,
})),
);
}
return newIssue;
});
}
@@ -51,13 +62,27 @@ export async function updateIssue(
title?: string;
description?: string;
sprintId?: number | null;
assigneeId?: number | null;
status?: string;
},
) {
return await db.update(Issue).set(updates).where(eq(Issue.id, id)).returning();
}
export async function setIssueAssignees(issueId: number, userIds: number[]) {
return await db.transaction(async (tx) => {
await tx.delete(IssueAssignee).where(eq(IssueAssignee.issueId, issueId));
if (userIds.length > 0) {
await tx.insert(IssueAssignee).values(
userIds.map((userId) => ({
issueId,
userId,
})),
);
}
});
}
export async function getIssues() {
return await db.select().from(Issue);
}
@@ -119,18 +144,41 @@ export async function replaceIssueStatus(organisationId: number, oldStatus: stri
return { updated: result.rowCount ?? 0 };
}
export async function getIssuesWithUsersByProject(projectId: number) {
export async function getIssuesWithUsersByProject(projectId: number): Promise<IssueResponse[]> {
const Creator = aliasedTable(User, "Creator");
const Assignee = aliasedTable(User, "Assignee");
return await db
const issuesWithCreators = await db
.select({
Issue: Issue,
Creator: Creator,
Assignee: Assignee,
})
.from(Issue)
.where(eq(Issue.projectId, projectId))
.innerJoin(Creator, eq(Issue.creatorId, Creator.id))
.leftJoin(Assignee, eq(Issue.assigneeId, Assignee.id));
.innerJoin(Creator, eq(Issue.creatorId, Creator.id));
const issueIds = issuesWithCreators.map((i) => i.Issue.id);
const assigneesData =
issueIds.length > 0
? await db
.select({
issueId: IssueAssignee.issueId,
User: User,
})
.from(IssueAssignee)
.innerJoin(User, eq(IssueAssignee.userId, User.id))
.where(inArray(IssueAssignee.issueId, issueIds))
: [];
const assigneesByIssue = new Map<number, UserRecord[]>();
for (const a of assigneesData) {
const existing = assigneesByIssue.get(a.issueId) || [];
existing.push(a.User);
assigneesByIssue.set(a.issueId, existing);
}
return issuesWithCreators.map((row) => ({
Issue: row.Issue,
Creator: row.Creator,
Assignees: assigneesByIssue.get(row.Issue.id) || [],
}));
}

View File

@@ -7,7 +7,7 @@ export default async function issueCreate(req: AuthedRequest) {
const parsed = await parseJsonBody(req, IssueCreateRequestSchema);
if ("error" in parsed) return parsed.error;
const { projectId, title, description = "", status, assigneeId, sprintId } = parsed.data;
const { projectId, title, description = "", status, assigneeIds, sprintId } = parsed.data;
const project = await getProjectByID(projectId);
if (!project) {
@@ -20,7 +20,7 @@ export default async function issueCreate(req: AuthedRequest) {
description,
req.userId,
sprintId ?? undefined,
assigneeId ?? undefined,
assigneeIds,
status,
);

View File

@@ -1,32 +1,43 @@
import { IssueUpdateRequestSchema } from "@sprint/shared";
import { type IssueRecord, IssueUpdateRequestSchema } from "@sprint/shared";
import type { BunRequest } from "bun";
import { updateIssue } from "../../db/queries";
import { getIssueByID, setIssueAssignees, updateIssue } from "../../db/queries";
import { errorResponse, parseJsonBody } from "../../validation";
export default async function issueUpdate(req: BunRequest) {
const parsed = await parseJsonBody(req, IssueUpdateRequestSchema);
if ("error" in parsed) return parsed.error;
const { id, title, description, status, assigneeId, sprintId } = parsed.data;
const { id, title, description, status, assigneeIds, sprintId } = parsed.data;
// check that at least one field is being updated
if (
title === undefined &&
description === undefined &&
status === undefined &&
assigneeId === undefined &&
assigneeIds === undefined &&
sprintId === undefined
) {
return errorResponse("no updates provided", "NO_UPDATES", 400);
}
const issue = await updateIssue(id, {
title,
description,
sprintId,
assigneeId,
status,
});
const hasIssueFieldUpdates =
title !== undefined || description !== undefined || status !== undefined || sprintId !== undefined;
let issue: IssueRecord | undefined;
if (hasIssueFieldUpdates) {
[issue] = await updateIssue(id, {
title,
description,
sprintId,
status,
});
} else {
issue = await getIssueByID(id);
}
if (assigneeIds !== undefined) {
await setIssueAssignees(id, assigneeIds ?? []);
}
return Response.json(issue);
}