diff --git a/packages/api/src/controllers/membersController.ts b/packages/api/src/controllers/membersController.ts index d633d1c..d09af88 100644 --- a/packages/api/src/controllers/membersController.ts +++ b/packages/api/src/controllers/membersController.ts @@ -1,13 +1,90 @@ -import { Controller, Route } from "@tsoa/runtime"; +import { + Body, + Controller, + Delete, + Get, + Path, + Post, + Put, + Response, + Route, + Security, + SuccessResponse, +} from "@tsoa/runtime"; import { inject } from "../infrastructure/di/index.js"; +import { ErrorStates } from "../infrastructure/errorStates.js"; +import { Claim } from "../models/claim.js"; +import type { Member, MemberCreateArgs } from "../models/member.js"; +import { KC_SECURITY_NAME } from "../services/keycloak/user/index.js"; import { MembersService } from "../services/membersService.js"; @Route("members") export class MembersContoller extends Controller { private readonly membersService = inject(MembersService); - // @Security(KC_SECURITY_NAME) - // @Get() - // public async getMembers(): Promise { - // return await this.membersService.getMembers(); - // } + @Security(KC_SECURITY_NAME, [Claim.MemberAdmin]) + @Get() + public async getMembers(): Promise { + return await this.membersService.getMembers(); + } + + @Security(KC_SECURITY_NAME, [Claim.MemberAdmin]) + @SuccessResponse("200", "OK") + @Response(404, "Member not found") + @Get("{memberId}") + public async getMember(@Path() memberId: string): Promise { + try { + const foundMember = + await this.membersService.getMemberById(memberId); + return foundMember; + } catch (error) { + if ( + error instanceof Error && + error.cause === ErrorStates.ENTITY_NOT_FOUND + ) { + this.setStatus(404); + throw new Error(`Member with id ${memberId} not found`); + } + throw error; + } + } + + @Security(KC_SECURITY_NAME, [Claim.MemberAdmin]) + @SuccessResponse("201", "Created") + @Post() + public async createMember( + @Body() createArgs: MemberCreateArgs, + ): Promise { + return await this.membersService.createMember(createArgs); + } + + @Security(KC_SECURITY_NAME, [Claim.MemberAdmin]) + @SuccessResponse("200", "OK") + @Response(400, "Bad Request") + @Response(404, "Member not found") + @Put() + public async updateMember(@Body() member: Member): Promise { + try { + await this.membersService.updateMember(member); + } catch (error) { + if ( + error instanceof Error && + error.cause === ErrorStates.BAD_REQUEST + ) { + this.setStatus(400); + throw new Error(`Member id is required for update`); + } + throw error; + } + } + + @Security(KC_SECURITY_NAME, [Claim.MemberAdmin]) + @SuccessResponse("200", "OK") + @Response(404, "Member not found") + @Delete("{memberId}") + public async scheduleDeletion( + @Path() memberId: string, + @Body() cancelDate: Date + ){ + await this.membersService.scheduleDeletion(memberId, cancelDate); + } } diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 95f8d80..f0614bf 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1,6 +1,6 @@ import express from "express"; import { RegisterRoutes } from "./generated/routes.js"; -import { sessionHandler } from "./infrastructure/session.js"; +import { sessionHandler } from "./infrastructure/sessionHandler.js"; import { keycloak } from "./services/keycloak/user/index.js"; const app = express(); diff --git a/packages/api/src/infrastructure/database.ts b/packages/api/src/infrastructure/database.ts deleted file mode 100644 index 139597f..0000000 --- a/packages/api/src/infrastructure/database.ts +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/packages/api/src/infrastructure/errorStates.ts b/packages/api/src/infrastructure/errorStates.ts new file mode 100644 index 0000000..d8a10b8 --- /dev/null +++ b/packages/api/src/infrastructure/errorStates.ts @@ -0,0 +1,5 @@ +export enum ErrorStates { + UNKNOWN_ERROR = 0, + ENTITY_NOT_FOUND = 1, + BAD_REQUEST = 2, +} diff --git a/packages/api/src/infrastructure/session.ts b/packages/api/src/infrastructure/sessionHandler.ts similarity index 100% rename from packages/api/src/infrastructure/session.ts rename to packages/api/src/infrastructure/sessionHandler.ts diff --git a/packages/api/src/models/member.ts b/packages/api/src/models/member.ts index d7e4bac..6a736ac 100644 --- a/packages/api/src/models/member.ts +++ b/packages/api/src/models/member.ts @@ -1,17 +1,38 @@ +import { type MemberModel as DbMember } from "../generated/prisma/models.js"; + export interface Member { - id?: string | undefined; - vorname?: string | undefined; - nachname?: string | undefined; - geburtsdatum?: string | undefined; - geburtsort?: string | undefined; - geschlecht?: string | undefined; - passnummer?: string | undefined; - uvertrag?: boolean | undefined; - verein?: boolean | undefined; - graduierung?: string | undefined; - letztePruefung?: string | undefined; - kontakt?: string | undefined; - kuendigungzum?: string | undefined; - loeschenam?: string | undefined; - level?: string | undefined; + id?: string | null; + vorname?: string | null; + nachname?: string | null; + geburtsdatum?: Date | null; + geburtsort?: string | null; + geschlecht?: string | null; + passnummer?: string | null; + uvertrag?: boolean | null; + verein?: boolean | null; + graduierung?: string | null; + letztePruefung?: Date | null; + kontakt?: string | null; + kuendigungzum?: Date | null; + loeschenam?: string | null; +} + +export type MemberCreateArgs = Omit; + +export function mapDbMemberToMember(dbMember: DbMember): Member { + return { + geburtsdatum: dbMember.geburtsdatum, + id: dbMember.id, + nachname: dbMember.nachname, + geburtsort: dbMember.geburtsort, + geschlecht: dbMember.geschlecht, + graduierung: dbMember.graduierung, + vorname: dbMember.vorname, + kontakt: dbMember.kontakt, + kuendigungzum: dbMember.kuendigungzum, + letztePruefung: dbMember.letztePruefung, + passnummer: dbMember.passnummer, + uvertrag: dbMember.uvertrag, + verein: dbMember.verein, + }; } diff --git a/packages/api/src/services/db/prisma.ts b/packages/api/src/services/db/prisma.ts new file mode 100644 index 0000000..8947b44 --- /dev/null +++ b/packages/api/src/services/db/prisma.ts @@ -0,0 +1,62 @@ +import { PrismaPg } from "@prisma/adapter-pg"; +import { PrismaClient } from "../../generated/prisma/client.js"; + +type ExtendedClient = ReturnType; + +export type SafeClient = Omit< + ExtendedClient, + | "$connect" + | "$disconnect" + | "$on" + | "$transaction" + | "$executeRaw" + | "$executeRawUnsafe" + | "$queryRaw" + | "$queryRawUnsafe" + | "$extends" +>; + +function createExtendedClient(client: PrismaClient) { + return client; + // .$extends({ + // name: "testExtension", + // model: { + // member: { + // withFullName(member: Member) { + // return { + // ...member, + // fullName: ` ${member.vorname} ${member.nachname}`, + // }; + // }, + // }, + // }, + // }); +} + +export class DatabaseService { + private readonly prismaClient: ExtendedClient; + private readonly safeClient: SafeClient; + + public constructor() { + const dbUser = process.env["TKD_DB_USER"]; + const dbPassword = process.env["TKD_DB_PASSWORD"]; + const dbHost = process.env["TKD_DB_HOST"]; + + const connectionString = `postgresql://${dbUser}:${dbPassword}@${dbHost}:5432/taekwondo?schema=public`; + + const adapter = new PrismaPg({ connectionString }); + this.prismaClient = createExtendedClient(new PrismaClient({ adapter })); + this.safeClient = this.prismaClient as SafeClient; + + // this.prismaClient.$connect(); + // this.prismaClient.$disconnect(); + // this.prismaClient.$on(); + // this.prismaClient.$transaction(); + } + + public async doRequest( + request: (prisma: SafeClient) => Promise, + ): Promise { + return await request(this.safeClient); + } +} diff --git a/packages/api/src/services/keycloak/user/keycloak-user.ts b/packages/api/src/services/keycloak/user/keycloak-user.ts index 078988f..c5f9439 100644 --- a/packages/api/src/services/keycloak/user/keycloak-user.ts +++ b/packages/api/src/services/keycloak/user/keycloak-user.ts @@ -1,5 +1,5 @@ import KeycloakConnect from "keycloak-connect"; -import { sessionStore } from "../../../infrastructure/session.js"; +import { sessionStore } from "../../../infrastructure/sessionHandler.js"; import keycloakConfig from "./keycloak.json" with { type: "json" }; export const KC_SECURITY_NAME = "bearerAuth"; diff --git a/packages/api/src/services/membersService.ts b/packages/api/src/services/membersService.ts index 5f2ee09..27060a6 100644 --- a/packages/api/src/services/membersService.ts +++ b/packages/api/src/services/membersService.ts @@ -1,6 +1,67 @@ -import { Injectable } from "../infrastructure/di/index.js"; +import { inject, Injectable } from "../infrastructure/di/index.js"; +import { ErrorStates } from "../infrastructure/errorStates.js"; +import { + type Member, + type MemberCreateArgs, + mapDbMemberToMember, +} from "../models/member.js"; +import { DatabaseService } from "./db/prisma.js"; @Injectable() export class MembersService { - // public async getMembers(): Promise {} + private readonly database = inject(DatabaseService); + + public async getMembers(): Promise { + const dbMembers = await this.database.doRequest(prisma => + prisma.member.findMany(), + ); + return dbMembers.map(mapDbMemberToMember); + } + + public async getMemberById(memberId: string): Promise { + const member = await this.database.doRequest(prisma => + prisma.member.findFirst({ where: { id: memberId } }), + ); + + if (!member) { + throw new Error(`Member with id ${memberId} not found`, { + cause: ErrorStates.ENTITY_NOT_FOUND, + }); + } + + return mapDbMemberToMember(member); + } + + public async createMember(createArgs: MemberCreateArgs): Promise { + const newMember = await this.database.doRequest(prisma => + prisma.member.create({ data: createArgs }), + ); + return mapDbMemberToMember(newMember); + } + + public async updateMember(member: Member): Promise { + const id = member.id; + if (!id) { + throw new Error("Member id is required for update", { + cause: ErrorStates.BAD_REQUEST, + }); + } + const modifiedMember = await this.database.doRequest(prisma => + prisma.member.update({ + data: { ...member, id: id }, + where: { id: id }, + }), + ); + + return mapDbMemberToMember(modifiedMember); + } + + public async scheduleDeletion(memberId: string, cancelDate: Date) { + await this.database.doRequest(prisma => + prisma.member.update({ + where: { id: memberId }, + data: { kuendigungzum: cancelDate }, + }), + ); + } }