diff --git a/packages/backend/src/db/queries/index.ts b/packages/backend/src/db/queries/index.ts index 4fc4e73..b4eae71 100644 --- a/packages/backend/src/db/queries/index.ts +++ b/packages/backend/src/db/queries/index.ts @@ -1,3 +1,4 @@ -export * from "./users"; -export * from "./projects"; export * from "./issues"; +export * from "./organisations"; +export * from "./projects"; +export * from "./users"; diff --git a/packages/backend/src/db/queries/organisations.ts b/packages/backend/src/db/queries/organisations.ts new file mode 100644 index 0000000..3b73a62 --- /dev/null +++ b/packages/backend/src/db/queries/organisations.ts @@ -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; +} diff --git a/packages/backend/src/db/queries/projects.ts b/packages/backend/src/db/queries/projects.ts index c63b50f..9b44b3c 100644 --- a/packages/backend/src/db/queries/projects.ts +++ b/packages/backend/src/db/queries/projects.ts @@ -2,13 +2,14 @@ import { Issue, Project, User } from "@issue/shared"; import { eq } from "drizzle-orm"; 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 .insert(Project) .values({ blob, name, - ownerId, + creatorId, + organisationId, }) .returning(); return project; @@ -16,7 +17,7 @@ export async function createProject(blob: string, name: string, ownerId: number) export async function updateProject( 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(); return project; @@ -39,13 +40,13 @@ export async function getProjectByBlob(projectBlob: string) { return project; } -export async function getProjectsByOwnerID(ownerId: number) { - const projectsWithOwners = await db +export async function getProjectsByCreatorID(creatorId: number) { + const projectsWithCreators = await db .select() .from(Project) - .where(eq(Project.ownerId, ownerId)) - .leftJoin(User, eq(Project.ownerId, User.id)); - return projectsWithOwners; + .where(eq(Project.creatorId, creatorId)) + .leftJoin(User, eq(Project.creatorId, User.id)); + return projectsWithCreators; } export async function getAllProjects() { @@ -53,16 +54,19 @@ export async function getAllProjects() { return projects; } -export async function getProjectsWithOwners() { - const projectsWithOwners = await db.select().from(Project).leftJoin(User, eq(Project.ownerId, User.id)); - return projectsWithOwners; -} - -export async function getProjectWithOwnerByID(projectId: number) { - const [projectWithOwner] = await db +export async function getProjectsWithCreators() { + const projectsWithCreators = await db .select() .from(Project) - .leftJoin(User, eq(Project.ownerId, User.id)) - .where(eq(Project.id, projectId)); - return projectWithOwner; + .leftJoin(User, eq(Project.creatorId, User.id)); + return projectsWithCreators; +} + +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; } diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index f615423..647b0b4 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -26,10 +26,10 @@ const main = async () => { "/project/create": withCors(withAuth(routes.projectCreate)), "/project/update": withCors(withAuth(routes.projectUpdate)), "/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/with-owners": withCors(withAuth(routes.projectsWithOwners)), - "/project/with-owner": withCors(withAuth(routes.projectWithOwner)), + "/projects/with-creators": withCors(withAuth(routes.projectsWithCreators)), + "/project/with-creator": withCors(withAuth(routes.projectWithCreator)), }, }); diff --git a/packages/backend/src/routes/index.ts b/packages/backend/src/routes/index.ts index 9afeed0..00a8535 100644 --- a/packages/backend/src/routes/index.ts +++ b/packages/backend/src/routes/index.ts @@ -7,12 +7,12 @@ import issueUpdate from "./issue/update"; import issuesInProject from "./issues/[projectBlob]"; import issues from "./issues/all"; import projectsAll from "./project/all"; -import projectsByOwner from "./project/by-owner"; +import projectsByCreator from "./project/by-creator"; import projectCreate from "./project/create"; import projectDelete from "./project/delete"; import projectUpdate from "./project/update"; -import projectWithOwner from "./project/with-owner"; -import projectsWithOwners from "./project/with-owners"; +import projectWithCreator from "./project/with-creator"; +import projectsWithCreators from "./project/with-creators"; export const routes = { issueCreate, @@ -25,10 +25,10 @@ export const routes = { projectCreate, projectUpdate, projectDelete, - projectsByOwner, + projectsByCreator, projectsAll, - projectsWithOwners, - projectWithOwner, + projectsWithCreators, + projectWithCreator, authRegister, authLogin, diff --git a/packages/backend/src/routes/project/all.ts b/packages/backend/src/routes/project/all.ts index 24aa525..c4b8997 100644 --- a/packages/backend/src/routes/project/all.ts +++ b/packages/backend/src/routes/project/all.ts @@ -1,9 +1,9 @@ import type { BunRequest } from "bun"; -import { getProjectsWithOwners } from "../../db/queries"; +import { getProjectsWithCreators } from "../../db/queries"; // /projects/all export default async function projectsAll(req: BunRequest) { - const projects = await getProjectsWithOwners(); + const projects = await getProjectsWithCreators(); return Response.json(projects); } diff --git a/packages/backend/src/routes/project/by-creator.ts b/packages/backend/src/routes/project/by-creator.ts new file mode 100644 index 0000000..36c8db8 --- /dev/null +++ b/packages/backend/src/routes/project/by-creator.ts @@ -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); +} diff --git a/packages/backend/src/routes/project/by-owner.ts b/packages/backend/src/routes/project/by-owner.ts deleted file mode 100644 index 8365c64..0000000 --- a/packages/backend/src/routes/project/by-owner.ts +++ /dev/null @@ -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); -} diff --git a/packages/backend/src/routes/project/create.ts b/packages/backend/src/routes/project/create.ts index 2d1521e..c479bf3 100644 --- a/packages/backend/src/routes/project/create.ts +++ b/packages/backend/src/routes/project/create.ts @@ -1,16 +1,17 @@ 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) { const url = new URL(req.url); const blob = url.searchParams.get("blob"); 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( - `missing parameters: ${!blob ? "blob " : ""}${!name ? "name " : ""}${!ownerId ? "ownerId" : ""}`, + `missing parameters: ${!blob ? "blob " : ""}${!name ? "name " : ""}${!creatorId ? "creatorId " : ""}${!organisationId ? "organisationId" : ""}`, { status: 400 }, ); } @@ -21,12 +22,12 @@ export default async function projectCreate(req: BunRequest) { return new Response(`project with blob ${blob} already exists`, { status: 400 }); } - const owner = await getUserById(parseInt(ownerId, 10)); - if (!owner) { - return new Response(`owner with id ${ownerId} not found`, { status: 404 }); + const creator = await getUserById(parseInt(creatorId, 10)); + if (!creator) { + 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); } diff --git a/packages/backend/src/routes/project/update.ts b/packages/backend/src/routes/project/update.ts index b1b817a..fc40a78 100644 --- a/packages/backend/src/routes/project/update.ts +++ b/packages/backend/src/routes/project/update.ts @@ -1,13 +1,14 @@ import type { BunRequest } from "bun"; 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) { const url = new URL(req.url); const id = url.searchParams.get("id"); const blob = url.searchParams.get("blob") || 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) { 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 }); } - if (!blob && !name && !ownerId) { - return new Response(`at least one of blob, name, or ownerId must be provided`, { + if (!blob && !name && !creatorId && !organisationId) { + return new Response(`at least one of blob, name, creatorId, or organisationId must be provided`, { 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 }); } - const newOwner = ownerId ? await getUserById(Number(ownerId)) : null; - if (ownerId && !newOwner) { - return new Response(`user with id ${ownerId} does not exist`, { status: 400 }); + const newCreator = creatorId ? await getUserById(Number(creatorId)) : null; + if (creatorId && !newCreator) { + return new Response(`user with id ${creatorId} does not exist`, { status: 400 }); } const project = await updateProject(Number(id), { blob: blob, name: name, - ownerId: newOwner?.id, + creatorId: newCreator?.id, + organisationId: organisationId ? Number(organisationId) : undefined, }); return Response.json(project); diff --git a/packages/backend/src/routes/project/with-owner.ts b/packages/backend/src/routes/project/with-creator.ts similarity index 57% rename from packages/backend/src/routes/project/with-owner.ts rename to packages/backend/src/routes/project/with-creator.ts index 62b53a4..032f108 100644 --- a/packages/backend/src/routes/project/with-owner.ts +++ b/packages/backend/src/routes/project/with-creator.ts @@ -1,8 +1,8 @@ import type { BunRequest } from "bun"; -import { getProjectWithOwnerByID } from "../../db/queries"; +import { getProjectWithCreatorByID } from "../../db/queries"; -// /project/with-owner?id=1 -export default async function projectWithOwnerByID(req: BunRequest) { +// /project/with-creator?id=1 +export default async function projectWithCreatorByID(req: BunRequest) { const url = new URL(req.url); 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 }); } - const projectWithOwner = await getProjectWithOwnerByID(projectId); - if (!projectWithOwner || !projectWithOwner.Project) { + const projectWithCreator = await getProjectWithCreatorByID(projectId); + if (!projectWithCreator || !projectWithCreator.Project) { return new Response(`project with id ${id} not found`, { status: 404 }); } - return Response.json(projectWithOwner); + return Response.json(projectWithCreator); } diff --git a/packages/backend/src/routes/project/with-creators.ts b/packages/backend/src/routes/project/with-creators.ts new file mode 100644 index 0000000..6ccffd5 --- /dev/null +++ b/packages/backend/src/routes/project/with-creators.ts @@ -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); +} diff --git a/packages/backend/src/routes/project/with-owners.ts b/packages/backend/src/routes/project/with-owners.ts deleted file mode 100644 index 770ff28..0000000 --- a/packages/backend/src/routes/project/with-owners.ts +++ /dev/null @@ -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); -} diff --git a/packages/backend/src/utils.ts b/packages/backend/src/utils.ts index 827d3eb..c04f696 100644 --- a/packages/backend/src/utils.ts +++ b/packages/backend/src/utils.ts @@ -1,5 +1,11 @@ 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 () => { const passwordHash = await hashPassword("changeme"); @@ -8,10 +14,23 @@ export const createDemoData = async () => { 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"]; let issuesToCreate = 3; 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) { throw new Error(`failed to create demo project: ${name}`); } diff --git a/packages/frontend/src/Index.tsx b/packages/frontend/src/Index.tsx index 4a89a9f..b440a54 100644 --- a/packages/frontend/src/Index.tsx +++ b/packages/frontend/src/Index.tsx @@ -22,7 +22,7 @@ function Index() { if (projectsRef.current) return; 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((data: ProjectResponse[]) => { setProjects(data); @@ -88,7 +88,7 @@ function Index() { {selectedProject && (