From 7c8fdfe891a72995309b898c757f2776913b80c8 Mon Sep 17 00:00:00 2001 From: TobiasH05 Date: Thu, 27 Feb 2025 11:18:35 +0100 Subject: [PATCH 1/5] feat: :sparkles: Added table for sponsors --- db/tables/departments.ts | 4 ++-- db/tables/sponsors.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 db/tables/sponsors.ts diff --git a/db/tables/departments.ts b/db/tables/departments.ts index 5532748..128d8de 100644 --- a/db/tables/departments.ts +++ b/db/tables/departments.ts @@ -4,7 +4,7 @@ import { teamsTable } from "@db/tables/team"; import { relations } from "drizzle-orm"; import { serial } from "drizzle-orm/pg-core"; -export const cities = mainSchema.enum("city", [ +export const citiesEnum = mainSchema.enum("city", [ "Trondheim", "Ås", "Bergen", @@ -13,7 +13,7 @@ export const cities = mainSchema.enum("city", [ export const departmentsTable = mainSchema.table("departments", { id: serial("id").primaryKey(), - city: cities("city").notNull(), + city: citiesEnum("city").notNull(), }); export const departmentsRelations = relations(departmentsTable, ({ many }) => ({ diff --git a/db/tables/sponsors.ts b/db/tables/sponsors.ts new file mode 100644 index 0000000..3e0315b --- /dev/null +++ b/db/tables/sponsors.ts @@ -0,0 +1,30 @@ +import { relations } from "drizzle-orm"; +import { departmentsTable } from "./departments"; +import mainSchema from "./schema"; +import { date, integer, serial, text } from "drizzle-orm/pg-core"; + +export const sponsorSizeEnum = mainSchema.enum("size", [ + "small", + "medium", + "large" +]); + +export const sponsorsTable = mainSchema.table("sponsors", { + id: serial("id").primaryKey(), + name: text("name").notNull(), + homePageURL: text("homePageURL").notNull(), + startDate: date("startDate", { mode: "date" }).notNull(), + endDate: date("endDate", { mode: "date" }), + size: sponsorSizeEnum("size").notNull(), + spesificDepartmentId: integer("spesificDepartmentId").references(() => departmentsTable.id) +}); + +export const sponsorsRelations = relations( + sponsorsTable, + ({ one }) => ({ + department: one(departmentsTable, { + fields: [sponsorsTable.spesificDepartmentId], + references: [departmentsTable.id] + }) + }), +) \ No newline at end of file From 52e5e0b4416d14450c479315beb31d7d693334e4 Mon Sep 17 00:00:00 2001 From: TobiasH05 Date: Thu, 6 Mar 2025 10:43:07 +0100 Subject: [PATCH 2/5] feat: :sparkles: Made it possible to add new sponsor and get sponsors with id --- db/tables/sponsors.ts | 39 ++++++++-------- src/db-access/sponsors.ts | 42 +++++++++++++++++ src/main.ts | 3 ++ src/request-handling/expenses.ts | 5 +- src/request-handling/sponsors.ts | 39 ++++++++++++++++ src/response-handling/sponsors.ts | 10 ++++ src/routers/sponsors.ts | 76 +++++++++++++++++++++++++++++++ 7 files changed, 191 insertions(+), 23 deletions(-) create mode 100644 src/db-access/sponsors.ts create mode 100644 src/request-handling/sponsors.ts create mode 100644 src/response-handling/sponsors.ts create mode 100644 src/routers/sponsors.ts diff --git a/db/tables/sponsors.ts b/db/tables/sponsors.ts index 3e0315b..8e198a2 100644 --- a/db/tables/sponsors.ts +++ b/db/tables/sponsors.ts @@ -1,30 +1,29 @@ import { relations } from "drizzle-orm"; +import { date, integer, serial, text } from "drizzle-orm/pg-core"; import { departmentsTable } from "./departments"; import mainSchema from "./schema"; -import { date, integer, serial, text } from "drizzle-orm/pg-core"; export const sponsorSizeEnum = mainSchema.enum("size", [ - "small", - "medium", - "large" + "small", + "medium", + "large", ]); export const sponsorsTable = mainSchema.table("sponsors", { - id: serial("id").primaryKey(), - name: text("name").notNull(), - homePageURL: text("homePageURL").notNull(), - startDate: date("startDate", { mode: "date" }).notNull(), - endDate: date("endDate", { mode: "date" }), - size: sponsorSizeEnum("size").notNull(), - spesificDepartmentId: integer("spesificDepartmentId").references(() => departmentsTable.id) + id: serial("id").primaryKey(), + name: text("name").notNull(), + homePageURL: text("homePageURL").notNull(), + startDate: date("startDate", { mode: "date" }).notNull(), + endDate: date("endDate", { mode: "date" }), + size: sponsorSizeEnum("size").notNull(), + spesificDepartmentId: integer("spesificDepartmentId").references( + () => departmentsTable.id, + ), }); -export const sponsorsRelations = relations( - sponsorsTable, - ({ one }) => ({ - department: one(departmentsTable, { - fields: [sponsorsTable.spesificDepartmentId], - references: [departmentsTable.id] - }) - }), -) \ No newline at end of file +export const sponsorsRelations = relations(sponsorsTable, ({ one }) => ({ + department: one(departmentsTable, { + fields: [sponsorsTable.spesificDepartmentId], + references: [departmentsTable.id], + }), +})); diff --git a/src/db-access/sponsors.ts b/src/db-access/sponsors.ts new file mode 100644 index 0000000..e298b41 --- /dev/null +++ b/src/db-access/sponsors.ts @@ -0,0 +1,42 @@ +import { database } from "@db/setup/queryPostgres"; +import { sponsorsTable } from "@db/tables/sponsors"; +import { + type DatabaseResult, + handleDatabaseFullfillment, + handleDatabaseRejection, + ormError, +} from "@src/error/ormError"; +import type { NewSponsor } from "@src/request-handling/sponsors"; +import type { Sponsor, SponsorKey } from "@src/response-handling/sponsors"; +import { inArray } from "drizzle-orm"; + +export async function insertSponsors( + sponsors: NewSponsor[], +): Promise> { + return database + .transaction(async (tx) => { + const insertResult = await tx + .insert(sponsorsTable) + .values(sponsors) + .returning(); + return insertResult; + }) + .then(handleDatabaseFullfillment, handleDatabaseRejection); +} + +export async function selectSponsorsById( + sponsorIds: SponsorKey[], +): Promise> { + return database + .transaction(async (tx) => { + const selectResult = await tx + .select() + .from(sponsorsTable) + .where(inArray(sponsorsTable.id, sponsorIds)); + if (selectResult.length !== sponsorIds.length) { + throw ormError("Couldn't select sponsors, id's didn's exist."); + } + return selectResult; + }) + .then(handleDatabaseFullfillment, handleDatabaseRejection); +} diff --git a/src/main.ts b/src/main.ts index 659af6e..9023ca8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -15,6 +15,7 @@ import { teamApplicationRouter } from "./routers/team_application"; import { openapiSpecification } from "@src/openapi/config"; import openapiExpressHandler from "swagger-ui-express"; +import { sponsorsRouter } from "./routers/sponsors"; import { usersRouter } from "./routers/users"; const app = express(); @@ -33,6 +34,8 @@ app.use("/", logger); app.use("/expense", expenseRouter); app.use("/expenses", expensesRouter); +app.use("/sponsors", sponsorsRouter); + app.use("/users", usersRouter); app.use("/teamapplications", teamApplicationRouter); diff --git a/src/request-handling/expenses.ts b/src/request-handling/expenses.ts index 856ed14..0c49794 100644 --- a/src/request-handling/expenses.ts +++ b/src/request-handling/expenses.ts @@ -10,9 +10,9 @@ import { z } from "zod"; export const expenseRequestParser = z .object({ userId: serialIdParser.describe("Id of user requesting expense"), - title: z.string().nonempty().describe("Title of expense"), + title: z.string().min(1).describe("Title of expense"), moneyAmount: currencyParser.describe("Amount of money used"), - description: z.string().nonempty().describe("Description of expense"), + description: z.string().min(1).describe("Description of expense"), bankAccountNumber: z .string() .length(11) @@ -37,4 +37,3 @@ export const expenseRequestToInsertParser = expenseRequestParser .pipe(createInsertSchema(expensesTable).strict().readonly()); export type NewExpense = z.infer; -type foo = NewExpense["handlingDate"]; diff --git a/src/request-handling/sponsors.ts b/src/request-handling/sponsors.ts new file mode 100644 index 0000000..290926d --- /dev/null +++ b/src/request-handling/sponsors.ts @@ -0,0 +1,39 @@ +import { sponsorsTable } from "@db/tables/sponsors"; +import { createInsertSchema } from "drizzle-zod"; +import { z } from "zod"; +import { serialIdParser } from "./common"; + +export const sponsorRequestParser = z + .object({ + id: serialIdParser.describe("Id of sponsor"), + name: z.string().describe("Name of sponsor"), + homePageURL: z.string().url().describe("URL to homepage of sponsor"), + startDate: z + .string() + .date("Must be valid datestring (YYYY-MM-DD") + .describe("Date when sponsor started support"), + endDate: z + .string() + .date("Must be valid datestring (YYYY-MM-DD") + .nullable() + .describe("Date when sponsor ended support"), + size: z + .enum(["small", "medium", "large"]) + .describe("Size of sponsor support"), + spesificDepartmentId: serialIdParser + .nullable() + .describe("Id of department that sponsor is connected to"), + }) + .strict(); + +export const sponsorRequestToInsertParser = sponsorRequestParser + .extend({ + name: sponsorRequestParser.shape.name.trim(), + startDate: sponsorRequestParser.shape.startDate.pipe( + z.coerce.date().max(new Date()), + ), + endDate: sponsorRequestParser.shape.endDate.pipe(z.coerce.date()), + }) + .pipe(createInsertSchema(sponsorsTable).strict().readonly()); + +export type NewSponsor = z.infer; diff --git a/src/response-handling/sponsors.ts b/src/response-handling/sponsors.ts new file mode 100644 index 0000000..9873997 --- /dev/null +++ b/src/response-handling/sponsors.ts @@ -0,0 +1,10 @@ +import { sponsorsTable } from "@db/tables/sponsors"; +import { createSelectSchema } from "drizzle-zod"; +import type { z } from "zod"; + +export const sponsorsSelectSchema = createSelectSchema(sponsorsTable) + .strict() + .readonly(); + +export type Sponsor = z.infer; +export type SponsorKey = Sponsor["id"]; diff --git a/src/routers/sponsors.ts b/src/routers/sponsors.ts new file mode 100644 index 0000000..29072e9 --- /dev/null +++ b/src/routers/sponsors.ts @@ -0,0 +1,76 @@ +import { insertSponsors, selectSponsorsById } from "@src/db-access/sponsors"; +import { clientError } from "@src/error/httpErrors"; +import { toSerialIdParser } from "@src/request-handling/common"; +import { sponsorRequestToInsertParser } from "@src/request-handling/sponsors"; +import { Router, json } from "express"; + +export const sponsorsRouter = Router(); +sponsorsRouter.use(json()); + +/** + * @openapi + * /sponsor/: + * post: + * tags: [sponsors] + * summary: Add sponsor + * description: Add sponsor + * requestBody: + * required: true + * content: + * json: + * schema: + * $ref: "#/components/schemas/sponsorRequest" + * responses: + * 201: + * description: Successfull response + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/sponsor" + */ +sponsorsRouter.post("/", async (req, res, next) => { + const sponsorRequest = sponsorRequestToInsertParser.safeParse(req.body); + if (!sponsorRequest.success) { + const error = clientError( + 400, + "Failed parsing sponsorrequest.", + sponsorRequest.error, + ); + return next(error); + } + const databaseResult = await insertSponsors([sponsorRequest.data]); + if (!databaseResult.success) { + const error = clientError(400, "Database error", databaseResult.error); + return next(error); + } + res.status(201).json(databaseResult.data); +}); + +/** + * @openapi + * /sponsor/{id}/: + * get: + * tags: [sponsors] + * summary: Get sponsor with id + * description: Get sponsor with id + * parameters: + * - $ref: "#/components/parameters/id" + * responses: + * 200: + * description: Successfull response + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/sponsor" + */ +sponsorsRouter.get("/:sponsorId", async (req, res, next) => { + const sponsorIdResult = toSerialIdParser.safeParse(req.params.sponsorId); + if (!sponsorIdResult.success) { + return next(clientError(400, "", sponsorIdResult.error)); + } + const databaseResult = await selectSponsorsById([sponsorIdResult.data]); + if (!databaseResult.success) { + return next(clientError(400, "Database error", databaseResult.error)); + } + res.json(databaseResult.data); +}); From eefb5e628a0a8c3f8a78f3a470e7063c69e7ae5a Mon Sep 17 00:00:00 2001 From: TobiasH05 Date: Thu, 6 Mar 2025 10:56:17 +0100 Subject: [PATCH 3/5] fix: :pencil2: Fixed variable names when merging with main --- src/db-access/sponsors.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/db-access/sponsors.ts b/src/db-access/sponsors.ts index e298b41..d102f24 100644 --- a/src/db-access/sponsors.ts +++ b/src/db-access/sponsors.ts @@ -1,7 +1,7 @@ import { database } from "@db/setup/queryPostgres"; import { sponsorsTable } from "@db/tables/sponsors"; import { - type DatabaseResult, + type ORMResult, handleDatabaseFullfillment, handleDatabaseRejection, ormError, @@ -12,7 +12,7 @@ import { inArray } from "drizzle-orm"; export async function insertSponsors( sponsors: NewSponsor[], -): Promise> { +): Promise> { return database .transaction(async (tx) => { const insertResult = await tx @@ -26,7 +26,7 @@ export async function insertSponsors( export async function selectSponsorsById( sponsorIds: SponsorKey[], -): Promise> { +): Promise> { return database .transaction(async (tx) => { const selectResult = await tx From fce0a5f133e4453f1e62b9b50bf3d39eb3d6f0ef Mon Sep 17 00:00:00 2001 From: TobiasH05 Date: Thu, 6 Mar 2025 11:10:53 +0100 Subject: [PATCH 4/5] docs: :memo: Fixed openApi docs --- src/openapi/config.ts | 4 ++++ src/routers/sponsors.ts | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/openapi/config.ts b/src/openapi/config.ts index f3ed343..099e8a1 100644 --- a/src/openapi/config.ts +++ b/src/openapi/config.ts @@ -24,6 +24,8 @@ import { } from "@src/response-handling/users"; import openapiFromJsdoc from "swagger-jsdoc"; import { createDocument } from "zod-openapi"; +import { sponsorsSelectSchema } from "@src/response-handling/sponsors"; +import { sponsorRequestParser } from "@src/request-handling/sponsors"; const openapiDocument = createDocument({ openapi: "3.1.0", @@ -70,6 +72,8 @@ const openapiDocument = createDocument({ schemas: { expenseRequest: expenseRequestParser, expense: expensesSelectSchema, + sponsorRequest: sponsorRequestParser, + sponsor: sponsorsSelectSchema, user: userSelectSchema, teamUser: teamUserSelectSchema, assistantUser: assistantUserSelectSchema, diff --git a/src/routers/sponsors.ts b/src/routers/sponsors.ts index 29072e9..d7beb34 100644 --- a/src/routers/sponsors.ts +++ b/src/routers/sponsors.ts @@ -9,7 +9,7 @@ sponsorsRouter.use(json()); /** * @openapi - * /sponsor/: + * /sponsors/: * post: * tags: [sponsors] * summary: Add sponsor @@ -48,7 +48,7 @@ sponsorsRouter.post("/", async (req, res, next) => { /** * @openapi - * /sponsor/{id}/: + * /sponsors/{id}/: * get: * tags: [sponsors] * summary: Get sponsor with id From 61024c15884b7b7a207cee57a5c8a069d7ec6c42 Mon Sep 17 00:00:00 2001 From: TobiasH05 Date: Thu, 6 Mar 2025 11:11:55 +0100 Subject: [PATCH 5/5] style: :art: check --- src/openapi/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/openapi/config.ts b/src/openapi/config.ts index 099e8a1..8c35960 100644 --- a/src/openapi/config.ts +++ b/src/openapi/config.ts @@ -9,6 +9,7 @@ import { 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_application"; import { assistantUserRequestParser, @@ -16,6 +17,7 @@ import { userRequestParser, } from "@src/request-handling/users"; import { expensesSelectSchema } from "@src/response-handling/expenses"; +import { sponsorsSelectSchema } from "@src/response-handling/sponsors"; import { teamApplicationSelectSchema } from "@src/response-handling/team_application"; import { assistantUserSelectSchema, @@ -24,8 +26,6 @@ import { } from "@src/response-handling/users"; import openapiFromJsdoc from "swagger-jsdoc"; import { createDocument } from "zod-openapi"; -import { sponsorsSelectSchema } from "@src/response-handling/sponsors"; -import { sponsorRequestParser } from "@src/request-handling/sponsors"; const openapiDocument = createDocument({ openapi: "3.1.0",