all fixes for Project.creatorId and Organisation tables

This commit is contained in:
Oliver Bryan
2025-12-22 20:13:08 +00:00
parent 551a868be9
commit 4bf4f832b7
17 changed files with 176 additions and 96 deletions

View File

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

View File

@@ -0,0 +1,45 @@
import { Organisation, OrganisationMember } from "@issue/shared";
import { eq } from "drizzle-orm";
import { db } from "../client";
export async function createOrganisation(name: string, slug: string, description?: string) {
const [organisation] = await db
.insert(Organisation)
.values({
name,
slug,
description,
})
.returning();
return organisation;
}
export async function createOrganisationMember(
organisationId: number,
userId: number,
role: string = "member",
) {
const [member] = await db
.insert(OrganisationMember)
.values({
organisationId,
userId,
role,
})
.returning();
return member;
}
export async function getOrganisationById(id: number) {
const [organisation] = await db.select().from(Organisation).where(eq(Organisation.id, id));
return organisation;
}
export async function getOrganisationsByUserId(userId: number) {
const organisations = await db
.select()
.from(OrganisationMember)
.where(eq(OrganisationMember.userId, userId))
.innerJoin(Organisation, eq(OrganisationMember.organisationId, Organisation.id));
return organisations;
}

View File

@@ -2,13 +2,14 @@ import { Issue, Project, User } from "@issue/shared";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { db } from "../client"; import { db } from "../client";
export async function createProject(blob: string, name: string, ownerId: number) { export async function createProject(blob: string, name: string, creatorId: number, organisationId: number) {
const [project] = await db const [project] = await db
.insert(Project) .insert(Project)
.values({ .values({
blob, blob,
name, name,
ownerId, creatorId,
organisationId,
}) })
.returning(); .returning();
return project; return project;
@@ -16,7 +17,7 @@ export async function createProject(blob: string, name: string, ownerId: number)
export async function updateProject( export async function updateProject(
projectId: number, projectId: number,
updates: { blob?: string; name?: string; ownerId?: number }, updates: { blob?: string; name?: string; creatorId?: number; organisationId?: number },
) { ) {
const [project] = await db.update(Project).set(updates).where(eq(Project.id, projectId)).returning(); const [project] = await db.update(Project).set(updates).where(eq(Project.id, projectId)).returning();
return project; return project;
@@ -39,13 +40,13 @@ export async function getProjectByBlob(projectBlob: string) {
return project; return project;
} }
export async function getProjectsByOwnerID(ownerId: number) { export async function getProjectsByCreatorID(creatorId: number) {
const projectsWithOwners = await db const projectsWithCreators = await db
.select() .select()
.from(Project) .from(Project)
.where(eq(Project.ownerId, ownerId)) .where(eq(Project.creatorId, creatorId))
.leftJoin(User, eq(Project.ownerId, User.id)); .leftJoin(User, eq(Project.creatorId, User.id));
return projectsWithOwners; return projectsWithCreators;
} }
export async function getAllProjects() { export async function getAllProjects() {
@@ -53,16 +54,19 @@ export async function getAllProjects() {
return projects; return projects;
} }
export async function getProjectsWithOwners() { export async function getProjectsWithCreators() {
const projectsWithOwners = await db.select().from(Project).leftJoin(User, eq(Project.ownerId, User.id)); const projectsWithCreators = await db
return projectsWithOwners;
}
export async function getProjectWithOwnerByID(projectId: number) {
const [projectWithOwner] = await db
.select() .select()
.from(Project) .from(Project)
.leftJoin(User, eq(Project.ownerId, User.id)) .leftJoin(User, eq(Project.creatorId, User.id));
.where(eq(Project.id, projectId)); return projectsWithCreators;
return projectWithOwner; }
export async function getProjectWithCreatorByID(projectId: number) {
const [projectWithCreator] = await db
.select()
.from(Project)
.leftJoin(User, eq(Project.creatorId, User.id))
.where(eq(Project.id, projectId));
return projectWithCreator;
} }

View File

@@ -26,10 +26,10 @@ const main = async () => {
"/project/create": withCors(withAuth(routes.projectCreate)), "/project/create": withCors(withAuth(routes.projectCreate)),
"/project/update": withCors(withAuth(routes.projectUpdate)), "/project/update": withCors(withAuth(routes.projectUpdate)),
"/project/delete": withCors(withAuth(routes.projectDelete)), "/project/delete": withCors(withAuth(routes.projectDelete)),
"/projects/by-owner": withCors(withAuth(routes.projectsByOwner)), "/projects/by-creator": withCors(withAuth(routes.projectsByCreator)),
"/projects/all": withCors(withAuth(routes.projectsAll)), "/projects/all": withCors(withAuth(routes.projectsAll)),
"/projects/with-owners": withCors(withAuth(routes.projectsWithOwners)), "/projects/with-creators": withCors(withAuth(routes.projectsWithCreators)),
"/project/with-owner": withCors(withAuth(routes.projectWithOwner)), "/project/with-creator": withCors(withAuth(routes.projectWithCreator)),
}, },
}); });

