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/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/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/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/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..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,4 +14,4 @@ enum UserRole { MANAGER ADMIN DEVELOPER -} +} \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index a040af7..4c2daab 100644 --- a/src/app.ts +++ b/src/app.ts @@ -38,9 +38,18 @@ app.use(express.urlencoded({ extended: true })); // Swagger docs app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec)); -// Module Routes +// Routes (central router) app.use("/", routes); +// Health check (keep this from your version) +app.get("/health", (_req, res) => { + res.json({ + success: true, + status: "ok", + timestamp: new Date().toISOString(), + }); +}); + // 404 & error handler app.use(notFound); app.use(errorHandler); diff --git a/src/routes/index.ts b/src/routes/index.ts index 3ad61b6..e10c604 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,4 +1,5 @@ import { Router } from "express"; + import authRoutes from "../modules/auth/auth.routes"; import { healthRoutes } from "../modules/health"; import { projectTreeTypesRoutes } from "../modules/project-tree-types"; @@ -9,11 +10,14 @@ import { localizationRoutes } from "../modules/localization"; import { adoptersRouter } from "../modules/adopters"; import { userProjectAssignmentRoutes } from "../modules/user-project-assignment"; import { partnersRoutes } from "../modules/partners"; - import treeScansRoutes from "../modules/tree-scans"; +// FEATURE +import projectOrganisationsRoutes from "./projectOrganisation"; + const router = Router(); +// system routes router.use("/health", healthRoutes); router.use("/auth", authRoutes); router.use("/adopters", adoptersRouter); @@ -24,7 +28,8 @@ router.use("/localized-strings", localizationRoutes); router.use("/user-projects", userProjectAssignmentRoutes); router.use("/project-tree-types", projectTreeTypesRoutes); router.use("/partners", partnersRoutes); - router.use("/tree-scans", treeScansRoutes); +router.use("/project-organisations", projectOrganisationsRoutes); + export default router; diff --git a/src/routes/projectOrganisation.ts b/src/routes/projectOrganisation.ts new file mode 100644 index 0000000..2741dd6 --- /dev/null +++ b/src/routes/projectOrganisation.ts @@ -0,0 +1,95 @@ +import { Router, Request, Response, NextFunction } from "express"; +import { prisma } from "../lib/prisma"; + +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, + }, + }); + + 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; + } + + const existing = await prisma.projectOrganisation.findUnique({ + where: { + projectId_organisationId: { + projectId, + 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); + } 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;