From 15c7320833660237021c2c7301b02bb0529988fa Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Tue, 6 Jan 2026 13:19:19 +0000 Subject: [PATCH] Issue.creatorId + implementation --- .../drizzle/0009_closed_metal_master.sql | 2 + .../backend/drizzle/meta/0009_snapshot.json | 456 ++++++++++++++++++ packages/backend/drizzle/meta/_journal.json | 7 + packages/backend/src/db/queries/issues.ts | 18 +- packages/backend/src/routes/issue/create.ts | 6 +- .../backend/src/routes/issues/by-project.ts | 4 +- packages/backend/src/utils.ts | 1 + .../src/components/issue-detail-pane.tsx | 11 +- .../frontend/src/components/issues-table.tsx | 8 +- packages/shared/src/schema.ts | 6 +- 10 files changed, 502 insertions(+), 17 deletions(-) create mode 100644 packages/backend/drizzle/0009_closed_metal_master.sql create mode 100644 packages/backend/drizzle/meta/0009_snapshot.json diff --git a/packages/backend/drizzle/0009_closed_metal_master.sql b/packages/backend/drizzle/0009_closed_metal_master.sql new file mode 100644 index 0000000..859d0fa --- /dev/null +++ b/packages/backend/drizzle/0009_closed_metal_master.sql @@ -0,0 +1,2 @@ +ALTER TABLE "Issue" ADD COLUMN "creatorId" integer NOT NULL;--> statement-breakpoint +ALTER TABLE "Issue" ADD CONSTRAINT "Issue_creatorId_User_id_fk" FOREIGN KEY ("creatorId") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action; \ No newline at end of file diff --git a/packages/backend/drizzle/meta/0009_snapshot.json b/packages/backend/drizzle/meta/0009_snapshot.json new file mode 100644 index 0000000..bc01593 --- /dev/null +++ b/packages/backend/drizzle/meta/0009_snapshot.json @@ -0,0 +1,456 @@ +{ + "id": "6abfed17-620d-4113-90e2-013e71a9a8d7", + "prevId": "808cb9e7-dabb-4186-8fc5-2d1b7635ffa8", + "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(256)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true + }, + "creatorId": { + "name": "creatorId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "assigneeId": { + "name": "assigneeId", + "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_assigneeId_User_id_fk": { + "name": "Issue_assigneeId_User_id_fk", + "tableFrom": "Issue", + "tableTo": "User", + "columnsFrom": [ + "assigneeId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "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(256)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar(64)", + "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": {}, + "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(256)", + "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.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(256)", + "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 + }, + "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": {} + } +} \ No newline at end of file diff --git a/packages/backend/drizzle/meta/_journal.json b/packages/backend/drizzle/meta/_journal.json index 4e95f03..92b88f3 100644 --- a/packages/backend/drizzle/meta/_journal.json +++ b/packages/backend/drizzle/meta/_journal.json @@ -64,6 +64,13 @@ "when": 1767250186823, "tag": "0008_certain_sharon_ventura", "breakpoints": true + }, + { + "idx": 9, + "version": "7", + "when": 1767704274330, + "tag": "0009_closed_metal_master", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/backend/src/db/queries/issues.ts b/packages/backend/src/db/queries/issues.ts index be7a915..36c31cb 100644 --- a/packages/backend/src/db/queries/issues.ts +++ b/packages/backend/src/db/queries/issues.ts @@ -1,11 +1,12 @@ import { Issue, User } from "@issue/shared"; -import { and, eq, sql } from "drizzle-orm"; +import { aliasedTable, and, eq, sql } from "drizzle-orm"; import { db } from "../client"; export async function createIssue( projectId: number, title: string, description: string, + creatorId: number, assigneeId?: number, ) { // prevents two issues with the same unique number @@ -27,6 +28,7 @@ export async function createIssue( title, description, number: nextNumber, + creatorId, assigneeId, }) .returning(); @@ -64,10 +66,18 @@ export async function getIssueByNumber(projectId: number, number: number) { return issue; } -export async function getIssuesWithAssigneeByProject(projectId: number) { +export async function getIssuesWithUsersByProject(projectId: number) { + const Creator = aliasedTable(User, "Creator"); + const Assignee = aliasedTable(User, "Assignee"); + return await db - .select() + .select({ + Issue: Issue, + Creator: Creator, + Assignee: Assignee, + }) .from(Issue) .where(eq(Issue.projectId, projectId)) - .leftJoin(User, eq(Issue.assigneeId, User.id)); + .innerJoin(Creator, eq(Issue.creatorId, Creator.id)) + .leftJoin(Assignee, eq(Issue.assigneeId, Assignee.id)); } diff --git a/packages/backend/src/routes/issue/create.ts b/packages/backend/src/routes/issue/create.ts index 0dab5a3..cdee789 100644 --- a/packages/backend/src/routes/issue/create.ts +++ b/packages/backend/src/routes/issue/create.ts @@ -1,10 +1,10 @@ -import type { BunRequest } from "bun"; +import type { AuthedRequest } from "../../auth/middleware"; import { createIssue, getProjectByID, getProjectByKey } from "../../db/queries"; // /issue/create?projectId=1&title=Testing&description=Description // OR // /issue/create?projectKey=projectKey&title=Testing&description=Description -export default async function issueCreate(req: BunRequest) { +export default async function issueCreate(req: AuthedRequest) { const url = new URL(req.url); const projectId = url.searchParams.get("projectId"); const projectKey = url.searchParams.get("projectKey"); @@ -24,7 +24,7 @@ export default async function issueCreate(req: BunRequest) { const title = url.searchParams.get("title") || "Untitled Issue"; const description = url.searchParams.get("description") || ""; - const issue = await createIssue(project.id, title, description); + const issue = await createIssue(project.id, title, description, req.userId); return Response.json(issue); } diff --git a/packages/backend/src/routes/issues/by-project.ts b/packages/backend/src/routes/issues/by-project.ts index e4b0ab1..9f34e53 100644 --- a/packages/backend/src/routes/issues/by-project.ts +++ b/packages/backend/src/routes/issues/by-project.ts @@ -1,5 +1,5 @@ import type { AuthedRequest } from "../../auth/middleware"; -import { getIssuesWithAssigneeByProject, getProjectByID } from "../../db/queries"; +import { getIssuesWithUsersByProject, getProjectByID } from "../../db/queries"; export default async function issuesByProject(req: AuthedRequest) { const url = new URL(req.url); @@ -9,7 +9,7 @@ export default async function issuesByProject(req: AuthedRequest) { if (!project) { return new Response(`project not found: provided ${projectId}`, { status: 404 }); } - const issues = await getIssuesWithAssigneeByProject(project.id); + const issues = await getIssuesWithUsersByProject(project.id); return Response.json(issues); } diff --git a/packages/backend/src/utils.ts b/packages/backend/src/utils.ts index a338301..a8a1633 100644 --- a/packages/backend/src/utils.ts +++ b/packages/backend/src/utils.ts @@ -121,6 +121,7 @@ export const createDemoData = async () => { project.id, `Issue ${i} in ${projectName}`, `This is a description for issue ${i} in ${projectName}.`, + config.creator.id, assignee, ); } diff --git a/packages/frontend/src/components/issue-detail-pane.tsx b/packages/frontend/src/components/issue-detail-pane.tsx index b962d69..e390627 100644 --- a/packages/frontend/src/components/issue-detail-pane.tsx +++ b/packages/frontend/src/components/issue-detail-pane.tsx @@ -27,17 +27,22 @@ export function IssueDetailPane({ -
{issueData.Issue.description}
- {issueData.User && ( + {issueData.Assignee && (