full comments system

This commit is contained in:
Oliver Bryan
2026-01-21 19:10:28 +00:00
parent 0d2195cab4
commit 8f87fc8acf
28 changed files with 1451 additions and 7 deletions

View File

@@ -0,0 +1,8 @@
CREATE TABLE "IssueComment" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
"issueId" integer NOT NULL REFERENCES "Issue" ("id") ON DELETE CASCADE,
"userId" integer NOT NULL REFERENCES "User" ("id") ON DELETE CASCADE,
"body" varchar(2048) NOT NULL,
"createdAt" timestamp NOT NULL DEFAULT now(),
"updatedAt" timestamp NOT NULL DEFAULT now()
);

View File

@@ -0,0 +1,11 @@
CREATE TABLE "IssueComment" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "IssueComment_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"issueId" integer NOT NULL,
"userId" integer NOT NULL,
"body" varchar(2048) NOT NULL,
"createdAt" timestamp DEFAULT now(),
"updatedAt" timestamp DEFAULT now()
);
--> statement-breakpoint
ALTER TABLE "IssueComment" ADD CONSTRAINT "IssueComment_issueId_Issue_id_fk" FOREIGN KEY ("issueId") REFERENCES "public"."Issue"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "IssueComment" ADD CONSTRAINT "IssueComment_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE no action;

View File

