mirror of
https://github.com/hex248/sprint.git
synced 2026-02-08 02:33:01 +00:00
full comments system
This commit is contained in:
8
packages/backend/drizzle/0021_issue_comments.sql
Normal file
8
packages/backend/drizzle/0021_issue_comments.sql
Normal 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()
|
||||||
|
);
|
||||||
11
packages/backend/drizzle/0021_skinny_sally_floyd.sql
Normal file
11
packages/backend/drizzle/0021_skinny_sally_floyd.sql
Normal 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;
|
||||||
909
packages/backend/drizzle/meta/0021_snapshot.json
Normal file
909
packages/backend/drizzle/meta/0021_snapshot.json
Normal 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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -148,6 +148,13 @@
|
|||||||
"when": 1768999911628,
|
"when": 1768999911628,
|
||||||
"tag": "0020_little_power_pack",
|
"tag": "0020_little_power_pack",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 21,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1769021149911,
|
||||||
|
"tag": "0021_skinny_sally_floyd",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from "./issue-comments";
|
||||||
export * from "./issues";
|
export * from "./issues";
|
||||||
export * from "./organisations";
|
export * from "./organisations";
|
||||||
export * from "./projects";
|
export * from "./projects";
|
||||||
|
|||||||
48
packages/backend/src/db/queries/issue-comments.ts
Normal file
48
packages/backend/src/db/queries/issue-comments.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -40,11 +40,14 @@ const main = async () => {
|
|||||||
"/issue/create": withCors(withAuth(withCSRF(routes.issueCreate))),
|
"/issue/create": withCors(withAuth(withCSRF(routes.issueCreate))),
|
||||||
"/issue/update": withCors(withAuth(withCSRF(routes.issueUpdate))),
|
"/issue/update": withCors(withAuth(withCSRF(routes.issueUpdate))),
|
||||||
"/issue/delete": withCors(withAuth(withCSRF(routes.issueDelete))),
|
"/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/by-project": withCors(withAuth(routes.issuesByProject)),
|
||||||
"/issues/replace-status": withCors(withAuth(withCSRF(routes.issuesReplaceStatus))),
|
"/issues/replace-status": withCors(withAuth(withCSRF(routes.issuesReplaceStatus))),
|
||||||
"/issues/status-count": withCors(withAuth(routes.issuesStatusCount)),
|
"/issues/status-count": withCors(withAuth(routes.issuesStatusCount)),
|
||||||
"/issues/all": withCors(withAuth(routes.issues)),
|
"/issues/all": withCors(withAuth(routes.issues)),
|
||||||
|
"/issue-comments/by-issue": withCors(withAuth(routes.issueCommentsByIssue)),
|
||||||
|
|
||||||
"/organisation/create": withCors(withAuth(withCSRF(routes.organisationCreate))),
|
"/organisation/create": withCors(withAuth(withCSRF(routes.organisationCreate))),
|
||||||
"/organisation/by-id": withCors(withAuth(routes.organisationById)),
|
"/organisation/by-id": withCors(withAuth(routes.organisationById)),
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import authRegister from "./auth/register";
|
|||||||
import issueCreate from "./issue/create";
|
import issueCreate from "./issue/create";
|
||||||
import issueDelete from "./issue/delete";
|
import issueDelete from "./issue/delete";
|
||||||
import issueUpdate from "./issue/update";
|
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 issues from "./issues/all";
|
||||||
import issuesByProject from "./issues/by-project";
|
import issuesByProject from "./issues/by-project";
|
||||||
import issuesReplaceStatus from "./issues/replace-status";
|
import issuesReplaceStatus from "./issues/replace-status";
|
||||||
@@ -54,6 +57,10 @@ export const routes = {
|
|||||||
issueDelete,
|
issueDelete,
|
||||||
issueUpdate,
|
issueUpdate,
|
||||||
|
|
||||||
|
issueCommentCreate,
|
||||||
|
issueCommentDelete,
|
||||||
|
issueCommentsByIssue,
|
||||||
|
|
||||||
issuesByProject,
|
issuesByProject,
|
||||||
issues,
|
issues,
|
||||||
issuesReplaceStatus,
|
issuesReplaceStatus,
|
||||||
|
|||||||
34
packages/backend/src/routes/issue-comment/create.ts
Normal file
34
packages/backend/src/routes/issue-comment/create.ts
Normal 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);
|
||||||
|
}
|
||||||
39
packages/backend/src/routes/issue-comment/delete.ts
Normal file
39
packages/backend/src/routes/issue-comment/delete.ts
Normal 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 });
|
||||||
|
}
|
||||||
35
packages/backend/src/routes/issue-comments/by-issue.ts
Normal file
35
packages/backend/src/routes/issue-comments/by-issue.ts
Normal 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);
|
||||||
|
}
|
||||||
137
packages/frontend/src/components/issue-comments.tsx
Normal file
137
packages/frontend/src/components/issue-comments.tsx
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import type { IssueCommentResponse } from "@sprint/shared";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { useSession } from "@/components/session-provider";
|
||||||
|
import SmallUserDisplay from "@/components/small-user-display";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import Icon from "@/components/ui/icon";
|
||||||
|
import { IconButton } from "@/components/ui/icon-button";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { useCreateIssueComment, useDeleteIssueComment, useIssueComments } from "@/lib/query/hooks";
|
||||||
|
import { parseError } from "@/lib/server";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const formatTimestamp = (value?: string | Date | null) => {
|
||||||
|
if (!value) return "";
|
||||||
|
const date = value instanceof Date ? value : new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return "";
|
||||||
|
return date.toLocaleString(undefined, {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export function IssueComments({ issueId, className }: { issueId: number; className?: string }) {
|
||||||
|
const { user } = useSession();
|
||||||
|
const { data = [], isLoading } = useIssueComments(issueId);
|
||||||
|
const createComment = useCreateIssueComment();
|
||||||
|
const deleteComment = useDeleteIssueComment();
|
||||||
|
|
||||||
|
const [body, setBody] = useState("");
|
||||||
|
const [deletingId, setDeletingId] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const sortedComments = useMemo(() => {
|
||||||
|
return [...data].sort((a, b) => {
|
||||||
|
const aDate =
|
||||||
|
a.Comment.createdAt instanceof Date ? a.Comment.createdAt : new Date(a.Comment.createdAt ?? 0);
|
||||||
|
const bDate =
|
||||||
|
b.Comment.createdAt instanceof Date ? b.Comment.createdAt : new Date(b.Comment.createdAt ?? 0);
|
||||||
|
return aDate.getTime() - bDate.getTime();
|
||||||
|
});
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const trimmed = body.trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createComment.mutateAsync({
|
||||||
|
issueId,
|
||||||
|
body: trimmed,
|
||||||
|
});
|
||||||
|
setBody("");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(`Error adding comment: ${parseError(error as Error)}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (comment: IssueCommentResponse) => {
|
||||||
|
setDeletingId(comment.Comment.id);
|
||||||
|
try {
|
||||||
|
await deleteComment.mutateAsync({ id: comment.Comment.id });
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(`Error deleting comment: ${parseError(error as Error)}`);
|
||||||
|
} finally {
|
||||||
|
setDeletingId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex flex-col gap-3", className)}>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-600">Comments</span>
|
||||||
|
<span className="text-xs text-muted-foreground">{sortedComments.length}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Textarea
|
||||||
|
value={body}
|
||||||
|
onChange={(event) => setBody(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
void handleSubmit();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="Leave a comment..."
|
||||||
|
className="text-sm resize-none !bg-background h-"
|
||||||
|
disabled={createComment.isPending}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={createComment.isPending || body.trim() === ""}
|
||||||
|
className="px-4"
|
||||||
|
>
|
||||||
|
{createComment.isPending ? "Posting..." : "Post comment"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="text-sm text-muted-foreground">Loading comments...</div>
|
||||||
|
) : sortedComments.length === 0 ? (
|
||||||
|
<div className="text-sm text-muted-foreground">No comments yet.</div>
|
||||||
|
) : (
|
||||||
|
sortedComments.map((comment) => {
|
||||||
|
const isAuthor = user?.id === comment.Comment.userId;
|
||||||
|
const timestamp = formatTimestamp(comment.Comment.createdAt);
|
||||||
|
return (
|
||||||
|
<div key={comment.Comment.id} className="border border-border/60 bg-muted/20 p-3">
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<SmallUserDisplay user={comment.User} className="text-sm" />
|
||||||
|
{timestamp ? (
|
||||||
|
<span className="text-[11px] text-muted-foreground">{timestamp}</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{isAuthor ? (
|
||||||
|
<IconButton
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => handleDelete(comment)}
|
||||||
|
disabled={deletingId === comment.Comment.id}
|
||||||
|
title="Delete comment"
|
||||||
|
>
|
||||||
|
<Icon icon="trash" className="size-4" />
|
||||||
|
</IconButton>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm whitespace-pre-wrap pt-2">{comment.Comment.body}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { IssueResponse, SprintRecord, UserRecord } from "@sprint/shared";
|
import type { IssueResponse, SprintRecord, UserRecord } from "@sprint/shared";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { IssueComments } from "@/components/issue-comments";
|
||||||
import { MultiAssigneeSelect } from "@/components/multi-assignee-select";
|
import { MultiAssigneeSelect } from "@/components/multi-assignee-select";
|
||||||
import { useSession } from "@/components/session-provider";
|
import { useSession } from "@/components/session-provider";
|
||||||
import SmallSprintDisplay from "@/components/small-sprint-display";
|
import SmallSprintDisplay from "@/components/small-sprint-display";
|
||||||
@@ -317,7 +318,7 @@ export function IssueDetails({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col w-full p-2 py-2 gap-2">
|
<div className="flex flex-col w-full p-2 py-2 gap-2 max-h-[75vh] overflow-y-scroll">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<StatusSelect
|
<StatusSelect
|
||||||
statuses={statuses}
|
statuses={statuses}
|
||||||
@@ -373,7 +374,7 @@ export function IssueDetails({
|
|||||||
}}
|
}}
|
||||||
placeholder="Add a description..."
|
placeholder="Add a description..."
|
||||||
disabled={isSavingDescription}
|
disabled={isSavingDescription}
|
||||||
className="text-sm border-input/50 hover:border-input focus:border-input resize-none !bg-background"
|
className="text-sm border-input/50 hover:border-input focus:border-input resize-none !bg-background min-h-fit"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
@@ -388,7 +389,6 @@ export function IssueDetails({
|
|||||||
Add description
|
Add description
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm">Sprint:</span>
|
<span className="text-sm">Sprint:</span>
|
||||||
<SprintSelect sprints={sprints} value={sprintId} onChange={handleSprintChange} />
|
<SprintSelect sprints={sprints} value={sprintId} onChange={handleSprintChange} />
|
||||||
@@ -423,6 +423,8 @@ export function IssueDetails({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<IssueComments issueId={issueData.Issue.id} className="pt-2" />
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={deleteOpen}
|
open={deleteOpen}
|
||||||
onOpenChange={setDeleteOpen}
|
onOpenChange={setDeleteOpen}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function IssuesTable({
|
|||||||
params.set("o", selectedOrganisation.Organisation.slug.toLowerCase());
|
params.set("o", selectedOrganisation.Organisation.slug.toLowerCase());
|
||||||
params.set("p", selectedProject.Project.key.toLowerCase());
|
params.set("p", selectedProject.Project.key.toLowerCase());
|
||||||
params.set("i", issueNumber.toString());
|
params.set("i", issueNumber.toString());
|
||||||
return `/app?${params.toString()}`;
|
return `/issues?${params.toString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLinkClick = (e: React.MouseEvent) => {
|
const handleLinkClick = (e: React.MouseEvent) => {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default function LogInForm() {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setCsrfToken(data.csrfToken);
|
setCsrfToken(data.csrfToken);
|
||||||
setUser(data.user);
|
setUser(data.user);
|
||||||
const next = searchParams.get("next") || "/app";
|
const next = searchParams.get("next") || "/issues";
|
||||||
navigate(next, { replace: true });
|
navigate(next, { replace: true });
|
||||||
}
|
}
|
||||||
// unauthorized
|
// unauthorized
|
||||||
@@ -94,7 +94,7 @@ export default function LogInForm() {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setCsrfToken(data.csrfToken);
|
setCsrfToken(data.csrfToken);
|
||||||
setUser(data.user);
|
setUser(data.user);
|
||||||
const next = searchParams.get("next") || "/app";
|
const next = searchParams.get("next") || "/issues";
|
||||||
navigate(next, { replace: true });
|
navigate(next, { replace: true });
|
||||||
}
|
}
|
||||||
// bad request (probably a bad user input)
|
// bad request (probably a bad user input)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from "@/lib/query/hooks/derived";
|
export * from "@/lib/query/hooks/derived";
|
||||||
|
export * from "@/lib/query/hooks/issue-comments";
|
||||||
export * from "@/lib/query/hooks/issues";
|
export * from "@/lib/query/hooks/issues";
|
||||||
export * from "@/lib/query/hooks/organisations";
|
export * from "@/lib/query/hooks/organisations";
|
||||||
export * from "@/lib/query/hooks/projects";
|
export * from "@/lib/query/hooks/projects";
|
||||||
|
|||||||
44
packages/frontend/src/lib/query/hooks/issue-comments.ts
Normal file
44
packages/frontend/src/lib/query/hooks/issue-comments.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import type {
|
||||||
|
IssueCommentCreateRequest,
|
||||||
|
IssueCommentDeleteRequest,
|
||||||
|
IssueCommentRecord,
|
||||||
|
IssueCommentResponse,
|
||||||
|
SuccessResponse,
|
||||||
|
} from "@sprint/shared";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { queryKeys } from "@/lib/query/keys";
|
||||||
|
import { issueComment } from "@/lib/server";
|
||||||
|
|
||||||
|
export function useIssueComments(issueId?: number | null) {
|
||||||
|
return useQuery<IssueCommentResponse[]>({
|
||||||
|
queryKey: queryKeys.issueComments.byIssue(issueId ?? 0),
|
||||||
|
queryFn: () => issueComment.byIssue(issueId ?? 0),
|
||||||
|
enabled: Boolean(issueId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateIssueComment() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<IssueCommentRecord, Error, IssueCommentCreateRequest>({
|
||||||
|
mutationKey: ["issue-comments", "create"],
|
||||||
|
mutationFn: issueComment.create,
|
||||||
|
onSuccess: (_data, variables) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.issueComments.byIssue(variables.issueId),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteIssueComment() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<SuccessResponse, Error, IssueCommentDeleteRequest>({
|
||||||
|
mutationKey: ["issue-comments", "delete"],
|
||||||
|
mutationFn: issueComment.delete,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: queryKeys.issueComments.all });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -16,6 +16,10 @@ export const queryKeys = {
|
|||||||
statusCount: (organisationId: number, status: string) =>
|
statusCount: (organisationId: number, status: string) =>
|
||||||
[...queryKeys.issues.all, "status-count", organisationId, status] as const,
|
[...queryKeys.issues.all, "status-count", organisationId, status] as const,
|
||||||
},
|
},
|
||||||
|
issueComments: {
|
||||||
|
all: ["issue-comments"] as const,
|
||||||
|
byIssue: (issueId: number) => [...queryKeys.issueComments.all, "by-issue", issueId] as const,
|
||||||
|
},
|
||||||
sprints: {
|
sprints: {
|
||||||
all: ["sprints"] as const,
|
all: ["sprints"] as const,
|
||||||
byProject: (projectId: number) => [...queryKeys.sprints.all, "by-project", projectId] as const,
|
byProject: (projectId: number) => [...queryKeys.sprints.all, "by-project", projectId] as const,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { ApiError } from "@sprint/shared";
|
import type { ApiError } from "@sprint/shared";
|
||||||
|
|
||||||
export * as issue from "@/lib/server/issue";
|
export * as issue from "@/lib/server/issue";
|
||||||
|
export * as issueComment from "@/lib/server/issue-comment";
|
||||||
export * as organisation from "@/lib/server/organisation";
|
export * as organisation from "@/lib/server/organisation";
|
||||||
export * as project from "@/lib/server/project";
|
export * as project from "@/lib/server/project";
|
||||||
export * as sprint from "@/lib/server/sprint";
|
export * as sprint from "@/lib/server/sprint";
|
||||||
|
|||||||
19
packages/frontend/src/lib/server/issue-comment/byIssue.ts
Normal file
19
packages/frontend/src/lib/server/issue-comment/byIssue.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { IssueCommentResponse } from "@sprint/shared";
|
||||||
|
import { getServerURL } from "@/lib/utils";
|
||||||
|
import { getErrorMessage } from "..";
|
||||||
|
|
||||||
|
export async function byIssue(issueId: number): Promise<IssueCommentResponse[]> {
|
||||||
|
const url = new URL(`${getServerURL()}/issue-comments/by-issue`);
|
||||||
|
url.searchParams.set("issueId", `${issueId}`);
|
||||||
|
|
||||||
|
const res = await fetch(url.toString(), {
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const message = await getErrorMessage(res, `failed to get issue comments (${res.status})`);
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
29
packages/frontend/src/lib/server/issue-comment/create.ts
Normal file
29
packages/frontend/src/lib/server/issue-comment/create.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { IssueCommentCreateRequest, IssueCommentRecord } from "@sprint/shared";
|
||||||
|
import { getCsrfToken, getServerURL } from "@/lib/utils";
|
||||||
|
import { getErrorMessage } from "..";
|
||||||
|
|
||||||
|
export async function create(request: IssueCommentCreateRequest): Promise<IssueCommentRecord> {
|
||||||
|
const csrfToken = getCsrfToken();
|
||||||
|
|
||||||
|
const res = await fetch(`${getServerURL()}/issue-comment/create`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
|
||||||
|
},
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const message = await getErrorMessage(res, `failed to create comment (${res.status})`);
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await res.json()) as IssueCommentRecord;
|
||||||
|
if (!data.id) {
|
||||||
|
throw new Error(`failed to create comment (${res.status})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
24
packages/frontend/src/lib/server/issue-comment/delete.ts
Normal file
24
packages/frontend/src/lib/server/issue-comment/delete.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { IssueCommentDeleteRequest, SuccessResponse } from "@sprint/shared";
|
||||||
|
import { getCsrfToken, getServerURL } from "@/lib/utils";
|
||||||
|
import { getErrorMessage } from "..";
|
||||||
|
|
||||||
|
export async function remove(request: IssueCommentDeleteRequest): Promise<SuccessResponse> {
|
||||||
|
const csrfToken = getCsrfToken();
|
||||||
|
|
||||||
|
const res = await fetch(`${getServerURL()}/issue-comment/delete`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
|
||||||
|
},
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const message = await getErrorMessage(res, `failed to delete comment (${res.status})`);
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
3
packages/frontend/src/lib/server/issue-comment/index.ts
Normal file
3
packages/frontend/src/lib/server/issue-comment/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { byIssue } from "@/lib/server/issue-comment/byIssue";
|
||||||
|
export { create } from "@/lib/server/issue-comment/create";
|
||||||
|
export { remove as delete } from "@/lib/server/issue-comment/delete";
|
||||||
@@ -7,8 +7,8 @@ import { SelectionProvider } from "@/components/selection-provider";
|
|||||||
import { RequireAuth, SessionProvider } from "@/components/session-provider";
|
import { RequireAuth, SessionProvider } from "@/components/session-provider";
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import Issues from "@/pages/Issues";
|
|
||||||
import Font from "@/pages/Font";
|
import Font from "@/pages/Font";
|
||||||
|
import Issues from "@/pages/Issues";
|
||||||
import Landing from "@/pages/Landing";
|
import Landing from "@/pages/Landing";
|
||||||
import Login from "@/pages/Login";
|
import Login from "@/pages/Login";
|
||||||
import NotFound from "@/pages/NotFound";
|
import NotFound from "@/pages/NotFound";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
ISSUE_DESCRIPTION_MAX_LENGTH,
|
ISSUE_DESCRIPTION_MAX_LENGTH,
|
||||||
|
ISSUE_COMMENT_MAX_LENGTH,
|
||||||
ISSUE_STATUS_MAX_LENGTH,
|
ISSUE_STATUS_MAX_LENGTH,
|
||||||
ISSUE_TITLE_MAX_LENGTH,
|
ISSUE_TITLE_MAX_LENGTH,
|
||||||
ORG_DESCRIPTION_MAX_LENGTH,
|
ORG_DESCRIPTION_MAX_LENGTH,
|
||||||
@@ -111,6 +112,25 @@ export const IssuesReplaceStatusRequestSchema = z.object({
|
|||||||
|
|
||||||
export type IssuesReplaceStatusRequest = z.infer<typeof IssuesReplaceStatusRequestSchema>;
|
export type IssuesReplaceStatusRequest = z.infer<typeof IssuesReplaceStatusRequestSchema>;
|
||||||
|
|
||||||
|
export const IssueCommentCreateRequestSchema = z.object({
|
||||||
|
issueId: z.number().int().positive("issueId must be a positive integer"),
|
||||||
|
body: z.string().min(1, "Comment is required").max(ISSUE_COMMENT_MAX_LENGTH),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IssueCommentCreateRequest = z.infer<typeof IssueCommentCreateRequestSchema>;
|
||||||
|
|
||||||
|
export const IssueCommentDeleteRequestSchema = z.object({
|
||||||
|
id: z.number().int().positive("id must be a positive integer"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IssueCommentDeleteRequest = z.infer<typeof IssueCommentDeleteRequestSchema>;
|
||||||
|
|
||||||
|
export const IssueCommentsByIssueQuerySchema = z.object({
|
||||||
|
issueId: z.coerce.number().int().positive("issueId must be a positive integer"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IssueCommentsByIssueQuery = z.infer<typeof IssueCommentsByIssueQuerySchema>;
|
||||||
|
|
||||||
// organisation schemas
|
// organisation schemas
|
||||||
|
|
||||||
export const OrgCreateRequestSchema = z.object({
|
export const OrgCreateRequestSchema = z.object({
|
||||||
@@ -375,6 +395,22 @@ export const IssueResponseSchema = z.object({
|
|||||||
|
|
||||||
export type IssueResponseType = z.infer<typeof IssueResponseSchema>;
|
export type IssueResponseType = z.infer<typeof IssueResponseSchema>;
|
||||||
|
|
||||||
|
export const IssueCommentRecordSchema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
issueId: z.number(),
|
||||||
|
userId: z.number(),
|
||||||
|
body: z.string(),
|
||||||
|
createdAt: z.string().nullable().optional(),
|
||||||
|
updatedAt: z.string().nullable().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const IssueCommentResponseSchema = z.object({
|
||||||
|
Comment: IssueCommentRecordSchema,
|
||||||
|
User: UserResponseSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IssueCommentResponseType = z.infer<typeof IssueCommentResponseSchema>;
|
||||||
|
|
||||||
export const OrganisationRecordSchema = z.object({
|
export const OrganisationRecordSchema = z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ export const PROJECT_SLUG_MAX_LENGTH = 64;
|
|||||||
export const ISSUE_TITLE_MAX_LENGTH = 64;
|
export const ISSUE_TITLE_MAX_LENGTH = 64;
|
||||||
export const ISSUE_DESCRIPTION_MAX_LENGTH = 2048;
|
export const ISSUE_DESCRIPTION_MAX_LENGTH = 2048;
|
||||||
export const ISSUE_STATUS_MAX_LENGTH = 24;
|
export const ISSUE_STATUS_MAX_LENGTH = 24;
|
||||||
|
export const ISSUE_COMMENT_MAX_LENGTH = 2048;
|
||||||
|
|||||||
@@ -2,8 +2,12 @@ export type {
|
|||||||
ApiError,
|
ApiError,
|
||||||
AuthResponse,
|
AuthResponse,
|
||||||
IssueCreateRequest,
|
IssueCreateRequest,
|
||||||
|
IssueCommentCreateRequest,
|
||||||
|
IssueCommentDeleteRequest,
|
||||||
|
IssueCommentsByIssueQuery,
|
||||||
IssueDeleteRequest,
|
IssueDeleteRequest,
|
||||||
IssueResponseType,
|
IssueResponseType,
|
||||||
|
IssueCommentResponseType,
|
||||||
IssuesByProjectQuery,
|
IssuesByProjectQuery,
|
||||||
IssuesReplaceStatusRequest,
|
IssuesReplaceStatusRequest,
|
||||||
IssuesStatusCountQuery,
|
IssuesStatusCountQuery,
|
||||||
@@ -47,7 +51,12 @@ export {
|
|||||||
ApiErrorSchema,
|
ApiErrorSchema,
|
||||||
AuthResponseSchema,
|
AuthResponseSchema,
|
||||||
IssueCreateRequestSchema,
|
IssueCreateRequestSchema,
|
||||||
|
IssueCommentCreateRequestSchema,
|
||||||
|
IssueCommentDeleteRequestSchema,
|
||||||
|
IssueCommentsByIssueQuerySchema,
|
||||||
IssueDeleteRequestSchema,
|
IssueDeleteRequestSchema,
|
||||||
|
IssueCommentResponseSchema,
|
||||||
|
IssueCommentRecordSchema,
|
||||||
IssueRecordSchema,
|
IssueRecordSchema,
|
||||||
IssueResponseSchema,
|
IssueResponseSchema,
|
||||||
IssuesByProjectQuerySchema,
|
IssuesByProjectQuerySchema,
|
||||||
@@ -93,6 +102,7 @@ export {
|
|||||||
} from "./api-schemas";
|
} from "./api-schemas";
|
||||||
export {
|
export {
|
||||||
ISSUE_DESCRIPTION_MAX_LENGTH,
|
ISSUE_DESCRIPTION_MAX_LENGTH,
|
||||||
|
ISSUE_COMMENT_MAX_LENGTH,
|
||||||
ISSUE_STATUS_MAX_LENGTH,
|
ISSUE_STATUS_MAX_LENGTH,
|
||||||
ISSUE_TITLE_MAX_LENGTH,
|
ISSUE_TITLE_MAX_LENGTH,
|
||||||
ORG_DESCRIPTION_MAX_LENGTH,
|
ORG_DESCRIPTION_MAX_LENGTH,
|
||||||
@@ -108,6 +118,9 @@ export type {
|
|||||||
IconStyle,
|
IconStyle,
|
||||||
IssueAssigneeInsert,
|
IssueAssigneeInsert,
|
||||||
IssueAssigneeRecord,
|
IssueAssigneeRecord,
|
||||||
|
IssueCommentInsert,
|
||||||
|
IssueCommentRecord,
|
||||||
|
IssueCommentResponse,
|
||||||
IssueInsert,
|
IssueInsert,
|
||||||
IssueRecord,
|
IssueRecord,
|
||||||
IssueResponse,
|
IssueResponse,
|
||||||
@@ -138,6 +151,9 @@ export {
|
|||||||
IssueAssignee,
|
IssueAssignee,
|
||||||
IssueAssigneeInsertSchema,
|
IssueAssigneeInsertSchema,
|
||||||
IssueAssigneeSelectSchema,
|
IssueAssigneeSelectSchema,
|
||||||
|
IssueComment,
|
||||||
|
IssueCommentInsertSchema,
|
||||||
|
IssueCommentSelectSchema,
|
||||||
IssueInsertSchema,
|
IssueInsertSchema,
|
||||||
IssueSelectSchema,
|
IssueSelectSchema,
|
||||||
iconStyles,
|
iconStyles,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
|||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import {
|
import {
|
||||||
ISSUE_DESCRIPTION_MAX_LENGTH,
|
ISSUE_DESCRIPTION_MAX_LENGTH,
|
||||||
|
ISSUE_COMMENT_MAX_LENGTH,
|
||||||
ISSUE_STATUS_MAX_LENGTH,
|
ISSUE_STATUS_MAX_LENGTH,
|
||||||
ISSUE_TITLE_MAX_LENGTH,
|
ISSUE_TITLE_MAX_LENGTH,
|
||||||
ORG_DESCRIPTION_MAX_LENGTH,
|
ORG_DESCRIPTION_MAX_LENGTH,
|
||||||
@@ -151,6 +152,19 @@ export const IssueAssignee = pgTable(
|
|||||||
(t) => [uniqueIndex("unique_issue_user").on(t.issueId, t.userId)],
|
(t) => [uniqueIndex("unique_issue_user").on(t.issueId, t.userId)],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const IssueComment = pgTable("IssueComment", {
|
||||||
|
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
||||||
|
issueId: integer()
|
||||||
|
.notNull()
|
||||||
|
.references(() => Issue.id, { onDelete: "cascade" }),
|
||||||
|
userId: integer()
|
||||||
|
.notNull()
|
||||||
|
.references(() => User.id, { onDelete: "cascade" }),
|
||||||
|
body: varchar({ length: ISSUE_COMMENT_MAX_LENGTH }).notNull(),
|
||||||
|
createdAt: timestamp({ withTimezone: false }).defaultNow(),
|
||||||
|
updatedAt: timestamp({ withTimezone: false }).defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
// Zod schemas
|
// Zod schemas
|
||||||
export const UserSelectSchema = createSelectSchema(User);
|
export const UserSelectSchema = createSelectSchema(User);
|
||||||
export const UserInsertSchema = createInsertSchema(User);
|
export const UserInsertSchema = createInsertSchema(User);
|
||||||
@@ -173,6 +187,9 @@ export const IssueInsertSchema = createInsertSchema(Issue);
|
|||||||
export const IssueAssigneeSelectSchema = createSelectSchema(IssueAssignee);
|
export const IssueAssigneeSelectSchema = createSelectSchema(IssueAssignee);
|
||||||
export const IssueAssigneeInsertSchema = createInsertSchema(IssueAssignee);
|
export const IssueAssigneeInsertSchema = createInsertSchema(IssueAssignee);
|
||||||
|
|
||||||
|
export const IssueCommentSelectSchema = createSelectSchema(IssueComment);
|
||||||
|
export const IssueCommentInsertSchema = createInsertSchema(IssueComment);
|
||||||
|
|
||||||
export const SessionSelectSchema = createSelectSchema(Session);
|
export const SessionSelectSchema = createSelectSchema(Session);
|
||||||
export const SessionInsertSchema = createInsertSchema(Session);
|
export const SessionInsertSchema = createInsertSchema(Session);
|
||||||
|
|
||||||
@@ -203,6 +220,9 @@ export type IssueInsert = z.infer<typeof IssueInsertSchema>;
|
|||||||
export type IssueAssigneeRecord = z.infer<typeof IssueAssigneeSelectSchema>;
|
export type IssueAssigneeRecord = z.infer<typeof IssueAssigneeSelectSchema>;
|
||||||
export type IssueAssigneeInsert = z.infer<typeof IssueAssigneeInsertSchema>;
|
export type IssueAssigneeInsert = z.infer<typeof IssueAssigneeInsertSchema>;
|
||||||
|
|
||||||
|
export type IssueCommentRecord = z.infer<typeof IssueCommentSelectSchema>;
|
||||||
|
export type IssueCommentInsert = z.infer<typeof IssueCommentInsertSchema>;
|
||||||
|
|
||||||
export type SessionRecord = z.infer<typeof SessionSelectSchema>;
|
export type SessionRecord = z.infer<typeof SessionSelectSchema>;
|
||||||
export type SessionInsert = z.infer<typeof SessionInsertSchema>;
|
export type SessionInsert = z.infer<typeof SessionInsertSchema>;
|
||||||
|
|
||||||
@@ -217,6 +237,11 @@ export type IssueResponse = {
|
|||||||
Assignees: UserRecord[];
|
Assignees: UserRecord[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type IssueCommentResponse = {
|
||||||
|
Comment: IssueCommentRecord;
|
||||||
|
User: UserRecord;
|
||||||
|
};
|
||||||
|
|
||||||
export type ProjectResponse = {
|
export type ProjectResponse = {
|
||||||
Project: ProjectRecord;
|
Project: ProjectRecord;
|
||||||
Organisation: OrganisationRecord;
|
Organisation: OrganisationRecord;
|
||||||
|
|||||||
Reference in New Issue
Block a user