diff --git a/package.json b/package.json index b4c8e9c..15272cc 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "start": "next start", "lint": "next lint", "clear-db": "npx ts-node ./src/prisma/seed/clear/index.ts", - "format": "npx prettier . --write", + "format": "npx prettier . --write; npx prisma format;", "format-watch": "npx onchange \"**/*\" -- prettier --write --ignore-unknown {{changed}}", "seed": "npx --max-old-space-size=4096 ts-node ./src/prisma/seed/index.ts" }, @@ -22,7 +22,7 @@ "@mapbox/search-js-core": "^1.0.0-beta.17", "@mapbox/search-js-react": "^1.0.0-beta.17", "@next/bundle-analyzer": "^14.0.3", - "@prisma/client": "^5.6.0", + "@prisma/client": "^5.7.0", "@react-email/components": "^0.0.11", "@react-email/render": "^0.0.9", "@react-email/tailwind": "^0.0.12", @@ -85,7 +85,7 @@ "prettier-plugin-jsdoc": "^1.0.2", "prettier-plugin-tailwindcss": "^0.5.7", "prettier": "^3.0.0", - "prisma": "^5.6.0", + "prisma": "^5.7.0", "tailwindcss-animate": "^1.0.6", "tailwindcss": "^3.3.3", "ts-node": "^10.9.1", diff --git a/src/config/cloudinary/helpers/deleteImage.ts b/src/config/cloudinary/helpers/deleteImage.ts new file mode 100644 index 0000000..ba54f22 --- /dev/null +++ b/src/config/cloudinary/helpers/deleteImage.ts @@ -0,0 +1,11 @@ +import { cloudinary } from '..'; + +/** + * Deletes an image from Cloudinary. + * + * @param path - The cloudinary path of the image to be deleted. + * @returns A promise that resolves when the image is successfully deleted. + */ +const deleteImage = (path: string) => cloudinary.uploader.destroy(path); + +export default deleteImage; diff --git a/src/config/cloudinary/index.ts b/src/config/cloudinary/index.ts index 625b176..e624940 100644 --- a/src/config/cloudinary/index.ts +++ b/src/config/cloudinary/index.ts @@ -4,6 +4,7 @@ import { NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME, CLOUDINARY_KEY, CLOUDINARY_SECRET, + NODE_ENV, } from '../env'; import CloudinaryStorage from './CloudinaryStorage'; @@ -14,6 +15,9 @@ cloudinary.config({ }); /** Cloudinary storage instance. */ -const storage = new CloudinaryStorage({ cloudinary, params: { folder: 'biergarten' } }); +const storage = new CloudinaryStorage({ + cloudinary, + params: { folder: NODE_ENV === 'production' ? 'biergarten' : 'biergarten-dev' }, +}); export { cloudinary, storage }; diff --git a/src/controllers/images/beer-images/index.ts b/src/controllers/images/beer-images/index.ts index 92ce733..208d249 100644 --- a/src/controllers/images/beer-images/index.ts +++ b/src/controllers/images/beer-images/index.ts @@ -1,9 +1,12 @@ import ServerError from '@/config/util/ServerError'; -import addBeerImageToDB from '@/services/images/beer-image/addBeerImageToDB'; +import { + addBeerImagesToDB, + deleteBeerImageFromDBAndStorage, +} from '@/services/images/beer-image'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { NextApiResponse } from 'next'; import { z } from 'zod'; -import { UploadImagesRequest } from '../types'; +import { DeleteImageRequest, UploadImagesRequest } from '../types'; // eslint-disable-next-line import/prefer-default-export export const processBeerImageData = async ( @@ -16,11 +19,10 @@ export const processBeerImageData = async ( throw new ServerError('No images uploaded', 400); } - const beerImages = await addBeerImageToDB({ - alt: body.alt, - caption: body.caption, + const beerImages = await addBeerImagesToDB({ beerPostId: req.query.id, userId: user!.id, + body, files, }); @@ -32,3 +34,18 @@ export const processBeerImageData = async ( statusCode: 200, }); }; + +export const deleteBeerImageData = async ( + req: DeleteImageRequest, + res: NextApiResponse>, +) => { + const { id } = req.query; + + await deleteBeerImageFromDBAndStorage({ beerImageId: id }); + + res.status(200).json({ + success: true, + message: `Successfully deleted image with id ${id}`, + statusCode: 200, + }); +}; diff --git a/src/controllers/images/brewery-images/index.ts b/src/controllers/images/brewery-images/index.ts index 3c7c910..c7b5bee 100644 --- a/src/controllers/images/brewery-images/index.ts +++ b/src/controllers/images/brewery-images/index.ts @@ -1,8 +1,9 @@ import ServerError from '@/config/util/ServerError'; -import addBreweryImageToDB from '@/services/images/brewery-image/addBreweryImageToDB'; + import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { NextApiResponse } from 'next'; import { z } from 'zod'; +import { addBreweryImagesToDB } from '@/services/images/brewery-image'; import { UploadImagesRequest } from '../types'; // eslint-disable-next-line import/prefer-default-export @@ -16,11 +17,10 @@ export const processBreweryImageData = async ( throw new ServerError('No images uploaded', 400); } - const breweryImages = await addBreweryImageToDB({ - alt: body.alt, - caption: body.caption, + const breweryImages = await addBreweryImagesToDB({ breweryPostId: req.query.id, userId: user!.id, + body, files, }); diff --git a/src/controllers/images/types/index.ts b/src/controllers/images/types/index.ts index f91a095..8d4b308 100644 --- a/src/controllers/images/types/index.ts +++ b/src/controllers/images/types/index.ts @@ -7,3 +7,7 @@ export interface UploadImagesRequest extends UserExtendedNextApiRequest { query: { id: string }; body: z.infer; } + +export interface DeleteImageRequest extends UserExtendedNextApiRequest { + query: { id: string }; +} diff --git a/src/controllers/likes/beer-posts-likes/index.ts b/src/controllers/likes/beer-posts-likes/index.ts index f8eb405..c37e953 100644 --- a/src/controllers/likes/beer-posts-likes/index.ts +++ b/src/controllers/likes/beer-posts-likes/index.ts @@ -4,12 +4,12 @@ import ServerError from '@/config/util/ServerError'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { NextApiResponse, NextApiRequest } from 'next'; import { z } from 'zod'; -import { LikeRequest } from '../types'; import createBeerPostLike from '@/services/likes/beer-post-like/createBeerPostLike'; import findBeerPostLikeById from '@/services/likes/beer-post-like/findBeerPostLikeById'; import getBeerPostLikeCountByBeerPostId from '@/services/likes/beer-post-like/getBeerPostLikeCount'; import removeBeerPostLikeById from '@/services/likes/beer-post-like/removeBeerPostLikeById'; import { getBeerPostById } from '@/services/posts/beer-post'; +import { LikeRequest } from '../types'; export const sendBeerPostLikeRequest = async ( req: LikeRequest, @@ -20,7 +20,7 @@ export const sendBeerPostLikeRequest = async ( const beer = await getBeerPostById({ beerPostId: id }); if (!beer) { - throw new ServerError('Could not find a beer post with that id', 404); + throw new ServerError('Could not find a beer post with that id.', 404); } const alreadyLiked = await findBeerPostLikeById({ diff --git a/src/controllers/posts/beer-styles/index.ts b/src/controllers/posts/beer-styles/index.ts index 75007e7..657b3dc 100644 --- a/src/controllers/posts/beer-styles/index.ts +++ b/src/controllers/posts/beer-styles/index.ts @@ -10,9 +10,9 @@ import getAllBeerStyles from '@/services/posts/beer-style-post/getAllBeerStyles' import ServerError from '@/config/util/ServerError'; +import { getBeerPostsByBeerStyleIdService } from '@/services/posts/beer-post'; import { CreateBeerStyleRequest, GetBeerStyleByIdRequest } from './types'; import { GetAllPostsByConnectedPostId, GetAllPostsRequest } from '../types'; -import { getBeerPostsByBeerStyleIdService } from '@/services/posts/beer-post'; export const getBeerStyle = async ( req: GetBeerStyleByIdRequest, diff --git a/src/prisma/seed/clear/clearCloudinaryStorage.ts b/src/prisma/seed/clear/clearCloudinaryStorage.ts new file mode 100644 index 0000000..e280644 --- /dev/null +++ b/src/prisma/seed/clear/clearCloudinaryStorage.ts @@ -0,0 +1,7 @@ +import { cloudinary } from '../../../config/cloudinary'; + +const clearCloudinaryStorage = async () => { + await cloudinary.api.delete_resources_by_prefix('biergarten-dev/'); +}; + +export default clearCloudinaryStorage; diff --git a/src/prisma/seed/clear/index.ts b/src/prisma/seed/clear/index.ts index 5410ec0..e9b0d50 100644 --- a/src/prisma/seed/clear/index.ts +++ b/src/prisma/seed/clear/index.ts @@ -1,7 +1,16 @@ import logger from '../../../config/pino/logger'; +import clearCloudinaryStorage from './clearCloudinaryStorage'; import clearDatabase from './clearDatabase'; -clearDatabase().then(() => { - logger.info('Database cleared'); - process.exit(0); -}); +(async () => { + await clearDatabase(); + await clearCloudinaryStorage(); +})() + .then(() => { + logger.info('Successfully cleared database and cloudinary storage.'); + process.exit(0); + }) + .catch((err) => { + logger.error(err); + process.exit(1); + }); diff --git a/src/prisma/seed/index.ts b/src/prisma/seed/index.ts index 059d2c6..3eeccfd 100644 --- a/src/prisma/seed/index.ts +++ b/src/prisma/seed/index.ts @@ -20,6 +20,7 @@ import createNewBeerStyleComments from './create/createNewBeerStyleComments'; import createNewBeerStyleLikes from './create/createNewBeerStyleLikes'; import createNewUserAvatars from './create/createNewUserAvatars'; import createNewUserFollows from './create/createNewUserFollows'; +import clearCloudinaryStorage from './clear/clearCloudinaryStorage'; (async () => { try { @@ -27,6 +28,7 @@ import createNewUserFollows from './create/createNewUserFollows'; logger.info('Clearing database.'); await clearDatabase(); + await clearCloudinaryStorage(); logger.info('Database cleared successfully, preparing to seed.'); await createAdminUser(); diff --git a/src/services/images/beer-image/addBeerImageToDB.ts b/src/services/images/beer-image/addBeerImageToDB.ts deleted file mode 100644 index e4e472a..0000000 --- a/src/services/images/beer-image/addBeerImageToDB.ts +++ /dev/null @@ -1,40 +0,0 @@ -import DBClient from '@/prisma/DBClient'; -import { BeerImage } from '@prisma/client'; -import { z } from 'zod'; -import ImageMetadataValidationSchema from '../../schema/ImageSchema/ImageMetadataValidationSchema'; - -interface ProcessImageDataArgs { - files: Express.Multer.File[]; - alt: z.infer['alt']; - caption: z.infer['caption']; - beerPostId: string; - userId: string; -} - -const addBeerImageToDB = ({ - alt, - caption, - files, - beerPostId, - userId, -}: ProcessImageDataArgs) => { - const beerImagePromises: Promise[] = []; - - files.forEach((file) => { - beerImagePromises.push( - DBClient.instance.beerImage.create({ - data: { - alt, - caption, - postedBy: { connect: { id: userId } }, - beerPost: { connect: { id: beerPostId } }, - path: file.path, - }, - }), - ); - }); - - return Promise.all(beerImagePromises); -}; - -export default addBeerImageToDB; diff --git a/src/services/images/beer-image/index.ts b/src/services/images/beer-image/index.ts new file mode 100644 index 0000000..16e5a63 --- /dev/null +++ b/src/services/images/beer-image/index.ts @@ -0,0 +1,87 @@ +import DBClient from '@/prisma/DBClient'; +import { BeerImage } from '@prisma/client'; +import { cloudinary } from '@/config/cloudinary'; +import { + AddBeerImagesToDB, + DeleteBeerImageFromDBAndStorage, + UpdateBeerImageMetadata, +} from './types'; + +/** + * Adds beer images to the database. + * + * @param options - The options for adding beer images. + * @param options.body - The body of the request. + * @param options.body.alt - The alt text for the beer image. + * @param options.body.caption - The caption for the beer image. + * @param options.files - The array of files to be uploaded as beer images. + * @param options.beerPostId - The ID of the beer post. + * @param options.userId - The ID of the user. + * @returns A promise that resolves to an array of created beer images. + */ +export const addBeerImagesToDB: AddBeerImagesToDB = ({ + body, + files, + beerPostId, + userId, +}) => { + const beerImagePromises: Promise[] = []; + + const { alt, caption } = body; + files.forEach((file) => { + beerImagePromises.push( + DBClient.instance.beerImage.create({ + data: { + alt, + caption, + postedBy: { connect: { id: userId } }, + beerPost: { connect: { id: beerPostId } }, + path: file.path, + }, + }), + ); + }); + + return Promise.all(beerImagePromises); +}; + +/** + * Deletes a beer image from the database and storage. + * + * @param options - The options for deleting a beer image. + * @param options.beerImageId - The ID of the beer image. + */ +export const deleteBeerImageFromDBAndStorage: DeleteBeerImageFromDBAndStorage = async ({ + beerImageId, +}) => { + const deleted = await DBClient.instance.beerImage.delete({ + where: { id: beerImageId }, + select: { path: true, id: true }, + }); + const { path } = deleted; + await cloudinary.uploader.destroy(path); +}; + +/** + * Updates the beer image metadata in the database. + * + * @param options - The options for updating the beer image metadata. + * @param options.beerImageId - The ID of the beer image. + * @param options.body - The body of the request containing the alt and caption. + * @param options.body.alt - The alt text for the beer image. + * @param options.body.caption - The caption for the beer image. + * @returns A promise that resolves to the updated beer image. + */ + +export const updateBeerImageMetadata: UpdateBeerImageMetadata = async ({ + beerImageId, + body, +}) => { + const { alt, caption } = body; + const updated = await DBClient.instance.beerImage.update({ + where: { id: beerImageId }, + data: { alt, caption }, + }); + + return updated; +}; diff --git a/src/services/images/beer-image/types/index.ts b/src/services/images/beer-image/types/index.ts new file mode 100644 index 0000000..e392c47 --- /dev/null +++ b/src/services/images/beer-image/types/index.ts @@ -0,0 +1,19 @@ +import ImageMetadataValidationSchema from '@/services/schema/ImageSchema/ImageMetadataValidationSchema'; +import { BeerImage } from '@prisma/client'; +import { z } from 'zod'; + +export type AddBeerImagesToDB = (args: { + files: Express.Multer.File[]; + body: z.infer; + beerPostId: string; + userId: string; +}) => Promise; + +export type DeleteBeerImageFromDBAndStorage = (args: { + beerImageId: string; +}) => Promise; + +export type UpdateBeerImageMetadata = (args: { + beerImageId: string; + body: z.infer; +}) => Promise; diff --git a/src/services/images/brewery-image/addBreweryImageToDB.ts b/src/services/images/brewery-image/addBreweryImageToDB.ts deleted file mode 100644 index 99992f8..0000000 --- a/src/services/images/brewery-image/addBreweryImageToDB.ts +++ /dev/null @@ -1,39 +0,0 @@ -import DBClient from '@/prisma/DBClient'; -import { BreweryImage } from '@prisma/client'; -import { z } from 'zod'; -import ImageMetadataValidationSchema from '../../schema/ImageSchema/ImageMetadataValidationSchema'; - -interface ProcessImageDataArgs { - files: Express.Multer.File[]; - alt: z.infer['alt']; - caption: z.infer['caption']; - breweryPostId: string; - userId: string; -} - -const addBreweryImageToDB = ({ - alt, - caption, - files, - breweryPostId, - userId, -}: ProcessImageDataArgs) => { - const breweryImagePromises: Promise[] = []; - files.forEach((file) => { - breweryImagePromises.push( - DBClient.instance.breweryImage.create({ - data: { - alt, - caption, - postedBy: { connect: { id: userId } }, - breweryPost: { connect: { id: breweryPostId } }, - path: file.path, - }, - }), - ); - }); - - return Promise.all(breweryImagePromises); -}; - -export default addBreweryImageToDB; diff --git a/src/services/images/brewery-image/index.ts b/src/services/images/brewery-image/index.ts new file mode 100644 index 0000000..22d8d28 --- /dev/null +++ b/src/services/images/brewery-image/index.ts @@ -0,0 +1,86 @@ +import DBClient from '@/prisma/DBClient'; +import { BreweryImage } from '@prisma/client'; +import { cloudinary } from '@/config/cloudinary'; +import { + AddBreweryImagesToDB, + DeleteBreweryImageFromDBAndStorage, + UpdateBreweryImageMetadata, +} from './types'; + +/** + * Adds brewery images to the database. + * + * @param options - The options for adding brewery images. + * @param options.body - The body of the request containing the alt and caption. + * @param options.body.alt - The alt text for the brewery image. + * @param options.body.caption - The caption for the brewery image. + * @param options.files - The array of files to be uploaded as brewery images. + * @param options.breweryPostId - The ID of the brewery post. + * @param options.userId - The ID of the user adding the images. + * @returns A promise that resolves to an array of created brewery images. + */ + +export const addBreweryImagesToDB: AddBreweryImagesToDB = ({ + body, + files, + breweryPostId, + userId, +}) => { + const breweryImagePromises: Promise[] = []; + + const { alt, caption } = body; + files.forEach((file) => { + breweryImagePromises.push( + DBClient.instance.breweryImage.create({ + data: { + alt, + caption, + postedBy: { connect: { id: userId } }, + breweryPost: { connect: { id: breweryPostId } }, + path: file.path, + }, + }), + ); + }); + + return Promise.all(breweryImagePromises); +}; + +/** + * Deletes a brewery image from the database and storage. + * + * @param options - The options for deleting a brewery image. + * @param options.breweryImageId - The ID of the brewery image. + */ +export const deleteBreweryImageFromDBAndStorage: DeleteBreweryImageFromDBAndStorage = + async ({ breweryImageId }) => { + const deleted = await DBClient.instance.breweryImage.delete({ + where: { id: breweryImageId }, + select: { path: true, id: true }, + }); + const { path } = deleted; + await cloudinary.uploader.destroy(path); + }; + +/** + * Updates the brewery image metadata in the database. + * + * @param options - The options for updating the brewery image metadata. + * @param options.breweryImageId - The ID of the brewery image. + * @param options.body - The body of the request containing the alt and caption. + * @param options.body.alt - The alt text for the brewery image. + * @param options.body.caption - The caption for the brewery image. + * @returns A promise that resolves to the updated brewery image. + */ +export const updateBreweryImageMetadata: UpdateBreweryImageMetadata = async ({ + breweryImageId, + body, +}) => { + const { alt, caption } = body; + const updated = await DBClient.instance.breweryImage.update({ + where: { id: breweryImageId }, + data: { alt, caption }, + }); + + return updated; +}; diff --git a/src/services/images/brewery-image/types/index.ts b/src/services/images/brewery-image/types/index.ts new file mode 100644 index 0000000..6da8221 --- /dev/null +++ b/src/services/images/brewery-image/types/index.ts @@ -0,0 +1,19 @@ +import ImageMetadataValidationSchema from '@/services/schema/ImageSchema/ImageMetadataValidationSchema'; +import { BreweryImage } from '@prisma/client'; +import { z } from 'zod'; + +export type AddBreweryImagesToDB = (args: { + files: Express.Multer.File[]; + body: z.infer; + breweryPostId: string; + userId: string; +}) => Promise; + +export type DeleteBreweryImageFromDBAndStorage = (args: { + breweryImageId: string; +}) => Promise; + +export type UpdateBreweryImageMetadata = (args: { + breweryImageId: string; + body: z.infer; +}) => Promise;