diff --git a/database-diagram.md b/database-diagram.md index 13fd15f..86adad1 100644 --- a/database-diagram.md +++ b/database-diagram.md @@ -7,9 +7,9 @@ title: Database prototype erDiagram USER { int id PK + int fieldOfStudyId FK string firstName string lastName - int fieldOfStudyId FK } EXPENSE { int id PK @@ -58,7 +58,7 @@ erDiagram int id PK int userId FK int interviewId FK - int admissionPeriodId FK + int semesterId FK string yearOfStudy int substitute int doublePosition @@ -71,7 +71,6 @@ erDiagram string specialNeeds string language string preferredSchool - } PRIORITY_DAY{ int id PK @@ -81,6 +80,35 @@ erDiagram int thursday int friday } + SEMESTER{ + int id PK + int lastSemesterId FK + int departmentId FK + date semesterStartDate + date semesterEndDate + date recruitmentStartDate + date recruitmentEndDate + string name + } + SCHOOL{ + int id PK + int departmentId FK + string name + string contactPersonName + string contactPersonPhoneNumber + string contactPersonEmail + boolean isInternational + } + SCHOOL_SEMESTER_ASSISTANT{ + int school FK + int semester FK + int user FK + } + TEAM_SEMESTER_USER{ + int team FK + int semester FK + int user FK + } USER ||--o{ EXPENSE : "pays" USER ||--o{ FIELD_OF_STUDY : studys VEKTOR_DEPARTMENT ||--o{ FIELD_OF_STUDY : oversee @@ -91,4 +119,13 @@ erDiagram USER ||--|| PRIORITY_DAY : wants TEAM }o--o{ ASSISTENT_APPLICATION : interest VEKTOR_DEPARTMENT ||--O{ TEAM : belongsTo + SEMESTER }|--|| VEKTOR_DEPARTMENT : department + SCHOOL }|--|| VEKTOR_DEPARTMENT : department + SEMESTER }o--|| ASSISTENT_APPLICATION : admissionPeriod + SCHOOL_SEMESTER_ASSISTANT ||--o{ SCHOOL : schoolSemesterAssistant + SCHOOL_SEMESTER_ASSISTANT ||--o{ SEMESTER : schoolSemesterAssistant + SCHOOL_SEMESTER_ASSISTANT ||--o{ USER : shcoolSemesterAssistant + TEAM_SEMESTER_USER ||--o{ TEAM : teamSemesterUser + TEAM_SEMESTER_USER ||--o{ SEMESTER : teamSemesterUser + TEAM_SEMESTER_USER ||--o{ USER : teamSemesterUser ``` diff --git a/db/tables/departments.ts b/db/tables/departments.ts index 95beeef..dd8eda6 100644 --- a/db/tables/departments.ts +++ b/db/tables/departments.ts @@ -3,6 +3,7 @@ import { mainSchema } from "@/db/tables/schema"; import { teamsTable } from "@/db/tables/teams"; import { relations } from "drizzle-orm"; import { serial } from "drizzle-orm/pg-core"; +import { semestersTable } from "./semesters"; export const citiesEnum = mainSchema.enum("city", [ "Trondheim", @@ -19,4 +20,5 @@ export const departmentsTable = mainSchema.table("departments", { export const departmentsRelations = relations(departmentsTable, ({ many }) => ({ fieldsOfStudy: many(fieldsOfStudyTable), teams: many(teamsTable), + semesters: many(semestersTable), })); diff --git a/db/tables/school-semester-assistant.ts b/db/tables/school-semester-assistant.ts new file mode 100644 index 0000000..5df90bd --- /dev/null +++ b/db/tables/school-semester-assistant.ts @@ -0,0 +1,33 @@ +import { relations } from "drizzle-orm"; +import { integer } from "drizzle-orm/pg-core"; +import { mainSchema } from "./schema"; +import { schoolsTable } from "./schools"; +import { semestersTable } from "./semesters"; +import { assistantUsersTable } from "./users"; + +export const schoolSemesterAssistantsTable = mainSchema.table( + "schoolSemesterAssistant", + { + schoolId: integer("schoolId").references(() => schoolsTable.id), + semesterId: integer("semesterId").references(() => semestersTable.id), + assistantUserId: integer("userId").references(() => assistantUsersTable.id), + }, +); + +export const schoolSemesterAssistantsRelations = relations( + schoolSemesterAssistantsTable, + ({ one }) => ({ + school: one(schoolsTable, { + fields: [schoolSemesterAssistantsTable.schoolId], + references: [schoolsTable.id], + }), + semester: one(semestersTable, { + fields: [schoolSemesterAssistantsTable.semesterId], + references: [semestersTable.id], + }), + assistantUser: one(assistantUsersTable, { + fields: [schoolSemesterAssistantsTable.assistantUserId], + references: [assistantUsersTable.id], + }), + }), +); diff --git a/db/tables/schools.ts b/db/tables/schools.ts new file mode 100644 index 0000000..2084e28 --- /dev/null +++ b/db/tables/schools.ts @@ -0,0 +1,23 @@ +import { relations } from "drizzle-orm"; +import { boolean, integer, serial, text } from "drizzle-orm/pg-core"; +import { departmentsTable } from "./departments"; +import { mainSchema } from "./schema"; +import { schoolSemesterAssistantsTable } from "./school-semester-assistant"; + +export const schoolsTable = mainSchema.table("schools", { + id: serial("id").primaryKey(), + departmentId: integer("departmentId").references(() => departmentsTable.id), + name: text("name").notNull(), + contactPersonName: text("contactPersonName").notNull(), + contactPersonPhoneNumber: text("contactPersonPhoneNumber").notNull(), + contactPersonEmail: text("contactpersonEmail").notNull(), + isInternational: boolean("isInternational").notNull(), +}); + +export const schoolsRelations = relations(schoolsTable, ({ one, many }) => ({ + department: one(departmentsTable, { + fields: [schoolsTable.departmentId], + references: [departmentsTable.id], + }), + semesterAssistants: many(schoolSemesterAssistantsTable), +})); diff --git a/db/tables/semesters.ts b/db/tables/semesters.ts new file mode 100644 index 0000000..dc9e824 --- /dev/null +++ b/db/tables/semesters.ts @@ -0,0 +1,47 @@ +import { relations } from "drizzle-orm"; +import { + type AnyPgColumn, + date, + integer, + serial, + text, +} from "drizzle-orm/pg-core"; +import { departmentsTable } from "./departments"; +import { mainSchema } from "./schema"; +import { schoolSemesterAssistantsTable } from "./school-semester-assistant"; +import { teamSemesterUsersTable } from "./team-semester-user"; + +export const semestersTable = mainSchema.table("semesters", { + id: serial("id").primaryKey(), + lastSemesterId: integer("lastSemesterId").references( + (): AnyPgColumn => semestersTable.id, + ), + semesterStartDate: date("semesterStartDate").notNull(), + semesterEndDate: date("semesterEndDate").notNull(), + recruitmentStartDate: date("recruitmentStartDate").notNull(), + recruitmentEndDate: date("recruitmentEndDate").notNull(), + departmentId: integer("departmentId") + .notNull() + .references(() => departmentsTable.id), + name: text("name").notNull(), +}); + +export const semestersRelations = relations( + semestersTable, + ({ one, many }) => ({ + department: one(departmentsTable, { + fields: [semestersTable.departmentId], + references: [departmentsTable.id], + }), + lastSemester: one(semestersTable, { + fields: [semestersTable.lastSemesterId], + references: [semestersTable.id], + }), + nextSemester: one(semestersTable, { + fields: [semestersTable.id], + references: [semestersTable.lastSemesterId], + }), + schoolAssistants: many(schoolSemesterAssistantsTable), + teamUsers: many(teamSemesterUsersTable), + }), +); diff --git a/db/tables/team-semester-user.ts b/db/tables/team-semester-user.ts new file mode 100644 index 0000000..86de272 --- /dev/null +++ b/db/tables/team-semester-user.ts @@ -0,0 +1,30 @@ +import { relations } from "drizzle-orm"; +import { integer } from "drizzle-orm/pg-core"; +import { mainSchema } from "./schema"; +import { semestersTable } from "./semesters"; +import { teamsTable } from "./teams"; +import { teamUsersTable } from "./users"; + +export const teamSemesterUsersTable = mainSchema.table("teamSemesterUser", { + teamId: integer("teamId").references(() => teamsTable.id), + semesterId: integer("semesterId").references(() => semestersTable.id), + teamUserId: integer("teamUserId").references(() => teamUsersTable.id), +}); + +export const teamSemesterUsersRelations = relations( + teamSemesterUsersTable, + ({ one }) => ({ + team: one(teamsTable, { + fields: [teamSemesterUsersTable.teamId], + references: [teamsTable.id], + }), + semester: one(semestersTable, { + fields: [teamSemesterUsersTable.semesterId], + references: [semestersTable.id], + }), + teamUser: one(teamUsersTable, { + fields: [teamSemesterUsersTable.teamUserId], + references: [teamUsersTable.id], + }), + }), +); diff --git a/db/tables/teams.ts b/db/tables/teams.ts index 480caa7..86e4df6 100644 --- a/db/tables/teams.ts +++ b/db/tables/teams.ts @@ -1,10 +1,10 @@ import { teamApplicationsTable } from "@/db/tables/applications"; import { departmentsTable } from "@/db/tables/departments"; import { mainSchema } from "@/db/tables/schema"; -import { teamUsersTable } from "@/db/tables/users"; import { relations } from "drizzle-orm"; import { boolean, serial, text, timestamp } from "drizzle-orm/pg-core"; import { integer } from "drizzle-orm/pg-core"; +import { teamSemesterUsersTable } from "./team-semester-user"; export const teamsTable = mainSchema.table("teams", { id: serial("id").primaryKey(), @@ -26,5 +26,5 @@ export const teamRelations = relations(teamsTable, ({ one, many }) => ({ references: [departmentsTable.id], }), teamApplications: many(teamApplicationsTable), - teamMembers: many(teamUsersTable), + teamUsersInSemester: many(teamSemesterUsersTable), })); diff --git a/db/tables/users.ts b/db/tables/users.ts index f0106ae..b0ba0ab 100644 --- a/db/tables/users.ts +++ b/db/tables/users.ts @@ -5,6 +5,8 @@ import { integer, serial, text } from "drizzle-orm/pg-core"; import { expensesTable } from "@/db/tables/expenses"; import { fieldsOfStudyTable } from "@/db/tables/fields-of-study"; import { teamsTable } from "@/db/tables/teams"; +import { schoolSemesterAssistantsTable } from "./school-semester-assistant"; +import { teamSemesterUsersTable } from "./team-semester-user"; export const usersTable = mainSchema.table("users", { id: serial("id").primaryKey(), @@ -36,16 +38,20 @@ export const teamUsersTable = mainSchema.table("teamUsers", { .references(() => teamsTable.id), username: text("username").notNull().unique(), }); -export const teamUsersRelations = relations(teamUsersTable, ({ one }) => ({ - superUser: one(usersTable, { - fields: [teamUsersTable.id], - references: [usersTable.id], - }), - team: one(teamsTable, { - fields: [teamUsersTable.teamId], - references: [teamsTable.id], +export const teamUsersRelations = relations( + teamUsersTable, + ({ one, many }) => ({ + superUser: one(usersTable, { + fields: [teamUsersTable.id], + references: [usersTable.id], + }), + team: one(teamsTable, { + fields: [teamUsersTable.teamId], + references: [teamsTable.id], + }), + teamSemesters: many(teamSemesterUsersTable), }), -})); +); export const assistantUsersTable = mainSchema.table("assistantUsers", { id: integer("id") @@ -54,10 +60,11 @@ export const assistantUsersTable = mainSchema.table("assistantUsers", { }); export const assistantUsersRelation = relations( assistantUsersTable, - ({ one }) => ({ + ({ one, many }) => ({ superUser: one(usersTable, { fields: [assistantUsersTable.id], references: [usersTable.id], }), + schoolSemesters: many(schoolSemesterAssistantsTable), }), ); diff --git a/lib/lib.ts b/lib/lib.ts index cc87441..1c91475 100644 --- a/lib/lib.ts +++ b/lib/lib.ts @@ -7,3 +7,7 @@ export function zodEnumFromObjKeys( const [firstKey, ...otherKeys] = Object.keys(obj) as K[]; return z.enum([firstKey, ...otherKeys]); } +const phoneNumberRegex = /^\d{8}$/; +export const phoneNumberParser = z + .string() + .regex(phoneNumberRegex, "Phone number must be 8 digits"); diff --git a/src/request-handling/schools.ts b/src/request-handling/schools.ts new file mode 100644 index 0000000..d766b99 --- /dev/null +++ b/src/request-handling/schools.ts @@ -0,0 +1,31 @@ +import { schoolsTable } from "@/db/tables/schools"; +import { phoneNumberParser } from "@/lib/lib"; +import { createInsertSchema } from "drizzle-zod"; +import { z } from "zod"; +import { serialIdParser } from "./common"; + +export const schoolRequestParser = z + .object({ + id: serialIdParser.describe("Id of school"), + departmentId: serialIdParser.describe("Id of corresponding department"), + name: z.string().describe("Name of school"), + contactPersonName: z.string().describe("Name of contact person on school"), + contactPersonPhoneNumber: phoneNumberParser.describe( + "Phone number of contact person", + ), + contactPersonEmail: z.string().describe("Email of contact person"), + isInternational: z + .boolean() + .describe("Whether the school is international or not"), + }) + .strict(); + +export const schoolRequestToInsertParser = schoolRequestParser + .extend({ + name: schoolRequestParser.shape.name.trim(), + contactPersonName: schoolRequestParser.shape.contactPersonName.trim(), + contactPersonEmail: schoolRequestParser.shape.contactPersonEmail.trim(), + }) + .pipe(createInsertSchema(schoolsTable).strict().readonly()); + +export type NewSchool = z.infer; diff --git a/src/request-handling/semesters.ts b/src/request-handling/semesters.ts new file mode 100644 index 0000000..9fda4d3 --- /dev/null +++ b/src/request-handling/semesters.ts @@ -0,0 +1,42 @@ +import { semestersTable } from "@/db/tables/semesters"; +import { timeStringParser } from "@/lib/time-parsers"; +import { createInsertSchema } from "drizzle-zod"; +import { z } from "zod"; +import { serialIdParser } from "./common"; + +export const semesterRequestParser = z + .object({ + id: serialIdParser.describe("Id of semester"), + lastSemesterId: serialIdParser.describe("Id of last semester"), + semesterStartDate: timeStringParser.describe("Date of semester start"), + semesterEndDate: timeStringParser.describe("Date of semester end"), + recruitmentStartDate: timeStringParser.describe( + "Date of recruitment period start", + ), + recruitmentEndDate: timeStringParser.describe( + "Date of recruitment period end", + ), + departmentId: serialIdParser.describe("Id of corresponding department"), + name: z.string().describe("Name of semester"), + }) + .strict(); + +export const semesterRequestToInsertParser = semesterRequestParser + .extend({ + name: semesterRequestParser.shape.name.trim(), + semesterStartDate: semesterRequestParser.shape.semesterStartDate.pipe( + z.coerce.date(), + ), + semesterEndDate: semesterRequestParser.shape.semesterEndDate.pipe( + z.coerce.date(), + ), + recruitmentStartDate: semesterRequestParser.shape.recruitmentStartDate.pipe( + z.coerce.date(), + ), + recruitmentEndDate: semesterRequestParser.shape.recruitmentEndDate.pipe( + z.coerce.date(), + ), + }) + .pipe(createInsertSchema(semestersTable).strict().readonly()); + +export type NewSemester = z.infer; diff --git a/src/response-handling/schools.ts b/src/response-handling/schools.ts new file mode 100644 index 0000000..72bbf00 --- /dev/null +++ b/src/response-handling/schools.ts @@ -0,0 +1,10 @@ +import { schoolsTable } from "@/db/tables/schools"; +import { createSelectSchema } from "drizzle-zod"; +import type { z } from "zod"; + +export const schoolsSelectSchema = createSelectSchema(schoolsTable) + .strict() + .readonly(); + +export type School = z.infer; +export type SchoolsKey = School["id"]; diff --git a/src/response-handling/semesters.ts b/src/response-handling/semesters.ts new file mode 100644 index 0000000..50bdd94 --- /dev/null +++ b/src/response-handling/semesters.ts @@ -0,0 +1,10 @@ +import { semestersTable } from "@/db/tables/semesters"; +import { createSelectSchema } from "drizzle-zod"; +import type { z } from "zod"; + +export const semestersSelectSchema = createSelectSchema(semestersTable) + .strict() + .readonly(); + +export type Semester = z.infer; +export type SemesterKey = Semester["id"]; diff --git a/src/routers/schools.ts b/src/routers/schools.ts new file mode 100644 index 0000000..c4e726a --- /dev/null +++ b/src/routers/schools.ts @@ -0,0 +1,4 @@ +import { Router, json } from "express"; + +export const schoolsRouter = Router(); +schoolsRouter.use(json()); diff --git a/src/routers/semesters.ts b/src/routers/semesters.ts new file mode 100644 index 0000000..f7e385d --- /dev/null +++ b/src/routers/semesters.ts @@ -0,0 +1,4 @@ +import { Router, json } from "express"; + +export const semestersRouter = Router(); +semestersRouter.use(json());