mirror of
https://github.com/hex248/sprint.git
synced 2026-02-08 10:33:01 +00:00
full status implementation
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Issue, User } from "@issue/shared";
|
||||
import { aliasedTable, and, eq, sql } from "drizzle-orm";
|
||||
import { aliasedTable, and, eq, inArray, sql } from "drizzle-orm";
|
||||
import { db } from "../client";
|
||||
|
||||
export async function createIssue(
|
||||
@@ -8,6 +8,7 @@ export async function createIssue(
|
||||
description: string,
|
||||
creatorId: number,
|
||||
assigneeId?: number,
|
||||
status?: string,
|
||||
) {
|
||||
// prevents two issues with the same unique number
|
||||
return await db.transaction(async (tx) => {
|
||||
@@ -30,6 +31,7 @@ export async function createIssue(
|
||||
number: nextNumber,
|
||||
creatorId,
|
||||
assigneeId,
|
||||
...(status && { status }),
|
||||
})
|
||||
.returning();
|
||||
|
||||
@@ -43,7 +45,7 @@ export async function deleteIssue(id: number) {
|
||||
|
||||
export async function updateIssue(
|
||||
id: number,
|
||||
updates: { title?: string; description?: string; assigneeId?: number | null },
|
||||
updates: { title?: string; description?: string; assigneeId?: number | null; status?: string },
|
||||
) {
|
||||
return await db.update(Issue).set(updates).where(eq(Issue.id, id)).returning();
|
||||
}
|
||||
@@ -69,6 +71,27 @@ export async function getIssueByNumber(projectId: number, number: number) {
|
||||
return issue;
|
||||
}
|
||||
|
||||
export async function replaceIssueStatus(organisationId: number, oldStatus: string, newStatus: string) {
|
||||
const { Project } = await import("@issue/shared");
|
||||
|
||||
// get all project IDs for this organisation
|
||||
const projects = await db
|
||||
.select({ id: Project.id })
|
||||
.from(Project)
|
||||
.where(eq(Project.organisationId, organisationId));
|
||||
const projectIds = projects.map((p) => p.id);
|
||||
|
||||
if (projectIds.length === 0) return { updated: 0 };
|
||||
|
||||
// update all issues with oldStatus to newStatus for projects in this organisation
|
||||
const result = await db
|
||||
.update(Issue)
|
||||
.set({ status: newStatus })
|
||||
.where(and(eq(Issue.status, oldStatus), inArray(Issue.projectId, projectIds)));
|
||||
|
||||
return { updated: result.rowCount ?? 0 };
|
||||
}
|
||||
|
||||
export async function getIssuesWithUsersByProject(projectId: number) {
|
||||
const Creator = aliasedTable(User, "Creator");
|
||||
const Assignee = aliasedTable(User, "Assignee");
|
||||
|
||||
@@ -81,7 +81,7 @@ export async function getOrganisationsByUserId(userId: number) {
|
||||
|
||||
export async function updateOrganisation(
|
||||
organisationId: number,
|
||||
updates: { name?: string; description?: string; slug?: string },
|
||||
updates: { name?: string; description?: string; slug?: string; statuses?: string[] },
|
||||
) {
|
||||
const [organisation] = await db
|
||||
.update(Organisation)
|
||||
|
||||
@@ -42,6 +42,7 @@ const main = async () => {
|
||||
"/issue/delete": withCors(withAuth(withCSRF(routes.issueDelete))),
|
||||
|
||||
"/issues/by-project": withCors(withAuth(routes.issuesByProject)),
|
||||
"/issues/replace-status": withCors(withAuth(withCSRF(routes.issuesReplaceStatus))),
|
||||
"/issues/all": withCors(withAuth(routes.issues)),
|
||||
|
||||
"/organisation/create": withCors(withAuth(withCSRF(routes.organisationCreate))),
|
||||
|
||||
@@ -7,6 +7,7 @@ import issueDelete from "./issue/delete";
|
||||
import issueUpdate from "./issue/update";
|
||||
import issues from "./issues/all";
|
||||
import issuesByProject from "./issues/by-project";
|
||||
import issuesReplaceStatus from "./issues/replace-status";
|
||||
import organisationAddMember from "./organisation/add-member";
|
||||
import organisationById from "./organisation/by-id";
|
||||
import organisationsByUser from "./organisation/by-user";
|
||||
@@ -48,6 +49,7 @@ export const routes = {
|
||||
|
||||
issuesByProject,
|
||||
issues,
|
||||
issuesReplaceStatus,
|
||||
|
||||
organisationCreate,
|
||||
organisationById,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { AuthedRequest } from "../../auth/middleware";
|
||||
import { createIssue, getProjectByID, getProjectByKey } from "../../db/queries";
|
||||
|
||||
// /issue/create?projectId=1&title=Testing&description=Description
|
||||
// /issue/create?projectId=1&title=Testing&description=Description&status=TO%20DO
|
||||
// OR
|
||||
// /issue/create?projectKey=projectKey&title=Testing&description=Description
|
||||
// /issue/create?projectKey=projectKey&title=Testing&description=Description&status=TO%20DO
|
||||
export default async function issueCreate(req: AuthedRequest) {
|
||||
const url = new URL(req.url);
|
||||
const projectId = url.searchParams.get("projectId");
|
||||
@@ -25,8 +25,9 @@ export default async function issueCreate(req: AuthedRequest) {
|
||||
const description = url.searchParams.get("description") || "";
|
||||
const assigneeIdParam = url.searchParams.get("assigneeId");
|
||||
const assigneeId = assigneeIdParam ? Number(assigneeIdParam) : undefined;
|
||||
const status = url.searchParams.get("status") || undefined;
|
||||
|
||||
const issue = await createIssue(project.id, title, description, req.userId, assigneeId);
|
||||
const issue = await createIssue(project.id, title, description, req.userId, assigneeId, status);
|
||||
|
||||
return Response.json(issue);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { BunRequest } from "bun";
|
||||
import { updateIssue } from "../../db/queries";
|
||||
|
||||
// /issue/update?id=1&title=Testing&description=Description&assigneeId=2
|
||||
// /issue/update?id=1&title=Testing&description=Description&assigneeId=2&status=IN%20PROGRESS
|
||||
// assigneeId can be "null" to unassign
|
||||
export default async function issueUpdate(req: BunRequest) {
|
||||
const url = new URL(req.url);
|
||||
@@ -13,6 +13,7 @@ export default async function issueUpdate(req: BunRequest) {
|
||||
const title = url.searchParams.get("title") || undefined;
|
||||
const description = url.searchParams.get("description") || undefined;
|
||||
const assigneeIdParam = url.searchParams.get("assigneeId");
|
||||
const status = url.searchParams.get("status") || undefined;
|
||||
|
||||
// Parse assigneeId: "null" means unassign, number means assign, undefined means no change
|
||||
let assigneeId: number | null | undefined;
|
||||
@@ -22,7 +23,7 @@ export default async function issueUpdate(req: BunRequest) {
|
||||
assigneeId = Number(assigneeIdParam);
|
||||
}
|
||||
|
||||
if (!title && !description && assigneeId === undefined) {
|
||||
if (!title && !description && assigneeId === undefined && !status) {
|
||||
return new Response("no updates provided", { status: 400 });
|
||||
}
|
||||
|
||||
@@ -30,6 +31,7 @@ export default async function issueUpdate(req: BunRequest) {
|
||||
title,
|
||||
description,
|
||||
assigneeId,
|
||||
status,
|
||||
});
|
||||
|
||||
return Response.json(issue);
|
||||
|
||||
41
packages/backend/src/routes/issues/replace-status.ts
Normal file
41
packages/backend/src/routes/issues/replace-status.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { AuthedRequest } from "../../auth/middleware";
|
||||
import { getOrganisationMemberRole, replaceIssueStatus } from "../../db/queries";
|
||||
|
||||
// /issues/replace-status?organisationId=1&oldStatus=TO%20DO&newStatus=IN%20PROGRESS
|
||||
export default async function issuesReplaceStatus(req: AuthedRequest) {
|
||||
const url = new URL(req.url);
|
||||
const organisationIdParam = url.searchParams.get("organisationId");
|
||||
const oldStatus = url.searchParams.get("oldStatus");
|
||||
const newStatus = url.searchParams.get("newStatus");
|
||||
|
||||
if (!organisationIdParam) {
|
||||
return new Response("missing organisationId", { status: 400 });
|
||||
}
|
||||
|
||||
if (!oldStatus) {
|
||||
return new Response("missing oldStatus", { status: 400 });
|
||||
}
|
||||
|
||||
if (!newStatus) {
|
||||
return new Response("missing newStatus", { status: 400 });
|
||||
}
|
||||
|
||||
const organisationId = Number(organisationIdParam);
|
||||
if (!Number.isInteger(organisationId)) {
|
||||
return new Response("organisationId must be an integer", { status: 400 });
|
||||
}
|
||||
|
||||
// check if user is admin or owner of the organisation
|
||||
const membership = await getOrganisationMemberRole(organisationId, req.userId);
|
||||
if (!membership) {
|
||||
return new Response("not a member of this organisation", { status: 403 });
|
||||
}
|
||||
|
||||
if (membership.role !== "owner" && membership.role !== "admin") {
|
||||
return new Response("only admins and owners can replace statuses", { status: 403 });
|
||||
}
|
||||
|
||||
const result = await replaceIssueStatus(organisationId, oldStatus, newStatus);
|
||||
|
||||
return Response.json(result);
|
||||
}
|
||||
@@ -1,13 +1,29 @@
|
||||
import type { BunRequest } from "bun";
|
||||
import { getOrganisationById, updateOrganisation } from "../../db/queries";
|
||||
|
||||
// /organisation/update?id=1&name=New%20Name&description=New%20Description&slug=new-slug
|
||||
// /organisation/update?id=1&name=New%20Name&description=New%20Description&slug=new-slug&statuses=["TO DO","IN PROGRESS"]
|
||||
export default async function organisationUpdate(req: BunRequest) {
|
||||
const url = new URL(req.url);
|
||||
const id = url.searchParams.get("id");
|
||||
const name = url.searchParams.get("name") || undefined;
|
||||
const description = url.searchParams.get("description") || undefined;
|
||||
const slug = url.searchParams.get("slug") || undefined;
|
||||
const statusesParam = url.searchParams.get("statuses");
|
||||
|
||||
let statuses: string[] | undefined;
|
||||
if (statusesParam) {
|
||||
try {
|
||||
statuses = JSON.parse(statusesParam);
|
||||
if (!Array.isArray(statuses) || !statuses.every((s) => typeof s === "string")) {
|
||||
return new Response("statuses must be an array of strings", { status: 400 });
|
||||
}
|
||||
if (statuses.length === 0) {
|
||||
return new Response("statuses must have at least one status", { status: 400 });
|
||||
}
|
||||
} catch {
|
||||
return new Response("invalid statuses format (must be JSON array)", { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
return new Response("organisation id is required", { status: 400 });
|
||||
@@ -23,8 +39,8 @@ export default async function organisationUpdate(req: BunRequest) {
|
||||
return new Response(`organisation with id ${id} does not exist`, { status: 404 });
|
||||
}
|
||||
|
||||
if (!name && !description && !slug) {
|
||||
return new Response("at least one of name, description, or slug must be provided", {
|
||||
if (!name && !description && !slug && !statuses) {
|
||||
return new Response("at least one of name, description, slug, or statuses must be provided", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
@@ -33,6 +49,7 @@ export default async function organisationUpdate(req: BunRequest) {
|
||||
name,
|
||||
description,
|
||||
slug,
|
||||
statuses,
|
||||
});
|
||||
|
||||
return Response.json(organisation);
|
||||
|
||||
Reference in New Issue
Block a user