Skip to content

Commit

Permalink
Add support for edit and delete reviews (#175)
Browse files Browse the repository at this point in the history
* Add support for edit and delete reviews

* fix test

* fix adding reviews and score populating
  • Loading branch information
cole-adams committed Jul 11, 2024
1 parent a4957bb commit d166d81
Show file tree
Hide file tree
Showing 27 changed files with 1,670 additions and 585 deletions.
3 changes: 3 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"plugins": ["prettier-plugin-tailwindcss"]
}
10 changes: 6 additions & 4 deletions netlify/functions/club/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Handler, HandlerContext, HandlerEvent } from "@netlify/functions";
import awardsRouter from "./awards";
import listRouter from "./list";
import membersRouter from "./members";
import reviewsRouter from "./reviews";
import ClubRepository from "../repositories/ClubRepository";
import WorkRepository from "../repositories/WorkRepository";
import { loggedIn, secured } from "../utils/auth";
Expand All @@ -19,6 +20,7 @@ const { faunaClient, q } = getFaunaClient();

const router = new Router("/api/club");
router.use("/:clubId<\\d+>/list", validClubId, listRouter);
router.use("/:clubId<\\d+>/reviews", validClubId, reviewsRouter);
router.use("/:clubId<\\d+>/members", validClubId, membersRouter);
router.use("/:clubId<\\d+>/awards", validClubId, mapIdToLegacyId, awardsRouter);

Expand Down Expand Up @@ -61,7 +63,7 @@ router.post("/", loggedIn, async ({ event }) => {
clubName: name,
members: members,
},
})
}),
);

await ClubRepository.insert(name, clubId);
Expand All @@ -75,7 +77,7 @@ router.get(
async ({ clubId }: ClubRequest) => {
const nextWork = await WorkRepository.getNextWork(clubId);
return ok(JSON.stringify({ workId: nextWork?.work_id }));
}
},
);

router.put(
Expand All @@ -91,12 +93,12 @@ router.put(
await WorkRepository.setNextWork(clubId, body.workId);

return ok();
}
},
);

const handler: Handler = async (
event: HandlerEvent,
context: HandlerContext
context: HandlerContext,
) => {
return router.route({ event, context });
};
Expand Down
71 changes: 25 additions & 46 deletions netlify/functions/club/list.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import ListRepository, { isWorkListType } from "../repositories/ListRepository";
import ReviewRepository from "../repositories/ReviewRepository";
import UserRepository from "../repositories/UserRepository";
import WorkRepository, { isWorkType } from "../repositories/WorkRepository";
import { AuthRequest, secured } from "../utils/auth";
import { secured } from "../utils/auth";
import { badRequest, internalServerError, ok } from "../utils/responses";
import { Router } from "../utils/router";
import { getDetailedWorks } from "../utils/tmdb";
import { ClubRequest } from "../utils/validation";

import { BadRequest } from "@/common/errorCodes";
import { WorkListType } from "@/common/types/generated/db";
import { ReviewListItem, WorkListItem } from "@/common/types/lists";
import { Review, ReviewListItem, WorkListItem } from "@/common/types/lists";

const router = new Router("/api/club/:clubId<\\d+>/list");

Expand Down Expand Up @@ -64,33 +63,43 @@ async function getReviewList(clubId: string): Promise<ReviewListItem[]> {
acc[key].push(review);
return acc;
},
{}
{},
);

