From b73eb40df057dce7ced70f6602ea69dec8396ff2 Mon Sep 17 00:00:00 2001 From: Godwin Pang Date: Mon, 16 Nov 2020 00:24:00 -0800 Subject: [PATCH] Add endpoint for querying all inductee points. (#153) --- src/controllers/PointsController.ts | 39 ++++++++++++++++ src/controllers/index.ts | 10 ++++- src/entities/InducteePointsView.ts | 4 ++ ...5509162887-AddEmailToInducteePointsView.ts | 45 +++++++++++++++++++ src/payloads/Points.ts | 25 +++++++++++ src/payloads/index.ts | 1 + src/services/AppUserService.ts | 5 +++ 7 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 src/controllers/PointsController.ts create mode 100644 src/migrations/1605509162887-AddEmailToInducteePointsView.ts create mode 100644 src/payloads/Points.ts diff --git a/src/controllers/PointsController.ts b/src/controllers/PointsController.ts new file mode 100644 index 00000000..5c71cf12 --- /dev/null +++ b/src/controllers/PointsController.ts @@ -0,0 +1,39 @@ +import { JsonController, Get, UseBefore } from 'routing-controllers'; +import { ResponseSchema, OpenAPI } from 'routing-controllers-openapi'; + +import { AppUserService, AppUserServiceImpl } from '@Services'; +import { OfficerAuthMiddleware } from '@Middlewares'; +import { MultipleInducteePointsResponse, InducteePointsResponse, AppUserResponse } from '@Payloads'; +import { InducteePointsView } from '@Entities'; + +@JsonController('/api/points') +export class PointsController { + constructor(private appUserService: AppUserService) {} + + @Get('/inductees') + @ResponseSchema(MultipleInducteePointsResponse) + @UseBefore(OfficerAuthMiddleware) + @OpenAPI({ security: [{ TokenAuth: [] }] }) + async getAllInducteePoints(): Promise { + const points: InducteePointsView[] = await this.appUserService.getAllInducteePoints(); + + const pointResponses = points.map((point: InducteePointsView) => { + const res = new InducteePointsResponse(); + res.points = point.points; + + // man i hate this + res.user = point.user; + res.email = point.email; + res.hasMentorshipRequirement = point.hasMentorshipRequirement; + res.hasProfessionalRequirement = point.hasProfessionalRequirement; + return res; + }); + + const multiplePointResponse = new MultipleInducteePointsResponse(); + multiplePointResponse.inducteePoints = pointResponses; + + return multiplePointResponse; + } +} + +export const PointsControllerImpl = new PointsController(AppUserServiceImpl); diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 2b9b0c2e..81066922 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -2,8 +2,15 @@ import { EventController, EventControllerImpl } from './EventController'; import { UserController, UserControllerImpl } from './UserController'; import { AuthController, AuthControllerImpl } from './AuthController'; import { TeapotController, TeapotControllerImpl } from './TeapotController'; +import { PointsController, PointsControllerImpl } from './PointsController'; -export const controllers = [EventController, UserController, AuthController, TeapotController]; +export const controllers = [ + EventController, + UserController, + AuthController, + TeapotController, + PointsController, +]; // map from name of controller to an instance // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -12,6 +19,7 @@ controllerMap.set(EventController.name, EventControllerImpl); controllerMap.set(UserController.name, UserControllerImpl); controllerMap.set(AuthController.name, AuthControllerImpl); controllerMap.set(TeapotController.name, TeapotControllerImpl); +controllerMap.set(PointsController.name, PointsControllerImpl); export const ControllerContainer = { // controller here is just a class and all classes have names in ts diff --git a/src/entities/InducteePointsView.ts b/src/entities/InducteePointsView.ts index ac0e17ed..a4c27b7c 100644 --- a/src/entities/InducteePointsView.ts +++ b/src/entities/InducteePointsView.ts @@ -9,6 +9,7 @@ import { Event } from './Event'; connection .createQueryBuilder() .select('appUser.id', 'user') + .addSelect('appUser.email', 'email') .addSelect('SUM(attendance.points)', 'points') .addSelect( "SUM(CASE WHEN event.type = 'professional' THEN 1 ELSE 0 END)::int::bool", @@ -28,6 +29,9 @@ export class InducteePointsView { @ViewColumn() user: number; + @ViewColumn() + email: string; + @ViewColumn() points: number; diff --git a/src/migrations/1605509162887-AddEmailToInducteePointsView.ts b/src/migrations/1605509162887-AddEmailToInducteePointsView.ts new file mode 100644 index 00000000..4ef06163 --- /dev/null +++ b/src/migrations/1605509162887-AddEmailToInducteePointsView.ts @@ -0,0 +1,45 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddEmailToInducteePointsView1605509162887 implements MigrationInterface { + name = 'AddEmailToInducteePointsView1605509162887'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DELETE FROM "typeorm_metadata" WHERE "type" = 'VIEW' AND "schema" = $1 AND "name" = $2`, + ['public', 'inductee_points_view'] + ); + await queryRunner.query(`DROP VIEW "inductee_points_view"`); + await queryRunner.query( + `CREATE VIEW "inductee_points_view" AS SELECT "appUser"."id" AS "user", "appUser"."email" AS "email", SUM("attendance"."points") AS "points", SUM(CASE WHEN "event"."type" = 'professional' THEN 1 ELSE 0 END)::int::bool AS "hasProfessionalRequirement", SUM(CASE WHEN "event"."type" = 'mentorship' THEN 1 ELSE 0 END)::int::bool AS "hasMentorshipRequirement" FROM "app_user" "appUser" INNER JOIN "attendance" "attendance" ON "appUser"."id" = "attendance"."attendeeId" INNER JOIN "event" "event" ON "event"."id" = "attendance"."eventId" WHERE "attendance"."isInductee" GROUP BY "appUser"."id"` + ); + await queryRunner.query( + `INSERT INTO "typeorm_metadata"("type", "schema", "name", "value") VALUES ($1, $2, $3, $4)`, + [ + 'VIEW', + 'public', + 'inductee_points_view', + 'SELECT "appUser"."id" AS "user", "appUser"."email" AS "email", SUM("attendance"."points") AS "points", SUM(CASE WHEN "event"."type" = \'professional\' THEN 1 ELSE 0 END)::int::bool AS "hasProfessionalRequirement", SUM(CASE WHEN "event"."type" = \'mentorship\' THEN 1 ELSE 0 END)::int::bool AS "hasMentorshipRequirement" FROM "app_user" "appUser" INNER JOIN "attendance" "attendance" ON "appUser"."id" = "attendance"."attendeeId" INNER JOIN "event" "event" ON "event"."id" = "attendance"."eventId" WHERE "attendance"."isInductee" GROUP BY "appUser"."id"', + ] + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DELETE FROM "typeorm_metadata" WHERE "type" = 'VIEW' AND "schema" = $1 AND "name" = $2`, + ['public', 'inductee_points_view'] + ); + await queryRunner.query(`DROP VIEW "inductee_points_view"`); + await queryRunner.query( + `CREATE VIEW "inductee_points_view" AS SELECT "appUser"."email" AS "email", SUM("attendance"."points") AS "points", SUM(CASE WHEN "event"."type" = 'professional' THEN 1 ELSE 0 END)::int::bool AS "hasProfessionalRequirement", SUM(CASE WHEN "event"."type" = 'mentorship' THEN 1 ELSE 0 END)::int::bool AS "hasMentorshipRequirement" FROM "app_user" "appUser" INNER JOIN "attendance" "attendance" ON "appUser"."id" = "attendance"."attendeeId" INNER JOIN "event" "event" ON "event"."id" = "attendance"."eventId" WHERE "attendance"."isInductee" GROUP BY "appUser"."id"` + ); + await queryRunner.query( + `INSERT INTO "typeorm_metadata"("type", "schema", "name", "value") VALUES ($1, $2, $3, $4)`, + [ + 'VIEW', + 'public', + 'inductee_points_view', + 'SELECT "appUser"."email" AS "email", SUM("attendance"."points") AS "points", SUM(CASE WHEN "event"."type" = \'professional\' THEN 1 ELSE 0 END)::int::bool AS "hasProfessionalRequirement", SUM(CASE WHEN "event"."type" = \'mentorship\' THEN 1 ELSE 0 END)::int::bool AS "hasMentorshipRequirement" FROM "app_user" "appUser" INNER JOIN "attendance" "attendance" ON "appUser"."id" = "attendance"."attendeeId" INNER JOIN "event" "event" ON "event"."id" = "attendance"."eventId" WHERE "attendance"."isInductee" GROUP BY "appUser"."id"', + ] + ); + } +} diff --git a/src/payloads/Points.ts b/src/payloads/Points.ts new file mode 100644 index 00000000..608d6e19 --- /dev/null +++ b/src/payloads/Points.ts @@ -0,0 +1,25 @@ +import { IsBoolean, IsInt, ValidateNested, IsEmail } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class InducteePointsResponse { + @IsInt() + user: number; + + @IsEmail() + email: string; + + @IsInt() + points: number; + + @IsBoolean() + hasProfessionalRequirement: boolean; + + @IsBoolean() + hasMentorshipRequirement: boolean; +} + +export class MultipleInducteePointsResponse { + @ValidateNested({ each: true }) + @Type(() => InducteePointsResponse) + inducteePoints: InducteePointsResponse[]; +} diff --git a/src/payloads/index.ts b/src/payloads/index.ts index 3a2b3fc4..edbc5820 100644 --- a/src/payloads/index.ts +++ b/src/payloads/index.ts @@ -27,3 +27,4 @@ export { MultipleAttendanceQuery, } from './Attendance'; export { RSVPResponse } from './RSVP'; +export { InducteePointsResponse, MultipleInducteePointsResponse } from './Points'; diff --git a/src/services/AppUserService.ts b/src/services/AppUserService.ts index 660c8793..d219f85a 100644 --- a/src/services/AppUserService.ts +++ b/src/services/AppUserService.ts @@ -145,6 +145,11 @@ export class AppUserService { return appUser.role === AppUserRole.GUEST; } + async getAllInducteePoints(): Promise { + const inducteePointsRepo = getRepository(InducteePointsView); + return await inducteePointsRepo.find({}); + } + /** * Gets inductee points for user * @param {number} appUserID ID of AppUser to get points for.