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
37 changes: 32 additions & 5 deletions src/controllers/EventController.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Expand Down Expand Up @@ -75,4 +81,25 @@ export class EventController {

return this.eventMapper.entityToResponse(deletedEvent);
}

@Post('/:eventID/signin')
@OnUndefined(409)
@ResponseSchema(Attendance)
godwinpang marked this conversation as resolved.
Show resolved Hide resolved
async signInToEvent(
@Param('eventID') eventID: number,
@Body() appUserRequest: EventSignInRequest
): Promise<Attendance | undefined> {
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);
}
}
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,
});
}
81 changes: 81 additions & 0 deletions src/mappers/AppUserMapper.ts
Original file line number Diff line number Diff line change
@@ -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<AppUser>;
private appUserService: AppUserService;

constructor(
@inject(AppUserRepositoryToken) appUserRepository: Repository<AppUser>,
@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<AppUser | undefined> {
const appUserObj: AppUser = appUserRequest as AppUser;
appUserObj.id = appUserId;

const appUser: AppUser = await this.appUserRepository.preload(appUserObj);

return appUser;
}

async requestToEntityByEmail(appUserRequest: EventSignInRequest): Promise<AppUser> {
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;
}
}
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';
16 changes: 15 additions & 1 deletion src/payloads/AppUser.ts
Original file line number Diff line number Diff line change
@@ -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;
}
14 changes: 12 additions & 2 deletions src/payloads/Event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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;
}
10 changes: 8 additions & 2 deletions src/payloads/index.ts
Original file line number Diff line number Diff line change
@@ -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';
34 changes: 32 additions & 2 deletions src/services/AppUserService.ts
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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 await this.appUserRepository.save(appUser);
}

/**
* Get multiple app users.
*
Expand All @@ -18,4 +28,24 @@ 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 | undefined> {
return this.appUserRepository.findOne({ email });
}

async saveNonAffiliate(appUser: AppUser): Promise<AppUser | undefined> {
const { role } = appUser;

if (role !== undefined && role !== AppUserRole.GUEST) {
return undefined;
}

return await this.saveAppUser(appUser);
}
}
28 changes: 28 additions & 0 deletions src/services/AttendanceService.ts
Original file line number Diff line number Diff line change
@@ -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<Attendance>;

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

async registerAttendance(event: Event, attendee: AppUser): Promise<Attendance | undefined> {
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;
}
}
Loading