Issue.creatorId + implementation

This commit is contained in:
Oliver Bryan
2026-01-06 13:19:19 +00:00
parent 6e8ffa0885
commit 15c7320833
10 changed files with 502 additions and 17 deletions

View File

@@ -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;

View File

@@ -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": {}
}
}

View File

@@ -64,6 +64,13 @@
"when": 1767250186823, "when": 1767250186823,
"tag": "0008_certain_sharon_ventura", "tag": "0008_certain_sharon_ventura",
"breakpoints": true "breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1767704274330,
"tag": "0009_closed_metal_master",
"breakpoints": true
} }
] ]
} }

View File

@@ -1,11 +1,12 @@
import { Issue, User } from "@issue/shared"; 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"; import { db } from "../client";
export async function createIssue( export async function createIssue(
projectId: number, projectId: number,
title: string, title: string,
description: string, description: string,
creatorId: number,
assigneeId?: number, assigneeId?: number,
) { ) {
// prevents two issues with the same unique number // prevents two issues with the same unique number
@@ -27,6 +28,7 @@ export async function createIssue(
title, title,
description, description,
number: nextNumber, number: nextNumber,
creatorId,
assigneeId, assigneeId,
}) })
.returning(); .returning();
@@ -64,10 +66,18 @@ export async function getIssueByNumber(projectId: number, number: number) {
return issue; 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 return await db
.select() .select({
Issue: Issue,
Creator: Creator,
Assignee: Assignee,
})
.from(Issue) .from(Issue)
.where(eq(Issue.projectId, projectId)) .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));
} }

View File

@@ -1,10 +1,10 @@
import type { BunRequest } from "bun"; import type { AuthedRequest } from "../../auth/middleware";
import { createIssue, getProjectByID, getProjectByKey } from "../../db/queries"; import { createIssue, getProjectByID, getProjectByKey } from "../../db/queries";
// /issue/create?projectId=1&title=Testing&description=Description // /issue/create?projectId=1&title=Testing&description=Description
// OR // OR
// /issue/create?projectKey=projectKey&title=Testing&description=Description // /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 url = new URL(req.url);
const projectId = url.searchParams.get("projectId"); const projectId = url.searchParams.get("projectId");
const projectKey = url.searchParams.get("projectKey"); 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 title = url.searchParams.get("title") || "Untitled Issue";
const description = url.searchParams.get("description") || ""; 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); return Response.json(issue);
} }

View File

@@ -1,5 +1,5 @@
import type { AuthedRequest } from "../../auth/middleware"; 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) { export default async function issuesByProject(req: AuthedRequest) {
const url = new URL(req.url); const url = new URL(req.url);
@@ -9,7 +9,7 @@ export default async function issuesByProject(req: AuthedRequest) {
if (!project) { if (!project) {
return new Response(`project not found: provided ${projectId}`, { status: 404 }); 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); return Response.json(issues);
} }

View File

@@ -121,6 +121,7 @@ export const createDemoData = async () => {
project.id, project.id,
`Issue ${i} in ${projectName}`, `Issue ${i} in ${projectName}`,
`This is a description for issue ${i} in ${projectName}.`, `This is a description for issue ${i} in ${projectName}.`,
config.creator.id,
assignee, assignee,
); );
} }

View File

@@ -27,17 +27,22 @@ export function IssueDetailPane({
</Button> </Button>
</div> </div>
<div className="flex flex-col w-full p-2 gap-2"> <div className="flex flex-col w-full p-2 py-1 gap-2">
<h1 className="text-md">{issueData.Issue.title}</h1> <h1 className="text-md">{issueData.Issue.title}</h1>
<p className="text-sm">{issueData.Issue.description}</p> <p className="text-sm">{issueData.Issue.description}</p>
{issueData.User && ( {issueData.Assignee && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
Assignee: Assignee:
{issueData.User ? <SmallUserDisplay user={issueData.User} /> : "Unassigned"} <SmallUserDisplay user={issueData.Assignee} />
</div> </div>
)} )}
</div> </div>
<div className="flex items-center gap-2 px-2 py-1 border-t text-sm text-muted-foreground">
Created by:
<SmallUserDisplay user={issueData.Creator} />
</div>
</div> </div>
); );
} }

View File

@@ -51,11 +51,11 @@ export function IssuesTable({
)} )}
{(columns.assignee == null || columns.assignee === true) && ( {(columns.assignee == null || columns.assignee === true) && (
<TableCell className={"flex items-center justify-end px-1 py-0 h-[32px]"}> <TableCell className={"flex items-center justify-end px-1 py-0 h-[32px]"}>
{issueData.User && ( {issueData.Assignee && (
<Avatar <Avatar
name={issueData.User?.name} name={issueData.Assignee?.name}
username={issueData.User?.username} username={issueData.Assignee?.username}
avatarURL={issueData.User?.avatarURL} avatarURL={issueData.Assignee?.avatarURL}
textClass="text-xs" textClass="text-xs"
/> />
)} )}

View File

@@ -58,6 +58,9 @@ export const Issue = pgTable(
title: varchar({ length: 256 }).notNull(), title: varchar({ length: 256 }).notNull(),
description: varchar({ length: 2048 }).notNull(), description: varchar({ length: 2048 }).notNull(),
creatorId: integer()
.notNull()
.references(() => User.id),
assigneeId: integer().references(() => User.id), assigneeId: integer().references(() => User.id),
}, },
(t) => [ (t) => [
@@ -103,7 +106,8 @@ export type IssueInsert = z.infer<typeof IssueInsertSchema>;
export type IssueResponse = { export type IssueResponse = {
Issue: IssueRecord; Issue: IssueRecord;
User?: UserRecord; Creator: UserRecord;
Assignee: UserRecord | null;
}; };
export type ProjectResponse = { export type ProjectResponse = {