diff --git a/db/seeding/seeding-tables.ts b/db/seeding/seeding-tables.ts index 9cf4415..c67ce82 100644 --- a/db/seeding/seeding-tables.ts +++ b/db/seeding/seeding-tables.ts @@ -1,7 +1,6 @@ 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"; @@ -12,6 +11,6 @@ export const seedingTables = { usersTable, //teamUsersTable, these two tables dont work currently //assistantUsersTable, - teamApplicationsTable, + //teamApplicationsTable, expensesTable, }; diff --git a/db/tables/applications.ts b/db/tables/applications.ts new file mode 100644 index 0000000..1082ed2 --- /dev/null +++ b/db/tables/applications.ts @@ -0,0 +1,85 @@ +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 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: gendersEnum("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, many }) => ({ + fieldOfStudy: one(fieldsOfStudyTable, { + fields: [applicationsTable.fieldOfStudyId], + references: [fieldsOfStudyTable.id], + }), + assistantApplication: one(assistantApplicationsTable, { + fields: [applicationsTable.id], + references: [assistantApplicationsTable.id], + }), + teamApplication: many(teamApplicationsTable), + }), +); + +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/fields-of-study.ts b/db/tables/fields-of-study.ts index bca8090..d1cd94d 100644 --- a/db/tables/fields-of-study.ts +++ b/db/tables/fields-of-study.ts @@ -1,8 +1,8 @@ import { departmentsTable } from "@/db/tables/departments"; import { mainSchema } from "@/db/tables/schema"; -import { teamApplicationsTable } from "@/db/tables/team-applications"; import { relations } from "drizzle-orm"; import { integer, serial, text } from "drizzle-orm/pg-core"; +import { applicationsTable } 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), }), ); diff --git a/db/tables/team-applications.ts b/db/tables/team-applications.ts deleted file mode 100644 index f99040a..0000000 --- a/db/tables/team-applications.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { mainSchema } from "@/db/tables/schema"; -import { relations } from "drizzle-orm"; -import { integer, serial, text, timestamp } from "drizzle-orm/pg-core"; - -import { fieldsOfStudyTable } from "@/db/tables/fields-of-study"; -import { teamsTable } from "@/db/tables/teams"; - -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(), - submitTime: timestamp("submitTime").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/db/tables/teams.ts b/db/tables/teams.ts index 716da91..480caa7 100644 --- a/db/tables/teams.ts +++ b/db/tables/teams.ts @@ -1,6 +1,6 @@ +import { teamApplicationsTable } from "@/db/tables/applications"; import { departmentsTable } from "@/db/tables/departments"; import { mainSchema } from "@/db/tables/schema"; -import { teamApplicationsTable } from "@/db/tables/team-applications"; import { teamUsersTable } from "@/db/tables/users"; import { relations } from "drizzle-orm"; import { boolean, serial, text, timestamp } from "drizzle-orm/pg-core"; diff --git a/src/db-access/applications.ts b/src/db-access/applications.ts new file mode 100644 index 0000000..b2a7893 --- /dev/null +++ b/src/db-access/applications.ts @@ -0,0 +1,147 @@ +import { database } from "@/db/setup/query-postgres"; +import { + applicationsTable, + teamApplicationsTable, +} from "@/db/tables/applications"; +import type { OrmResult } from "@/src/error/orm-error"; +import type { + NewApplication, + NewTeamApplication, +} from "@/src/request-handling/applications"; +import type { QueryParameters } from "@/src/request-handling/common"; +import type { + ApplicationKey, + TeamApplication, + 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 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; + }); +}; + +export const selectTeamApplicationsByTeamId = async ( + teamId: TeamKey[], + parameters: QueryParameters, +): Promise> => { + 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; + }); +}; + +export const selectTeamApplicationsById = async ( + applicationIds: ApplicationKey[], +): Promise> => { + 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; + }); +}; + +export async function insertTeamApplication( + teamApplication: NewTeamApplication & NewApplication, +): Promise> { + 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(); + + return { + ...newApplication[0], + ...newTeamApplicationResult[0], + }; + }); +} diff --git a/src/db-access/team-applications.ts b/src/db-access/team-applications.ts deleted file mode 100644 index 2dabf1a..0000000 --- a/src/db-access/team-applications.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { database } from "@/db/setup/query-postgres"; -import { teamApplicationsTable } from "@/db/tables/team-applications"; -import { - type OrmResult, - handleDatabaseFullfillment, - handleDatabaseRejection, -} from "@/src/error/orm-error"; -import type { QueryParameters } from "@/src/request-handling/common"; -import type { NewTeamApplication } from "@/src/request-handling/team-applications"; -import type { - TeamApplication, - TeamKey, -} from "@/src/response-handling/team-applications"; -import { asc, inArray } from "drizzle-orm"; - -export const selectTeamApplications = async ( - parameters: QueryParameters, -): Promise> => { - return await database - .transaction(async (tx) => { - return await tx - .select() - .from(teamApplicationsTable) - .orderBy(asc(teamApplicationsTable.id)) - .limit(parameters.limit) - .offset(parameters.offset); - }) - .then(handleDatabaseFullfillment, handleDatabaseRejection); -}; - -export const selectTeamApplicationsByTeamId = async ( - teamId: TeamKey[], - parameters: QueryParameters, -): Promise> => { - return await database - .transaction(async (tx) => { - const selectResult = await tx - .select() - .from(teamApplicationsTable) - .where(inArray(teamApplicationsTable.teamId, teamId)) - .limit(parameters.limit) - .offset(parameters.offset); - return selectResult; - }) - .then(handleDatabaseFullfillment, handleDatabaseRejection); -}; - -export async function insertTeamApplication( - teamApplication: NewTeamApplication[], -): Promise> { - return await database - .transaction(async (tx) => { - const insertResult = await tx - .insert(teamApplicationsTable) - .values(teamApplication) - .returning(); - return insertResult; - }) - .then(handleDatabaseFullfillment, handleDatabaseRejection); -} diff --git a/src/error/error-messages.ts b/src/error/error-messages.ts index eb09e59..813c390 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", diff --git a/src/main.ts b/src/main.ts index ac0be3b..e8b2935 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,11 +6,10 @@ 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/team-applications"; - import { openapiSpecification } from "@/src/openapi/config"; import { sponsorsRouter } from "@/src/routers/sponsors"; import { teamsRouter } from "@/src/routers/teams"; diff --git a/src/openapi/config.ts b/src/openapi/config.ts index 65cfe23..3ee5199 100644 --- a/src/openapi/config.ts +++ b/src/openapi/config.ts @@ -1,23 +1,25 @@ 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 { expenseRequestParser } from "@/src/request-handling/expenses"; import { sponsorRequestParser } from "@/src/request-handling/sponsors"; -import { teamApplicationParser } from "@/src/request-handling/team-applications"; import { assistantUserRequestParser, teamUserRequestParser, userRequestParser, } from "@/src/request-handling/users"; +import { teamApplicationSelectSchema } from "@/src/response-handling/applications"; import { expensesSelectSchema } from "@/src/response-handling/expenses"; import { sponsorsSelectSchema } from "@/src/response-handling/sponsors"; -import { teamApplicationSelectSchema } from "@/src/response-handling/team-applications"; + import { assistantUserSelectSchema, teamUserSelectSchema, diff --git a/src/request-handling/applications.ts b/src/request-handling/applications.ts new file mode 100644 index 0000000..57e19a7 --- /dev/null +++ b/src/request-handling/applications.ts @@ -0,0 +1,93 @@ +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 assistantApplicationParser = z + .object({}) + .merge(applicationParser) + .strict(); + +export const applicationToInsertParser = applicationParser + .extend({}) + .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(), + }) + .pipe( + createInsertSchema(teamApplicationsTable) + .merge(createInsertSchema(applicationsTable)) + .strict() + .readonly(), + ); + +export const assistantApplicationToInsertParser = assistantApplicationParser + .extend({}) + .pipe( + createInsertSchema(assistantApplicationsTable) + .merge(createInsertSchema(applicationsTable)) + .strict() + .readonly(), + ); + +export type NewApplication = z.infer; +export type NewTeamApplication = z.infer; +export type NewAssistantApplication = z.infer< + typeof assistantApplicationToInsertParser +>; diff --git a/src/request-handling/team-applications.ts b/src/request-handling/team-applications.ts deleted file mode 100644 index 352629c..0000000 --- a/src/request-handling/team-applications.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { teamApplicationsTable } from "@/db/tables/team-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"), - 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", - ), - yearOfStudy: z - .number() - .finite() - .safe() - .positive() - .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"), -}); - -export const teamApplicationToInsertParser = teamApplicationParser - .extend({ - email: teamApplicationParser.shape.email.trim().toLowerCase(), - motivationText: teamApplicationParser.shape.motivationText.trim(), - biography: teamApplicationParser.shape.biography.trim(), - }) - .pipe(createInsertSchema(teamApplicationsTable).strict().readonly()); - -export type NewTeamApplication = z.infer; diff --git a/src/response-handling/team-applications.ts b/src/response-handling/applications.ts similarity index 51% rename from src/response-handling/team-applications.ts rename to src/response-handling/applications.ts index faadaf0..fa09a77 100644 --- a/src/response-handling/team-applications.ts +++ b/src/response-handling/applications.ts @@ -1,11 +1,22 @@ import { createSelectSchema } from "drizzle-zod"; import type { z } from "zod"; -import { teamApplicationsTable } from "@/db/tables/team-applications"; +import { + applicationsTable, + teamApplicationsTable, +} from "@/db/tables/applications"; + +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(); diff --git a/src/routers/team-applications.ts b/src/routers/applications.ts similarity index 89% rename from src/routers/team-applications.ts rename to src/routers/applications.ts index 1427e8a..d2a2c33 100644 --- a/src/routers/team-applications.ts +++ b/src/routers/applications.ts @@ -2,10 +2,13 @@ import { insertTeamApplication, selectTeamApplications, selectTeamApplicationsByTeamId, -} from "@/src/db-access/team-applications"; +} from "@/src/db-access/applications"; import { clientError } from "@/src/error/http-errors"; -import { listQueryParser, serialIdParser } from "@/src/request-handling/common"; -import { teamApplicationToInsertParser } from "@/src/request-handling/team-applications"; +import { teamApplicationToInsertParser } from "@/src/request-handling/applications"; +import { + toListQueryParser, + toSerialIdParser, +} from "@/src/request-handling/common"; import { Router, json } from "express"; export const teamApplicationRouter = Router(); @@ -32,7 +35,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 +74,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), @@ -122,6 +125,7 @@ teamApplicationRouter.post("/", async (req, res, next) => { const teamApplicationBodyResult = teamApplicationToInsertParser.safeParse( req.body, ); + if (!teamApplicationBodyResult.success) { const error = clientError( 400, @@ -130,9 +134,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, 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 {