Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
Warnings:

- Added the required column `title` to the `event` table without a default value. This is not possible if the table is not empty.

*/
-- AlterTable
ALTER TABLE "event" ADD COLUMN "description" TEXT,
ADD COLUMN "location" TEXT,
ADD COLUMN "title" TEXT NOT NULL;
13 changes: 8 additions & 5 deletions server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,14 @@ model Event {
timetable Timetable @relation(fields: [timetableId], references: [id], onDelete: Cascade)
timetableId String

colour String
dayOfWeek Int
start Int
end Int
type EventType
title String
location String?
description String?
colour String
dayOfWeek Int
start Int
end Int
type EventType

@@map("event")
}
Expand Down
89 changes: 89 additions & 0 deletions server/src/timetable/timetable.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Controller,
Delete,
Get,
HttpException,
HttpStatus,
Param,
Patch,
Expand All @@ -14,6 +15,7 @@ import {
import { TimetableService } from './timetable.service';
import { AuthenticatedGuard } from 'src/auth/authenticated.guard';
import { AuthenticatedRequest } from 'src/auth/auth.controller';
import { EventParametersDto } from './types';

@Controller('user/timetables')
export class TimetableController {
Expand Down Expand Up @@ -193,4 +195,91 @@ export class TimetableController {
classId,
);
}

@Get('event/:eventId')
@UseGuards(AuthenticatedGuard)
async getEventById(
@Req() req: AuthenticatedRequest,
@Param('eventId') eventId: string,
) {
try {
const eventDetails = await this.timetableService.getEvent(
req.user.id,
eventId,
);
return eventDetails;
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
'Failed to get event details',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}

@Post('event/:timetableId')
@UseGuards(AuthenticatedGuard)
async addEvent(
@Req() req: AuthenticatedRequest,
@Param('timetableId') timetableId: string,
@Body() body: { event: EventParametersDto },
) {
try {
await this.timetableService.addEvent(
req.user.id,
body.event,
timetableId,
);
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
'Failed to add event',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
return HttpStatus.CREATED;
}

@Delete('event/:eventId')
@UseGuards(AuthenticatedGuard)
async deleteEvent(
@Req() req: AuthenticatedRequest,
@Param('eventId') eventId: string,
) {
try {
await this.timetableService.removeEvent(req.user.id, eventId);
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
'Failed to delete event',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}

@Patch('event/:eventId')
@UseGuards(AuthenticatedGuard)
async updateEvent(
@Req() req: AuthenticatedRequest,
@Param('eventId') eventId: string,
@Body() body: EventParametersDto,
) {
try {
await this.timetableService.updateEvent(req.user.id, eventId, body);
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
'Failed to update event',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
139 changes: 138 additions & 1 deletion server/src/timetable/timetable.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { GraphqlService } from 'src/graphql/graphql.service';
import { validate } from 'src/utils/validate';
import { CourseDetails, UserTimetable } from './types';
import { CourseDetails, UserTimetable, EventParametersDto } from './types';
import type { ClassDetails } from 'src/graphql/types';
import { EventType } from '../generated/prisma/enums';
import { EventMinAggregateOutputType } from '../generated/prisma/models/Event';
import type { Event } from 'src/generated/prisma/client';

@Injectable()
export class TimetableService {
Expand Down Expand Up @@ -428,4 +431,138 @@ export class TimetableService {
}),
]);
}

async getEvent(userId: string, eventId: string): Promise<Event> {
try {
const event = await this.prisma.event.findUnique({
select: {
id: true,
colour: true,
dayOfWeek: true,
start: true,
end: true,
type: true,
title: true,
description: true,
location: true,
timetable: { select: { userId: true, id: true } },
},
where: { id: eventId },
});

if (!event || event.timetable.userId !== userId) {
throw new HttpException('Event not found', HttpStatus.NOT_FOUND);
}

const eventData = {
id: event.id,
timetableId: event.timetable.id,
colour: event.colour,
dayOfWeek: event.dayOfWeek,
start: event.start,
end: event.end,
type: event.type,
title: event.title,
description: event.description ?? null,
location: event.location ?? null,
};

return eventData;
} catch {
throw new HttpException('Event not in timetable', HttpStatus.NOT_FOUND);
}
}

async getAllEvent(
userId: string,
timetableId: string,
): Promise<EventMinAggregateOutputType[]> {
const timetableExists = await this.isTimetableOwnedByUser(
userId,
timetableId,
);
validate(timetableExists, 'Timetable does not exist', HttpStatus.NOT_FOUND);

const events = (await this.prisma.event.findMany({
where: { timetableId },
})) as EventMinAggregateOutputType[];

return events;
}

async addEvent(
userId: string,
eventDetails: EventParametersDto,
timetableId: string,
): Promise<void> {
const timetableExists = await this.isTimetableOwnedByUser(
userId,
timetableId,
);
validate(timetableExists, 'Timetable does not exist', HttpStatus.NOT_FOUND);

const colourValid = this.isColourCodeValid(eventDetails.colour);
validate(colourValid, 'Colour code is not valid', HttpStatus.BAD_REQUEST);

if (!(eventDetails.type in EventType)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious if this is actually possible. Might be worth testing to see what happens when you pass an invalid type... Ask @jason4193 about using Postman later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lhvy Tested on the Postman, only default-<1 to 8>, #<3 letters 0-9/A-F> & #<6 letters 0-9/A-F> will pass the validation.
Something like #default, default-9, #xxx will fail

throw new HttpException('Invalid event type', HttpStatus.BAD_REQUEST);
}

await this.prisma.event.create({
data: {
title: eventDetails.title,
description: eventDetails.description ?? undefined,
location: eventDetails.location ?? undefined,
colour: eventDetails.colour,
dayOfWeek: eventDetails.dayOfWeek,
start: eventDetails.start,
end: eventDetails.end,
type: eventDetails.type,
timetable: { connect: { id: timetableId } },
},
});
}

async removeEvent(userId: string, eventId: string): Promise<void> {
if (!(await this.isEventOwnedByUser(userId, eventId))) {
throw new HttpException('Event could not be found', HttpStatus.NOT_FOUND);
}

await this.prisma.event.delete({
where: {
id: eventId,
},
});
}

async updateEvent(
userId: string,
eventId: string,
eventDetails: EventParametersDto,
): Promise<void> {
if (!(await this.isEventOwnedByUser(userId, eventId))) {
throw new HttpException('Event could not be found', HttpStatus.NOT_FOUND);
}

const dtoKeys = Object.keys(new EventParametersDto());

const filteredData = Object.fromEntries(
Object.entries(eventDetails).filter(
([key, value]) => value !== undefined && dtoKeys.includes(key),
),
);

await this.prisma.event.update({
where: { id: eventId },
data: filteredData,
});
}

async isEventOwnedByUser(userId: string, eventId: string): Promise<boolean> {
const hit = await this.prisma.event.findFirst({
where: { id: eventId, timetable: { userId } },
select: { id: true },
});
return !!hit;
}
}
18 changes: 18 additions & 0 deletions server/src/timetable/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import { EventType } from '../generated/prisma/enums';

export class AddCourseDto {
term: string;
colour: string;
}

export class CourseDetails {
id: string;
selectedClasses: string[];
Expand All @@ -17,3 +24,14 @@ export enum Term {
T2 = 'T2',
T3 = 'T3',
}

export class EventParametersDto {
colour: string;
dayOfWeek: number; // 0 = Monday, 6 = Sunday
start: number; // Mins since midnight
end: number; // Mins since midnight
type: EventType;
title: string;
description?: string;
location?: string;
}