diff --git a/src/controllers/EventController.ts b/src/controllers/EventController.ts index c496f53d..2995254e 100644 --- a/src/controllers/EventController.ts +++ b/src/controllers/EventController.ts @@ -1,22 +1,28 @@ -import { JsonController, Param, Get, Post, Delete, Body } from 'routing-controllers'; +import { JsonController, Param, Get, Post, Delete, Body, OnUndefined } from 'routing-controllers'; import { singleton, inject } from 'tsyringe'; import { ResponseSchema } from 'routing-controllers-openapi'; -import { Event } from '@Entities'; -import { EventRequest, EventResponse, MultipleEventResponse } from '@Payloads'; -import { EventService } from '@Services'; -import { EventMapper } from '@Mappers'; +import { Event, Attendance } from '@Entities'; +import { EventRequest, EventResponse, MultipleEventResponse, EventSignInRequest } from '@Payloads'; +import { AppUserService, EventService } from '@Services'; +import { AppUserMapper, EventMapper } from '@Mappers'; @singleton() @JsonController('/api/events') export class EventController { + private appUserService: AppUserService; + private appUserMapper: AppUserMapper; private eventService: EventService; private eventMapper: EventMapper; constructor( + @inject(AppUserService) appUserService: AppUserService, + @inject(AppUserMapper) appUserMapper: AppUserMapper, @inject(EventService) eventService: EventService, @inject(EventMapper) eventMapper: EventMapper ) { + this.appUserService = appUserService; + this.appUserMapper = appUserMapper; this.eventService = eventService; this.eventMapper = eventMapper; } @@ -75,4 +81,25 @@ export class EventController { return this.eventMapper.entityToResponse(deletedEvent); } + + @Post('/:eventID/signin') + @OnUndefined(409) + @ResponseSchema(Attendance) + async signInToEvent( + @Param('eventID') eventID: number, + @Body() appUserRequest: EventSignInRequest + ): Promise { + const appUserToSave = await this.appUserMapper.requestToEntityByEmail(appUserRequest); + const savedAppUser = await this.appUserService.saveNonAffiliate(appUserToSave); + + if (savedAppUser === undefined) { + /* + * TODO: Handle the case where user is an affiliate + * If affiliated user has a token, then verify it and proceed. If said + * user does not have a token, then send back an HTTP error. + */ + } + + return await this.eventService.registerForEventAttendance(eventID, savedAppUser); + } } diff --git a/src/entities/AppUser.ts b/src/entities/AppUser.ts index 010b40d1..8ae0502a 100644 --- a/src/entities/AppUser.ts +++ b/src/entities/AppUser.ts @@ -7,6 +7,7 @@ export enum AppUserRole { OFFICER = 'officer', MEMBER = 'member', INDUCTEE = 'inductee', + GUEST = 'guest', } /** @@ -41,7 +42,7 @@ export class AppUser { @Column({ type: 'enum', enum: AppUserRole, - default: AppUserRole.INDUCTEE, + default: AppUserRole.GUEST, }) role: string; } diff --git a/src/entities/Attendance.ts b/src/entities/Attendance.ts index 17787d57..07a42203 100644 --- a/src/entities/Attendance.ts +++ b/src/entities/Attendance.ts @@ -1,4 +1,4 @@ -import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm'; +import { Entity, Column, ManyToOne } from 'typeorm'; import { AppUser } from './AppUser'; import { Event } from './Event'; @@ -10,21 +10,18 @@ import { Event } from './Event'; */ @Entity() export class Attendance { - @PrimaryGeneratedColumn() - id: number; - - @ManyToOne(() => AppUser) + @ManyToOne(() => AppUser, { primary: true }) attendee: AppUser; // Officer who checked off attendee @ManyToOne(() => AppUser) officer: AppUser; - @ManyToOne(() => Event) + @ManyToOne(() => Event, { primary: true }) event: Event; // num_hours - @Column('float') + @Column('float', { nullable: true }) duration: number; // indicates whether or not attendee was inductee at time of attendance diff --git a/src/entities/index.ts b/src/entities/index.ts index 3ebafc08..d58ce2ee 100644 --- a/src/entities/index.ts +++ b/src/entities/index.ts @@ -1,4 +1,4 @@ -export { AppUser } from './AppUser'; +export { AppUser, AppUserRole } from './AppUser'; export { Attendance } from './Attendance'; export { Event, EventStatus, EventType } from './Event'; export { InducteeStat } from './InducteeStat'; diff --git a/src/loaders/RepositoryLoader.ts b/src/loaders/RepositoryLoader.ts index fa52fb28..a688d2af 100644 --- a/src/loaders/RepositoryLoader.ts +++ b/src/loaders/RepositoryLoader.ts @@ -1,10 +1,25 @@ import { container } from 'tsyringe'; -import { EventRepositoryToken, EventRepositoryFactory } from '@Repositories'; -import { Event } from '@Entities'; +import { + EventRepositoryToken, + EventRepositoryFactory, + AppUserRepositoryToken, + AppUserRepositoryFactory, + AttendanceRepositoryToken, + AttendanceRepositoryFactory, +} from '@Repositories'; +import { Event, AppUser, Attendance } from '@Entities'; import { Repository } from 'typeorm'; export function loadRepositories(): void { container.register>(EventRepositoryToken, { useFactory: EventRepositoryFactory, }); + + container.register>(AppUserRepositoryToken, { + useFactory: AppUserRepositoryFactory, + }); + + container.register>(AttendanceRepositoryToken, { + useFactory: AttendanceRepositoryFactory, + }); } diff --git a/src/mappers/AppUserMapper.ts b/src/mappers/AppUserMapper.ts new file mode 100644 index 00000000..9f05af6a --- /dev/null +++ b/src/mappers/AppUserMapper.ts @@ -0,0 +1,81 @@ +import { EventSignInRequest, EventSignInResponse } from '@Payloads'; +import { AppUser } from '@Entities'; +import { AppUserService } from '@Services'; +import { AppUserRepositoryToken } from '@Repositories'; + +import { Repository } from 'typeorm'; +import { classToPlain, plainToClass } from 'class-transformer'; +import { singleton, inject } from 'tsyringe'; + +@singleton() +export class AppUserMapper { + private appUserRepository: Repository; + private appUserService: AppUserService; + + constructor( + @inject(AppUserRepositoryToken) appUserRepository: Repository, + @inject(AppUserService) appUserService: AppUserService + ) { + this.appUserRepository = appUserRepository; + this.appUserService = appUserService; + } + + /** + * Converts an EventSignInRequest payload to an AppUser entity and + * returns the newly created entity to the caller. + * + * @param appUserRequest The request payload from which the AppUser entity + * is created. + */ + requestToNewEntity(appUserRequest: EventSignInRequest): AppUser { + const plainAppUserRequest: Object = classToPlain(appUserRequest); + return this.appUserRepository.create(plainAppUserRequest); + } + + /** + * Updates an existing AppUser entity with new data from the request payload. + * If there is no AppUser entity with the passed-in appUserId, then return undefined. + * + * @param appUserRequest The request payload from which the updated existing AppUser + * entity is created + * @param appUserId The supposed ID of an existing AppUser entity. + */ + async requestToExistingEntity( + appUserRequest: EventSignInRequest, + appUserId: number + ): Promise { + const appUserObj: AppUser = appUserRequest as AppUser; + appUserObj.id = appUserId; + + const appUser: AppUser = await this.appUserRepository.preload(appUserObj); + + return appUser; + } + + async requestToEntityByEmail(appUserRequest: EventSignInRequest): Promise { + const { email } = appUserRequest; + const appUserFromEmail = await this.appUserService.getAppUserByEmail(email); + + if (appUserFromEmail === undefined) { + return this.requestToNewEntity(appUserRequest); + } else { + const { id } = appUserFromEmail; + + return await this.requestToExistingEntity(appUserRequest, id); + } + } + + /** + * Converts an AppUser entity to an EventSignInResponse payload and returns the + * newly created response payload to the caller. + * + * @param appUser The AppUser entity to be converted to an EventSignInResponse + * payload. + */ + entityToResponse(appUser: AppUser): EventSignInResponse { + const plainAppUser: Object = classToPlain(appUser); + const appUserResponse: EventSignInResponse = plainToClass(EventSignInResponse, plainAppUser); + + return appUserResponse; + } +} diff --git a/src/mappers/index.ts b/src/mappers/index.ts index a2cb8d47..063e97a1 100644 --- a/src/mappers/index.ts +++ b/src/mappers/index.ts @@ -1 +1,2 @@ export { EventMapper } from './EventMapper'; +export { AppUserMapper } from './AppUserMapper'; diff --git a/src/payloads/AppUser.ts b/src/payloads/AppUser.ts index ce030a74..e62b8f49 100644 --- a/src/payloads/AppUser.ts +++ b/src/payloads/AppUser.ts @@ -1,6 +1,20 @@ -import { IsInt } from 'class-validator'; +import { IsInt, IsString, IsEmail } from 'class-validator'; export class AppUserPKPayload { @IsInt() readonly id: number; } + +export class BaseAppUserPayload { + @IsString() + readonly firstName: string; + + @IsString() + readonly lastName: string; + + @IsEmail() + readonly email: string; + + @IsString() + readonly major: string; +} diff --git a/src/payloads/Event.ts b/src/payloads/Event.ts index bc850e2d..19863ebc 100644 --- a/src/payloads/Event.ts +++ b/src/payloads/Event.ts @@ -10,8 +10,8 @@ import { } from 'class-validator'; import { Type } from 'class-transformer'; -import { AppUser, EventType, EventStatus } from '@Entities'; -import { AppUserPKPayload } from '@Payloads'; +import { AppUser, AppUserRole, EventType, EventStatus } from '@Entities'; +import { AppUserPKPayload, BaseAppUserPayload } from './AppUser'; abstract class BaseEventPayload { @IsString() @@ -73,3 +73,13 @@ export class MultipleEventResponse { @Type(() => EventResponse) events: EventResponse[]; } + +export class EventSignInRequest extends BaseAppUserPayload {} + +export class EventSignInResponse extends BaseAppUserPayload { + @IsInt() + id: number; + + @IsEnum(AppUserRole) + role: string; +} diff --git a/src/payloads/index.ts b/src/payloads/index.ts index 00981544..af8c05bb 100644 --- a/src/payloads/index.ts +++ b/src/payloads/index.ts @@ -1,2 +1,8 @@ -export { EventRequest, EventResponse, MultipleEventResponse } from './Event'; -export { AppUserPKPayload } from './AppUser'; +export { + EventRequest, + EventResponse, + MultipleEventResponse, + EventSignInRequest, + EventSignInResponse, +} from './Event'; +export { AppUserPKPayload, BaseAppUserPayload } from './AppUser'; diff --git a/src/services/AppUserService.ts b/src/services/AppUserService.ts index 905e7ac7..a30a235f 100644 --- a/src/services/AppUserService.ts +++ b/src/services/AppUserService.ts @@ -1,5 +1,5 @@ -import { AppUser } from '@Entities'; -import { singleton } from 'tsyringe'; +import { AppUser, AppUserRole } from '@Entities'; +import { singleton, inject } from 'tsyringe'; import { Any, Repository, getRepository } from 'typeorm'; @singleton() @@ -10,6 +10,16 @@ export class AppUserService { this.appUserRepository = getRepository(AppUser); } + /** + * Stores the AppUser passed in as a parameter to the + * AppUser table. + * + * @param appUser The AppUser to be stored to the db. + */ + async saveAppUser(appUser: AppUser): Promise { + return await this.appUserRepository.save(appUser); + } + /** * Get multiple app users. * @@ -18,4 +28,24 @@ export class AppUserService { getMultipleAppUsers(ids: number[]): Promise { return this.appUserRepository.find({ id: Any(ids) }); } + + /** + * Get an existing AppUser by their email address. + * + * @param email The email used to look for the corresponding AppUser. + * + */ + getAppUserByEmail(email: string): Promise { + return this.appUserRepository.findOne({ email }); + } + + async saveNonAffiliate(appUser: AppUser): Promise { + const { role } = appUser; + + if (role !== undefined && role !== AppUserRole.GUEST) { + return undefined; + } + + return await this.saveAppUser(appUser); + } } diff --git a/src/services/AttendanceService.ts b/src/services/AttendanceService.ts new file mode 100644 index 00000000..6c6a632b --- /dev/null +++ b/src/services/AttendanceService.ts @@ -0,0 +1,28 @@ +import { Attendance, AppUser, AppUserRole, Event } from '@Entities'; +import { AttendanceRepositoryToken } from '@Repositories'; + +import { Repository } from 'typeorm'; +import { singleton, inject } from 'tsyringe'; + +@singleton() +export class AttendanceService { + private attendanceRepository: Repository; + + constructor(@inject(AttendanceRepositoryToken) attendanceRepository: Repository) { + this.attendanceRepository = attendanceRepository; + } + + async registerAttendance(event: Event, attendee: AppUser): Promise { + const { role } = attendee; + const attendance = { event, attendee, isInductee: role === AppUserRole.INDUCTEE }; + const newAttendance = this.attendanceRepository.create(attendance); + + try { + await this.attendanceRepository.insert(newAttendance); + } catch { + return undefined; + } + + return newAttendance; + } +} diff --git a/src/services/EventService.ts b/src/services/EventService.ts index df07f7cf..6c32a63b 100644 --- a/src/services/EventService.ts +++ b/src/services/EventService.ts @@ -1,5 +1,6 @@ -import { Event } from '@Entities'; +import { Event, AppUser, Attendance } from '@Entities'; import { EventRepositoryToken } from '@Repositories'; +import { AttendanceService } from './AttendanceService'; import { Repository } from 'typeorm'; import { singleton, inject } from 'tsyringe'; @@ -7,9 +8,14 @@ import { singleton, inject } from 'tsyringe'; @singleton() export class EventService { private eventRepository: Repository; + private attendanceService: AttendanceService; - constructor(@inject(EventRepositoryToken) eventRepository: Repository) { + constructor( + @inject(EventRepositoryToken) eventRepository: Repository, + @inject(AttendanceService) attendanceService: AttendanceService + ) { this.eventRepository = eventRepository; + this.attendanceService = attendanceService; } /** @@ -50,4 +56,19 @@ export class EventService { const event = await this.eventRepository.findOne({ id }); return event ? this.eventRepository.remove(event) : undefined; } + + /** + * Creates an Attendance entity using the event obtained from eventId and + * the passed-in AppUser entity, then stores that Attendance entity to the + * Attendance table. + * + * @param eventId + * @param appUser + */ + async registerForEventAttendance(eventId: number, appUser: AppUser): Promise { + const event = await this.eventRepository.findOne({ id: eventId }); + const newAttendance = await this.attendanceService.registerAttendance(event, appUser); + + return newAttendance; + } } diff --git a/src/services/index.ts b/src/services/index.ts index b6985aca..65e2ab2e 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,2 +1,3 @@ export { EventService } from './EventService'; export { AppUserService } from './AppUserService'; +export { AttendanceService } from './AttendanceService';