From 093632034fb5dce10743d64718f5eae74375fe9c Mon Sep 17 00:00:00 2001 From: solvhold Date: Mon, 3 Mar 2025 17:44:09 +0100 Subject: [PATCH 01/17] feat: :art: structure the application tables to be a superclass with two subclasses and update imports form this file a super class for applications with subclasses assistant application and team application vf-247 --- db/tables/applications.ts | 77 +++++++++++++++++++++++ db/tables/fieldsOfStudy.ts | 2 +- db/tables/team.ts | 2 +- db/tables/teamApplication.ts | 37 ----------- src/db-access/team_applications.ts | 2 +- src/request-handling/team_application.ts | 2 +- src/response-handling/team_application.ts | 2 +- 7 files changed, 82 insertions(+), 42 deletions(-) create mode 100644 db/tables/applications.ts delete mode 100644 db/tables/teamApplication.ts diff --git a/db/tables/applications.ts b/db/tables/applications.ts new file mode 100644 index 0000000..a5dfd15 --- /dev/null +++ b/db/tables/applications.ts @@ -0,0 +1,77 @@ +import mainSchema from "@db/tables/schema"; +import { relations } from "drizzle-orm"; +import { date, integer, serial, text } from "drizzle-orm/pg-core"; + +import { teamsTable } from "@db/tables/team"; +import { fieldsOfStudyTable } from "./fieldsOfStudy"; + +export const genders = mainSchema.enum("gender", [ + "Female", + "Male", + "Other", +]); + +export const applicationsTable = mainSchema.table("applications", { + id: serial("id").primaryKey(), + firstname: text("name").notNull(), + lastname: text("name").notNull(), + gender: genders("gender").notNull(), + email: text("email").notNull(), + fieldOfStudyId: integer("fieldOfStudyId") + .notNull() + .references(() => fieldsOfStudyTable.id), + yearOfStudy: integer("yearOfStudy").notNull(), + phonenumber: text("phonenumber").notNull(), + submitDate: date("submitDate", { mode: "date" }).defaultNow().notNull(), +}); + +export const applicationsRelations = relations( + applicationsTable, + ({ one }) => ({ + fieldOfStudy: one(fieldsOfStudyTable, { + fields: [applicationsTable.fieldOfStudyId], + references: [fieldsOfStudyTable.id], + }), + }), +); + +export const teamApplicationsTable = mainSchema.table("teamApplications", { + id: integer("id") + .primaryKey() + .references(() => applicationsTable.id), + teamId: integer("teamId") + .notNull() + .references(() => teamsTable.id), + motivationText: text("motivationText").notNull(), + biography: text("biography").notNull(), +}) + +export const teamApplicationsRelations = relations( + teamApplicationsTable, ({ one }) => ({ + superApplication: one( applicationsTable,{ + fields: [teamApplicationsTable.id], + references: [applicationsTable.id] + }), + team: one(teamsTable, { + fields: [teamApplicationsTable.teamId], + references: [teamsTable.id], + }), + }) +) + +export const assistantApplicationsTable = mainSchema.table("assistantApplications", { + id: integer("id") + .primaryKey() + .references(() => applicationsTable.id) +}) + +export const assistantApplicationsRelations = relations( + assistantApplicationsTable, ({ one }) => ({ + superApplication: one( applicationsTable,{ + fields: [assistantApplicationsTable.id], + references: [applicationsTable.id] + }) + }) +) + + diff --git a/db/tables/fieldsOfStudy.ts b/db/tables/fieldsOfStudy.ts index c79b7f9..7b4e546 100644 --- a/db/tables/fieldsOfStudy.ts +++ b/db/tables/fieldsOfStudy.ts @@ -2,7 +2,7 @@ import { departmentsTable } from "@db/tables/departments"; import mainSchema from "@db/tables/schema"; import { relations } from "drizzle-orm"; import { integer, serial, text } from "drizzle-orm/pg-core"; -import { teamApplicationsTable } from "./teamApplication"; +import { teamApplicationsTable } from "./applications"; export const fieldsOfStudyTable = mainSchema.table("fieldsOfStudy", { id: serial("id").primaryKey(), diff --git a/db/tables/team.ts b/db/tables/team.ts index 879e7da..6424ff8 100644 --- a/db/tables/team.ts +++ b/db/tables/team.ts @@ -1,6 +1,6 @@ import { departmentsTable } from "@db/tables/departments"; import mainSchema from "@db/tables/schema"; -import { teamApplicationsTable } from "@db/tables/teamApplication"; +import { teamApplicationsTable } from "@db/tables/applications"; import { relations } from "drizzle-orm"; import { boolean, date, serial, text } from "drizzle-orm/pg-core"; import { integer } from "drizzle-orm/pg-core"; diff --git a/db/tables/teamApplication.ts b/db/tables/teamApplication.ts deleted file mode 100644 index 02cd44b..0000000 --- a/db/tables/teamApplication.ts +++ /dev/null @@ -1,37 +0,0 @@ -import mainSchema from "@db/tables/schema"; -import { relations } from "drizzle-orm"; -import { date, integer, serial, text } from "drizzle-orm/pg-core"; - -import { teamsTable } from "@db/tables/team"; -import { fieldsOfStudyTable } from "./fieldsOfStudy"; - -export const teamApplicationsTable = mainSchema.table("teamApplications", { - id: serial("id").primaryKey(), - teamId: integer("teamId") - .notNull() - .references(() => teamsTable.id), - name: text("name").notNull(), - email: text("email").notNull(), - motivationText: text("motivationText").notNull(), - fieldOfStudyId: integer("fieldOfStudyId") - .notNull() - .references(() => fieldsOfStudyTable.id), - yearOfStudy: integer("yearOfStudy").notNull(), - biography: text("biography").notNull(), - phonenumber: text("phonenumber").notNull(), - submitDate: date("submitDate", { mode: "date" }).defaultNow().notNull(), -}); - -export const teamApplicationsRelations = relations( - teamApplicationsTable, - ({ one }) => ({ - team: one(teamsTable, { - fields: [teamApplicationsTable.teamId], - references: [teamsTable.id], - }), - fieldOfStudy: one(fieldsOfStudyTable, { - fields: [teamApplicationsTable.fieldOfStudyId], - references: [fieldsOfStudyTable.id], - }), - }), -); diff --git a/src/db-access/team_applications.ts b/src/db-access/team_applications.ts index 8a167de..2d79d8f 100644 --- a/src/db-access/team_applications.ts +++ b/src/db-access/team_applications.ts @@ -1,5 +1,5 @@ import { database } from "@db/setup/queryPostgres"; -import { teamApplicationsTable } from "@db/tables/teamApplication"; +import { teamApplicationsTable } from "@db/tables/applications"; import { type ORMResult, handleDatabaseFullfillment, diff --git a/src/request-handling/team_application.ts b/src/request-handling/team_application.ts index fa5497d..0c15d86 100644 --- a/src/request-handling/team_application.ts +++ b/src/request-handling/team_application.ts @@ -1,4 +1,4 @@ -import { teamApplicationsTable } from "@db/tables/teamApplication"; +import { teamApplicationsTable } from "@db/tables/applications"; import { maxTextLength } from "@lib/globalVariables"; import { createInsertSchema } from "drizzle-zod"; import { z } from "zod"; diff --git a/src/response-handling/team_application.ts b/src/response-handling/team_application.ts index 38b653a..c8bb41b 100644 --- a/src/response-handling/team_application.ts +++ b/src/response-handling/team_application.ts @@ -1,7 +1,7 @@ import { createSelectSchema } from "drizzle-zod"; import type { z } from "zod"; -import { teamApplicationsTable } from "@db/tables/teamApplication"; +import { teamApplicationsTable } from "@db/tables/applications"; export const teamApplicationSelectSchema = createSelectSchema( teamApplicationsTable, From e23b10c4c5d25410be89b8672381f9cb5a78c175 Mon Sep 17 00:00:00 2001 From: solvhold Date: Mon, 10 Mar 2025 17:41:23 +0100 Subject: [PATCH 02/17] style: change filename vf-247 --- src/db-access/{team_applications.ts => applications.ts} | 4 ++-- src/main.ts | 2 +- src/openapi/config.ts | 4 ++-- src/request-handling/{team_application.ts => applications.ts} | 0 .../{team_application.ts => applications.ts} | 0 src/routers/{team_application.ts => applications.ts} | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) rename src/db-access/{team_applications.ts => applications.ts} (92%) rename src/request-handling/{team_application.ts => applications.ts} (100%) rename src/response-handling/{team_application.ts => applications.ts} (100%) rename src/routers/{team_application.ts => applications.ts} (98%) diff --git a/src/db-access/team_applications.ts b/src/db-access/applications.ts similarity index 92% rename from src/db-access/team_applications.ts rename to src/db-access/applications.ts index 2d79d8f..80e07cd 100644 --- a/src/db-access/team_applications.ts +++ b/src/db-access/applications.ts @@ -6,11 +6,11 @@ import { handleDatabaseRejection, } from "@src/error/ormError"; import type { QueryParameters } from "@src/request-handling/common"; -import type { NewTeamApplication } from "@src/request-handling/team_application"; +import type { NewTeamApplication } from "@src/request-handling/applications"; import type { TeamApplication, TeamKey, -} from "@src/response-handling/team_application"; +} from "@src/response-handling/applications"; import { asc, inArray } from "drizzle-orm"; export const selectTeamApplications = async ( diff --git a/src/main.ts b/src/main.ts index 659af6e..2a16ff9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,7 +11,7 @@ import { logger } from "@src/middleware/loggingMiddleware"; import { customCors, customHelmetSecurity } from "@src/security"; import { expenseRouter, expensesRouter } from "./routers/expenses"; -import { teamApplicationRouter } from "./routers/team_application"; +import { teamApplicationRouter } from "./routers/applications"; import { openapiSpecification } from "@src/openapi/config"; import openapiExpressHandler from "swagger-ui-express"; diff --git a/src/openapi/config.ts b/src/openapi/config.ts index f3ed343..78d5e9b 100644 --- a/src/openapi/config.ts +++ b/src/openapi/config.ts @@ -9,14 +9,14 @@ import { sortParser, } from "@src/request-handling/common"; import { expenseRequestParser } from "@src/request-handling/expenses"; -import { teamApplicationParser } from "@src/request-handling/team_application"; +import { teamApplicationParser } from "@src/request-handling/applications"; import { assistantUserRequestParser, teamUserRequestParser, userRequestParser, } from "@src/request-handling/users"; import { expensesSelectSchema } from "@src/response-handling/expenses"; -import { teamApplicationSelectSchema } from "@src/response-handling/team_application"; +import { teamApplicationSelectSchema } from "@src/response-handling/applications"; import { assistantUserSelectSchema, teamUserSelectSchema, diff --git a/src/request-handling/team_application.ts b/src/request-handling/applications.ts similarity index 100% rename from src/request-handling/team_application.ts rename to src/request-handling/applications.ts diff --git a/src/response-handling/team_application.ts b/src/response-handling/applications.ts similarity index 100% rename from src/response-handling/team_application.ts rename to src/response-handling/applications.ts diff --git a/src/routers/team_application.ts b/src/routers/applications.ts similarity index 98% rename from src/routers/team_application.ts rename to src/routers/applications.ts index 79161db..9afc6b6 100644 --- a/src/routers/team_application.ts +++ b/src/routers/applications.ts @@ -2,10 +2,10 @@ import { insertTeamApplication, selectTeamApplications, selectTeamApplicationsByTeamId, -} from "@src/db-access/team_applications"; +} from "@src/db-access/applications"; import { clientError } from "@src/error/httpErrors"; import { listQueryParser, serialIdParser } from "@src/request-handling/common"; -import { teamApplicationToInsertParser } from "@src/request-handling/team_application"; +import { teamApplicationToInsertParser } from "@src/request-handling/applications"; import { Router, json } from "express"; export const teamApplicationRouter = Router(); From 34816104c04f335438d13a91eb7d78f77fce53d3 Mon Sep 17 00:00:00 2001 From: solvhold Date: Sun, 16 Mar 2025 12:55:26 +0100 Subject: [PATCH 03/17] fix: fix wrong name vf-247 --- db/tables/applications.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db/tables/applications.ts b/db/tables/applications.ts index a5dfd15..5ffd567 100644 --- a/db/tables/applications.ts +++ b/db/tables/applications.ts @@ -13,8 +13,8 @@ export const genders = mainSchema.enum("gender", [ export const applicationsTable = mainSchema.table("applications", { id: serial("id").primaryKey(), - firstname: text("name").notNull(), - lastname: text("name").notNull(), + firstName: text("firstname").notNull(), + lastName: text("lastname").notNull(), gender: genders("gender").notNull(), email: text("email").notNull(), fieldOfStudyId: integer("fieldOfStudyId") From 72de2411b818e9cd00e6bbc08770da4ddd30013d Mon Sep 17 00:00:00 2001 From: solvhold Date: Sun, 16 Mar 2025 12:56:44 +0100 Subject: [PATCH 04/17] feat: :sparkles: update db access request for new table structure vf-247 --- src/db-access/applications.ts | 97 +++++++++++++++++++++++---- src/response-handling/applications.ts | 14 ++-- 2 files changed, 94 insertions(+), 17 deletions(-) diff --git a/src/db-access/applications.ts b/src/db-access/applications.ts index 80e07cd..d21e8f5 100644 --- a/src/db-access/applications.ts +++ b/src/db-access/applications.ts @@ -1,33 +1,52 @@ import { database } from "@db/setup/queryPostgres"; -import { teamApplicationsTable } from "@db/tables/applications"; +import { applicationsTable, teamApplicationsTable } from "@db/tables/applications"; import { type ORMResult, handleDatabaseFullfillment, handleDatabaseRejection, + ormError, } from "@src/error/ormError"; import type { QueryParameters } from "@src/request-handling/common"; import type { NewTeamApplication } from "@src/request-handling/applications"; import type { + ApplicationKey, TeamApplication, TeamKey, } from "@src/response-handling/applications"; -import { asc, inArray } from "drizzle-orm"; +import { eq, inArray } from "drizzle-orm"; export const selectTeamApplications = async ( parameters: QueryParameters, ): Promise> => { - return database + return await database .transaction(async (tx) => { - return await tx - .select() + const teamApplications = await tx + .select({ + id: applicationsTable.id, + teamId: teamApplicationsTable.teamId, + firstName: applicationsTable.firstName, + lastName: applicationsTable.lastName, + gender: applicationsTable.gender, + email: applicationsTable.email, + fieldOfStudyId: applicationsTable.fieldOfStudyId, + yearOfStudy: applicationsTable.yearOfStudy, + phonenumber: applicationsTable.phonenumber, + motivationText: teamApplicationsTable.motivationText, + biography: teamApplicationsTable.biography, + submitDate: applicationsTable.submitDate + }) .from(teamApplicationsTable) - .orderBy(asc(teamApplicationsTable.id)) + .innerJoin(applicationsTable, eq(teamApplicationsTable.id, applicationsTable.id)) .limit(parameters.limit) .offset(parameters.offset); + + return teamApplications; }) .then(handleDatabaseFullfillment, handleDatabaseRejection); + }; + export const selectTeamApplicationsByTeamId = async ( teamId: TeamKey[], parameters: QueryParameters, @@ -35,11 +54,55 @@ export const selectTeamApplicationsByTeamId = async ( return database .transaction(async (tx) => { const selectResult = await tx - .select() - .from(teamApplicationsTable) - .where(inArray(teamApplicationsTable.teamId, teamId)) - .limit(parameters.limit) - .offset(parameters.offset); + .select({ + id: applicationsTable.id, + teamId: teamApplicationsTable.teamId, + firstName: applicationsTable.firstName, + lastName: applicationsTable.lastName, + gender: applicationsTable.gender, + email: applicationsTable.email, + fieldOfStudyId: applicationsTable.fieldOfStudyId, + yearOfStudy: applicationsTable.yearOfStudy, + phonenumber: applicationsTable.phonenumber, + motivationText: teamApplicationsTable.motivationText, + biography: teamApplicationsTable.biography, + submitDate: applicationsTable.submitDate + }) + .from(teamApplicationsTable) + .where(inArray(teamApplicationsTable.id, teamId)) + .innerJoin(applicationsTable, eq(teamApplicationsTable.id, applicationsTable.id)) + .limit(parameters.limit) + .offset(parameters.offset); + + return selectResult; + }) + .then(handleDatabaseFullfillment, handleDatabaseRejection); +}; + +export const selectTeamApplicationsById = async ( + applicationIds: ApplicationKey[], +): Promise> => { + return database + .transaction(async (tx) => { + const selectResult = await tx + .select({ + id: applicationsTable.id, + teamId: teamApplicationsTable.teamId, + firstName: applicationsTable.firstName, + lastName: applicationsTable.lastName, + gender: applicationsTable.gender, + email: applicationsTable.email, + fieldOfStudyId: applicationsTable.fieldOfStudyId, + yearOfStudy: applicationsTable.yearOfStudy, + phonenumber: applicationsTable.phonenumber, + motivationText: teamApplicationsTable.motivationText, + biography: teamApplicationsTable.biography, + submitDate: applicationsTable.submitDate + }) + .from(teamApplicationsTable) + .where(inArray(teamApplicationsTable.id, applicationIds)) + .innerJoin(applicationsTable, eq(teamApplicationsTable.id, applicationsTable.id)) + return selectResult; }) .then(handleDatabaseFullfillment, handleDatabaseRejection); @@ -50,11 +113,19 @@ export async function insertTeamApplication( ): Promise> { return database .transaction(async (tx) => { - const insertResult = await tx + const newTeamApplication = await tx .insert(teamApplicationsTable) .values(teamApplication) .returning(); - return insertResult; + const newTeamApplicationId = newTeamApplication.map((application) => application.id); + const newTeamApplicationResult = await selectTeamApplicationsById(newTeamApplicationId); + if (!newTeamApplicationResult.success) { + throw ormError( + "Error when inserting team users", + newTeamApplicationResult.error, + ); + } + return newTeamApplicationResult.data; }) .then(handleDatabaseFullfillment, handleDatabaseRejection); } diff --git a/src/response-handling/applications.ts b/src/response-handling/applications.ts index c8bb41b..476f35b 100644 --- a/src/response-handling/applications.ts +++ b/src/response-handling/applications.ts @@ -1,11 +1,17 @@ import { createSelectSchema } from "drizzle-zod"; import type { z } from "zod"; -import { teamApplicationsTable } from "@db/tables/applications"; +import { applicationsTable, teamApplicationsTable } from "@db/tables/applications"; -export const teamApplicationSelectSchema = createSelectSchema( - teamApplicationsTable, -) +export const applicationSelectSchema = createSelectSchema(applicationsTable) + .strict() + .readonly(); + +export type Application = z.infer; +export type ApplicationKey = Application["id"]; + +export const teamApplicationSelectSchema = createSelectSchema(teamApplicationsTable) + .merge(createSelectSchema(applicationsTable)) .strict() .readonly(); From 961bb5946f1249deaadb7cf8f52a1206401f5f1d Mon Sep 17 00:00:00 2001 From: solvhold Date: Sun, 16 Mar 2025 13:15:46 +0100 Subject: [PATCH 05/17] fix: fix imports vf-247 --- db/tables/applications.ts | 6 +++--- src/db-access/applications.ts | 22 +++++++++++----------- src/error/error-messages.ts | 1 + 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/db/tables/applications.ts b/db/tables/applications.ts index 5ffd567..b51acbc 100644 --- a/db/tables/applications.ts +++ b/db/tables/applications.ts @@ -1,9 +1,9 @@ -import mainSchema from "@db/tables/schema"; +import {mainSchema} from "@/db/tables/schema"; import { relations } from "drizzle-orm"; import { date, integer, serial, text } from "drizzle-orm/pg-core"; -import { teamsTable } from "@db/tables/team"; -import { fieldsOfStudyTable } from "./fieldsOfStudy"; +import { teamsTable } from "@/db/tables/teams"; +import { fieldsOfStudyTable } from "./fields-of-study"; export const genders = mainSchema.enum("gender", [ "Female", diff --git a/src/db-access/applications.ts b/src/db-access/applications.ts index d21e8f5..4b0ca1a 100644 --- a/src/db-access/applications.ts +++ b/src/db-access/applications.ts @@ -1,23 +1,23 @@ -import { database } from "@db/setup/queryPostgres"; -import { applicationsTable, teamApplicationsTable } from "@db/tables/applications"; +import { database } from "@/db/setup/query-postgres"; +import { applicationsTable, teamApplicationsTable } from "@/db/tables/applications"; import { - type ORMResult, + type OrmResult, handleDatabaseFullfillment, handleDatabaseRejection, ormError, -} from "@src/error/ormError"; -import type { QueryParameters } from "@src/request-handling/common"; -import type { NewTeamApplication } from "@src/request-handling/applications"; +} from "@/src/error/orm-error"; +import type { QueryParameters } from "@/src/request-handling/common"; +import type { NewTeamApplication } from "@/src/request-handling/applications"; import type { ApplicationKey, TeamApplication, TeamKey, -} from "@src/response-handling/applications"; +} from "@/src/response-handling/applications"; import { eq, inArray } from "drizzle-orm"; export const selectTeamApplications = async ( parameters: QueryParameters, -): Promise> => { +): Promise> => { return await database .transaction(async (tx) => { const teamApplications = await tx @@ -50,7 +50,7 @@ export const selectTeamApplications = async ( export const selectTeamApplicationsByTeamId = async ( teamId: TeamKey[], parameters: QueryParameters, -): Promise> => { +): Promise> => { return database .transaction(async (tx) => { const selectResult = await tx @@ -81,7 +81,7 @@ export const selectTeamApplicationsByTeamId = async ( export const selectTeamApplicationsById = async ( applicationIds: ApplicationKey[], -): Promise> => { +): Promise> => { return database .transaction(async (tx) => { const selectResult = await tx @@ -110,7 +110,7 @@ export const selectTeamApplicationsById = async ( export async function insertTeamApplication( teamApplication: NewTeamApplication[], -): Promise> { +): Promise> { return database .transaction(async (tx) => { const newTeamApplication = await tx diff --git a/src/error/error-messages.ts b/src/error/error-messages.ts index eb09e59..f943f25 100644 --- a/src/error/error-messages.ts +++ b/src/error/error-messages.ts @@ -29,6 +29,7 @@ const ORM_ERROR_MESSAGES = [ "Couln't find all entries", "Wrong database response format", "Failed to insert all entries", + "Error when inserting team users" ] as const; const HTTP_CLIENT_ERROR_MESSAGES = [ "Invalid request format", From fa903011461f118d329e20f27fd2c6d8c52d86d8 Mon Sep 17 00:00:00 2001 From: solvhold Date: Sun, 16 Mar 2025 13:27:26 +0100 Subject: [PATCH 06/17] fix: update import vf-247 --- src/response-handling/applications.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/response-handling/applications.ts b/src/response-handling/applications.ts index 476f35b..42df677 100644 --- a/src/response-handling/applications.ts +++ b/src/response-handling/applications.ts @@ -1,7 +1,7 @@ import { createSelectSchema } from "drizzle-zod"; import type { z } from "zod"; -import { applicationsTable, teamApplicationsTable } from "@db/tables/applications"; +import { applicationsTable, teamApplicationsTable } from "@/db/tables/applications"; export const applicationSelectSchema = createSelectSchema(applicationsTable) .strict() From 65a9134f80a39cf6d82a59edc63b2244498cb5d9 Mon Sep 17 00:00:00 2001 From: solvhold Date: Mon, 31 Mar 2025 16:46:20 +0200 Subject: [PATCH 07/17] fix: fix wrong names and imports vf-247 --- db/tables/fields-of-study.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db/tables/fields-of-study.ts b/db/tables/fields-of-study.ts index d33ca12..d927624 100644 --- a/db/tables/fields-of-study.ts +++ b/db/tables/fields-of-study.ts @@ -2,7 +2,7 @@ import { departmentsTable } from "@/db/tables/departments"; import { mainSchema } from "@/db/tables/schema"; import { relations } from "drizzle-orm"; import { integer, serial, text } from "drizzle-orm/pg-core"; -import { teamApplicationsTable } from "./applications"; +import { applicationsTable, teamApplicationsTable } from "./applications"; export const fieldsOfStudyTable = mainSchema.table("fieldsOfStudy", { id: serial("id").primaryKey(), @@ -20,6 +20,6 @@ export const fieldsOfStudyRelations = relations( fields: [fieldsOfStudyTable.departmentId], references: [departmentsTable.id], }), - teamApplication: many(teamApplicationsTable), + applications: many(applicationsTable), }), ); From 4ad0039e83bcd4942c0797cc8fb15e2c04cc5514 Mon Sep 17 00:00:00 2001 From: solvhold Date: Mon, 31 Mar 2025 16:47:23 +0200 Subject: [PATCH 08/17] feat: update import, update insert db access vf-247 --- src/db-access/applications.ts | 39 ++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/db-access/applications.ts b/src/db-access/applications.ts index 4b0ca1a..d4e9341 100644 --- a/src/db-access/applications.ts +++ b/src/db-access/applications.ts @@ -7,7 +7,7 @@ import { ormError, } from "@/src/error/orm-error"; import type { QueryParameters } from "@/src/request-handling/common"; -import type { NewTeamApplication } from "@/src/request-handling/applications"; +import { teamApplicationToInsertParser, type NewApplication, type NewTeamApplication } from "@/src/request-handling/applications"; import type { ApplicationKey, TeamApplication, @@ -109,23 +109,34 @@ export const selectTeamApplicationsById = async ( }; export async function insertTeamApplication( - teamApplication: NewTeamApplication[], + teamApplication: NewTeamApplication & NewApplication ): Promise> { return database .transaction(async (tx) => { - const newTeamApplication = await tx - .insert(teamApplicationsTable) - .values(teamApplication) + const newApplication = await tx + .insert(applicationsTable) + .values({ + firstName: teamApplication.firstName, + lastName: teamApplication.lastName, + gender: teamApplication.gender, + email: teamApplication.email, + fieldOfStudyId: teamApplication.fieldOfStudyId, + yearOfStudy: teamApplication.yearOfStudy, + phonenumber: teamApplication.phonenumber,}) .returning(); - const newTeamApplicationId = newTeamApplication.map((application) => application.id); - const newTeamApplicationResult = await selectTeamApplicationsById(newTeamApplicationId); - if (!newTeamApplicationResult.success) { - throw ormError( - "Error when inserting team users", - newTeamApplicationResult.error, - ); - } - return newTeamApplicationResult.data; + const newApplicationId = newApplication[0].id; + + const newTeamApplicationResult = await tx + .insert(teamApplicationsTable) + .values({ + id:newApplicationId, + teamId: teamApplication.teamId, + motivationText: teamApplication.motivationText, + biography: teamApplication.biography + }).returning(); + + + return newTeamApplicationResult.data }) .then(handleDatabaseFullfillment, handleDatabaseRejection); } From 89eea8f6d7bf4e3642290804fe55398915662934 Mon Sep 17 00:00:00 2001 From: solvhold Date: Mon, 31 Mar 2025 16:48:18 +0200 Subject: [PATCH 09/17] feat: update request handling to fit new table structure for applications vf-247 --- src/request-handling/applications.ts | 45 ++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/src/request-handling/applications.ts b/src/request-handling/applications.ts index b8b5304..c6dda99 100644 --- a/src/request-handling/applications.ts +++ b/src/request-handling/applications.ts @@ -1,18 +1,14 @@ -import { teamApplicationsTable } from "@/db/tables/applications"; +import { applicationsTable, assistantApplicationsTable, teamApplicationsTable } from "@/db/tables/applications"; import { MAX_TEXT_LENGTH } from "@/lib/global-variables"; import { serialIdParser } from "@/src/request-handling/common"; import { createInsertSchema } from "drizzle-zod"; import { z } from "zod"; -export const teamApplicationParser = z.object({ - teamId: serialIdParser.describe("Id of team applied for"), - name: z.string().min(1).describe("Name of user applying for a team"), +export const applicationParser = z.object({ + firstName: z.string().min(1).describe("First name of user applying for a team"), + lastName: z.string().min(1).describe("Last name of user applying for a team"), email: z.string().email().describe("Email of user applying for a team"), - motivationText: z - .string() - .max(MAX_TEXT_LENGTH) - .describe("The motivation text of user applying for a team"), fieldOfStudyId: serialIdParser.describe( "Studyfield of user applying for a team", ), @@ -24,15 +20,31 @@ export const teamApplicationParser = z.object({ .int() .max(7) .describe("The year of study the user applying for a team is in"), - biography: z - .string() - .max(MAX_TEXT_LENGTH) - .describe("The biography of the user applying for a team"), phonenumber: z .string() .regex(/^\d{8}$/, "Phone number must be 8 digits") .describe("The phonenumber of the user applying for a team"), -}); +}).strict(); + +export const teamApplicationParser = z.object({ + teamId: serialIdParser.describe("Id of team applied for"), + motivationText: z + .string() + .max(MAX_TEXT_LENGTH) + .describe("The motivation text of user applying for a team"), + biography: z + .string() + .max(MAX_TEXT_LENGTH) + .describe("The biography of the user applying for a team"), +}).merge(applicationParser).strict() + +export const assistantApplicationParser = z.object({ + +}).merge(applicationParser).strict() + +export const applicationToInsertParser = applicationParser + .extend({}) + .pipe(createInsertSchema(applicationsTable).strict().readonly()); export const teamApplicationToInsertParser = teamApplicationParser .extend({ @@ -42,4 +54,11 @@ export const teamApplicationToInsertParser = teamApplicationParser }) .pipe(createInsertSchema(teamApplicationsTable).strict().readonly()); +export const assistantApplicationToInsertParser = assistantApplicationParser + .extend({}) + .pipe(createInsertSchema(assistantApplicationsTable).strict().readonly()); + +export type NewApplication = z.infer; export type NewTeamApplication = z.infer; +export type NewAssistantApplication = z.infer; + From d4f212ee198bb42b1b31f4731a06deefa97bf12c Mon Sep 17 00:00:00 2001 From: solvhold Date: Mon, 31 Mar 2025 16:48:45 +0200 Subject: [PATCH 10/17] fix: update variable name vf-247 --- src/routers/applications.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/routers/applications.ts b/src/routers/applications.ts index 96b3321..8b4c2c5 100644 --- a/src/routers/applications.ts +++ b/src/routers/applications.ts @@ -5,7 +5,7 @@ import { } from "@/src/db-access/applications"; import { teamApplicationToInsertParser } from "@/src/request-handling/applications"; import { clientError } from "@/src/error/http-errors"; -import { listQueryParser, serialIdParser } from "@/src/request-handling/common"; +import { serialIdParser, toListQueryParser, toSerialIdParser } from "@/src/request-handling/common"; import { Router, json } from "express"; export const teamApplicationRouter = Router(); @@ -32,7 +32,7 @@ teamApplicationRouter.use(json()); */ teamApplicationRouter.get("/", async (req, res, next) => { - const queryParametersResult = listQueryParser.safeParse(req.query); + const queryParametersResult = toListQueryParser.safeParse(req.query); if (!queryParametersResult.success) { return next( clientError(400, "Invalid request format", queryParametersResult.error), @@ -71,11 +71,11 @@ teamApplicationRouter.get("/", async (req, res, next) => { * $ref: "#/components/schemas/teamApplication" */ teamApplicationRouter.get("/:teamID/", async (req, res, next) => { - const teamIdResult = serialIdParser.safeParse(req.params.teamID); + const teamIdResult = toSerialIdParser.safeParse(req.params.teamID); if (!teamIdResult.success) { return next(clientError(400, "Invalid request format", teamIdResult.error)); } - const queryParametersResult = listQueryParser.safeParse(req.query); + const queryParametersResult = toListQueryParser.safeParse(req.query); if (!queryParametersResult.success) { return next( clientError(400, "Invalid request format", queryParametersResult.error), From 98e117e9fe5d06aa67472f59d8a8e2a3577db631 Mon Sep 17 00:00:00 2001 From: solvhold Date: Mon, 7 Apr 2025 16:45:49 +0200 Subject: [PATCH 11/17] feat: fix router error vf-247 --- src/db-access/applications.ts | 12 +++++++----- src/request-handling/applications.ts | 9 +++++---- src/routers/applications.ts | 8 +++++--- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/db-access/applications.ts b/src/db-access/applications.ts index d4e9341..eba3aeb 100644 --- a/src/db-access/applications.ts +++ b/src/db-access/applications.ts @@ -4,10 +4,9 @@ import { type OrmResult, handleDatabaseFullfillment, handleDatabaseRejection, - ormError, } from "@/src/error/orm-error"; import type { QueryParameters } from "@/src/request-handling/common"; -import { teamApplicationToInsertParser, type NewApplication, type NewTeamApplication } from "@/src/request-handling/applications"; +import { type NewApplication, type NewTeamApplication } from "@/src/request-handling/applications"; import type { ApplicationKey, TeamApplication, @@ -110,7 +109,7 @@ export const selectTeamApplicationsById = async ( export async function insertTeamApplication( teamApplication: NewTeamApplication & NewApplication -): Promise> { +): Promise> { return database .transaction(async (tx) => { const newApplication = await tx @@ -135,8 +134,11 @@ export async function insertTeamApplication( biography: teamApplication.biography }).returning(); - - return newTeamApplicationResult.data + + return { + ...newApplication[0], + ...newTeamApplicationResult[0] + } }) .then(handleDatabaseFullfillment, handleDatabaseRejection); } diff --git a/src/request-handling/applications.ts b/src/request-handling/applications.ts index c6dda99..fd745ca 100644 --- a/src/request-handling/applications.ts +++ b/src/request-handling/applications.ts @@ -9,6 +9,7 @@ export const applicationParser = z.object({ firstName: z.string().min(1).describe("First name of user applying for a team"), lastName: z.string().min(1).describe("Last name of user applying for a team"), email: z.string().email().describe("Email of user applying for a team"), + gender: z.enum(["Female","Male","Other"]).describe("The gender of the user applying for a team"), fieldOfStudyId: serialIdParser.describe( "Studyfield of user applying for a team", ), @@ -47,16 +48,16 @@ export const applicationToInsertParser = applicationParser .pipe(createInsertSchema(applicationsTable).strict().readonly()); export const teamApplicationToInsertParser = teamApplicationParser - .extend({ + .merge(z.object({ email: teamApplicationParser.shape.email.trim().toLowerCase(), motivationText: teamApplicationParser.shape.motivationText.trim(), biography: teamApplicationParser.shape.biography.trim(), - }) - .pipe(createInsertSchema(teamApplicationsTable).strict().readonly()); + })) + .pipe(createInsertSchema(teamApplicationsTable).merge(createInsertSchema(applicationsTable)).strict().readonly()); export const assistantApplicationToInsertParser = assistantApplicationParser .extend({}) - .pipe(createInsertSchema(assistantApplicationsTable).strict().readonly()); + .pipe(createInsertSchema(assistantApplicationsTable).merge(createInsertSchema(applicationsTable)).strict().readonly()); export type NewApplication = z.infer; export type NewTeamApplication = z.infer; diff --git a/src/routers/applications.ts b/src/routers/applications.ts index 8b4c2c5..da479fb 100644 --- a/src/routers/applications.ts +++ b/src/routers/applications.ts @@ -5,9 +5,10 @@ import { } from "@/src/db-access/applications"; import { teamApplicationToInsertParser } from "@/src/request-handling/applications"; import { clientError } from "@/src/error/http-errors"; -import { serialIdParser, toListQueryParser, toSerialIdParser } from "@/src/request-handling/common"; +import { toListQueryParser, toSerialIdParser } from "@/src/request-handling/common"; import { Router, json } from "express"; + export const teamApplicationRouter = Router(); teamApplicationRouter.use(json()); @@ -122,6 +123,7 @@ teamApplicationRouter.post("/", async (req, res, next) => { const teamApplicationBodyResult = teamApplicationToInsertParser.safeParse( req.body, ); + if (!teamApplicationBodyResult.success) { const error = clientError( 400, @@ -130,9 +132,9 @@ teamApplicationRouter.post("/", async (req, res, next) => { ); return next(error); } - const databaseResult = await insertTeamApplication([ + const databaseResult = await insertTeamApplication( teamApplicationBodyResult.data, - ]); + ); if (!databaseResult.success) { const error = clientError( 400, From e634165c8c3fa133bc162ba97d433e81f324b307 Mon Sep 17 00:00:00 2001 From: solvhold Date: Mon, 7 Apr 2025 16:52:31 +0200 Subject: [PATCH 12/17] fix: run pnpm check vf-247 --- db/tables/applications.ts | 59 ++++++------ db/tables/fields-of-study.ts | 2 +- src/db-access/applications.ts | 130 ++++++++++++++------------ src/error/error-messages.ts | 2 +- src/main.ts | 3 +- src/openapi/config.ts | 4 +- src/request-handling/applications.ts | 122 +++++++++++++++--------- src/response-handling/applications.ts | 9 +- src/routers/applications.ts | 10 +- 9 files changed, 194 insertions(+), 147 deletions(-) diff --git a/db/tables/applications.ts b/db/tables/applications.ts index b51acbc..7589079 100644 --- a/db/tables/applications.ts +++ b/db/tables/applications.ts @@ -1,15 +1,11 @@ -import {mainSchema} from "@/db/tables/schema"; +import { mainSchema } from "@/db/tables/schema"; import { relations } from "drizzle-orm"; import { date, integer, serial, text } from "drizzle-orm/pg-core"; import { teamsTable } from "@/db/tables/teams"; import { fieldsOfStudyTable } from "./fields-of-study"; -export const genders = mainSchema.enum("gender", [ - "Female", - "Male", - "Other", -]); +export const genders = mainSchema.enum("gender", ["Female", "Male", "Other"]); export const applicationsTable = mainSchema.table("applications", { id: serial("id").primaryKey(), @@ -27,7 +23,7 @@ export const applicationsTable = mainSchema.table("applications", { export const applicationsRelations = relations( applicationsTable, - ({ one }) => ({ + ({ one }) => ({ fieldOfStudy: one(fieldsOfStudyTable, { fields: [applicationsTable.fieldOfStudyId], references: [fieldsOfStudyTable.id], @@ -37,41 +33,44 @@ export const applicationsRelations = relations( export const teamApplicationsTable = mainSchema.table("teamApplications", { id: integer("id") - .primaryKey() - .references(() => applicationsTable.id), + .primaryKey() + .references(() => applicationsTable.id), teamId: integer("teamId") - .notNull() - .references(() => teamsTable.id), + .notNull() + .references(() => teamsTable.id), motivationText: text("motivationText").notNull(), biography: text("biography").notNull(), -}) +}); export const teamApplicationsRelations = relations( - teamApplicationsTable, ({ one }) => ({ - superApplication: one( applicationsTable,{ + teamApplicationsTable, + ({ one }) => ({ + superApplication: one(applicationsTable, { fields: [teamApplicationsTable.id], - references: [applicationsTable.id] + references: [applicationsTable.id], }), team: one(teamsTable, { fields: [teamApplicationsTable.teamId], references: [teamsTable.id], }), - }) -) + }), +); -export const assistantApplicationsTable = mainSchema.table("assistantApplications", { - id: integer("id") - .primaryKey() - .references(() => applicationsTable.id) -}) +export const assistantApplicationsTable = mainSchema.table( + "assistantApplications", + { + id: integer("id") + .primaryKey() + .references(() => applicationsTable.id), + }, +); export const assistantApplicationsRelations = relations( - assistantApplicationsTable, ({ one }) => ({ - superApplication: one( applicationsTable,{ + assistantApplicationsTable, + ({ one }) => ({ + superApplication: one(applicationsTable, { fields: [assistantApplicationsTable.id], - references: [applicationsTable.id] - }) - }) -) - - + references: [applicationsTable.id], + }), + }), +); diff --git a/db/tables/fields-of-study.ts b/db/tables/fields-of-study.ts index d927624..d1cd94d 100644 --- a/db/tables/fields-of-study.ts +++ b/db/tables/fields-of-study.ts @@ -2,7 +2,7 @@ import { departmentsTable } from "@/db/tables/departments"; import { mainSchema } from "@/db/tables/schema"; import { relations } from "drizzle-orm"; import { integer, serial, text } from "drizzle-orm/pg-core"; -import { applicationsTable, teamApplicationsTable } from "./applications"; +import { applicationsTable } from "./applications"; export const fieldsOfStudyTable = mainSchema.table("fieldsOfStudy", { id: serial("id").primaryKey(), diff --git a/src/db-access/applications.ts b/src/db-access/applications.ts index eba3aeb..d05377b 100644 --- a/src/db-access/applications.ts +++ b/src/db-access/applications.ts @@ -1,12 +1,18 @@ import { database } from "@/db/setup/query-postgres"; -import { applicationsTable, teamApplicationsTable } from "@/db/tables/applications"; +import { + applicationsTable, + teamApplicationsTable, +} from "@/db/tables/applications"; import { type OrmResult, handleDatabaseFullfillment, handleDatabaseRejection, } from "@/src/error/orm-error"; +import type { + NewApplication, + NewTeamApplication, +} from "@/src/request-handling/applications"; import type { QueryParameters } from "@/src/request-handling/common"; -import { type NewApplication, type NewTeamApplication } from "@/src/request-handling/applications"; import type { ApplicationKey, TeamApplication, @@ -32,46 +38,50 @@ export const selectTeamApplications = async ( phonenumber: applicationsTable.phonenumber, motivationText: teamApplicationsTable.motivationText, biography: teamApplicationsTable.biography, - submitDate: applicationsTable.submitDate + submitDate: applicationsTable.submitDate, }) .from(teamApplicationsTable) - .innerJoin(applicationsTable, eq(teamApplicationsTable.id, applicationsTable.id)) + .innerJoin( + applicationsTable, + eq(teamApplicationsTable.id, applicationsTable.id), + ) .limit(parameters.limit) .offset(parameters.offset); return teamApplications; }) .then(handleDatabaseFullfillment, handleDatabaseRejection); - }; - export const selectTeamApplicationsByTeamId = async ( teamId: TeamKey[], parameters: QueryParameters, ): Promise> => { - return database + return await database .transaction(async (tx) => { const selectResult = await tx - .select({ - id: applicationsTable.id, - teamId: teamApplicationsTable.teamId, - firstName: applicationsTable.firstName, - lastName: applicationsTable.lastName, - gender: applicationsTable.gender, - email: applicationsTable.email, - fieldOfStudyId: applicationsTable.fieldOfStudyId, - yearOfStudy: applicationsTable.yearOfStudy, - phonenumber: applicationsTable.phonenumber, - motivationText: teamApplicationsTable.motivationText, - biography: teamApplicationsTable.biography, - submitDate: applicationsTable.submitDate - }) - .from(teamApplicationsTable) - .where(inArray(teamApplicationsTable.id, teamId)) - .innerJoin(applicationsTable, eq(teamApplicationsTable.id, applicationsTable.id)) - .limit(parameters.limit) - .offset(parameters.offset); + .select({ + id: applicationsTable.id, + teamId: teamApplicationsTable.teamId, + firstName: applicationsTable.firstName, + lastName: applicationsTable.lastName, + gender: applicationsTable.gender, + email: applicationsTable.email, + fieldOfStudyId: applicationsTable.fieldOfStudyId, + yearOfStudy: applicationsTable.yearOfStudy, + phonenumber: applicationsTable.phonenumber, + motivationText: teamApplicationsTable.motivationText, + biography: teamApplicationsTable.biography, + submitDate: applicationsTable.submitDate, + }) + .from(teamApplicationsTable) + .where(inArray(teamApplicationsTable.id, teamId)) + .innerJoin( + applicationsTable, + eq(teamApplicationsTable.id, applicationsTable.id), + ) + .limit(parameters.limit) + .offset(parameters.offset); return selectResult; }) @@ -81,26 +91,29 @@ export const selectTeamApplicationsByTeamId = async ( export const selectTeamApplicationsById = async ( applicationIds: ApplicationKey[], ): Promise> => { - return database + return await database .transaction(async (tx) => { const selectResult = await tx - .select({ - id: applicationsTable.id, - teamId: teamApplicationsTable.teamId, - firstName: applicationsTable.firstName, - lastName: applicationsTable.lastName, - gender: applicationsTable.gender, - email: applicationsTable.email, - fieldOfStudyId: applicationsTable.fieldOfStudyId, - yearOfStudy: applicationsTable.yearOfStudy, - phonenumber: applicationsTable.phonenumber, - motivationText: teamApplicationsTable.motivationText, - biography: teamApplicationsTable.biography, - submitDate: applicationsTable.submitDate - }) - .from(teamApplicationsTable) - .where(inArray(teamApplicationsTable.id, applicationIds)) - .innerJoin(applicationsTable, eq(teamApplicationsTable.id, applicationsTable.id)) + .select({ + id: applicationsTable.id, + teamId: teamApplicationsTable.teamId, + firstName: applicationsTable.firstName, + lastName: applicationsTable.lastName, + gender: applicationsTable.gender, + email: applicationsTable.email, + fieldOfStudyId: applicationsTable.fieldOfStudyId, + yearOfStudy: applicationsTable.yearOfStudy, + phonenumber: applicationsTable.phonenumber, + motivationText: teamApplicationsTable.motivationText, + biography: teamApplicationsTable.biography, + submitDate: applicationsTable.submitDate, + }) + .from(teamApplicationsTable) + .where(inArray(teamApplicationsTable.id, applicationIds)) + .innerJoin( + applicationsTable, + eq(teamApplicationsTable.id, applicationsTable.id), + ); return selectResult; }) @@ -108,9 +121,9 @@ export const selectTeamApplicationsById = async ( }; export async function insertTeamApplication( - teamApplication: NewTeamApplication & NewApplication + teamApplication: NewTeamApplication & NewApplication, ): Promise> { - return database + return await database .transaction(async (tx) => { const newApplication = await tx .insert(applicationsTable) @@ -121,24 +134,25 @@ export async function insertTeamApplication( email: teamApplication.email, fieldOfStudyId: teamApplication.fieldOfStudyId, yearOfStudy: teamApplication.yearOfStudy, - phonenumber: teamApplication.phonenumber,}) + phonenumber: teamApplication.phonenumber, + }) .returning(); const newApplicationId = newApplication[0].id; - + const newTeamApplicationResult = await tx - .insert(teamApplicationsTable) - .values({ - id:newApplicationId, - teamId: teamApplication.teamId, - motivationText: teamApplication.motivationText, - biography: teamApplication.biography - }).returning(); + .insert(teamApplicationsTable) + .values({ + id: newApplicationId, + teamId: teamApplication.teamId, + motivationText: teamApplication.motivationText, + biography: teamApplication.biography, + }) + .returning(); - return { ...newApplication[0], - ...newTeamApplicationResult[0] - } + ...newTeamApplicationResult[0], + }; }) .then(handleDatabaseFullfillment, handleDatabaseRejection); } diff --git a/src/error/error-messages.ts b/src/error/error-messages.ts index f943f25..813c390 100644 --- a/src/error/error-messages.ts +++ b/src/error/error-messages.ts @@ -29,7 +29,7 @@ const ORM_ERROR_MESSAGES = [ "Couln't find all entries", "Wrong database response format", "Failed to insert all entries", - "Error when inserting team users" + "Error when inserting team users", ] as const; const HTTP_CLIENT_ERROR_MESSAGES = [ "Invalid request format", diff --git a/src/main.ts b/src/main.ts index 0020c87..3ee7362 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,10 +6,9 @@ import { import { logger } from "@/src/middleware/logging-middleware"; import express from "express"; +import { teamApplicationRouter } from "@/src/routers/applications"; import { expensesRouter } from "@/src/routers/expenses"; import { customCors, customHelmetSecurity } from "@/src/security"; -import { teamApplicationRouter } from "@/src/routers/applications"; - import { openapiSpecification } from "@/src/openapi/config"; import { sponsorsRouter } from "@/src/routers/sponsors"; diff --git a/src/openapi/config.ts b/src/openapi/config.ts index 8e3bf46..3ee5199 100644 --- a/src/openapi/config.ts +++ b/src/openapi/config.ts @@ -1,14 +1,13 @@ import "zod-openapi/extend"; import { datePeriodParser } from "@/lib/time-parsers"; import { hostOptions } from "@/src/enviroment"; +import { teamApplicationParser } from "@/src/request-handling/applications"; import { limitParser, offsetParser, serialIdParser, sortParser, } from "@/src/request-handling/common"; -import { teamApplicationParser } from "@/src/request-handling/applications"; - import { expenseRequestParser } from "@/src/request-handling/expenses"; import { sponsorRequestParser } from "@/src/request-handling/sponsors"; @@ -16,7 +15,6 @@ import { assistantUserRequestParser, teamUserRequestParser, userRequestParser, - } from "@/src/request-handling/users"; import { teamApplicationSelectSchema } from "@/src/response-handling/applications"; import { expensesSelectSchema } from "@/src/response-handling/expenses"; diff --git a/src/request-handling/applications.ts b/src/request-handling/applications.ts index fd745ca..538c136 100644 --- a/src/request-handling/applications.ts +++ b/src/request-handling/applications.ts @@ -1,65 +1,95 @@ - -import { applicationsTable, assistantApplicationsTable, teamApplicationsTable } from "@/db/tables/applications"; +import { + applicationsTable, + assistantApplicationsTable, + teamApplicationsTable, +} from "@/db/tables/applications"; import { MAX_TEXT_LENGTH } from "@/lib/global-variables"; import { serialIdParser } from "@/src/request-handling/common"; import { createInsertSchema } from "drizzle-zod"; import { z } from "zod"; -export const applicationParser = z.object({ - firstName: z.string().min(1).describe("First name of user applying for a team"), - lastName: z.string().min(1).describe("Last name of user applying for a team"), - email: z.string().email().describe("Email of user applying for a team"), - gender: z.enum(["Female","Male","Other"]).describe("The gender of the user applying for a team"), - fieldOfStudyId: serialIdParser.describe( - "Studyfield of user applying for a team", - ), - yearOfStudy: z - .number() - .finite() - .safe() - .positive() - .int() - .max(7) - .describe("The year of study the user applying for a team is in"), - phonenumber: z - .string() - .regex(/^\d{8}$/, "Phone number must be 8 digits") - .describe("The phonenumber of the user applying for a team"), -}).strict(); - -export const teamApplicationParser = z.object({ - teamId: serialIdParser.describe("Id of team applied for"), - motivationText: z - .string() - .max(MAX_TEXT_LENGTH) - .describe("The motivation text of user applying for a team"), - biography: z - .string() - .max(MAX_TEXT_LENGTH) - .describe("The biography of the user applying for a team"), -}).merge(applicationParser).strict() +export const applicationParser = z + .object({ + firstName: z + .string() + .min(1) + .describe("First name of user applying for a team"), + lastName: z + .string() + .min(1) + .describe("Last name of user applying for a team"), + email: z.string().email().describe("Email of user applying for a team"), + gender: z + .enum(["Female", "Male", "Other"]) + .describe("The gender of the user applying for a team"), + fieldOfStudyId: serialIdParser.describe( + "Studyfield of user applying for a team", + ), + yearOfStudy: z + .number() + .finite() + .safe() + .positive() + .int() + .max(7) + .describe("The year of study the user applying for a team is in"), + phonenumber: z + .string() + .regex(/^\d{8}$/, "Phone number must be 8 digits") + .describe("The phonenumber of the user applying for a team"), + }) + .strict(); -export const assistantApplicationParser = z.object({ +export const teamApplicationParser = z + .object({ + teamId: serialIdParser.describe("Id of team applied for"), + motivationText: z + .string() + .max(MAX_TEXT_LENGTH) + .describe("The motivation text of user applying for a team"), + biography: z + .string() + .max(MAX_TEXT_LENGTH) + .describe("The biography of the user applying for a team"), + }) + .merge(applicationParser) + .strict(); -}).merge(applicationParser).strict() +export const assistantApplicationParser = z + .object({}) + .merge(applicationParser) + .strict(); export const applicationToInsertParser = applicationParser .extend({}) .pipe(createInsertSchema(applicationsTable).strict().readonly()); export const teamApplicationToInsertParser = teamApplicationParser - .merge(z.object({ - email: teamApplicationParser.shape.email.trim().toLowerCase(), - motivationText: teamApplicationParser.shape.motivationText.trim(), - biography: teamApplicationParser.shape.biography.trim(), - })) - .pipe(createInsertSchema(teamApplicationsTable).merge(createInsertSchema(applicationsTable)).strict().readonly()); + .merge( + z.object({ + email: teamApplicationParser.shape.email.trim().toLowerCase(), + motivationText: teamApplicationParser.shape.motivationText.trim(), + biography: teamApplicationParser.shape.biography.trim(), + }), + ) + .pipe( + createInsertSchema(teamApplicationsTable) + .merge(createInsertSchema(applicationsTable)) + .strict() + .readonly(), + ); export const assistantApplicationToInsertParser = assistantApplicationParser .extend({}) - .pipe(createInsertSchema(assistantApplicationsTable).merge(createInsertSchema(applicationsTable)).strict().readonly()); + .pipe( + createInsertSchema(assistantApplicationsTable) + .merge(createInsertSchema(applicationsTable)) + .strict() + .readonly(), + ); export type NewApplication = z.infer; export type NewTeamApplication = z.infer; -export type NewAssistantApplication = z.infer; - +export type NewAssistantApplication = z.infer< + typeof assistantApplicationToInsertParser +>; diff --git a/src/response-handling/applications.ts b/src/response-handling/applications.ts index 42df677..fa09a77 100644 --- a/src/response-handling/applications.ts +++ b/src/response-handling/applications.ts @@ -1,7 +1,10 @@ import { createSelectSchema } from "drizzle-zod"; import type { z } from "zod"; -import { applicationsTable, teamApplicationsTable } from "@/db/tables/applications"; +import { + applicationsTable, + teamApplicationsTable, +} from "@/db/tables/applications"; export const applicationSelectSchema = createSelectSchema(applicationsTable) .strict() @@ -10,7 +13,9 @@ export const applicationSelectSchema = createSelectSchema(applicationsTable) export type Application = z.infer; export type ApplicationKey = Application["id"]; -export const teamApplicationSelectSchema = createSelectSchema(teamApplicationsTable) +export const teamApplicationSelectSchema = createSelectSchema( + teamApplicationsTable, +) .merge(createSelectSchema(applicationsTable)) .strict() .readonly(); diff --git a/src/routers/applications.ts b/src/routers/applications.ts index da479fb..d2a2c33 100644 --- a/src/routers/applications.ts +++ b/src/routers/applications.ts @@ -3,12 +3,14 @@ import { selectTeamApplications, selectTeamApplicationsByTeamId, } from "@/src/db-access/applications"; -import { teamApplicationToInsertParser } from "@/src/request-handling/applications"; import { clientError } from "@/src/error/http-errors"; -import { toListQueryParser, toSerialIdParser } from "@/src/request-handling/common"; +import { teamApplicationToInsertParser } from "@/src/request-handling/applications"; +import { + toListQueryParser, + toSerialIdParser, +} from "@/src/request-handling/common"; import { Router, json } from "express"; - export const teamApplicationRouter = Router(); teamApplicationRouter.use(json()); @@ -134,7 +136,7 @@ teamApplicationRouter.post("/", async (req, res, next) => { } const databaseResult = await insertTeamApplication( teamApplicationBodyResult.data, - ); + ); if (!databaseResult.success) { const error = clientError( 400, From d54cd72ed0279b80609cd4bc9f27091deb4e711b Mon Sep 17 00:00:00 2001 From: solvhold Date: Mon, 7 Apr 2025 17:02:58 +0200 Subject: [PATCH 13/17] fix: remove old file vf-247 --- src/response-handling/team-applications.ts | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 src/response-handling/team-applications.ts diff --git a/src/response-handling/team-applications.ts b/src/response-handling/team-applications.ts deleted file mode 100644 index faadaf0..0000000 --- a/src/response-handling/team-applications.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { createSelectSchema } from "drizzle-zod"; -import type { z } from "zod"; - -import { teamApplicationsTable } from "@/db/tables/team-applications"; - -export const teamApplicationSelectSchema = createSelectSchema( - teamApplicationsTable, -) - .strict() - .readonly(); - -export type TeamApplication = z.infer; -export type TeamApplicationKey = TeamApplication["id"]; -export type TeamKey = TeamApplication["teamId"]; From d7a445460f32022a1e1be289b33712673569b5b3 Mon Sep 17 00:00:00 2001 From: solvhold Date: Mon, 7 Apr 2025 17:11:29 +0200 Subject: [PATCH 14/17] fix: merge fix vf-247 --- db/seeding/seeding-tables.ts | 2 +- src/routers/teams.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/db/seeding/seeding-tables.ts b/db/seeding/seeding-tables.ts index 9cf4415..cf54342 100644 --- a/db/seeding/seeding-tables.ts +++ b/db/seeding/seeding-tables.ts @@ -1,7 +1,7 @@ +import { teamApplicationsTable } from "@/db/tables/applications"; import { departmentsTable } from "@/db/tables/departments"; import { expensesTable } from "@/db/tables/expenses"; import { fieldsOfStudyTable } from "@/db/tables/fields-of-study"; -import { teamApplicationsTable } from "@/db/tables/team-applications"; import { teamsTable } from "@/db/tables/teams"; import { usersTable } from "@/db/tables/users"; diff --git a/src/routers/teams.ts b/src/routers/teams.ts index c1d13f1..8dc58fa 100644 --- a/src/routers/teams.ts +++ b/src/routers/teams.ts @@ -1,4 +1,4 @@ -import { selectTeamApplicationsByTeamId } from "@/src/db-access/team-applications"; +import { selectTeamApplicationsByTeamId } from "@/src/db-access/applications"; import { selectTeamsById } from "@/src/db-access/teams"; import { clientError } from "@/src/error/http-errors"; import { From 7ae24a97c4861af8db45ea86aa3487c554e05bdc Mon Sep 17 00:00:00 2001 From: solvhold Date: Mon, 7 Apr 2025 17:14:25 +0200 Subject: [PATCH 15/17] fix: fix seeding vf-247 --- db/seeding/seeding-tables.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/db/seeding/seeding-tables.ts b/db/seeding/seeding-tables.ts index cf54342..c67ce82 100644 --- a/db/seeding/seeding-tables.ts +++ b/db/seeding/seeding-tables.ts @@ -1,4 +1,3 @@ -import { teamApplicationsTable } from "@/db/tables/applications"; import { departmentsTable } from "@/db/tables/departments"; import { expensesTable } from "@/db/tables/expenses"; import { fieldsOfStudyTable } from "@/db/tables/fields-of-study"; @@ -12,6 +11,6 @@ export const seedingTables = { usersTable, //teamUsersTable, these two tables dont work currently //assistantUsersTable, - teamApplicationsTable, + //teamApplicationsTable, expensesTable, }; From 2aee151b3670934babc39c59ce210c23c38c7413 Mon Sep 17 00:00:00 2001 From: solvhold Date: Mon, 7 Apr 2025 17:42:44 +0200 Subject: [PATCH 16/17] feat: replace .transaction().then() with newDatabaseTransaction vf-247 --- db/tables/applications.ts | 11 +- src/db-access/applications.ts | 212 +++++++++++++-------------- src/request-handling/applications.ts | 6 +- 3 files changed, 114 insertions(+), 115 deletions(-) diff --git a/db/tables/applications.ts b/db/tables/applications.ts index 7589079..fa97937 100644 --- a/db/tables/applications.ts +++ b/db/tables/applications.ts @@ -5,13 +5,13 @@ import { date, integer, serial, text } from "drizzle-orm/pg-core"; import { teamsTable } from "@/db/tables/teams"; import { fieldsOfStudyTable } from "./fields-of-study"; -export const genders = mainSchema.enum("gender", ["Female", "Male", "Other"]); +export const gendersEnum = mainSchema.enum("gender", ["female", "male", "other"]); export const applicationsTable = mainSchema.table("applications", { id: serial("id").primaryKey(), firstName: text("firstname").notNull(), lastName: text("lastname").notNull(), - gender: genders("gender").notNull(), + gender: gendersEnum("gender").notNull(), email: text("email").notNull(), fieldOfStudyId: integer("fieldOfStudyId") .notNull() @@ -23,11 +23,16 @@ export const applicationsTable = mainSchema.table("applications", { export const applicationsRelations = relations( applicationsTable, - ({ one }) => ({ + ({ one, many }) => ({ fieldOfStudy: one(fieldsOfStudyTable, { fields: [applicationsTable.fieldOfStudyId], references: [fieldsOfStudyTable.id], }), + assistantApplication: one(assistantApplicationsTable, { + fields: [applicationsTable.id], + references: [assistantApplicationsTable.id], + }), + teamApplication: many(teamApplicationsTable) }), ); diff --git a/src/db-access/applications.ts b/src/db-access/applications.ts index d05377b..8f5fd2b 100644 --- a/src/db-access/applications.ts +++ b/src/db-access/applications.ts @@ -19,140 +19,134 @@ import type { TeamKey, } from "@/src/response-handling/applications"; import { eq, inArray } from "drizzle-orm"; +import { newDatabaseTransaction } from "./common"; export const selectTeamApplications = async ( parameters: QueryParameters, ): Promise> => { - return await database - .transaction(async (tx) => { - const teamApplications = await tx - .select({ - id: applicationsTable.id, - teamId: teamApplicationsTable.teamId, - firstName: applicationsTable.firstName, - lastName: applicationsTable.lastName, - gender: applicationsTable.gender, - email: applicationsTable.email, - fieldOfStudyId: applicationsTable.fieldOfStudyId, - yearOfStudy: applicationsTable.yearOfStudy, - phonenumber: applicationsTable.phonenumber, - motivationText: teamApplicationsTable.motivationText, - biography: teamApplicationsTable.biography, - submitDate: applicationsTable.submitDate, - }) - .from(teamApplicationsTable) - .innerJoin( - applicationsTable, - eq(teamApplicationsTable.id, applicationsTable.id), - ) - .limit(parameters.limit) - .offset(parameters.offset); + return await newDatabaseTransaction(database, async (tx) => { + const teamApplications = await tx + .select({ + id: applicationsTable.id, + teamId: teamApplicationsTable.teamId, + firstName: applicationsTable.firstName, + lastName: applicationsTable.lastName, + gender: applicationsTable.gender, + email: applicationsTable.email, + fieldOfStudyId: applicationsTable.fieldOfStudyId, + yearOfStudy: applicationsTable.yearOfStudy, + phonenumber: applicationsTable.phonenumber, + motivationText: teamApplicationsTable.motivationText, + biography: teamApplicationsTable.biography, + submitDate: applicationsTable.submitDate, + }) + .from(teamApplicationsTable) + .innerJoin( + applicationsTable, + eq(teamApplicationsTable.id, applicationsTable.id), + ) + .limit(parameters.limit) + .offset(parameters.offset); return teamApplications; }) - .then(handleDatabaseFullfillment, handleDatabaseRejection); }; export const selectTeamApplicationsByTeamId = async ( teamId: TeamKey[], parameters: QueryParameters, ): Promise> => { - return await database - .transaction(async (tx) => { - const selectResult = await tx - .select({ - id: applicationsTable.id, - teamId: teamApplicationsTable.teamId, - firstName: applicationsTable.firstName, - lastName: applicationsTable.lastName, - gender: applicationsTable.gender, - email: applicationsTable.email, - fieldOfStudyId: applicationsTable.fieldOfStudyId, - yearOfStudy: applicationsTable.yearOfStudy, - phonenumber: applicationsTable.phonenumber, - motivationText: teamApplicationsTable.motivationText, - biography: teamApplicationsTable.biography, - submitDate: applicationsTable.submitDate, - }) - .from(teamApplicationsTable) - .where(inArray(teamApplicationsTable.id, teamId)) - .innerJoin( - applicationsTable, - eq(teamApplicationsTable.id, applicationsTable.id), - ) - .limit(parameters.limit) - .offset(parameters.offset); + return await newDatabaseTransaction(database, async (tx) => { + const selectResult = await tx + .select({ + id: applicationsTable.id, + teamId: teamApplicationsTable.teamId, + firstName: applicationsTable.firstName, + lastName: applicationsTable.lastName, + gender: applicationsTable.gender, + email: applicationsTable.email, + fieldOfStudyId: applicationsTable.fieldOfStudyId, + yearOfStudy: applicationsTable.yearOfStudy, + phonenumber: applicationsTable.phonenumber, + motivationText: teamApplicationsTable.motivationText, + biography: teamApplicationsTable.biography, + submitDate: applicationsTable.submitDate, + }) + .from(teamApplicationsTable) + .where(inArray(teamApplicationsTable.id, teamId)) + .innerJoin( + applicationsTable, + eq(teamApplicationsTable.id, applicationsTable.id), + ) + .limit(parameters.limit) + .offset(parameters.offset); - return selectResult; - }) - .then(handleDatabaseFullfillment, handleDatabaseRejection); + return selectResult; + }) }; export const selectTeamApplicationsById = async ( applicationIds: ApplicationKey[], ): Promise> => { - return await database - .transaction(async (tx) => { - const selectResult = await tx - .select({ - id: applicationsTable.id, - teamId: teamApplicationsTable.teamId, - firstName: applicationsTable.firstName, - lastName: applicationsTable.lastName, - gender: applicationsTable.gender, - email: applicationsTable.email, - fieldOfStudyId: applicationsTable.fieldOfStudyId, - yearOfStudy: applicationsTable.yearOfStudy, - phonenumber: applicationsTable.phonenumber, - motivationText: teamApplicationsTable.motivationText, - biography: teamApplicationsTable.biography, - submitDate: applicationsTable.submitDate, - }) - .from(teamApplicationsTable) - .where(inArray(teamApplicationsTable.id, applicationIds)) - .innerJoin( - applicationsTable, - eq(teamApplicationsTable.id, applicationsTable.id), - ); + return await newDatabaseTransaction(database, async (tx) => { + const selectResult = await tx + .select({ + id: applicationsTable.id, + teamId: teamApplicationsTable.teamId, + firstName: applicationsTable.firstName, + lastName: applicationsTable.lastName, + gender: applicationsTable.gender, + email: applicationsTable.email, + fieldOfStudyId: applicationsTable.fieldOfStudyId, + yearOfStudy: applicationsTable.yearOfStudy, + phonenumber: applicationsTable.phonenumber, + motivationText: teamApplicationsTable.motivationText, + biography: teamApplicationsTable.biography, + submitDate: applicationsTable.submitDate, + }) + .from(teamApplicationsTable) + .where(inArray(teamApplicationsTable.id, applicationIds)) + .innerJoin( + applicationsTable, + eq(teamApplicationsTable.id, applicationsTable.id), + ); - return selectResult; - }) - .then(handleDatabaseFullfillment, handleDatabaseRejection); + return selectResult; + }) }; export async function insertTeamApplication( teamApplication: NewTeamApplication & NewApplication, ): Promise> { - return await database - .transaction(async (tx) => { - const newApplication = await tx - .insert(applicationsTable) - .values({ - firstName: teamApplication.firstName, - lastName: teamApplication.lastName, - gender: teamApplication.gender, - email: teamApplication.email, - fieldOfStudyId: teamApplication.fieldOfStudyId, - yearOfStudy: teamApplication.yearOfStudy, - phonenumber: teamApplication.phonenumber, - }) - .returning(); - const newApplicationId = newApplication[0].id; + return await newDatabaseTransaction(database, async (tx) => { + const newApplication = await tx + .insert(applicationsTable) + .values({ + firstName: teamApplication.firstName, + lastName: teamApplication.lastName, + gender: teamApplication.gender, + email: teamApplication.email, + fieldOfStudyId: teamApplication.fieldOfStudyId, + yearOfStudy: teamApplication.yearOfStudy, + phonenumber: teamApplication.phonenumber, + }) + .returning(); + const newApplicationId = newApplication[0].id; - const newTeamApplicationResult = await tx - .insert(teamApplicationsTable) - .values({ - id: newApplicationId, - teamId: teamApplication.teamId, - motivationText: teamApplication.motivationText, - biography: teamApplication.biography, - }) - .returning(); + const newTeamApplicationResult = await tx + .insert(teamApplicationsTable) + .values({ + id: newApplicationId, + teamId: teamApplication.teamId, + motivationText: teamApplication.motivationText, + biography: teamApplication.biography, + }) + .returning(); + + return { + ...newApplication[0], + ...newTeamApplicationResult[0], + }; + }) - return { - ...newApplication[0], - ...newTeamApplicationResult[0], - }; - }) - .then(handleDatabaseFullfillment, handleDatabaseRejection); } diff --git a/src/request-handling/applications.ts b/src/request-handling/applications.ts index 538c136..c299d8a 100644 --- a/src/request-handling/applications.ts +++ b/src/request-handling/applications.ts @@ -65,12 +65,12 @@ export const applicationToInsertParser = applicationParser .pipe(createInsertSchema(applicationsTable).strict().readonly()); export const teamApplicationToInsertParser = teamApplicationParser - .merge( - z.object({ + .extend( + { email: teamApplicationParser.shape.email.trim().toLowerCase(), motivationText: teamApplicationParser.shape.motivationText.trim(), biography: teamApplicationParser.shape.biography.trim(), - }), + }, ) .pipe( createInsertSchema(teamApplicationsTable) From a8f471d52780be4d8e2043b992b624f86ef80f08 Mon Sep 17 00:00:00 2001 From: solvhold Date: Mon, 7 Apr 2025 17:47:08 +0200 Subject: [PATCH 17/17] fix: pnpm check vf-247 --- db/tables/applications.ts | 8 ++++++-- src/db-access/applications.ts | 17 ++++++----------- src/request-handling/applications.ts | 12 +++++------- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/db/tables/applications.ts b/db/tables/applications.ts index fa97937..1082ed2 100644 --- a/db/tables/applications.ts +++ b/db/tables/applications.ts @@ -5,7 +5,11 @@ import { date, integer, serial, text } from "drizzle-orm/pg-core"; import { teamsTable } from "@/db/tables/teams"; import { fieldsOfStudyTable } from "./fields-of-study"; -export const gendersEnum = mainSchema.enum("gender", ["female", "male", "other"]); +export const gendersEnum = mainSchema.enum("gender", [ + "female", + "male", + "other", +]); export const applicationsTable = mainSchema.table("applications", { id: serial("id").primaryKey(), @@ -32,7 +36,7 @@ export const applicationsRelations = relations( fields: [applicationsTable.id], references: [assistantApplicationsTable.id], }), - teamApplication: many(teamApplicationsTable) + teamApplication: many(teamApplicationsTable), }), ); diff --git a/src/db-access/applications.ts b/src/db-access/applications.ts index 8f5fd2b..b2a7893 100644 --- a/src/db-access/applications.ts +++ b/src/db-access/applications.ts @@ -3,11 +3,7 @@ import { applicationsTable, teamApplicationsTable, } from "@/db/tables/applications"; -import { - type OrmResult, - handleDatabaseFullfillment, - handleDatabaseRejection, -} from "@/src/error/orm-error"; +import type { OrmResult } from "@/src/error/orm-error"; import type { NewApplication, NewTeamApplication, @@ -48,8 +44,8 @@ export const selectTeamApplications = async ( .limit(parameters.limit) .offset(parameters.offset); - return teamApplications; - }) + return teamApplications; + }); }; export const selectTeamApplicationsByTeamId = async ( @@ -82,7 +78,7 @@ export const selectTeamApplicationsByTeamId = async ( .offset(parameters.offset); return selectResult; - }) + }); }; export const selectTeamApplicationsById = async ( @@ -112,7 +108,7 @@ export const selectTeamApplicationsById = async ( ); return selectResult; - }) + }); }; export async function insertTeamApplication( @@ -147,6 +143,5 @@ export async function insertTeamApplication( ...newApplication[0], ...newTeamApplicationResult[0], }; - }) - + }); } diff --git a/src/request-handling/applications.ts b/src/request-handling/applications.ts index c299d8a..57e19a7 100644 --- a/src/request-handling/applications.ts +++ b/src/request-handling/applications.ts @@ -65,13 +65,11 @@ export const applicationToInsertParser = applicationParser .pipe(createInsertSchema(applicationsTable).strict().readonly()); export const teamApplicationToInsertParser = teamApplicationParser - .extend( - { - email: teamApplicationParser.shape.email.trim().toLowerCase(), - motivationText: teamApplicationParser.shape.motivationText.trim(), - biography: teamApplicationParser.shape.biography.trim(), - }, - ) + .extend({ + email: teamApplicationParser.shape.email.trim().toLowerCase(), + motivationText: teamApplicationParser.shape.motivationText.trim(), + biography: teamApplicationParser.shape.biography.trim(), + }) .pipe( createInsertSchema(teamApplicationsTable) .merge(createInsertSchema(applicationsTable))