mirror of
https://github.com/hex248/sprint.git
synced 2026-02-07 18:23:03 +00:00
route + query updates for "assignees" update
This commit is contained in:
@@ -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 { aliasedTable, and, eq, inArray, sql } from "drizzle-orm";
|
||||||
import { db } from "../client";
|
import { db } from "../client";
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ export async function createIssue(
|
|||||||
description: string,
|
description: string,
|
||||||
creatorId: number,
|
creatorId: number,
|
||||||
sprintId?: number,
|
sprintId?: number,
|
||||||
assigneeId?: number,
|
assigneeIds?: number[],
|
||||||
status?: string,
|
status?: string,
|
||||||
) {
|
) {
|
||||||
// prevents two issues with the same unique number
|
// prevents two issues with the same unique number
|
||||||
@@ -22,7 +22,6 @@ export async function createIssue(
|
|||||||
|
|
||||||
const nextNumber = (lastIssue?.max || 0) + 1;
|
const nextNumber = (lastIssue?.max || 0) + 1;
|
||||||
|
|
||||||
// 2. create new issue
|
|
||||||
const [newIssue] = await tx
|
const [newIssue] = await tx
|
||||||
.insert(Issue)
|
.insert(Issue)
|
||||||
.values({
|
.values({
|
||||||
@@ -32,11 +31,23 @@ export async function createIssue(
|
|||||||
number: nextNumber,
|
number: nextNumber,
|
||||||
creatorId,
|
creatorId,
|
||||||
sprintId,
|
sprintId,
|
||||||
assigneeId,
|
|
||||||
...(status && { status }),
|
...(status && { status }),
|
||||||
})
|
})
|
||||||
.returning();
|
.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;
|
return newIssue;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -51,13 +62,27 @@ export async function updateIssue(
|
|||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
sprintId?: number | null;
|
sprintId?: number | null;
|
||||||
assigneeId?: number | null;
|
|
||||||
status?: string;
|
status?: string;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
return await db.update(Issue).set(updates).where(eq(Issue.id, id)).returning();
|
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() {
|
export async function getIssues() {
|
||||||
return await db.select().from(Issue);
|
return await db.select().from(Issue);
|
||||||
}
|
}
|
||||||
@@ -119,18 +144,41 @@ export async function replaceIssueStatus(organisationId: number, oldStatus: stri
|
|||||||
return { updated: result.rowCount ?? 0 };
|
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 Creator = aliasedTable(User, "Creator");
|
||||||
const Assignee = aliasedTable(User, "Assignee");
|
|
||||||
|
|
||||||
return await db
|
const issuesWithCreators = await db
|
||||||
.select({
|
.select({
|
||||||
Issue: Issue,
|
Issue: Issue,
|
||||||
Creator: Creator,
|
Creator: Creator,
|
||||||
Assignee: Assignee,
|
|
||||||
})
|
})
|
||||||
.from(Issue)
|
.from(Issue)
|
||||||
.where(eq(Issue.projectId, projectId))
|
.where(eq(Issue.projectId, projectId))
|
||||||
.innerJoin(Creator, eq(Issue.creatorId, Creator.id))
|
.innerJoin(Creator, eq(Issue.creatorId, Creator.id));
|
||||||
.leftJoin(Assignee, eq(Issue.assigneeId, Assignee.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) || [],
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default async function issueCreate(req: AuthedRequest) {
|
|||||||
const parsed = await parseJsonBody(req, IssueCreateRequestSchema);
|
const parsed = await parseJsonBody(req, IssueCreateRequestSchema);
|
||||||
if ("error" in parsed) return parsed.error;
|
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);
|
const project = await getProjectByID(projectId);
|
||||||
if (!project) {
|
if (!project) {
|
||||||
@@ -20,7 +20,7 @@ export default async function issueCreate(req: AuthedRequest) {
|
|||||||
description,
|
description,
|
||||||
req.userId,
|
req.userId,
|
||||||
sprintId ?? undefined,
|
sprintId ?? undefined,
|
||||||
assigneeId ?? undefined,
|
assigneeIds,
|
||||||
status,
|
status,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,43 @@
|
|||||||
import { IssueUpdateRequestSchema } from "@sprint/shared";
|
import { type IssueRecord, IssueUpdateRequestSchema } from "@sprint/shared";
|
||||||
import type { BunRequest } from "bun";
|
import type { BunRequest } from "bun";
|
||||||
import { updateIssue } from "../../db/queries";
|
import { getIssueByID, setIssueAssignees, updateIssue } from "../../db/queries";
|
||||||
import { errorResponse, parseJsonBody } from "../../validation";
|
import { errorResponse, parseJsonBody } from "../../validation";
|
||||||
|
|
||||||
export default async function issueUpdate(req: BunRequest) {
|
export default async function issueUpdate(req: BunRequest) {
|
||||||
const parsed = await parseJsonBody(req, IssueUpdateRequestSchema);
|
const parsed = await parseJsonBody(req, IssueUpdateRequestSchema);
|
||||||
if ("error" in parsed) return parsed.error;
|
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
|
// check that at least one field is being updated
|
||||||
if (
|
if (
|
||||||
title === undefined &&
|
title === undefined &&
|
||||||
description === undefined &&
|
description === undefined &&
|
||||||
status === undefined &&
|
status === undefined &&
|
||||||
assigneeId === undefined &&
|
assigneeIds === undefined &&
|
||||||
sprintId === undefined
|
sprintId === undefined
|
||||||
) {
|
) {
|
||||||
return errorResponse("no updates provided", "NO_UPDATES", 400);
|
return errorResponse("no updates provided", "NO_UPDATES", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const issue = await updateIssue(id, {
|
const hasIssueFieldUpdates =
|
||||||
|
title !== undefined || description !== undefined || status !== undefined || sprintId !== undefined;
|
||||||
|
|
||||||
|
let issue: IssueRecord | undefined;
|
||||||
|
if (hasIssueFieldUpdates) {
|
||||||
|
[issue] = await updateIssue(id, {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
sprintId,
|
sprintId,
|
||||||
assigneeId,
|
|
||||||
status,
|
status,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
issue = await getIssueByID(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assigneeIds !== undefined) {
|
||||||
|
await setIssueAssignees(id, assigneeIds ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
return Response.json(issue);
|
return Response.json(issue);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const IssueCreateRequestSchema = z.object({
|
|||||||
title: z.string().min(1, "title is required").max(ISSUE_TITLE_MAX_LENGTH),
|
title: z.string().min(1, "title is required").max(ISSUE_TITLE_MAX_LENGTH),
|
||||||
description: z.string().max(ISSUE_DESCRIPTION_MAX_LENGTH).default(""),
|
description: z.string().max(ISSUE_DESCRIPTION_MAX_LENGTH).default(""),
|
||||||
status: z.string().max(ISSUE_STATUS_MAX_LENGTH).optional(),
|
status: z.string().max(ISSUE_STATUS_MAX_LENGTH).optional(),
|
||||||
assigneeId: z.number().int().positive().nullable().optional(),
|
assigneeIds: z.array(z.number().int().positive()).optional(),
|
||||||
sprintId: z.number().int().positive().nullable().optional(),
|
sprintId: z.number().int().positive().nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ export const IssueUpdateRequestSchema = z.object({
|
|||||||
title: z.string().min(1).max(ISSUE_TITLE_MAX_LENGTH).optional(),
|
title: z.string().min(1).max(ISSUE_TITLE_MAX_LENGTH).optional(),
|
||||||
description: z.string().max(ISSUE_DESCRIPTION_MAX_LENGTH).optional(),
|
description: z.string().max(ISSUE_DESCRIPTION_MAX_LENGTH).optional(),
|
||||||
status: z.string().max(ISSUE_STATUS_MAX_LENGTH).optional(),
|
status: z.string().max(ISSUE_STATUS_MAX_LENGTH).optional(),
|
||||||
assigneeId: z.number().int().positive().nullable().optional(),
|
assigneeIds: z.array(z.number().int().positive()).nullable().optional(),
|
||||||
sprintId: z.number().int().positive().nullable().optional(),
|
sprintId: z.number().int().positive().nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -328,14 +328,13 @@ export const IssueRecordSchema = z.object({
|
|||||||
description: z.string(),
|
description: z.string(),
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
creatorId: z.number(),
|
creatorId: z.number(),
|
||||||
assigneeId: z.number().nullable(),
|
|
||||||
sprintId: z.number().nullable(),
|
sprintId: z.number().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const IssueResponseSchema = z.object({
|
export const IssueResponseSchema = z.object({
|
||||||
Issue: IssueRecordSchema,
|
Issue: IssueRecordSchema,
|
||||||
Creator: UserResponseSchema,
|
Creator: UserResponseSchema,
|
||||||
Assignee: UserResponseSchema.nullable(),
|
Assignees: z.array(UserResponseSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type IssueResponseType = z.infer<typeof IssueResponseSchema>;
|
export type IssueResponseType = z.infer<typeof IssueResponseSchema>;
|
||||||
|
|||||||
Reference in New Issue
Block a user