From fcea18f40d7c52bf415d9b59daa9e591d33d8c53 Mon Sep 17 00:00:00 2001 From: LabibaVIC Date: Sat, 16 May 2026 00:14:00 +1000 Subject: [PATCH 1/6] Add project-organisation API with Prisma schema and routes --- .vscode/settings.json | 3 + .../20260515093156_init/migration.sql | 2 + prisma/migrations/migration_lock.toml | 3 + prisma/models/organisation.prisma | 7 ++ prisma/models/projectOrganisation.prisma | 10 ++ prisma/schema.prisma | 25 +++++ src/app.ts | 98 ++++++++++--------- src/routes/projectOrganisation.ts | 84 ++++++++++++++++ 8 files changed, 184 insertions(+), 48 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 prisma/migrations/20260515093156_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/models/organisation.prisma create mode 100644 prisma/models/projectOrganisation.prisma create mode 100644 src/routes/projectOrganisation.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3c358ef --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "js/ts.tsdk.path": "node_modules/typescript/lib" +} \ No newline at end of file diff --git a/prisma/migrations/20260515093156_init/migration.sql b/prisma/migrations/20260515093156_init/migration.sql new file mode 100644 index 0000000..952a522 --- /dev/null +++ b/prisma/migrations/20260515093156_init/migration.sql @@ -0,0 +1,2 @@ +-- CreateEnum +CREATE TYPE "UserRole" AS ENUM ('FARMER', 'INSPECTOR', 'MANAGER', 'ADMIN', 'DEVELOPER'); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/models/organisation.prisma b/prisma/models/organisation.prisma new file mode 100644 index 0000000..1831218 --- /dev/null +++ b/prisma/models/organisation.prisma @@ -0,0 +1,7 @@ +model Organisation { + id Int @id @default(autoincrement()) + name String + createdAt DateTime @default(now()) + + projectOrganisations ProjectOrganisation[] +} \ No newline at end of file diff --git a/prisma/models/projectOrganisation.prisma b/prisma/models/projectOrganisation.prisma new file mode 100644 index 0000000..89f0289 --- /dev/null +++ b/prisma/models/projectOrganisation.prisma @@ -0,0 +1,10 @@ +model ProjectOrganisation { + projectId Int + organisationId Int + + project Project @relation(fields: [projectId], references: [id]) + organisation Organisation @relation(fields: [organisationId], references: [id]) + + @@id([projectId, organisationId]) + @@map("project_organisations") +} \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d37099d..b403e26 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -14,3 +14,28 @@ enum UserRole { ADMIN DEVELOPER } +model Organisation { + id Int @id @default(autoincrement()) + name String + createdAt DateTime @default(now()) + + projectOrganisations ProjectOrganisation[] +} + +model Project { + id Int @id @default(autoincrement()) + name String + + projectOrganisations ProjectOrganisation[] +} + +model ProjectOrganisation { + projectId Int + organisationId Int + + project Project @relation(fields: [projectId], references: [id]) + organisation Organisation @relation(fields: [organisationId], references: [id]) + + @@id([projectId, organisationId]) + @@map("project_organisations") +} \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 2ffa37d..1f302e5 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,53 +1,55 @@ -import express from "express"; -import helmet from "helmet"; -import cors from "cors"; -import compression from "compression"; -import rateLimit from "express-rate-limit"; -import { env } from "./config/env"; -import { errorHandler, notFound } from "./middleware/errorHandler"; - -import * as swaggerUi from "swagger-ui-express"; -import { swaggerSpec } from "./config/swagger"; - -const app = express(); - -// Security -app.use(helmet()); -app.use(cors({ origin: env.NODE_ENV === "production" ? false : "*" })); - -// Rate limiting -app.use( - rateLimit({ - windowMs: env.RATE_LIMIT_WINDOW_MS, - max: env.RATE_LIMIT_MAX, - message: { - success: false, - message: "Too many requests, please try again later.", - }, - }), -); - -// Parsing -app.use(compression()); -app.use(express.json({ limit: "10mb" })); -app.use(express.urlencoded({ extended: true })); - -// Health check -app.get("/health", (_req, res) => { - res.json({ - success: true, - status: "ok", - timestamp: new Date().toISOString(), + import projectOrganisationsRoutes from "./routes/projectOrganisation"; + import express from "express"; + import helmet from "helmet"; + import cors from "cors"; + import compression from "compression"; + import rateLimit from "express-rate-limit"; + import { env } from "./config/env"; + import { errorHandler, notFound } from "./middleware/errorHandler"; + + import * as swaggerUi from "swagger-ui-express"; + import { swaggerSpec } from "./config/swagger"; + + const app = express(); + + // Security + app.use(helmet()); + app.use(cors({ origin: env.NODE_ENV === "production" ? false : "*" })); + + // Rate limiting + app.use( + rateLimit({ + windowMs: env.RATE_LIMIT_WINDOW_MS, + max: env.RATE_LIMIT_MAX, + message: { + success: false, + message: "Too many requests, please try again later.", + }, + }), + ); + + // Parsing + app.use(compression()); + app.use(express.json({ limit: "10mb" })); + app.use(express.urlencoded({ extended: true })); + + // Health check + app.get("/health", (_req, res) => { + res.json({ + success: true, + status: "ok", + timestamp: new Date().toISOString(), + }); }); -}); -// Swagger docs -app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec)); + // Swagger docs + app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec)); -// Routes + // Routes + app.use("/project-organisations", projectOrganisationsRoutes); -// 404 & error handler -app.use(notFound); -app.use(errorHandler); + // 404 & error handler + app.use(notFound); + app.use(errorHandler); -export default app; + export default app; diff --git a/src/routes/projectOrganisation.ts b/src/routes/projectOrganisation.ts new file mode 100644 index 0000000..60a7fa3 --- /dev/null +++ b/src/routes/projectOrganisation.ts @@ -0,0 +1,84 @@ +import { Router } from "express"; +import { prisma } from "../lib/prisma"; + +const router = Router(); + +// GET all relations +router.get("/", async (_req, res, next) => { + try { + const data = await prisma.projectOrganisation.findMany({ + include: { + organisation: true, + project: true, + }, + }); + + return res.json(data); + } catch (error) { + next(error); + } +}); + +// CREATE relation +router.post("/", async (req, res, next) => { + try { + const projectId = Number(req.body.projectId); + const organisationId = Number(req.body.organisationId); + + if (!projectId || !organisationId) { + return res.status(400).json({ + message: "projectId and organisationId are required", + }); + } + + // SAFETY CHECK + const existing = await prisma.projectOrganisation.findUnique({ + where: { + projectId_organisationId: { + projectId, + organisationId, + }, + }, + }); + + if (existing) { + return res.status(409).json({ + message: "Relation already exists", + }); + } + + const result = await prisma.projectOrganisation.create({ + data: { + projectId, + organisationId, + }, + }); + + return res.status(201).json(result); + } catch (error) { + next(error); + } +}); + +// DELETE relation +router.delete("/:projectId/:organisationId", async (req, res, next) => { + try { + const projectId = Number(req.params.projectId); + const organisationId = Number(req.params.organisationId); + + const result = await prisma.projectOrganisation.delete({ + where: { + projectId_organisationId: { + projectId, + organisationId, + }, + }, + }); + + return res.json(result); + } catch (error) { + next(error); + } +}); + +export default router; \ No newline at end of file From 92d839bba604b2cdb921f98b3f50b82fe97c989a Mon Sep 17 00:00:00 2001 From: LabibaVIC Date: Sat, 16 May 2026 00:35:47 +1000 Subject: [PATCH 2/6] Add routes index for centralized routing --- src/routes/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/routes/index.ts diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 0000000..67645fb --- /dev/null +++ b/src/routes/index.ts @@ -0,0 +1,8 @@ +import { Router } from "express"; +import projectOrganisationsRoutes from "./projectOrganisation"; + +const router = Router(); + +router.use("/project-organisations", projectOrganisationsRoutes); + +export default router; \ No newline at end of file From 02f2e83a32c66c4a5533eacb2b6b3ce5fb6ea413 Mon Sep 17 00:00:00 2001 From: LabibaVIC Date: Sat, 16 May 2026 09:46:35 +1000 Subject: [PATCH 3/6] fix prisma schema and regenerate client --- package-lock.json | 2 +- prisma/models/project.prisma | 1 + prisma/schema.prisma | 26 +------------------------- 3 files changed, 3 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index e1688a5..fd30482 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8660,4 +8660,4 @@ } } } -} \ No newline at end of file +} diff --git a/prisma/models/project.prisma b/prisma/models/project.prisma index 77985f9..44be224 100644 --- a/prisma/models/project.prisma +++ b/prisma/models/project.prisma @@ -12,6 +12,7 @@ model Project { adminLocation Location? @relation("ProjectAdminLocation", fields: [adminLocationId], references: [id], onDelete: Restrict) userProjects UserProject[] projectTreeTypes ProjectTreeType[] + projectOrganisations ProjectOrganisation[] scanBatches ScanBatch[] @relation("ProjectScanBatches") treeScans TreeScan[] @relation("ProjectTreeScans") diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b403e26..eadba1f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,5 +1,6 @@ generator client { provider = "prisma-client-js" + schema = "./models" } datasource db { @@ -13,29 +14,4 @@ enum UserRole { MANAGER ADMIN DEVELOPER -} -model Organisation { - id Int @id @default(autoincrement()) - name String - createdAt DateTime @default(now()) - - projectOrganisations ProjectOrganisation[] -} - -model Project { - id Int @id @default(autoincrement()) - name String - - projectOrganisations ProjectOrganisation[] -} - -model ProjectOrganisation { - projectId Int - organisationId Int - - project Project @relation(fields: [projectId], references: [id]) - organisation Organisation @relation(fields: [organisationId], references: [id]) - - @@id([projectId, organisationId]) - @@map("project_organisations") } \ No newline at end of file From 0eb6048e0dbde7b574911ca091cdf5f80ccaa3b3 Mon Sep 17 00:00:00 2001 From: LabibaVIC Date: Sat, 16 May 2026 09:58:14 +1000 Subject: [PATCH 4/6] fix CI issues in projectOrganisation routes --- src/routes/projectOrganisation.ts | 44 +++++++++++++++---------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/routes/projectOrganisation.ts b/src/routes/projectOrganisation.ts index 60a7fa3..474f620 100644 --- a/src/routes/projectOrganisation.ts +++ b/src/routes/projectOrganisation.ts @@ -1,10 +1,9 @@ -import { Router } from "express"; +import { Router, Request, Response, NextFunction } from "express"; import { prisma } from "../lib/prisma"; const router = Router(); -// GET all relations -router.get("/", async (_req, res, next) => { +router.get("/", async (_req: Request, res: Response, next: NextFunction): Promise => { try { const data = await prisma.projectOrganisation.findMany({ include: { @@ -13,25 +12,24 @@ router.get("/", async (_req, res, next) => { }, }); - return res.json(data); + res.json(data); } catch (error) { next(error); } }); -// CREATE relation -router.post("/", async (req, res, next) => { +router.post("/", async (req: Request, res: Response, next: NextFunction): Promise => { try { - const projectId = Number(req.body.projectId); - const organisationId = Number(req.body.organisationId); + const { projectId, organisationId } = req.body as { + projectId: number; + organisationId: number; + }; if (!projectId || !organisationId) { - return res.status(400).json({ - message: "projectId and organisationId are required", - }); + res.status(400).json({ message: "projectId and organisationId are required" }); + return; } - // SAFETY CHECK const existing = await prisma.projectOrganisation.findUnique({ where: { projectId_organisationId: { @@ -42,9 +40,8 @@ router.post("/", async (req, res, next) => { }); if (existing) { - return res.status(409).json({ - message: "Relation already exists", - }); + res.status(409).json({ message: "Relation already exists" }); + return; } const result = await prisma.projectOrganisation.create({ @@ -54,28 +51,29 @@ router.post("/", async (req, res, next) => { }, }); - return res.status(201).json(result); + res.status(201).json(result); } catch (error) { next(error); } }); -// DELETE relation -router.delete("/:projectId/:organisationId", async (req, res, next) => { +router.delete("/:projectId/:organisationId", async (req: Request, res: Response, next: NextFunction): Promise => { try { - const projectId = Number(req.params.projectId); - const organisationId = Number(req.params.organisationId); + const { projectId, organisationId } = req.params as { + projectId: string; + organisationId: string; + }; const result = await prisma.projectOrganisation.delete({ where: { projectId_organisationId: { - projectId, - organisationId, + projectId: Number(projectId), + organisationId: Number(organisationId), }, }, }); - return res.json(result); + res.json(result); } catch (error) { next(error); } From 4364e378a0d3dcbff60e68f3892b16bd9f37f091 Mon Sep 17 00:00:00 2001 From: LabibaVIC Date: Sat, 16 May 2026 10:05:25 +1000 Subject: [PATCH 5/6] fix CI issues in projectOrganisation routes --- src/routes/projectOrganisation.ts | 151 ++++++++++++++++-------------- 1 file changed, 82 insertions(+), 69 deletions(-) diff --git a/src/routes/projectOrganisation.ts b/src/routes/projectOrganisation.ts index 474f620..c33db08 100644 --- a/src/routes/projectOrganisation.ts +++ b/src/routes/projectOrganisation.ts @@ -1,82 +1,95 @@ -import { Router, Request, Response, NextFunction } from "express"; -import { prisma } from "../lib/prisma"; + import { Router, Request, Response, NextFunction } from "express"; + import { prisma } from "../lib/prisma"; -const router = Router(); + const router = Router(); -router.get("/", async (_req: Request, res: Response, next: NextFunction): Promise => { - try { - const data = await prisma.projectOrganisation.findMany({ - include: { - organisation: true, - project: true, - }, - }); + router.get("/", (req: Request, res: Response, next: NextFunction): void => { + void (async () => { + try { + const data = await prisma.projectOrganisation.findMany({ + include: { + organisation: true, + project: true, + }, + }); - res.json(data); - } catch (error) { - next(error); - } -}); + res.json(data); + } catch (error) { + next(error); + } + })(); + }); -router.post("/", async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const { projectId, organisationId } = req.body as { - projectId: number; - organisationId: number; - }; + router.post("/", (req: Request, res: Response, next: NextFunction): void => { + void (async () => { + try { + const { projectId, organisationId } = req.body as { + projectId: number; + organisationId: number; + }; - if (!projectId || !organisationId) { - res.status(400).json({ message: "projectId and organisationId are required" }); - return; - } + if (!projectId || !organisationId) { + res.status(400).json({ + message: "projectId and organisationId are required", + }); + return; + } - const existing = await prisma.projectOrganisation.findUnique({ - where: { - projectId_organisationId: { - projectId, - organisationId, - }, - }, - }); + const existing = await prisma.projectOrganisation.findUnique({ + where: { + projectId_organisationId: { + projectId, + organisationId, + }, + }, + }); - if (existing) { - res.status(409).json({ message: "Relation already exists" }); - return; - } + if (existing) { + res.status(409).json({ + message: "Relation already exists", + }); + return; + } - const result = await prisma.projectOrganisation.create({ - data: { - projectId, - organisationId, - }, - }); + const result = await prisma.projectOrganisation.create({ + data: { + projectId, + organisationId, + }, + }); - res.status(201).json(result); - } catch (error) { - next(error); - } -}); + res.status(201).json(result); + } catch (error) { + next(error); + } + })(); + }); -router.delete("/:projectId/:organisationId", async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const { projectId, organisationId } = req.params as { - projectId: string; - organisationId: string; - }; + router.delete( + "/:projectId/:organisationId", + (req: Request, res: Response, next: NextFunction): void => { + void (async () => { + try { + const { projectId, organisationId } = req.params as { + projectId: string; + organisationId: string; + }; - const result = await prisma.projectOrganisation.delete({ - where: { - projectId_organisationId: { - projectId: Number(projectId), - organisationId: Number(organisationId), - }, - }, - }); + const result = await prisma.projectOrganisation.delete({ + where: { + projectId_organisationId: { + projectId: Number(projectId), + organisationId: Number(organisationId), + }, + }, + }); - res.json(result); - } catch (error) { - next(error); - } -}); + res.json(result); + } catch (error) { + next(error); + } + })(); + } + ); -export default router; \ No newline at end of file + export default router; \ No newline at end of file From f85b694a900746d7f47fcc9265a3784565224d79 Mon Sep 17 00:00:00 2001 From: LabibaVIC Date: Sat, 16 May 2026 10:08:59 +1000 Subject: [PATCH 6/6] fix code formatting --- src/routes/index.ts | 2 +- src/routes/projectOrganisation.ts | 148 +++++++++++++++--------------- 2 files changed, 75 insertions(+), 75 deletions(-) diff --git a/src/routes/index.ts b/src/routes/index.ts index 9cb2733..e10c604 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -32,4 +32,4 @@ router.use("/tree-scans", treeScansRoutes); router.use("/project-organisations", projectOrganisationsRoutes); -export default router; \ No newline at end of file +export default router; diff --git a/src/routes/projectOrganisation.ts b/src/routes/projectOrganisation.ts index c33db08..2741dd6 100644 --- a/src/routes/projectOrganisation.ts +++ b/src/routes/projectOrganisation.ts @@ -1,95 +1,95 @@ - import { Router, Request, Response, NextFunction } from "express"; - import { prisma } from "../lib/prisma"; +import { Router, Request, Response, NextFunction } from "express"; +import { prisma } from "../lib/prisma"; - const router = Router(); +const router = Router(); - router.get("/", (req: Request, res: Response, next: NextFunction): void => { - void (async () => { - try { - const data = await prisma.projectOrganisation.findMany({ - include: { - organisation: true, - project: true, - }, +router.get("/", (req: Request, res: Response, next: NextFunction): void => { + void (async () => { + try { + const data = await prisma.projectOrganisation.findMany({ + include: { + organisation: true, + project: true, + }, + }); + + res.json(data); + } catch (error) { + next(error); + } + })(); +}); + +router.post("/", (req: Request, res: Response, next: NextFunction): void => { + void (async () => { + try { + const { projectId, organisationId } = req.body as { + projectId: number; + organisationId: number; + }; + + if (!projectId || !organisationId) { + res.status(400).json({ + message: "projectId and organisationId are required", }); + return; + } - res.json(data); - } catch (error) { - next(error); + const existing = await prisma.projectOrganisation.findUnique({ + where: { + projectId_organisationId: { + projectId, + organisationId, + }, + }, + }); + + if (existing) { + res.status(409).json({ + message: "Relation already exists", + }); + return; } - })(); - }); - router.post("/", (req: Request, res: Response, next: NextFunction): void => { + const result = await prisma.projectOrganisation.create({ + data: { + projectId, + organisationId, + }, + }); + + res.status(201).json(result); + } catch (error) { + next(error); + } + })(); +}); + +router.delete( + "/:projectId/:organisationId", + (req: Request, res: Response, next: NextFunction): void => { void (async () => { try { - const { projectId, organisationId } = req.body as { - projectId: number; - organisationId: number; + const { projectId, organisationId } = req.params as { + projectId: string; + organisationId: string; }; - if (!projectId || !organisationId) { - res.status(400).json({ - message: "projectId and organisationId are required", - }); - return; - } - - const existing = await prisma.projectOrganisation.findUnique({ + const result = await prisma.projectOrganisation.delete({ where: { projectId_organisationId: { - projectId, - organisationId, + projectId: Number(projectId), + organisationId: Number(organisationId), }, }, }); - if (existing) { - res.status(409).json({ - message: "Relation already exists", - }); - return; - } - - const result = await prisma.projectOrganisation.create({ - data: { - projectId, - organisationId, - }, - }); - - res.status(201).json(result); + res.json(result); } catch (error) { next(error); } })(); - }); - - router.delete( - "/:projectId/:organisationId", - (req: Request, res: Response, next: NextFunction): void => { - void (async () => { - try { - const { projectId, organisationId } = req.params as { - projectId: string; - organisationId: string; - }; - - const result = await prisma.projectOrganisation.delete({ - where: { - projectId_organisationId: { - projectId: Number(projectId), - organisationId: Number(organisationId), - }, - }, - }); - - res.json(result); - } catch (error) { - next(error); - } - })(); - } - ); + }, +); - export default router; \ No newline at end of file +export default router;