return Object.keys(groupedReviews)
.map((key) => {
const userScores: Record<string, number> = groupedReviews[key]?.reduce(
const userScores: Record<string, Review> = groupedReviews[key]?.reduce(
(acc, review) => {
if (review.user_id && review.score) {
return {
...acc,
[review.user_id]: parseFloat(review.score),
[review.user_id]: {
id: review.review_id,
created_date: review.time_added.toISOString(),
score: parseFloat(review.score),
},
};
} else {
return acc;
}
},
{}
{},
);
let scores: Record<string, number>;
let scores: Record<string, Review>;
if (Object.keys(userScores).length === 0) {
scores = {};
} else {
scores = {
...userScores,
average:
Object.values(userScores).reduce((acc, score) => acc + score, 0) /
Object.keys(userScores).length,
average: {
id: "average",
created_date: new Date().toISOString(),
score:
Object.values(userScores).reduce(
(acc, review) => acc + review.score,
0,
) / Object.keys(userScores).length,
},
};
}
return {
Expand Down Expand Up @@ -126,7 +135,7 @@ router.post(
const existingWork = await WorkRepository.findByType(
clubId,
body.type,
body.externalId
body.externalId,
);
workId = existingWork?.id;
}
Expand All @@ -139,14 +148,14 @@ router.post(
const isItemInList = await ListRepository.isItemInList(
clubId,
type,
workId
workId,
);
if (isItemInList) {
return badRequest(BadRequest.ItemInList);
}
await ListRepository.insertItemInList(clubId, type, workId);
return ok();
}
},
);

router.delete(
Expand All @@ -164,7 +173,7 @@ router.delete(
const isItemInList = await ListRepository.isItemInList(
clubId,
type,
workId
workId,
);
if (!isItemInList) {
return badRequest("This movie does not exist in the list");
Expand All @@ -179,37 +188,7 @@ router.delete(
}
}
return ok();
}
);

router.put(
`/${WorkListType.reviews}/:workId`,
secured,
async ({ clubId, email, params, event }: ClubRequest & AuthRequest) => {
if (!params.workId) return badRequest("No workId provided");
if (!event.body) return badRequest("No body provided");
const body = JSON.parse(event.body);
if (!body.score) return badRequest("No score provided");
const score = parseFloat(body.score);
if (isNaN(score) || score < 0 || score > 10) {
return badRequest("Invalid score provided");
}
const workId = params.workId;
const isItemInList = await ListRepository.isItemInList(
clubId,
WorkListType.reviews,
workId
);
if (!isItemInList) {
return badRequest("This movie does not exist in the list");
}
const user = await UserRepository.getByEmail(email);
if (!user) {
return internalServerError("Failed to find user");
}
await ReviewRepository.insertReview(clubId, workId, user.id, score);
return ok();
}
},
);

export default router;
75 changes: 75 additions & 0 deletions netlify/functions/club/reviews.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import ListRepository from "../repositories/ListRepository";
import ReviewRepository from "../repositories/ReviewRepository";
import UserRepository from "../repositories/UserRepository";
import { AuthRequest, secured } from "../utils/auth";
import { badRequest, internalServerError, ok } from "../utils/responses";
import { Router } from "../utils/router";
import { ClubRequest } from "../utils/validation";

import { WorkListType } from "@/common/types/generated/db";

const router = new Router("/api/club/:clubId<\\d+>/reviews");

router.post(
"/",
secured,
async ({ clubId, email, event }: ClubRequest & AuthRequest) => {
if (!event.body) return badRequest("No body provided");
const body = JSON.parse(event.body);
if (!body.score) return badRequest("No score provided");
if (!body.workId) return badRequest("No workId provided");

const score = parseFloat(body.score);
if (isNaN(score) || score < 0 || score > 10) {
return badRequest("Invalid score provided");
}
const workId = body.workId;
const isItemInList = await ListRepository.isItemInList(
clubId,
WorkListType.reviews,
workId,
);
if (!isItemInList) {
return badRequest("This movie does not exist in the list");
}
const user = await UserRepository.getByEmail(email);
if (!user) {
return internalServerError("Failed to find user");
}
await ReviewRepository.insertReview(clubId, workId, user.id, score);
return ok();
},
);

router.put(
`/:reviewId`,
secured,
async ({ clubId, email, params, event }: ClubRequest & AuthRequest) => {
if (!params.reviewId) {
return badRequest("No reviewId provided");
}
if (!event.body) return badRequest("No body provided");
const body = JSON.parse(event.body);
if (!body.score) return badRequest("No score provided");
const score = parseFloat(body.score);
if (isNaN(score) || score < 0 || score > 10) {
return badRequest("Invalid score provided");
}
const reviewId = params.reviewId;
const user = await UserRepository.getByEmail(email);
if (!user) {
return internalServerError("Failed to find user");
}
const review = await ReviewRepository.getById(reviewId, clubId);
if (!review) {
return internalServerError("Failed to find review");
}
if (review.user_id !== user.id) {
return badRequest("You are not allowed to edit this review");
}
await ReviewRepository.updateScore(reviewId, score);
return ok();
},
);

export default router;
22 changes: 21 additions & 1 deletion netlify/functions/repositories/ReviewRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class ReviewRepository {
.innerJoin("work", "work.id", "work_list_item.work_id")
.leftJoin("review", "review.work_id", "work.id")
.select([
"review.id as review_id",
"work.id",
"work.title",
"work.type",
Expand All @@ -28,7 +29,7 @@ class ReviewRepository {
clubId: string,
workId: string,
userId: string,
score: number
score: number,
) {
const listId = await db
.selectFrom("work_list")
Expand All @@ -46,6 +47,25 @@ class ReviewRepository {
})
.execute();
}

async getById(id: string, clubId: string) {
return db
.selectFrom("review")
.selectAll()
.innerJoin("work_list", "work_list.id", "review.list_id")
.where("work_list.club_id", "=", clubId)
.where("review.id", "=", id)
.executeTakeFirstOrThrow();
}

async updateScore(id: string, score: number) {
return db
.updateTable("review")
.set("score", score)
.set("created_date", new Date())
.where("id", "=", id)
.execute();
}
}

export default new ReviewRepository();
36 changes: 26 additions & 10 deletions netlify/functions/utils/tmdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,32 @@ export async function getDetailedMovie<T extends BaseMovie>(
export async function getDetailedWorks<T extends WorkListItem>(
works: T[]
): Promise<(T & ExternalWorkData<TMDBMovieData>)[]> {
return await Promise.all(
works.map(async (work) => {
if (!work.externalId) {
return work;
}
const response = await getTMDBMovieData(parseInt(work.externalId));
return {
...work,
externalData: response.data,
};
const chunkArray = <T>(array: T[], chunkSize: number): T[][] => {
const result = [];
for (let i = 0; i < array.length; i += chunkSize) {
result.push(array.slice(i, i + chunkSize));
}
return result;
};

const chunks = chunkArray(works, 50);

const processedChunks = await Promise.all(
chunks.map(async (chunk) => {
return await Promise.all(
chunk.map(async (work) => {
if (!work.externalId) {
return work;
}
const response = await getTMDBMovieData(parseInt(work.externalId));
return {
...work,
externalData: response.data,
};
})
);
})
);

return processedChunks.flat();
}
Loading

0 comments on commit d166d81

Please sign in to comment.