@@ -0,0 +1,909 @@
{
"id": "c19db523-bbc5-4069-9eea-bdd37cf36c64",
"prevId": "fb4c4093-9a4d-43b2-844d-f8e9912c564a",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.Issue": {
"name": "Issue",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "Issue_id_seq",
"schema": "public",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"projectId": {
"name": "projectId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"number": {
"name": "number",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"title": {
"name": "title",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "varchar(2048)",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "varchar(24)",
"primaryKey": false,
"notNull": true,
"default": "'TO DO'"
},
"creatorId": {
"name": "creatorId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"sprintId": {
"name": "sprintId",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"unique_project_issue_number": {
"name": "unique_project_issue_number",
"columns": [
{
"expression": "projectId",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "number",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"Issue_projectId_Project_id_fk": {
"name": "Issue_projectId_Project_id_fk",
"tableFrom": "Issue",
"tableTo": "Project",
"columnsFrom": [
"projectId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"Issue_creatorId_User_id_fk": {
"name": "Issue_creatorId_User_id_fk",
"tableFrom": "Issue",
"tableTo": "User",
"columnsFrom": [
"creatorId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"Issue_sprintId_Sprint_id_fk": {
"name": "Issue_sprintId_Sprint_id_fk",
"tableFrom": "Issue",
"tableTo": "Sprint",
"columnsFrom": [
"sprintId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.IssueAssignee": {
"name": "IssueAssignee",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "IssueAssignee_id_seq",
"schema": "public",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"issueId": {
"name": "issueId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"assignedAt": {
"name": "assignedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {
"unique_issue_user": {
"name": "unique_issue_user",
"columns": [
{
"expression": "issueId",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "userId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"IssueAssignee_issueId_Issue_id_fk": {
"name": "IssueAssignee_issueId_Issue_id_fk",
"tableFrom": "IssueAssignee",
"tableTo": "Issue",
"columnsFrom": [
"issueId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"IssueAssignee_userId_User_id_fk": {
"name": "IssueAssignee_userId_User_id_fk",
"tableFrom": "IssueAssignee",
"tableTo": "User",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.IssueComment": {
"name": "IssueComment",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "IssueComment_id_seq",
"schema": "public",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"issueId": {
"name": "issueId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"body": {
"name": "body",
"type": "varchar(2048)",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"IssueComment_issueId_Issue_id_fk": {
"name": "IssueComment_issueId_Issue_id_fk",
"tableFrom": "IssueComment",
"tableTo": "Issue",
"columnsFrom": [
"issueId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"IssueComment_userId_User_id_fk": {
"name": "IssueComment_userId_User_id_fk",
"tableFrom": "IssueComment",
"tableTo": "User",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Organisation": {
"name": "Organisation",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "Organisation_id_seq",
"schema": "public",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"name": {
"name": "name",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "varchar(1024)",
"primaryKey": false,
"notNull": false
},
"slug": {
"name": "slug",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"iconURL": {
"name": "iconURL",
"type": "varchar(512)",
"primaryKey": false,
"notNull": false
},
"statuses": {
"name": "statuses",
"type": "json",
"primaryKey": false,
"notNull": true,
"default": "'{\"TO DO\":\"#fafafa\",\"IN PROGRESS\":\"#f97316\",\"REVIEW\":\"#8952bc\",\"DONE\":\"#22c55e\",\"REJECTED\":\"#ef4444\",\"ARCHIVED\":\"#a1a1a1\",\"MERGED\":\"#a1a1a1\"}'::json"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"Organisation_slug_unique": {
"name": "Organisation_slug_unique",
"nullsNotDistinct": false,
"columns": [
"slug"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.OrganisationMember": {
"name": "OrganisationMember",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "OrganisationMember_id_seq",
"schema": "public",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"organisationId": {
"name": "organisationId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"OrganisationMember_organisationId_Organisation_id_fk": {
"name": "OrganisationMember_organisationId_Organisation_id_fk",
"tableFrom": "OrganisationMember",
"tableTo": "Organisation",
"columnsFrom": [
"organisationId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"OrganisationMember_userId_User_id_fk": {
"name": "OrganisationMember_userId_User_id_fk",
"tableFrom": "OrganisationMember",
"tableTo": "User",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Project": {
"name": "Project",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "Project_id_seq",
"schema": "public",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"key": {
"name": "key",
"type": "varchar(4)",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"organisationId": {
"name": "organisationId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"creatorId": {
"name": "creatorId",
"type": "integer",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Project_organisationId_Organisation_id_fk": {
"name": "Project_organisationId_Organisation_id_fk",
"tableFrom": "Project",
"tableTo": "Organisation",
"columnsFrom": [
"organisationId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"Project_creatorId_User_id_fk": {
"name": "Project_creatorId_User_id_fk",
"tableFrom": "Project",
"tableTo": "User",
"columnsFrom": [
"creatorId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Session": {
"name": "Session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "Session_id_seq",
"schema": "public",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"userId": {
"name": "userId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"csrfToken": {
"name": "csrfToken",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"expiresAt": {
"name": "expiresAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"Session_userId_User_id_fk": {
"name": "Session_userId_User_id_fk",
"tableFrom": "Session",
"tableTo": "User",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.Sprint": {
"name": "Sprint",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "Sprint_id_seq",
"schema": "public",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"projectId": {
"name": "projectId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"color": {
"name": "color",
"type": "varchar(7)",
"primaryKey": false,
"notNull": true,
"default": "'#a1a1a1'"
},
"startDate": {
"name": "startDate",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"endDate": {
"name": "endDate",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"Sprint_projectId_Project_id_fk": {
"name": "Sprint_projectId_Project_id_fk",
"tableFrom": "Sprint",
"tableTo": "Project",
"columnsFrom": [
"projectId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.TimedSession": {
"name": "TimedSession",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "TimedSession_id_seq",
"schema": "public",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"userId": {
"name": "userId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"issueId": {
"name": "issueId",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"timestamps": {
"name": "timestamps",
"type": "timestamp[]",
"primaryKey": false,
"notNull": true
},
"endedAt": {
"name": "endedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"TimedSession_userId_User_id_fk": {
"name": "TimedSession_userId_User_id_fk",
"tableFrom": "TimedSession",
"tableTo": "User",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"TimedSession_issueId_Issue_id_fk": {
"name": "TimedSession_issueId_Issue_id_fk",
"tableFrom": "TimedSession",
"tableTo": "Issue",
"columnsFrom": [
"issueId"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.User": {
"name": "User",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "User_id_seq",
"schema": "public",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"name": {
"name": "name",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
},
"passwordHash": {
"name": "passwordHash",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"avatarURL": {
"name": "avatarURL",
"type": "varchar(512)",
"primaryKey": false,
"notNull": false
},
"iconPreference": {
"name": "iconPreference",
"type": "varchar(10)",
"primaryKey": false,
"notNull": true,
"default": "'lucide'"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"User_username_unique": {
"name": "User_username_unique",
"nullsNotDistinct": false,
"columns": [
"username"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -148,6 +148,13 @@
"when": 1768999911628,
"tag": "0020_little_power_pack",
"breakpoints": true
},
{
"idx": 21,
"version": "7",
"when": 1769021149911,
"tag": "0021_skinny_sally_floyd",
"breakpoints": true
}
]
}

View File

@@ -1,3 +1,4 @@
export * from "./issue-comments";
export * from "./issues";
export * from "./organisations";
export * from "./projects";

View File

@@ -0,0 +1,48 @@
import { Issue, IssueComment, type IssueCommentResponse, Project, User } from "@sprint/shared";
import { asc, eq } from "drizzle-orm";
import { db } from "../client";
export async function createIssueComment(issueId: number, userId: number, body: string) {
const [comment] = await db
.insert(IssueComment)
.values({
issueId,
userId,
body,
})
.returning();
return comment;
}
export async function deleteIssueComment(id: number) {
return await db.delete(IssueComment).where(eq(IssueComment.id, id));
}
export async function getIssueCommentById(id: number) {
const [comment] = await db.select().from(IssueComment).where(eq(IssueComment.id, id));
return comment;
}
export async function getIssueCommentsByIssueId(issueId: number): Promise<IssueCommentResponse[]> {
const comments = await db
.select({
Comment: IssueComment,
User: User,
})
.from(IssueComment)
.where(eq(IssueComment.issueId, issueId))
.innerJoin(User, eq(IssueComment.userId, User.id))
.orderBy(asc(IssueComment.createdAt));
return comments;
}
export async function getIssueOrganisationId(issueId: number) {
const [result] = await db
.select({ organisationId: Project.organisationId })
.from(Issue)
.innerJoin(Project, eq(Issue.projectId, Project.id))
.where(eq(Issue.id, issueId));
return result?.organisationId ?? null;
}

View File

@@ -40,11 +40,14 @@ const main = async () => {
"/issue/create": withCors(withAuth(withCSRF(routes.issueCreate))),
"/issue/update": withCors(withAuth(withCSRF(routes.issueUpdate))),
"/issue/delete": withCors(withAuth(withCSRF(routes.issueDelete))),
"/issue-comment/create": withCors(withAuth(withCSRF(routes.issueCommentCreate))),
"/issue-comment/delete": withCors(withAuth(withCSRF(routes.issueCommentDelete))),
"/issues/by-project": withCors(withAuth(routes.issuesByProject)),
"/issues/replace-status": withCors(withAuth(withCSRF(routes.issuesReplaceStatus))),
"/issues/status-count": withCors(withAuth(routes.issuesStatusCount)),
"/issues/all": withCors(withAuth(routes.issues)),
"/issue-comments/by-issue": withCors(withAuth(routes.issueCommentsByIssue)),
"/organisation/create": withCors(withAuth(withCSRF(routes.organisationCreate))),
"/organisation/by-id": withCors(withAuth(routes.organisationById)),

View File

@@ -5,6 +5,9 @@ import authRegister from "./auth/register";
import issueCreate from "./issue/create";
import issueDelete from "./issue/delete";
import issueUpdate from "./issue/update";
import issueCommentCreate from "./issue-comment/create";
import issueCommentDelete from "./issue-comment/delete";
import issueCommentsByIssue from "./issue-comments/by-issue";
import issues from "./issues/all";
import issuesByProject from "./issues/by-project";
import issuesReplaceStatus from "./issues/replace-status";
@@ -54,6 +57,10 @@ export const routes = {
issueDelete,
issueUpdate,
issueCommentCreate,
issueCommentDelete,
issueCommentsByIssue,
issuesByProject,
issues,
issuesReplaceStatus,

View File

@@ -0,0 +1,34 @@
import { IssueCommentCreateRequestSchema } from "@sprint/shared";
import type { AuthedRequest } from "../../auth/middleware";
import {
createIssueComment,
getIssueByID,
getIssueOrganisationId,
getOrganisationMemberRole,
} from "../../db/queries";
import { errorResponse, parseJsonBody } from "../../validation";
export default async function issueCommentCreate(req: AuthedRequest) {
const parsed = await parseJsonBody(req, IssueCommentCreateRequestSchema);
if ("error" in parsed) return parsed.error;
const { issueId, body } = parsed.data;
const issue = await getIssueByID(issueId);
if (!issue) {
return errorResponse(`issue not found: ${issueId}`, "ISSUE_NOT_FOUND", 404);
}
const organisationId = await getIssueOrganisationId(issueId);
if (!organisationId) {
return errorResponse(`organisation not found for issue ${issueId}`, "ORG_NOT_FOUND", 404);
}
const member = await getOrganisationMemberRole(organisationId, req.userId);
if (!member) {
return errorResponse("forbidden", "FORBIDDEN", 403);
}
const comment = await createIssueComment(issueId, req.userId, body);
return Response.json(comment);
}

View File

@@ -0,0 +1,39 @@
import { IssueCommentDeleteRequestSchema } from "@sprint/shared";
import type { AuthedRequest } from "../../auth/middleware";
import {
deleteIssueComment,
getIssueCommentById,
getIssueOrganisationId,
getOrganisationMemberRole,
} from "../../db/queries";
import { errorResponse, parseJsonBody } from "../../validation";
export default async function issueCommentDelete(req: AuthedRequest) {
const parsed = await parseJsonBody(req, IssueCommentDeleteRequestSchema);
if ("error" in parsed) return parsed.error;
const { id } = parsed.data;
const comment = await getIssueCommentById(id);
if (!comment) {
return errorResponse(`comment not found: ${id}`, "COMMENT_NOT_FOUND", 404);
}
if (comment.userId !== req.userId) {
return errorResponse("forbidden", "FORBIDDEN", 403);
}
const organisationId = await getIssueOrganisationId(comment.issueId);
if (!organisationId) {
return errorResponse("organisation not found", "ORG_NOT_FOUND", 404);
}
const member = await getOrganisationMemberRole(organisationId, req.userId);
if (!member) {
return errorResponse("forbidden", "FORBIDDEN", 403);
}
await deleteIssueComment(id);
return Response.json({ success: true });
}

View File

@@ -0,0 +1,35 @@
import { IssueCommentsByIssueQuerySchema } from "@sprint/shared";
import type { AuthedRequest } from "../../auth/middleware";
import {
getIssueByID,
getIssueCommentsByIssueId,
getIssueOrganisationId,
getOrganisationMemberRole,
} from "../../db/queries";
import { errorResponse, parseQueryParams } from "../../validation";
export default async function issueCommentsByIssue(req: AuthedRequest) {
const url = new URL(req.url);
const parsed = parseQueryParams(url, IssueCommentsByIssueQuerySchema);
if ("error" in parsed) return parsed.error;
const { issueId } = parsed.data;
const issue = await getIssueByID(issueId);
if (!issue) {
return errorResponse(`issue not found: ${issueId}`, "ISSUE_NOT_FOUND", 404);
}
const organisationId = await getIssueOrganisationId(issueId);
if (!organisationId) {
return errorResponse(`organisation not found for issue ${issueId}`, "ORG_NOT_FOUND", 404);
}
const member = await getOrganisationMemberRole(organisationId, req.userId);
if (!member) {
return errorResponse("forbidden", "FORBIDDEN", 403);
}
const comments = await getIssueCommentsByIssueId(issueId);
return Response.json(comments);
}