View File

@@ -7,12 +7,12 @@ import issueUpdate from "./issue/update";
import issuesInProject from "./issues/[projectBlob]"; import issuesInProject from "./issues/[projectBlob]";
import issues from "./issues/all"; import issues from "./issues/all";
import projectsAll from "./project/all"; import projectsAll from "./project/all";
import projectsByOwner from "./project/by-owner"; import projectsByCreator from "./project/by-creator";
import projectCreate from "./project/create"; import projectCreate from "./project/create";
import projectDelete from "./project/delete"; import projectDelete from "./project/delete";
import projectUpdate from "./project/update"; import projectUpdate from "./project/update";
import projectWithOwner from "./project/with-owner"; import projectWithCreator from "./project/with-creator";
import projectsWithOwners from "./project/with-owners"; import projectsWithCreators from "./project/with-creators";
export const routes = { export const routes = {
issueCreate, issueCreate,
@@ -25,10 +25,10 @@ export const routes = {
projectCreate, projectCreate,
projectUpdate, projectUpdate,
projectDelete, projectDelete,
projectsByOwner, projectsByCreator,
projectsAll, projectsAll,
projectsWithOwners, projectsWithCreators,
projectWithOwner, projectWithCreator,
authRegister, authRegister,
authLogin, authLogin,

View File

@@ -1,9 +1,9 @@
import type { BunRequest } from "bun"; import type { BunRequest } from "bun";
import { getProjectsWithOwners } from "../../db/queries"; import { getProjectsWithCreators } from "../../db/queries";
// /projects/all // /projects/all
export default async function projectsAll(req: BunRequest) { export default async function projectsAll(req: BunRequest) {
const projects = await getProjectsWithOwners(); const projects = await getProjectsWithCreators();
return Response.json(projects); return Response.json(projects);
} }

View File

@@ -0,0 +1,26 @@
import type { BunRequest } from "bun";
import { getProjectsByCreatorID, getUserById } from "../../db/queries";
// /projects/by-creator?creatorId=1
export default async function projectsByCreator(req: BunRequest) {
const url = new URL(req.url);
const creatorId = url.searchParams.get("creatorId");
if (!creatorId) {
return new Response("creatorId is required", { status: 400 });
}
const creatorIdNumber = Number(creatorId);
if (!Number.isInteger(creatorIdNumber)) {
return new Response("creatorId must be an integer", { status: 400 });
}
const creator = await getUserById(creatorIdNumber);
if (!creator) {
return new Response(`user with id ${creatorId} not found`, { status: 404 });
}
const projects = await getProjectsByCreatorID(creator.id);
return Response.json(projects);
}

View File

@@ -1,26 +0,0 @@
import type { BunRequest } from "bun";
import { getProjectsByOwnerID, getUserById } from "../../db/queries";
// /projects/by-owner?ownerId=1
export default async function projectsByOwner(req: BunRequest) {
const url = new URL(req.url);
const ownerId = url.searchParams.get("ownerId");
if (!ownerId) {
return new Response("ownerId is required", { status: 400 });
}
const ownerIdNumber = Number(ownerId);
if (!Number.isInteger(ownerIdNumber)) {
return new Response("ownerId must be an integer", { status: 400 });
}
const owner = await getUserById(ownerIdNumber);
if (!owner) {
return new Response(`user with id ${ownerId} not found`, { status: 404 });
}
const projects = await getProjectsByOwnerID(owner.id);
return Response.json(projects);
}

View File

@@ -1,16 +1,17 @@
import type { BunRequest } from "bun"; import type { BunRequest } from "bun";
import { createProject, getUserById, getProjectByBlob } from "../../db/queries"; import { createProject, getProjectByBlob, getUserById } from "../../db/queries";
// /project/create?blob=BLOB&name=Testing&ownerId=1 // /project/create?blob=BLOB&name=Testing&creatorId=1&organisationId=1
export default async function projectCreate(req: BunRequest) { export default async function projectCreate(req: BunRequest) {
const url = new URL(req.url); const url = new URL(req.url);
const blob = url.searchParams.get("blob"); const blob = url.searchParams.get("blob");
const name = url.searchParams.get("name"); const name = url.searchParams.get("name");
const ownerId = url.searchParams.get("ownerId"); const creatorId = url.searchParams.get("creatorId");
const organisationId = url.searchParams.get("organisationId");
if (!blob || !name || !ownerId) { if (!blob || !name || !creatorId || !organisationId) {
return new Response( return new Response(
`missing parameters: ${!blob ? "blob " : ""}${!name ? "name " : ""}${!ownerId ? "ownerId" : ""}`, `missing parameters: ${!blob ? "blob " : ""}${!name ? "name " : ""}${!creatorId ? "creatorId " : ""}${!organisationId ? "organisationId" : ""}`,
{ status: 400 }, { status: 400 },
); );
} }
@@ -21,12 +22,12 @@ export default async function projectCreate(req: BunRequest) {
return new Response(`project with blob ${blob} already exists`, { status: 400 }); return new Response(`project with blob ${blob} already exists`, { status: 400 });
} }
const owner = await getUserById(parseInt(ownerId, 10)); const creator = await getUserById(parseInt(creatorId, 10));
if (!owner) { if (!creator) {
return new Response(`owner with id ${ownerId} not found`, { status: 404 }); return new Response(`creator with id ${creatorId} not found`, { status: 404 });
} }
const project = await createProject(blob, name, owner.id); const project = await createProject(blob, name, creator.id, parseInt(organisationId, 10));
return Response.json(project); return Response.json(project);
} }

View File

@@ -1,13 +1,14 @@
import type { BunRequest } from "bun"; import type { BunRequest } from "bun";
import { getProjectByBlob, getProjectByID, getUserById, updateProject } from "../../db/queries"; import { getProjectByBlob, getProjectByID, getUserById, updateProject } from "../../db/queries";
// /project/update?id=1&blob=NEW&name=new%20name&ownerId=1 // /project/update?id=1&blob=NEW&name=new%20name&creatorId=1&organisationId=1
export default async function projectUpdate(req: BunRequest) { export default async function projectUpdate(req: BunRequest) {
const url = new URL(req.url); const url = new URL(req.url);
const id = url.searchParams.get("id"); const id = url.searchParams.get("id");
const blob = url.searchParams.get("blob") || undefined; const blob = url.searchParams.get("blob") || undefined;
const name = url.searchParams.get("name") || undefined; const name = url.searchParams.get("name") || undefined;
const ownerId = url.searchParams.get("ownerId") || undefined; const creatorId = url.searchParams.get("creatorId") || undefined;
const organisationId = url.searchParams.get("organisationId") || undefined;
if (!id) { if (!id) {
return new Response(`project id is required`, { status: 400 }); return new Response(`project id is required`, { status: 400 });
@@ -18,8 +19,8 @@ export default async function projectUpdate(req: BunRequest) {
return new Response(`project with id ${id} does not exist`, { status: 404 }); return new Response(`project with id ${id} does not exist`, { status: 404 });
} }
if (!blob && !name && !ownerId) { if (!blob && !name && !creatorId && !organisationId) {
return new Response(`at least one of blob, name, or ownerId must be provided`, { return new Response(`at least one of blob, name, creatorId, or organisationId must be provided`, {
status: 400, status: 400,
}); });
} }
@@ -29,15 +30,16 @@ export default async function projectUpdate(req: BunRequest) {
return new Response(`a project with blob "${blob}" already exists`, { status: 400 }); return new Response(`a project with blob "${blob}" already exists`, { status: 400 });
} }
const newOwner = ownerId ? await getUserById(Number(ownerId)) : null; const newCreator = creatorId ? await getUserById(Number(creatorId)) : null;
if (ownerId && !newOwner) { if (creatorId && !newCreator) {
return new Response(`user with id ${ownerId} does not exist`, { status: 400 }); return new Response(`user with id ${creatorId} does not exist`, { status: 400 });
} }
const project = await updateProject(Number(id), { const project = await updateProject(Number(id), {
blob: blob, blob: blob,
name: name, name: name,
ownerId: newOwner?.id, creatorId: newCreator?.id,
organisationId: organisationId ? Number(organisationId) : undefined,
}); });
return Response.json(project); return Response.json(project);

View File

@@ -1,8 +1,8 @@
import type { BunRequest } from "bun"; import type { BunRequest } from "bun";
import { getProjectWithOwnerByID } from "../../db/queries"; import { getProjectWithCreatorByID } from "../../db/queries";
// /project/with-owner?id=1 // /project/with-creator?id=1
export default async function projectWithOwnerByID(req: BunRequest) { export default async function projectWithCreatorByID(req: BunRequest) {
const url = new URL(req.url); const url = new URL(req.url);
const id = url.searchParams.get("id"); const id = url.searchParams.get("id");
@@ -15,10 +15,10 @@ export default async function projectWithOwnerByID(req: BunRequest) {
return new Response("project id must be an integer", { status: 400 }); return new Response("project id must be an integer", { status: 400 });
} }
const projectWithOwner = await getProjectWithOwnerByID(projectId); const projectWithCreator = await getProjectWithCreatorByID(projectId);
if (!projectWithOwner || !projectWithOwner.Project) { if (!projectWithCreator || !projectWithCreator.Project) {
return new Response(`project with id ${id} not found`, { status: 404 }); return new Response(`project with id ${id} not found`, { status: 404 });
} }
return Response.json(projectWithOwner); return Response.json(projectWithCreator);
} }

View File

@@ -0,0 +1,9 @@
import type { BunRequest } from "bun";
import { getProjectsWithCreators } from "../../db/queries";
// /projects/with-creators
export default async function projectsWithCreators(req: BunRequest) {
const projects = await getProjectsWithCreators();
return Response.json(projects);
}

View File

@@ -1,9 +0,0 @@
import type { BunRequest } from "bun";
import { getProjectsWithOwners } from "../../db/queries";
// /projects/with-owners
export default async function projectsWithOwners(req: BunRequest) {
const projects = await getProjectsWithOwners();
return Response.json(projects);
}

View File

@@ -1,5 +1,11 @@
import { hashPassword } from "./auth/utils"; import { hashPassword } from "./auth/utils";
import { createIssue, createProject, createUser } from "./db/queries"; import {
createIssue,
createOrganisation,
createOrganisationMember,
createProject,
createUser,
} from "./db/queries";
export const createDemoData = async () => { export const createDemoData = async () => {
const passwordHash = await hashPassword("changeme"); const passwordHash = await hashPassword("changeme");
@@ -8,10 +14,23 @@ export const createDemoData = async () => {
throw new Error("failed to create demo user"); throw new Error("failed to create demo user");
} }
// create demo organisation
const organisation = await createOrganisation(
"Demo Organisation",
"demo-org",
"A demo organisation for testing",
);
if (!organisation) {
throw new Error("failed to create demo organisation");
}
// add user as owner of the organisation
await createOrganisationMember(organisation.id, user.id, "owner");
const projectNames = ["PROJ", "TEST", "SAMPLE"]; const projectNames = ["PROJ", "TEST", "SAMPLE"];
let issuesToCreate = 3; let issuesToCreate = 3;
for (const name of projectNames) { for (const name of projectNames) {
const project = await createProject(name.slice(0, 4), name, user.id); const project = await createProject(name.slice(0, 4), name, user.id, organisation.id);
if (!project) { if (!project) {
throw new Error(`failed to create demo project: ${name}`); throw new Error(`failed to create demo project: ${name}`);
} }

View File

@@ -22,7 +22,7 @@ function Index() {
if (projectsRef.current) return; if (projectsRef.current) return;
projectsRef.current = true; projectsRef.current = true;
fetch(`${serverURL}/projects/by-owner?ownerId=${user.id}`, { headers: getAuthHeaders() }) fetch(`${serverURL}/projects/by-creator?creatorId=${user.id}`, { headers: getAuthHeaders() })
.then((res) => res.json()) .then((res) => res.json())
.then((data: ProjectResponse[]) => { .then((data: ProjectResponse[]) => {
setProjects(data); setProjects(data);
@@ -88,7 +88,7 @@ function Index() {
</Select> </Select>
{selectedProject && ( {selectedProject && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
Owner: <SmallUserDisplay user={selectedProject?.User} /> Creator: <SmallUserDisplay user={selectedProject?.User} />
</div> </div>
)} )}
</div> </div>

View File

@@ -1,10 +1,14 @@
// Drizzle tables // Drizzle tables
export { User, Project, Issue } from "./schema"; export { User, Project, Issue, Organisation, OrganisationMember } from "./schema";
// Types // Types
export type { export type {
UserRecord, UserRecord,
UserInsert, UserInsert,
OrganisationRecord,
OrganisationInsert,
OrganisationMemberRecord,
OrganisationMemberInsert,
ProjectRecord, ProjectRecord,
ProjectInsert, ProjectInsert,
IssueRecord, IssueRecord,
@@ -15,6 +19,10 @@ export type {
export { export {
UserSelectSchema, UserSelectSchema,
UserInsertSchema, UserInsertSchema,
OrganisationSelectSchema,
OrganisationInsertSchema,
OrganisationMemberSelectSchema,
OrganisationMemberInsertSchema,
ProjectSelectSchema, ProjectSelectSchema,
ProjectInsertSchema, ProjectInsertSchema,
IssueSelectSchema, IssueSelectSchema,
@@ -25,4 +33,4 @@ export {
export type { export type {
IssueResponse, IssueResponse,
ProjectResponse, ProjectResponse,
} from "./schema" } from "./schema";

View File

@@ -108,5 +108,5 @@ export type IssueResponse = {
export type ProjectResponse = { export type ProjectResponse = {
Project: ProjectRecord; Project: ProjectRecord;
Organisation: OrganisationRecord; Organisation: OrganisationRecord;
Creator: UserRecord; User: UserRecord;
}; };