Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Event sign in endpoint (Ready for review) #44

Merged
merged 9 commits into from
Aug 16, 2020
48 changes: 44 additions & 4 deletions src/controllers/EventController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,27 @@ import { JsonController, Param, Get, Post, Delete, Body } from 'routing-controll
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, AppUserRole, 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;
}
Expand Down Expand Up @@ -75,4 +81,38 @@ export class EventController {

return this.eventMapper.entityToResponse(deletedEvent);
}

@Post('/:eventID/signin')
@ResponseSchema(Attendance)
godwinpang marked this conversation as resolved.
Show resolved Hide resolved
async signInToEvent(
@Param('eventID') eventID: number,
@Body() appUserRequest: EventSignInRequest
): Promise<Attendance> {
godwinpang marked this conversation as resolved.
Show resolved Hide resolved
const { email } = appUserRequest;
const appUserFromEmail = await this.appUserService.getAppUserByEmail(email);

if (appUserFromEmail == undefined) {
const newAppUser = this.appUserMapper.requestToNewEntity(appUserRequest);
const savedAppUser = await this.appUserService.saveAppUser(newAppUser);
await this.eventService.registerAttendance(eventID, savedAppUser);

return null;
} else {
const { id, role } = appUserFromEmail;

if (role !== AppUserRole.GUEST) {
/*
* 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.
*/
} else {
const updatedAppUser = await this.appUserMapper.requestToExistingEntity(appUserRequest, id);
const savedUpdatedUser = await this.appUserService.saveAppUser(updatedAppUser);
await this.eventService.registerAttendance(eventID, savedUpdatedUser);

return null;
}
}
}
}
3 changes: 2 additions & 1 deletion src/entities/AppUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export enum AppUserRole {
OFFICER = 'officer',
MEMBER = 'member',
INDUCTEE = 'inductee',
GUEST = 'guest',
}

/**
Expand Down Expand Up @@ -41,7 +42,7 @@ export class AppUser {
@Column({
type: 'enum',
enum: AppUserRole,
default: AppUserRole.INDUCTEE,
default: AppUserRole.GUEST,
})
role: string;
}
11 changes: 4 additions & 7 deletions src/entities/Attendance.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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 })
godwinpang marked this conversation as resolved.
Show resolved Hide resolved
duration: number;

// indicates whether or not attendee was inductee at time of attendance
Expand Down
2 changes: 1 addition & 1 deletion src/entities/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
19 changes: 17 additions & 2 deletions src/loaders/RepositoryLoader.ts
Original file line number Diff line number Diff line change
@@ -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<Repository<Event>>(EventRepositoryToken, {
useFactory: EventRepositoryFactory,
});

container.register<Repository<AppUser>>(AppUserRepositoryToken, {
useFactory: AppUserRepositoryFactory,
});

container.register<Repository<Attendance>>(AttendanceRepositoryToken, {
useFactory: AttendanceRepositoryFactory,
});
}
65 changes: 65 additions & 0 deletions src/mappers/AppUserMapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { EventSignInRequest, EventSignInResponse } from '@Payloads';
import { AppUser } from '@Entities';
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<AppUser>;

constructor(@inject(AppUserRepositoryToken) appUserRepository: Repository<AppUser>) {
this.appUserRepository = appUserRepository;
}

/**
* 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<AppUser | undefined> {
const appUserObj: AppUser = appUserRequest as AppUser;
appUserObj.id = appUserId;

const appUser: AppUser = await this.appUserRepository.preload(appUserObj);
if (appUser == undefined) {
godwinpang marked this conversation as resolved.
Show resolved Hide resolved
return undefined;
}

return appUser;
}

/**
* 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;
}
}
1 change: 1 addition & 0 deletions src/mappers/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { EventMapper } from './EventMapper';
export { AppUserMapper } from './AppUserMapper';
39 changes: 38 additions & 1 deletion src/payloads/AppUser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,43 @@
import { IsInt } from 'class-validator';
import { IsInt, IsString, IsEmail, IsEnum } from 'class-validator';
import { AppUserRole } from '@Entities';

export class AppUserPKPayload {
@IsInt()
readonly id: number;
}

abstract class BaseAppUserPayload {
@IsString()
readonly firstName: string;

@IsString()
readonly lastName: string;

@IsEmail()
readonly email: string;

@IsString()
readonly major: string;
}

export class EventSignInRequest extends BaseAppUserPayload {}
godwinpang marked this conversation as resolved.
Show resolved Hide resolved

export class EventSignInResponse {
@IsInt()
id: number;

@IsString()
firstName: string;

@IsString()
lastName: string;

@IsEmail()
email: string;

@IsString()
major: string;

@IsEnum(AppUserRole)
role: string;
}
2 changes: 1 addition & 1 deletion src/payloads/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { EventRequest, EventResponse, MultipleEventResponse } from './Event';
export { AppUserPKPayload } from './AppUser';
export { AppUserPKPayload, EventSignInRequest, EventSignInResponse } from './AppUser';
20 changes: 20 additions & 0 deletions src/services/AppUserService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppUser> {
return this.appUserRepository.save(appUser);
}

/**
* Get multiple app users.
*
Expand All @@ -18,4 +28,14 @@ export class AppUserService {
getMultipleAppUsers(ids: number[]): Promise<AppUser[]> {
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<AppUser> {
return this.appUserRepository.findOne({ email });
}
}
24 changes: 24 additions & 0 deletions src/services/AttendanceService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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<Attendance>;

constructor(@inject(AttendanceRepositoryToken) attendanceRepository: Repository<Attendance>) {
this.attendanceRepository = attendanceRepository;
}

createAttendance(event: Event, attendee: AppUser): Attendance {
godwinpang marked this conversation as resolved.
Show resolved Hide resolved
const { role } = attendee;
const attendance = { event, attendee, isInductee: role === AppUserRole.INDUCTEE };
return this.attendanceRepository.create(attendance);
}

async saveAttendance(attendance: Attendance): Promise<Attendance> {
return this.attendanceRepository.save(attendance);
}
}
28 changes: 25 additions & 3 deletions src/services/EventService.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { Event } from '@Entities';
import { Event, AppUser, Attendance } from '@Entities';
import { EventRepositoryToken } from '@Repositories';
import { AttendanceService } from '@Services';

import { Repository } from 'typeorm';
import { singleton, inject } from 'tsyringe';
import { singleton, inject, delay } from 'tsyringe';

@singleton()
export class EventService {
private eventRepository: Repository<Event>;
private attendanceService: AttendanceService;

constructor(@inject(EventRepositoryToken) eventRepository: Repository<Event>) {
constructor(
@inject(EventRepositoryToken) eventRepository: Repository<Event>,
@inject(delay(() => AttendanceService)) attendanceService: AttendanceService
) {
this.eventRepository = eventRepository;
this.attendanceService = attendanceService;
}

/**
Expand Down Expand Up @@ -50,4 +56,20 @@ 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 registerAttendance(eventId: number, appUser: AppUser): Promise<Attendance> {
const event = await this.eventRepository.findOne({ id: eventId });
const attendance = this.attendanceService.createAttendance(event, appUser);
const savedAttendance = await this.attendanceService.saveAttendance(attendance);
godwinpang marked this conversation as resolved.
Show resolved Hide resolved

return savedAttendance;
}
}
1 change: 1 addition & 0 deletions src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { EventService } from './EventService';
export { AppUserService } from './AppUserService';
export { AttendanceService } from './AttendanceService';