From 33e52da6907dae31c71ae40896d92be30f349c0f Mon Sep 17 00:00:00 2001 From: Kristoffer Fredrik Dahle Melen Date: Wed, 14 Oct 2026 18:42:18 +0200 Subject: [PATCH 1/6] Added a teams/active/overview/ endpoint that includes department and memberCount. --- src/db-access/teams.ts | 38 +++++++++++++++++++++++++++++++++++++- src/routers/teams.ts | 41 +++++++++++++++++++++++++++++++++++++++++ src/security.ts | 11 ++++++++--- 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/src/db-access/teams.ts b/src/db-access/teams.ts index 3ce46a8..a8c5e69 100644 --- a/src/db-access/teams.ts +++ b/src/db-access/teams.ts @@ -2,9 +2,19 @@ import { database } from "@/db/setup/query-postgres"; import { teamsTable } from "@/db/tables/teams"; import { type OrmResult, ormError } from "@/src/error/orm-error"; import type { Team, TeamKey } from "@/src/response-handling/teams"; -import { eq, inArray } from "drizzle-orm"; +import { count, eq, inArray } from "drizzle-orm"; import type { QueryParameters } from "../request-handling/common"; import { newDatabaseTransaction } from "./common"; +import { departmentsTable } from "@/db/tables/departments"; +import { teamUsersTable } from "@/db/tables/users"; + + + +type TeamOverview = { + team: Team; + department: string; + memberCount: number; +}; export async function selectTeamsById( teamIds: TeamKey[], @@ -35,6 +45,7 @@ export const selectActiveTeams = async ( return teams; }); }; + export const selectInActiveTeams = async ( parameters: QueryParameters, ): Promise> => { @@ -49,3 +60,28 @@ export const selectInActiveTeams = async ( return teams; }); }; + +export const selectActiveTeamsOverview = async ( + parameters: QueryParameters, +): Promise> => { + return await newDatabaseTransaction(database, async (tx) => { + const teams = await tx + .select({ + team: teamsTable, + department: departmentsTable.city, + memberCount: count(teamUsersTable.id), + }) + .from(teamsTable) + .innerJoin( + departmentsTable, + eq(teamsTable.departmentId, departmentsTable.id), + ) + .leftJoin(teamUsersTable, eq(teamUsersTable.teamId, teamsTable.id)) + .where(eq(teamsTable.active, true)) + .groupBy(teamsTable.id, departmentsTable.id) + .limit(parameters.limit) + .offset(parameters.offset); + + return teams; + }); +}; diff --git a/src/routers/teams.ts b/src/routers/teams.ts index 6f3127c..d4e551d 100644 --- a/src/routers/teams.ts +++ b/src/routers/teams.ts @@ -1,6 +1,7 @@ import { selectActiveTeams, selectInActiveTeams, + selectActiveTeamsOverview, selectTeamsById, } from "@/src/db-access/teams"; import { clientError } from "@/src/error/http-errors"; @@ -88,6 +89,46 @@ teamsRouter.get("/inactive", async (req, res, next) => { res.json(results.data); }); + +/** + * @openapi + * /teams/active/overview/: + * get: + * tags: [teams] + * summary: Get all active teams with department name and number of members + * description: Return all active teams with extra info + * parameters: + * - $ref: "#/components/parameters/offset" + * - $ref: "#/components/parameters/limit" + * responses: + * 200: + * description: Successfull response + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/teams" + */ +teamsRouter.get("/active/overview", async (req, res, next) => { + const queryParametersResult = toListQueryParser.safeParse(req.query); + if (!queryParametersResult.success) { + return next( + clientError(400, "Invalid request format", queryParametersResult.error), + ); + } + + const results = await selectActiveTeamsOverview(queryParametersResult.data); + if (!results.success) { + return next( + clientError( + 400, + "Failed to retrieve data from the database", + results.error, + ), + ); + } + res.json(results.data); +}); + /** * @openapi * /teams/{teamId}/: diff --git a/src/security.ts b/src/security.ts index 65cfb67..98196f4 100644 --- a/src/security.ts +++ b/src/security.ts @@ -8,11 +8,10 @@ import type { RequestHandler } from "express"; UPDATE THIS COMMENT IF YOU UPDATE THE SECURITY HEADERS */ const SECURITY_HEADERS = { - // "Content-Security-Policy": "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests", "Cross-Origin-Opener-Policy": "same-origin", - "Cross-Origin-Resource-Policy": "same-origin", + "Cross-Origin-Resource-Policy": "cross-origin", "Origin-Agent-Cluster": "?1", "Referrer-Policy": "no-referrer", "Strict-Transport-Security": "max-age=15552000; includeSubDomains", @@ -30,7 +29,13 @@ export const customHelmetSecurity: RequestHandler = (_req, res, next) => { }; const corsOptions: CorsOptions = { - // origin: 'http://example.com', + origin: [ + "http://localhost:5173", + "https://vektorprogrammet.no", + "https://www.vektorprogrammet.no", + ], + methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization"], optionsSuccessStatus: 200, }; From 4eb10573c138429ecbbebfe84ede0e7568066611 Mon Sep 17 00:00:00 2001 From: Kristoffer Fredrik Dahle Melen Date: Wed, 14 Oct 2026 20:54:21 +0200 Subject: [PATCH 2/6] Fixed the biome lint issues. --- src/db-access/teams.ts | 40 +++++++++++++++++++--------------------- src/routers/teams.ts | 3 +-- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/db-access/teams.ts b/src/db-access/teams.ts index a8c5e69..e71c107 100644 --- a/src/db-access/teams.ts +++ b/src/db-access/teams.ts @@ -1,19 +1,17 @@ import { database } from "@/db/setup/query-postgres"; +import { departmentsTable } from "@/db/tables/departments"; import { teamsTable } from "@/db/tables/teams"; +import { teamUsersTable } from "@/db/tables/users"; import { type OrmResult, ormError } from "@/src/error/orm-error"; import type { Team, TeamKey } from "@/src/response-handling/teams"; import { count, eq, inArray } from "drizzle-orm"; import type { QueryParameters } from "../request-handling/common"; import { newDatabaseTransaction } from "./common"; -import { departmentsTable } from "@/db/tables/departments"; -import { teamUsersTable } from "@/db/tables/users"; - - type TeamOverview = { - team: Team; - department: string; - memberCount: number; + team: Team; + department: string; + memberCount: number; }; export async function selectTeamsById( @@ -67,20 +65,20 @@ export const selectActiveTeamsOverview = async ( return await newDatabaseTransaction(database, async (tx) => { const teams = await tx .select({ - team: teamsTable, - department: departmentsTable.city, - memberCount: count(teamUsersTable.id), - }) - .from(teamsTable) - .innerJoin( - departmentsTable, - eq(teamsTable.departmentId, departmentsTable.id), - ) - .leftJoin(teamUsersTable, eq(teamUsersTable.teamId, teamsTable.id)) - .where(eq(teamsTable.active, true)) - .groupBy(teamsTable.id, departmentsTable.id) - .limit(parameters.limit) - .offset(parameters.offset); + team: teamsTable, + department: departmentsTable.city, + memberCount: count(teamUsersTable.id), + }) + .from(teamsTable) + .innerJoin( + departmentsTable, + eq(teamsTable.departmentId, departmentsTable.id), + ) + .leftJoin(teamUsersTable, eq(teamUsersTable.teamId, teamsTable.id)) + .where(eq(teamsTable.active, true)) + .groupBy(teamsTable.id, departmentsTable.id) + .limit(parameters.limit) + .offset(parameters.offset); return teams; }); diff --git a/src/routers/teams.ts b/src/routers/teams.ts index d4e551d..45804b2 100644 --- a/src/routers/teams.ts +++ b/src/routers/teams.ts @@ -1,7 +1,7 @@ import { selectActiveTeams, - selectInActiveTeams, selectActiveTeamsOverview, + selectInActiveTeams, selectTeamsById, } from "@/src/db-access/teams"; import { clientError } from "@/src/error/http-errors"; @@ -89,7 +89,6 @@ teamsRouter.get("/inactive", async (req, res, next) => { res.json(results.data); }); - /** * @openapi * /teams/active/overview/: From 57131cc1e2b1f5fccd3744be3ffaeef684240191 Mon Sep 17 00:00:00 2001 From: Kristoffer Fredrik Dahle Melen Date: Wed, 14 Oct 2026 23:19:46 +0200 Subject: [PATCH 3/6] Removed the limit and order from the overview endpoint. --- src/db-access/teams.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/db-access/teams.ts b/src/db-access/teams.ts index e71c107..c980669 100644 --- a/src/db-access/teams.ts +++ b/src/db-access/teams.ts @@ -59,9 +59,7 @@ export const selectInActiveTeams = async ( }); }; -export const selectActiveTeamsOverview = async ( - parameters: QueryParameters, -): Promise> => { +export const selectActiveTeamsOverview = async (): Promise> => { return await newDatabaseTransaction(database, async (tx) => { const teams = await tx .select({ @@ -76,9 +74,7 @@ export const selectActiveTeamsOverview = async ( ) .leftJoin(teamUsersTable, eq(teamUsersTable.teamId, teamsTable.id)) .where(eq(teamsTable.active, true)) - .groupBy(teamsTable.id, departmentsTable.id) - .limit(parameters.limit) - .offset(parameters.offset); + .groupBy(teamsTable.id, departmentsTable.id); return teams; }); From b5ce81d58b6f2cf8077d84018f21d7aea70a3fd6 Mon Sep 17 00:00:00 2001 From: Kristoffer Fredrik Dahle Melen Date: Wed, 14 Oct 2026 23:28:27 +0200 Subject: [PATCH 4/6] Fixed biome lint issues. --- src/db-access/teams.ts | 3 ++- src/routers/teams.ts | 11 ++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/db-access/teams.ts b/src/db-access/teams.ts index c980669..d6eb727 100644 --- a/src/db-access/teams.ts +++ b/src/db-access/teams.ts @@ -59,7 +59,8 @@ export const selectInActiveTeams = async ( }); }; -export const selectActiveTeamsOverview = async (): Promise> => { +export const selectActiveTeamsOverview = async (): Promise< + OrmResult> => { return await newDatabaseTransaction(database, async (tx) => { const teams = await tx .select({ diff --git a/src/routers/teams.ts b/src/routers/teams.ts index 45804b2..b1274f0 100644 --- a/src/routers/teams.ts +++ b/src/routers/teams.ts @@ -107,15 +107,8 @@ teamsRouter.get("/inactive", async (req, res, next) => { * schema: * $ref: "#/components/schemas/teams" */ -teamsRouter.get("/active/overview", async (req, res, next) => { - const queryParametersResult = toListQueryParser.safeParse(req.query); - if (!queryParametersResult.success) { - return next( - clientError(400, "Invalid request format", queryParametersResult.error), - ); - } - - const results = await selectActiveTeamsOverview(queryParametersResult.data); +teamsRouter.get("/active/overview", async (_req, res, next) => { + const results = await selectActiveTeamsOverview(); if (!results.success) { return next( clientError( From 7b51110703800b13597c857cdd40d5a7cd9cf0a1 Mon Sep 17 00:00:00 2001 From: Kristoffer Fredrik Dahle Melen Date: Wed, 14 Oct 2026 23:31:26 +0200 Subject: [PATCH 5/6] Made the order of overviews to be ordered by ids so it follows the db. --- src/db-access/teams.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/db-access/teams.ts b/src/db-access/teams.ts index d6eb727..d8a5b5c 100644 --- a/src/db-access/teams.ts +++ b/src/db-access/teams.ts @@ -60,7 +60,8 @@ export const selectInActiveTeams = async ( }; export const selectActiveTeamsOverview = async (): Promise< - OrmResult> => { + OrmResult +> => { return await newDatabaseTransaction(database, async (tx) => { const teams = await tx .select({ @@ -75,7 +76,8 @@ export const selectActiveTeamsOverview = async (): Promise< ) .leftJoin(teamUsersTable, eq(teamUsersTable.teamId, teamsTable.id)) .where(eq(teamsTable.active, true)) - .groupBy(teamsTable.id, departmentsTable.id); + .groupBy(teamsTable.id, departmentsTable.id) + .orderBy(teamsTable.id); return teams; }); From e035be27b71ee2c493a19dd40bff40d8a480fdd2 Mon Sep 17 00:00:00 2001 From: Kristoffer Fredrik Dahle Melen Date: Thu, 15 Oct 2026 01:47:44 +0200 Subject: [PATCH 6/6] Changed the name from department to departmentCity in TeamOverview. --- src/db-access/teams.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/db-access/teams.ts b/src/db-access/teams.ts index d8a5b5c..a735f82 100644 --- a/src/db-access/teams.ts +++ b/src/db-access/teams.ts @@ -10,7 +10,7 @@ import { newDatabaseTransaction } from "./common"; type TeamOverview = { team: Team; - department: string; + departmentCity: string; memberCount: number; }; @@ -66,7 +66,7 @@ export const selectActiveTeamsOverview = async (): Promise< const teams = await tx .select({ team: teamsTable, - department: departmentsTable.city, + departmentCity: departmentsTable.city, memberCount: count(teamUsersTable.id), }) .from(teamsTable)