From 912008e68d0062ef3a3b857b389ef198f374d0f5 Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Sat, 11 Feb 2023 21:42:22 -0500 Subject: [PATCH] More work on beer image upload patFix schema so beer image and brewery image have createdBy column. Rename 'url' to 'path' in schema, add 'caption' column. --- components/BeerIndex/BeerCard.tsx | 7 +- config/nextConnect/NextConnectOptions.ts | 25 ++++- .../middleware/checkIfBeerPostOwner.ts | 31 +++++++ .../nextConnect/middleware/validateRequest.ts | 3 +- .../[id]/{comments.ts => comments/index.ts} | 0 pages/api/beers/[id]/images/index.ts | 91 ++++++++++++------- pages/beers/[id].tsx | 2 +- .../migrations/20230211021836_/migration.sql | 30 ++++++ prisma/schema.prisma | 12 ++- prisma/seed/clean/cleanDatabase.ts | 5 + prisma/seed/create/createNewBeerImages.ts | 15 ++- prisma/seed/create/createNewBreweryImages.ts | 15 ++- prisma/seed/index.ts | 4 +- services/BeerPost/getAllBeerPosts.ts | 3 +- services/BeerPost/getBeerPostById.ts | 3 +- services/BeerPost/getBeerRecommendations.ts | 2 +- .../BeerPost/schema/BeerPostQueryResult.ts | 3 +- 17 files changed, 193 insertions(+), 58 deletions(-) create mode 100644 config/nextConnect/middleware/checkIfBeerPostOwner.ts rename pages/api/beers/[id]/{comments.ts => comments/index.ts} (100%) create mode 100644 prisma/migrations/20230211021836_/migration.sql diff --git a/components/BeerIndex/BeerCard.tsx b/components/BeerIndex/BeerCard.tsx index 9a68635..0741ce7 100644 --- a/components/BeerIndex/BeerCard.tsx +++ b/components/BeerIndex/BeerCard.tsx @@ -8,7 +8,12 @@ const BeerCard: FC<{ post: BeerPostQueryResult }> = ({ post }) => {
{post.beerImages.length > 0 && ( - {post.name} + {post.name} )}
diff --git a/config/nextConnect/NextConnectOptions.ts b/config/nextConnect/NextConnectOptions.ts index 36ac5fc..a72c300 100644 --- a/config/nextConnect/NextConnectOptions.ts +++ b/config/nextConnect/NextConnectOptions.ts @@ -1,15 +1,32 @@ -import { NextApiRequest, NextApiResponse } from 'next'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import type { RequestHandler } from 'next-connect/dist/types/node'; +import type { HandlerOptions } from 'next-connect/dist/types/types'; +import { z } from 'zod'; +import logger from '../pino/logger'; + import ServerError from '../util/ServerError'; -const NextConnectOptions = { - onNoMatch(req: NextApiRequest, res: NextApiResponse) { +type NextConnectOptionsT = HandlerOptions< + RequestHandler< + NextApiRequest, + NextApiResponse> + > +>; + +const NextConnectOptions: NextConnectOptionsT = { + onNoMatch(req, res) { res.status(405).json({ message: 'Method not allowed.', statusCode: 405, success: false, }); }, - onError(error: unknown, req: NextApiRequest, res: NextApiResponse) { + onError(error, req, res) { + if (process.env.NODE_ENV !== 'production') { + logger.error(error); + } + const message = error instanceof Error ? error.message : 'Internal server error.'; const statusCode = error instanceof ServerError ? error.statusCode : 500; res.status(statusCode).json({ diff --git a/config/nextConnect/middleware/checkIfBeerPostOwner.ts b/config/nextConnect/middleware/checkIfBeerPostOwner.ts new file mode 100644 index 0000000..f5e1169 --- /dev/null +++ b/config/nextConnect/middleware/checkIfBeerPostOwner.ts @@ -0,0 +1,31 @@ +import { UserExtendedNextApiRequest } from '@/config/auth/types'; +import ServerError from '@/config/util/ServerError'; +import getBeerPostById from '@/services/BeerPost/getBeerPostById'; +import { NextApiResponse } from 'next'; +import { NextHandler } from 'next-connect'; + +interface CheckIfBeerPostOwnerRequest extends UserExtendedNextApiRequest { + query: { id: string }; +} + +const checkIfBeerPostOwner = async ( + req: RequestType, + res: NextApiResponse, + next: NextHandler, +) => { + const { id } = req.query; + const user = req.user!; + const beerPost = await getBeerPostById(id); + + if (!beerPost) { + throw new ServerError('Beer post not found', 404); + } + + if (beerPost.postedBy.id !== user.id) { + throw new ServerError('You are not authorized to edit this beer post', 403); + } + + return next(); +}; + +export default checkIfBeerPostOwner; diff --git a/config/nextConnect/middleware/validateRequest.ts b/config/nextConnect/middleware/validateRequest.ts index 6ae494d..37f5bfd 100644 --- a/config/nextConnect/middleware/validateRequest.ts +++ b/config/nextConnect/middleware/validateRequest.ts @@ -28,10 +28,11 @@ const validateRequest = }) => async (req: NextApiRequest, res: NextApiResponse, next: NextHandler) => { if (bodySchema) { - const parsed = bodySchema.safeParse(req.body); + const parsed = bodySchema.safeParse(JSON.parse(JSON.stringify(req.body))); if (!parsed.success) { throw new ServerError('Invalid request body.', 400); } + req.body = parsed.data; } if (querySchema) { diff --git a/pages/api/beers/[id]/comments.ts b/pages/api/beers/[id]/comments/index.ts similarity index 100% rename from pages/api/beers/[id]/comments.ts rename to pages/api/beers/[id]/comments/index.ts diff --git a/pages/api/beers/[id]/images/index.ts b/pages/api/beers/[id]/images/index.ts index fec38be..5e5b29c 100644 --- a/pages/api/beers/[id]/images/index.ts +++ b/pages/api/beers/[id]/images/index.ts @@ -1,10 +1,11 @@ +import DBClient from '@/prisma/DBClient'; +import { BeerImage } from '@prisma/client'; import NextConnectOptions from '@/config/nextConnect/NextConnectOptions'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { UserExtendedNextApiRequest } from '@/config/auth/types'; -import { NextHandler, createRouter, expressWrapper } from 'next-connect'; +import { createRouter, expressWrapper } from 'next-connect'; import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser'; -import getBeerPostById from '@/services/BeerPost/getBeerPostById'; import multer from 'multer'; @@ -12,55 +13,70 @@ import cloudinaryConfig from '@/config/cloudinary'; import { NextApiResponse } from 'next'; import { z } from 'zod'; import ServerError from '@/config/util/ServerError'; +import validateRequest from '@/config/nextConnect/middleware/validateRequest'; const { storage } = cloudinaryConfig; const fileFilter: multer.Options['fileFilter'] = (req, file, cb) => { - if ( - file.mimetype === 'image/png' || - file.mimetype === 'image/jpg' || - file.mimetype === 'image/jpeg' - ) { - cb(null, true); - } else { + const { mimetype } = file; + + const isImage = mimetype.startsWith('image/'); + + if (!isImage) { cb(null, false); } + cb(null, true); }; -const uploadMiddleware = multer({ storage, fileFilter }).array('images'); +const uploadMiddleware = expressWrapper( + multer({ storage, fileFilter, limits: { files: 3 } }).array('images'), +); + +const BeerPostImageValidationSchema = z.object({ + caption: z.string(), + alt: z.string(), +}); interface UploadBeerPostImagesRequest extends UserExtendedNextApiRequest { - files?: - | Express.Multer.File[] - | { - [fieldname: string]: Express.Multer.File[]; - }; - - query: { - id: string; - }; - - // beerPost?: BeerPostQueryResult; + files?: Express.Multer.File[]; + query: { id: string }; + body: z.infer; } -const checkIfBeerPostOwner = async ( +const processImageData = async ( req: UploadBeerPostImagesRequest, - res: NextApiResponse, - next: NextHandler, + res: NextApiResponse>, ) => { - const { id } = req.query; - const user = req.user!; - const beerPost = await getBeerPostById(id); + const { files, user, body } = req; - if (!beerPost) { - throw new ServerError('Beer post not found', 404); + if (!files || !files.length) { + throw new ServerError('No images uploaded', 400); } + const beerImagePromises: Promise[] = []; - if (beerPost.postedBy.id !== user.id) { - throw new ServerError('You are not authorized to edit this beer post', 403); - } + files.forEach((file) => { + beerImagePromises.push( + DBClient.instance.beerImage.create({ + data: { + alt: body.alt, + postedBy: { connect: { id: user!.id } }, + beerPost: { connect: { id: req.query.id } }, + path: file.path, + caption: body.caption, + }, + }), + ); + }); - return next(); + const beerImages = await Promise.all(beerImagePromises); + + res.status(200).json({ + success: true, + message: `Successfully uploaded ${beerImages.length} image${ + beerImages.length > 1 ? 's' : '' + }`, + statusCode: 200, + }); }; const router = createRouter< @@ -68,8 +84,13 @@ const router = createRouter< NextApiResponse> >(); -// @ts-expect-error -router.post(getCurrentUser, expressWrapper(uploadMiddleware), checkIfBeerPostOwner); +router.post( + getCurrentUser, + // @ts-expect-error + uploadMiddleware, + validateRequest({ bodySchema: BeerPostImageValidationSchema }), + processImageData, +); const handler = router.handler(NextConnectOptions); export default handler; diff --git a/pages/beers/[id].tsx b/pages/beers/[id].tsx index cb0e65f..08690c9 100644 --- a/pages/beers/[id].tsx +++ b/pages/beers/[id].tsx @@ -53,7 +53,7 @@ const BeerByIdPage: NextPage = ({ {beerPost.beerImages[0] && ( {beerPost.beerImages[0].alt} { await prisma.$executeRaw`TRUNCATE TABLE "BreweryPost" CASCADE`; await prisma.$executeRaw`TRUNCATE TABLE "BeerComment" CASCADE`; await prisma.$executeRaw`TRUNCATE TABLE "BreweryComment" CASCADE`; + await prisma.$executeRaw`TRUNCATE TABLE "BeerPostLike" CASCADE`; + await prisma.$executeRaw`TRUNCATE TABLE "BeerImage" CASCADE`; + await prisma.$executeRaw`TRUNCATE TABLE "BreweryImage" CASCADE`; + + await prisma.$disconnect(); }; export default cleanDatabase; diff --git a/prisma/seed/create/createNewBeerImages.ts b/prisma/seed/create/createNewBeerImages.ts index f71b872..8dbd0c5 100644 --- a/prisma/seed/create/createNewBeerImages.ts +++ b/prisma/seed/create/createNewBeerImages.ts @@ -1,15 +1,19 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { faker } from '@faker-js/faker'; -import { BeerPost, BeerImage } from '@prisma/client'; +import { BeerPost, BeerImage, User } from '@prisma/client'; import DBClient from '../../DBClient'; interface CreateNewBeerImagesArgs { numberOfImages: number; - beerPosts: BeerPost[]; + + joinData: { + beerPosts: BeerPost[]; + users: User[]; + }; } const createNewBeerImages = async ({ numberOfImages, - beerPosts, + joinData: { beerPosts, users }, }: CreateNewBeerImagesArgs) => { const prisma = DBClient.instance; const createdAt = faker.date.past(1); @@ -18,12 +22,15 @@ const createNewBeerImages = async ({ // eslint-disable-next-line no-plusplus for (let i = 0; i < numberOfImages; i++) { const beerPost = beerPosts[Math.floor(Math.random() * beerPosts.length)]; + const user = users[Math.floor(Math.random() * users.length)]; beerImagesPromises.push( prisma.beerImage.create({ data: { - url: 'https://picsum.photos/900/1600', + path: 'https://picsum.photos/900/1600', alt: 'Placeholder beer image.', + caption: 'Placeholder beer image caption.', beerPost: { connect: { id: beerPost.id } }, + postedBy: { connect: { id: user.id } }, createdAt, }, }), diff --git a/prisma/seed/create/createNewBreweryImages.ts b/prisma/seed/create/createNewBreweryImages.ts index 67c2909..897b664 100644 --- a/prisma/seed/create/createNewBreweryImages.ts +++ b/prisma/seed/create/createNewBreweryImages.ts @@ -1,15 +1,19 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { faker } from '@faker-js/faker'; -import { BreweryPost, BreweryImage } from '@prisma/client'; +import { BreweryPost, BreweryImage, User } from '@prisma/client'; import DBClient from '../../DBClient'; interface CreateBreweryImagesArgs { numberOfImages: number; - breweryPosts: BreweryPost[]; + + joinData: { + breweryPosts: BreweryPost[]; + users: User[]; + }; } const createNewBreweryImages = async ({ numberOfImages, - breweryPosts, + joinData: { breweryPosts, users }, }: CreateBreweryImagesArgs) => { const prisma = DBClient.instance; const createdAt = faker.date.past(1); @@ -18,13 +22,16 @@ const createNewBreweryImages = async ({ // eslint-disable-next-line no-plusplus for (let i = 0; i < numberOfImages; i++) { const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)]; + const user = users[Math.floor(Math.random() * users.length)]; breweryImagesPromises.push( prisma.breweryImage.create({ data: { - url: 'https://picsum.photos/900/1600', + path: 'https://picsum.photos/900/1600', alt: 'Placeholder brewery image.', + caption: 'Placeholder brewery image caption.', breweryPost: { connect: { id: breweryPost.id } }, + postedBy: { connect: { id: user.id } }, createdAt, }, }), diff --git a/prisma/seed/index.ts b/prisma/seed/index.ts index 1012ffd..d6b52a1 100644 --- a/prisma/seed/index.ts +++ b/prisma/seed/index.ts @@ -54,11 +54,11 @@ import createNewUsers from './create/createNewUsers'; }), createNewBeerImages({ numberOfImages: 1000, - beerPosts, + joinData: { beerPosts, users }, }), createNewBreweryImages({ numberOfImages: 1000, - breweryPosts, + joinData: { breweryPosts, users }, }), ]); diff --git a/services/BeerPost/getAllBeerPosts.ts b/services/BeerPost/getAllBeerPosts.ts index 6e20cc6..25d7d6c 100644 --- a/services/BeerPost/getAllBeerPosts.ts +++ b/services/BeerPost/getAllBeerPosts.ts @@ -34,7 +34,8 @@ const getAllBeerPosts = async (pageNum: number, pageSize: number) => { }, beerImages: { select: { - url: true, + path: true, + caption: true, id: true, alt: true, }, diff --git a/services/BeerPost/getBeerPostById.ts b/services/BeerPost/getBeerPostById.ts index bc67ca3..1410d29 100644 --- a/services/BeerPost/getBeerPostById.ts +++ b/services/BeerPost/getBeerPostById.ts @@ -39,7 +39,8 @@ const getBeerPostById = async (id: string) => { beerImages: { select: { alt: true, - url: true, + path: true, + caption: true, id: true, }, }, diff --git a/services/BeerPost/getBeerRecommendations.ts b/services/BeerPost/getBeerRecommendations.ts index 4c7dae1..a64b3ba 100644 --- a/services/BeerPost/getBeerRecommendations.ts +++ b/services/BeerPost/getBeerRecommendations.ts @@ -10,7 +10,7 @@ const getBeerRecommendations = async ( NOT: { id: beerPost.id }, }, include: { - beerImages: { select: { id: true, url: true, alt: true } }, + beerImages: { select: { id: true, path: true, caption: true, alt: true } }, brewery: { select: { id: true, name: true } }, }, }); diff --git a/services/BeerPost/schema/BeerPostQueryResult.ts b/services/BeerPost/schema/BeerPostQueryResult.ts index fd08595..7585da3 100644 --- a/services/BeerPost/schema/BeerPostQueryResult.ts +++ b/services/BeerPost/schema/BeerPostQueryResult.ts @@ -7,7 +7,8 @@ export default interface BeerPostQueryResult { }; description: string; beerImages: { - url: string; + path: string; + caption: string; id: string; alt: string; }[];