diff --git a/.env.example b/.env.example index ba6462de..207d90b7 100644 --- a/.env.example +++ b/.env.example @@ -20,4 +20,10 @@ GOOGLE_CLIENT_SECRET= #Redis Configuration REDIS_HOST= REDIS_PORT= -GOOGLE_AUTH_CALLBACK_URL= \ No newline at end of file + +# Cloudinary setup +CLOUDINARY_CLOUD_NAME= +CLOUDINARY_API_KEY= +CLOUDINARY_API_SECRET= +======= +GOOGLE_AUTH_CALLBACK_URL= diff --git a/contributors.md b/contributors.md index 88f5fd09..449cb74e 100644 --- a/contributors.md +++ b/contributors.md @@ -1,4 +1,3 @@ - -[Nainah23](https://github.com/Nainah23) -Erasmus Tayviah (StarmannRassy) - +- [Nainah23](https://github.com/Nainah23) +- Erasmus Tayviah (StarmannRassy) +- [Adekolu Samuel Samixx](https://github.com/samixYasuke) diff --git a/package.json b/package.json index c2a70d05..a58afa61 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@types/express": "^4.17.21", "@types/fs-extra": "^11.0.4", "@types/jest": "^29.5.12", + "@types/multer": "^1", "@types/node": "^16.18.103", "@types/passport": "^1.0.16", "@types/supertest": "^6.0.2", @@ -59,6 +60,7 @@ "bcryptjs": "^2.4.3", "bull": "^4.15.1", "class-validator": "^0.14.1", + "cloudinary": "^2.3.0", "config": "^3.3.12", "cors": "^2.8.5", "dayjs": "^1.11.12", @@ -68,13 +70,12 @@ "express-validator": "^7.1.0", "fs-extra": "^11.2.0", "handlebars": "^4.7.8", - "jest-mock-extended": "^3.0.7", - "jest": "^29.7.0", "json2csv": "^6.0.0-alpha.2", - "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", + "multer-storage-cloudinary": "^4.0.0", "node-mailer": "^0.1.1", "pdfkit": "^0.15.0", "passport": "^0.7.0", @@ -88,6 +89,7 @@ "ts-node-dev": "^2.0.0", "twilio": "^5.2.2", "typeorm": "^0.3.20", + "uuid": "^10.0.0", "zod": "^3.23.8" }, "lint-staged": { diff --git a/src/config/cloudinary.ts b/src/config/cloudinary.ts new file mode 100644 index 00000000..4e8a9b1e --- /dev/null +++ b/src/config/cloudinary.ts @@ -0,0 +1,16 @@ +import * as cloudinary from "cloudinary"; +import { ConfigOptions } from "cloudinary"; + +const cloudinaryConfig = ( + cloudName: string, + apiKey: string, + apiSecret: string, +): ConfigOptions => { + return cloudinary.v2.config({ + cloud_name: cloudName, + api_key: apiKey, + api_secret: apiSecret, + }); +}; + +export default cloudinaryConfig; diff --git a/src/config/google.passport.config.ts b/src/config/google.passport.config.ts index 25cc0540..ce4f6072 100644 --- a/src/config/google.passport.config.ts +++ b/src/config/google.passport.config.ts @@ -1,20 +1,31 @@ import config from "."; import passport from "passport"; -import { Strategy as GoogleStrategy, Profile, VerifyCallback } from "passport-google-oauth2"; +import { + Strategy as GoogleStrategy, + Profile, + VerifyCallback, +} from "passport-google-oauth2"; -passport.use(new GoogleStrategy({ - clientID: config.GOOGLE_CLIENT_ID, - clientSecret: config.GOOGLE_CLIENT_SECRET, - callbackURL: config.GOOGLE_AUTH_CALLBACK_URL - }, - async (_accessToken: string, _refreshToken: string, profile: Profile, done: VerifyCallback) => { - try { - return done(null, profile); - } catch (error) { - return done(error); - } - } -)); +passport.use( + new GoogleStrategy( + { + clientID: config.GOOGLE_CLIENT_ID, + clientSecret: config.GOOGLE_CLIENT_SECRET, + callbackURL: config.GOOGLE_AUTH_CALLBACK_URL, + }, + async ( + _accessToken: string, + _refreshToken: string, + profile: Profile, + done: VerifyCallback, + ) => { + try { + return done(null, profile); + } catch (error) { + return done(error); + } + }, + ), +); - -export default passport; \ No newline at end of file +export default passport; diff --git a/src/config/index.ts b/src/config/index.ts index be29a8eb..c57f297a 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -22,9 +22,9 @@ const config = { TWILIO_SID: process.env.TWILIO_SID, TWILIO_AUTH_TOKEN: process.env.TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER: process.env.TWILIO_PHONE_NUMBER, - GOOGLE_CLIENT_ID:process.env.GOOGLE_CLIENT_ID, - GOOGLE_CLIENT_SECRET:process.env.GOOGLE_CLIENT_SECRET, - GOOGLE_AUTH_CALLBACK_URL: process.env.GOOGLE_AUTH_CALLBACK_URL + GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, + GOOGLE_AUTH_CALLBACK_URL: process.env.GOOGLE_AUTH_CALLBACK_URL, }; export default config; diff --git a/src/config/multer.ts b/src/config/multer.ts new file mode 100644 index 00000000..a4b2beb4 --- /dev/null +++ b/src/config/multer.ts @@ -0,0 +1,47 @@ +import multer from "multer"; +import { v2 as cloudinary } from "cloudinary"; +import { CloudinaryStorage } from "multer-storage-cloudinary"; +import cloudinaryConfig from "./cloudinary"; +import * as dotenv from "dotenv"; +dotenv.config(); + +interface CustomParams { + folder: string; + allowedFormats: string[]; +} + +const cloudinaryConfigOptions = cloudinaryConfig( + process.env["CLOUDINARY_CLOUD_NAME"] as string, + process.env["CLOUDINARY_API_KEY"] as string, + process.env["CLOUDINARY_API_SECRET"] as string, +); + +cloudinary.config(cloudinaryConfigOptions); + +const storage = new CloudinaryStorage({ + cloudinary, + params: { + folder: "images", + allowedFormats: ["jpg", "png", "jpeg"], + } as CustomParams, +}); + +const FILE_SIZE = 1024 * 1024 * 2; // 2MB + +export const multerConfig = multer({ + storage, + limits: { + fileSize: FILE_SIZE, + }, + fileFilter: (_req, file, cb) => { + if (!file.mimetype.startsWith("image/")) { + return cb(new Error("Only images are allowed")); + } + if (file.size > FILE_SIZE) { + return cb(new Error("Image should not be more than 4MB")); + } + cb(null, true); + }, +}); + +export { cloudinary }; diff --git a/src/controllers/AdminController.ts b/src/controllers/AdminController.ts index 24188c25..40ba075f 100644 --- a/src/controllers/AdminController.ts +++ b/src/controllers/AdminController.ts @@ -4,6 +4,89 @@ import { HttpError } from "../middleware"; import { check, param, validationResult } from "express-validator"; import { UserRole } from "../enums/userRoles"; +/** + * @swagger + * tags: + * name: Admin + * description: Admin Related Routes + */ + +/** + * @swagger + * /api/v1/admin/organisation/:id: + * patch: + * summary: Admin-Update an existing organisation + * tags: [Admin] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * email: + * type: string + * slug: + * type: string + * type: + * type: string + * industry: + * type: string + * state: + * type: string + * country: + * type: string + * address: + * type: string + * responses: + * 200: + * description: Organisation Updated Successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * type: object + * properties: + * id: + * type: string + * name: + * type: string + * email: + * type: string + * slug: + * type: string + * type: + * type: string + * industry: + * type: string + * state: + * type: string + * country: + * type: string + * address: + * type: string + * created_at: + * type: string + * format: date-time + * updated_at: + * type: string + * format: date-time + * status_code: + * type: integer + * 400: + * description: Bad Request + * 500: + * description: Internal Server Error + */ + class AdminOrganisationController { private adminService: AdminOrganisationService; @@ -45,8 +128,14 @@ class AdminOrganisationController { async setUserRole(req: Request, res: Response): Promise { try { - await param("user_id").isUUID().withMessage("Valid user ID must be provided.").run(req); - await check("role").isIn(Object.values(UserRole)).withMessage("Valid role must be provided.").run(req); + await param("user_id") + .isUUID() + .withMessage("Valid user ID must be provided.") + .run(req); + await check("role") + .isIn(Object.values(UserRole)) + .withMessage("Valid role must be provided.") + .run(req); const errors = validationResult(req); @@ -69,7 +158,9 @@ class AdminOrganisationController { }, }); } catch (error) { - res.status(error.status_code || 500).json({ message: error.message || "Internal Server Error" }); + res + .status(error.status_code || 500) + .json({ message: error.message || "Internal Server Error" }); } } } @@ -81,6 +172,75 @@ class AdminUserController { this.adminUserService = new AdminUserService(); } + /** + * @swagger + * /api/v1/admin/users/:id: + * patch: + * summary: Admin-Update an existing user + * tags: [Admin] + * parameters: + * - in: path + * name: id + * required: true + * description: The ID of the user to update + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * email: + * type: string + * role: + * type: string + * isverified: + * type: boolean + * responses: + * 200: + * description: User Updated Successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * type: object + * properties: + * id: + * type: string + * name: + * type: string + * email: + * type: string + * role: + * type: string + * isverified: + * type: boolean + * createdAt: + * type: string + * format: date-time + * updatedAt: + * type: string + * format: date-time + * status_code: + * type: integer + * 400: + * description: Bad Request + * 404: + * description: User Not Found + * 500: + * description: Internal Server Error + */ + //Update Single User async updateUser(req: Request, res: Response): Promise { try { @@ -110,6 +270,71 @@ class AdminUserController { } } + /** + * @swagger + * /api/v1/admin/users: + * get: + * summary: Admin-List users with pagination + * tags: [Admin] + * parameters: + * - in: query + * name: page + * required: false + * description: Page number for pagination + * schema: + * type: integer + * default: 1 + * - in: query + * name: limit + * required: false + * description: Number of users per page + * schema: + * type: integer + * default: 5 + * responses: + * 200: + * description: Users retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * users: + * type: array + * items: + * type: object + * properties: + * name: + * type: string + * email: + * type: string + * createdAt: + * type: string + * format: date-time + * updatedAt: + * type: string + * format: date-time + * pagination: + * type: object + * properties: + * totalUsers: + * type: integer + * totalPages: + * type: integer + * currentPage: + * type: integer + * status_code: + * type: integer + * 400: + * description: Bad Request + * 500: + * description: Internal Server Error + */ + async listUsers(req: Request, res: Response): Promise { try { const page = parseInt(req.query.page as string) || 1; diff --git a/src/controllers/GoogleAuthController.ts b/src/controllers/GoogleAuthController.ts index 7894f81e..31c7c335 100644 --- a/src/controllers/GoogleAuthController.ts +++ b/src/controllers/GoogleAuthController.ts @@ -1,27 +1,38 @@ import passport from "../config/google.passport.config"; -import {ServerError, Unauthorized } from "../middleware"; +import { ServerError, Unauthorized } from "../middleware"; import { Request, Response, NextFunction } from "express"; import { GoogleAuthService } from "../services/google.passport.service"; +export const initiateGoogleAuthRequest = passport.authenticate("google", { + scope: ["openid", "email", "profile"], +}); -export const initiateGoogleAuthRequest = passport.authenticate('google', { scope: [ 'openid', 'email', 'profile' ] }) - -export const googleAuthCallback = (req: Request, res: Response, next: NextFunction) => { - const authenticate = passport.authenticate('google', async (error, user, info) => { - const googleAuthService = new GoogleAuthService(); +export const googleAuthCallback = ( + req: Request, + res: Response, + next: NextFunction, +) => { + const authenticate = passport.authenticate( + "google", + async (error, user, info) => { + const googleAuthService = new GoogleAuthService(); try { - if (error) { - throw new ServerError("Authentication error"); + if (error) { + throw new ServerError("Authentication error"); } if (!user) { - throw new Unauthorized("Authentication failed!") + throw new Unauthorized("Authentication failed!"); } const isDbUser = await googleAuthService.getUserByGoogleId(user.id); - const dbUser = await googleAuthService.handleGoogleAuthUser(user, isDbUser) + const dbUser = await googleAuthService.handleGoogleAuthUser( + user, + isDbUser, + ); res.status(200).json(dbUser); - } catch(error) { - next(error) + } catch (error) { + next(error); } - }); - authenticate(req, res, next) -} \ No newline at end of file + }, + ); + authenticate(req, res, next); +}; diff --git a/src/controllers/ProductController.ts b/src/controllers/ProductController.ts index 40636bef..567ad3d0 100644 --- a/src/controllers/ProductController.ts +++ b/src/controllers/ProductController.ts @@ -168,7 +168,7 @@ export class ProductController { * @swagger * /api/v1/products/{product_id}: * get: - * summary: Fetch a product by {id} + * summary: Fetch a product by its ID * tags: [Product] * parameters: * - in: path @@ -176,17 +176,17 @@ export class ProductController { * required: true * schema: * type: integer - * description: String ID of the product + * description: The ID of the product to fetch * responses: * 200: - * description: Successful response + * description: Product retrieved successfully * content: * application/json: * schema: * type: object * properties: * id: - * type: string + * type: integer * example: 123 * name: * type: string @@ -196,30 +196,42 @@ export class ProductController { * example: Product is robust * price: * type: number - * exanple: 19 + * example: 19 * category: * type: string * example: Gadgets * 400: - * description: Bad request + * description: Bad request due to invalid product ID * content: * application/json: * schema: * type: object * properties: - * error: + * status: + * type: string + * example: Bad Request + * message: * type: string - * example: Invalid product ID + * example: Invalid Product Id + * status_code: + * type: integer + * example: 400 * 404: - * description: Not found + * description: Product not found * content: * application/json: * schema: * type: object * properties: - * error: + * status: + * type: string + * example: Not Found + * message: * type: string * example: Product not found + * status_code: + * type: integer + * example: 404 * 500: * description: Internal server error * content: @@ -227,11 +239,49 @@ export class ProductController { * schema: * type: object * properties: - * error: + * status: * type: string * example: An unexpected error occurred + * message: + * type: string + * example: Internal server error + * status_code: + * type: integer + * example: 500 */ - async fetchProductById(req: Request, res: Response) {} + + async fetchProductById(req: Request, res: Response) { + + const productId = req.params.product_id; + + if(isNaN(Number(productId))){ + return res.status(400).json({ + status: "Bad Request", + message: "Invalid Product Id", + status_code: 400, + }) + } + + try { + const product = await this.productService.getOneProduct(productId) + if (!product) { + return res.status(404).json({ + status: "Not found", + message: "Product not found", + status_code: 404, + }); + + } + return res.status(200).json(product); + } catch (error) { + return res.status(500).json({ + status: "An unexpected error occurred", + message: "Internal server error", + status_code: 500, + }); + } + + } /** * @swagger @@ -515,7 +565,10 @@ export class ProductController { * type: string * example: Product not found */ - async deleteProduct(req: Request, res: Response) {} + async deleteProduct(req: Request, res: Response) { + + + } } export default ProductController; diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts index c85c06b1..14ca51fc 100644 --- a/src/controllers/UserController.ts +++ b/src/controllers/UserController.ts @@ -15,16 +15,17 @@ class UserController { static async getProfile(req: Request, res: Response, next: NextFunction) { try { const { id } = req.user; + // const id = "96cf0567-9ca6-4ce0-b9f7-e3fa816fc070"; if (!id) { return res.status(401).json({ status_code: 401, - error: "Unauthorized", + error: "Unauthorized! no ID provided", }); } if (!validate(id)) { - return res.status(401).json({ - status_code: 401, + return res.status(400).json({ + status_code: 400, error: "Unauthorized! Invalid User Id Format", }); } @@ -81,36 +82,53 @@ class UserController { if (!id || !isUUID(id)) { return res.status(400).json({ - status: "unsuccesful", - status_code: 400, - message: "Valid id must be provided", - }); + status: "unsuccesful", + status_code: 400, + message: "Valid id must be provided", + }); } try { - await this.userService.softDeleteUser(id); return res.status(202).json({ - status: "sucess", - message: "User deleted successfully", - status_code: 202, - }); - + status: "sucess", + message: "User deleted successfully", + status_code: 202, + }); } catch (error) { - if (error instanceof HttpError) { return res.status(error.status_code).json({ - message: error.message + message: error.message, }); } else { return res.status(500).json({ - message: error.message || "Internal Server Error" + message: error.message || "Internal Server Error", }); } + } + } + public async updateUserProfile(req: Request, res: Response) { + try { + const user = await this.userService.updateUserProfile( + req.params.id, + req.body, + req.file, + ); + res.status(200).json(user); + } catch (error) { + if (error instanceof HttpError) { + return res.status(error.status_code).json({ + message: error.message, + }); + } else { + return res.status(500).json({ + message: error.message || "Internal Server Error", + }); + } } } } -export default UserController; \ No newline at end of file +export { UserController }; diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 7a382622..38bbe5ac 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -8,3 +8,4 @@ export * from "./AdminController"; export * from "./NotificationController"; export * from "./BlogController"; export * from "./exportController"; + diff --git a/src/data-source.ts b/src/data-source.ts index 24a9b60e..18824d9e 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -7,7 +7,7 @@ const isDevelopment = config.NODE_ENV === "development"; const AppDataSource = new DataSource({ type: "postgres", host: config.DB_HOST, - port: 5432, + port: Number(config.DB_PORT) || 5432, username: config.DB_USER, password: config.DB_PASSWORD, database: config.DB_NAME, diff --git a/src/middleware/checkUserRole.ts b/src/middleware/checkUserRole.ts index e0557500..20208d56 100644 --- a/src/middleware/checkUserRole.ts +++ b/src/middleware/checkUserRole.ts @@ -2,28 +2,39 @@ import { Request, Response, NextFunction } from "express"; import { UserRole } from "../enums/userRoles"; import { Unauthorized } from "./error"; import { User } from "../models"; -import AppDataSource from "../data-source"; -import jwt from 'jsonwebtoken'; - +import AppDataSource from "../data-source"; +import jwt from "jsonwebtoken"; export const checkPermissions = (roles: UserRole[]) => { - return async (req: Request & { user?: User }, res: Response, next: NextFunction) => { - const authHeader = req.headers['authorization']; - const token = authHeader && authHeader.split(' ')[1]; - try { - const decodedToken = jwt.decode(token); - if (typeof decodedToken === 'string' || !decodedToken) { - return res.status(401).json({ status: 'error', message: 'Access denied. Invalid token' }); - } - const userRepository = AppDataSource.getRepository(User); - const user = await userRepository.findOne({ where: { id: decodedToken.userId } }); - // if (user.role !== 'super_admin' ) - if (!user || !roles.includes(user.role)) { - return res.status(401).json({ status: 'error', message: 'Access denied. Not an admin' }); + return async ( + req: Request & { user?: User }, + res: Response, + next: NextFunction, + ) => { + const authHeader = req.headers["authorization"]; + const token = authHeader && authHeader.split(" ")[1]; + try { + const decodedToken = jwt.decode(token); + if (typeof decodedToken === "string" || !decodedToken) { + return res + .status(401) + .json({ status: "error", message: "Access denied. Invalid token" }); + } + const userRepository = AppDataSource.getRepository(User); + const user = await userRepository.findOne({ + where: { id: decodedToken.userId }, + }); + // if (user.role !== 'super_admin' ) + if (!user || !roles.includes(user.role)) { + return res + .status(401) + .json({ status: "error", message: "Access denied. Not an admin" }); + } + next(); + } catch (error) { + res + .status(401) + .json({ status: "error", message: "Access denied. Invalid token" }); } - next(); - } catch (error) { - res.status(401).json({ status: 'error', message: 'Access denied. Invalid token' }); - } -} -} \ No newline at end of file + }; +}; diff --git a/src/models/product.ts b/src/models/product.ts index 962ee4a8..dcad994c 100644 --- a/src/models/product.ts +++ b/src/models/product.ts @@ -1,7 +1,6 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; -import { User } from './user'; -import ExtendedBaseEntity from './extended-base-entity'; - +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from "typeorm"; +import { User } from "./user"; +import ExtendedBaseEntity from "./extended-base-entity"; /** * @swagger @@ -34,7 +33,7 @@ import ExtendedBaseEntity from './extended-base-entity'; @Entity() export class Product extends ExtendedBaseEntity { - @PrimaryGeneratedColumn('uuid') + @PrimaryGeneratedColumn("uuid") id: string; @Column() diff --git a/src/routes/admin.ts b/src/routes/admin.ts index 9e52fcb3..6002ff46 100644 --- a/src/routes/admin.ts +++ b/src/routes/admin.ts @@ -30,16 +30,17 @@ adminRouter.get( // User adminRouter.patch( - "/user/:id", + "/users/:id", authMiddleware, checkPermissions([UserRole.SUPER_ADMIN]), adminUserController.updateUser.bind(adminUserController), // Use updateUser method ); adminRouter.post( - "/users/:user_id/roles", authMiddleware, - checkPermissions([UserRole.SUPER_ADMIN]), - adminOrganisationController.setUserRole.bind(adminOrganisationController), - ); + "/users/:user_id/roles", + authMiddleware, + checkPermissions([UserRole.SUPER_ADMIN]), + adminOrganisationController.setUserRole.bind(adminOrganisationController), +); export { adminRouter }; diff --git a/src/routes/help-center.ts b/src/routes/help-center.ts index 95d18638..9c36160c 100644 --- a/src/routes/help-center.ts +++ b/src/routes/help-center.ts @@ -1,10 +1,18 @@ // src/routes/help-center.ts import { Router } from "express"; import HelpController from "../controllers/HelpController"; -import { authMiddleware} from "../middleware/auth"; +import { authMiddleware } from "../middleware/auth"; const helpRouter = Router(); const helpController = new HelpController(); -helpRouter.post("/topics", authMiddleware, helpController.createTopic.bind(helpController)); -helpRouter.patch("/topics/:id", authMiddleware, helpController.updateTopic.bind(helpController)); -export { helpRouter }; \ No newline at end of file +helpRouter.post( + "/topics", + authMiddleware, + helpController.createTopic.bind(helpController), +); +helpRouter.patch( + "/topics/:id", + authMiddleware, + helpController.updateTopic.bind(helpController), +); +export { helpRouter }; diff --git a/src/routes/product.ts b/src/routes/product.ts index ee3c1ec1..aa0ebb65 100644 --- a/src/routes/product.ts +++ b/src/routes/product.ts @@ -1,6 +1,6 @@ -import express from 'express'; -import ProductController from '../controllers/ProductController'; -import { authMiddleware } from '../middleware'; +import express from "express"; +import ProductController from "../controllers/ProductController"; +import { authMiddleware } from "../middleware"; const productRouter = express.Router(); const productController = new ProductController(); @@ -85,9 +85,9 @@ const productController = new ProductController(); */ productRouter.get( - '/', + "/", authMiddleware, - productController.getProductPagination.bind(productController) + productController.getProductPagination.bind(productController), ); export { productRouter }; diff --git a/src/routes/user.ts b/src/routes/user.ts index 4d4c9d9f..2e3e7d42 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -1,6 +1,9 @@ import { Router } from "express"; -import UserController from "../controllers/UserController"; +import { UserController } from "../controllers"; import { authMiddleware } from "../middleware"; +import { multerConfig } from "../config/multer"; + +const upload = multerConfig.single("avatarUrl"); const userRouter = Router(); const userController = new UserController(); @@ -12,5 +15,11 @@ userRouter.delete( userController.deleteUser.bind(userController), ); userRouter.get("/me", authMiddleware, UserController.getProfile); +userRouter.put( + "/:id", + authMiddleware, + upload, + userController.updateUserProfile.bind(userController), +); export { userRouter }; diff --git a/src/seeder.ts b/src/seeder.ts index 8a1f2502..c9bc0e21 100644 --- a/src/seeder.ts +++ b/src/seeder.ts @@ -1,153 +1,125 @@ -// src/seeder.ts -import AppDataSource from "./data-source"; -import { User, Organization, Product, Profile } from "./models"; -import log from "./utils/logger"; - -const createUsers = async () => { - try { - log.info("Creating user1..."); - const user1 = new User(); - user1.name = "John Doe"; - user1.email = "johndoe@example.com"; - user1.password = "password"; - user1.otp = Math.floor(Math.random() * 10000); - user1.otp_expires_at = new Date(Date.now() + 3600 * 1000); - user1.profile = new Profile(); - user1.profile.first_name = "John"; - user1.profile.last_name = "Doe"; - user1.profile.phone = "1234567890"; - user1.profile.avatarUrl = "http://example.com/avatar.jpg"; - - log.info("User1 created: ", user1); - - log.info("Creating user2..."); - const user2 = new User(); - user2.name = "Jane Doe"; - user2.email = "janedoe@example.com"; - user2.password = "password"; - user2.otp = Math.floor(Math.random() * 10000); - user2.otp_expires_at = new Date(Date.now() + 3600 * 1000); - user2.profile = new Profile(); - user2.profile.first_name = "Jane"; - user2.profile.last_name = "Doe"; - user2.profile.phone = "0987654321"; - user2.profile.avatarUrl = "http://example.com/avatar.jpg"; - - log.info("User2 created: ", user2); - - log.info("Saving users..."); - await AppDataSource.manager.save([user1, user2]); - log.info("Users created successfully"); - return [user1, user2]; - } catch (error) { - log.error("Error creating users: ", error.message); - log.error(error.stack); - throw error; - } -}; - -const createProducts = async (users: User[]) => { - try { - log.info("Creating products..."); - const product1 = new Product(); - product1.name = "Product 1"; - product1.description = "Description for product 1"; - product1.price = 1099; - product1.category = "Category 1"; - product1.user = users[0]; - - const product2 = new Product(); - product2.name = "Product 2"; - product2.description = "Description for product 2"; - product2.price = 1999; - product2.category = "Category 2"; - product2.user = users[0]; - - const product3 = new Product(); - product3.name = "Product 3"; - product3.description = "Description for product 3"; - product3.price = 2999; - product3.category = "Category 3"; - product3.user = users[1]; - - const product4 = new Product(); - product4.name = "Product 4"; - product4.description = "Description for product 4"; - product4.price = 3999; - product4.category = "Category 4"; - product4.user = users[1]; - - log.info("Saving products..."); - await AppDataSource.manager.save([product1, product2, product3, product4]); - log.info("Products created successfully"); - } catch (error) { - log.error("Error creating products: ", error.message); - log.error(error.stack); - throw error; - } -}; - -const createOrganizations = async (users: User[]) => { - try { - log.info("Creating organizations..."); - const organization1 = new Organization(); - organization1.name = "Org 1"; - organization1.owner_id = users[0].id; // Set owner_id - organization1.description = "Description for org 1"; - - const organization2 = new Organization(); - organization2.name = "Org 2"; - organization2.owner_id = users[0].id; // Set owner_id - organization2.description = "Description for org 2"; - - const organization3 = new Organization(); - organization3.name = "Org 3"; - organization3.owner_id = users[1].id; // Set owner_id - organization3.description = "Description for org 3"; - - log.info("Saving organizations..."); - await AppDataSource.manager.save([organization1, organization2, organization3]); - log.info("Organizations created successfully"); - return [organization1, organization2, organization3]; - } catch (error) { - log.error("Error creating organizations: ", error.message); - log.error(error.stack); - throw error; - } -}; - -const assignOrganizationsToUsers = async (users: User[], organizations: Organization[]) => { - try { - log.info("Assigning organizations to users..."); - users[0].organizations = [organizations[0], organizations[1]]; - users[1].organizations = [organizations[0], organizations[1], organizations[2]]; - - log.info("Saving user-organization relationships..."); - await AppDataSource.manager.save(users); - log.info("Organizations assigned to users successfully"); - } catch (error) { - log.error("Error assigning organizations to users: ", error.message); - log.error(error.stack); - throw error; - } -}; - -const seed = async () => { - try { - await AppDataSource.initialize(); - await AppDataSource.manager.transaction(async transactionalEntityManager => { - const users = await createUsers(); - await createProducts(users); - const organizations = await createOrganizations(users); // Pass users to createOrganizations - await assignOrganizationsToUsers(users, organizations); - }); - log.info("Seeding completed successfully."); - } catch (error) { - log.error("Seeding failed: ", error.message); - log.error(error.stack); - } finally { - await AppDataSource.destroy(); - } -}; - -seed(); \ No newline at end of file +// // // src/seeder.ts +// // import AppDataSource from "./data-source"; +// // import { User, Organization, Product, Profile } from "./models"; +// // import log from "./utils/logger"; + +// const createUsers = async () => { +// try { +// log.info("Creating user1..."); +// const user1 = new User(); +// user1.name = "John Doe"; +// user1.email = "johndoe@example.com"; +// user1.password = "password"; +// user1.otp = Math.floor(Math.random() * 10000); +// user1.otp_expires_at = new Date(Date.now() + 3600 * 1000); +// user1.profile = new Profile(); +// user1.profile.first_name = "John"; +// user1.profile.last_name = "Doe"; +// user1.profile.phone = "1234567890"; +// user1.profile.avatarUrl = "http://example.com/avatar.jpg"; + +// log.info("User1 created: ", user1); + +// log.info("Creating user2..."); +// const user2 = new User(); +// user2.name = "Jane Doe"; +// user2.email = "janedoe@example.com"; +// user2.password = "password"; +// user2.otp = Math.floor(Math.random() * 10000); +// user2.otp_expires_at = new Date(Date.now() + 3600 * 1000); +// user2.profile = new Profile(); +// user2.profile.first_name = "Jane"; +// user2.profile.last_name = "Doe"; +// user2.profile.phone = "0987654321"; +// user2.profile.avatarUrl = "http://example.com/avatar.jpg"; + +// log.info("User2 created: ", user2); + +// log.info("Saving users..."); +// await AppDataSource.manager.save([user1, user2]); +// log.info("Users created successfully"); +// return [user1, user2]; +// } catch (error) { +// log.error("Error creating users: ", error.message); +// log.error(error.stack); +// throw error; +// } +// }; + +// const createProducts = async (users: User[]) => { +// try { +// log.info("Creating products..."); +// const product1 = new Product(); +// product1.name = "Product 1"; +// product1.description = "Description for product 1"; +// product1.price = 1099; +// product1.category = "Category 1"; +// product1.user = users[0]; + +// const product2 = new Product(); +// product2.name = "Product 2"; +// product2.description = "Description for product 2"; +// product2.price = 1999; +// product2.category = "Category 2"; +// product2.user = users[0]; + +// const product3 = new Product(); +// product3.name = "Product 3"; +// product3.description = "Description for product 3"; +// product3.price = 2999; +// product3.category = "Category 3"; +// product3.user = users[1]; + +// const product4 = new Product(); +// product4.name = "Product 4"; +// product4.description = "Description for product 4"; +// product4.price = 3999; +// product4.category = "Category 4"; +// product4.user = users[1]; + +// log.info("Saving products..."); +// await AppDataSource.manager.save([product1, product2, product3, product4]); +// log.info("Products created successfully"); +// } catch (error) { +// log.error("Error creating products: ", error.message); +// log.error(error.stack); +// throw error; +// } +// }; + +// const createOrganizations = async (users: User[]) => { +// try { +// log.info("Creating organizations..."); +// const organization1 = new Organization(); +// organization1.name = "Org 1"; +// organization1.owner_id = users[0].id; // Set owner_id +// organization1.description = "Description for org 1"; + +// const organization2 = new Organization(); +// organization2.name = "Org 2"; +// organization2.owner_id = users[0].id; // Set owner_id +// organization2.description = "Description for org 2"; + +// // const organization3 = new Organization(); +// // organization3.name = "Org 3"; +// // organization3.description = "Description for org 3"; +// // organization3.owner_id = user2.id; + +// // // Assign organizations to users +// // user1.organizations = [organization1, organization2]; +// // user2.organizations = [organization1, organization2, organization3]; + +// // // Save entities + +// // await AppDataSource.manager.save(organization1); +// // await AppDataSource.manager.save(organization2); +// // await AppDataSource.manager.save(organization3); +// // await AppDataSource.manager.save(product1); +// // await AppDataSource.manager.save(product2); +// // await AppDataSource.manager.save(product3); +// // await AppDataSource.manager.save(product4); + +// // log.info("Seeding completed successfully."); +// // }; + +// // export { seed }; diff --git a/src/services/admin.services.ts b/src/services/admin.services.ts index 18b6062e..3664effa 100644 --- a/src/services/admin.services.ts +++ b/src/services/admin.services.ts @@ -52,11 +52,11 @@ export class AdminOrganisationService { const user = await userRepository.findOne({ where: { id: user_id }, }); - + if (!user) { throw new HttpError(404, "User not Found"); } - + // Update User Role on the Database user.role = role; await userRepository.save(user); diff --git a/src/services/google.passport.service.ts b/src/services/google.passport.service.ts index df29aa71..5927fb0b 100644 --- a/src/services/google.passport.service.ts +++ b/src/services/google.passport.service.ts @@ -1,89 +1,93 @@ import AppDataSource from "../data-source"; import { User } from "../models"; -import { Profile } from "passport-google-oauth2"; +import { Profile } from "passport-google-oauth2"; import config from "../config"; import jwt from "jsonwebtoken"; import { HttpError } from "../middleware"; import { Profile as UserProfile } from "../models"; - interface IGoogleAuthService { - handleGoogleAuthUser(payload: Profile, authUser: User | null): Promise<{ - status: string, - message: string, + handleGoogleAuthUser( + payload: Profile, + authUser: User | null, + ): Promise<{ + status: string; + message: string; user: Partial; access_token: string; - }> - getUserByGoogleId(google_id: string): Promise + }>; + getUserByGoogleId(google_id: string): Promise; } - export class GoogleAuthService implements IGoogleAuthService { - public async handleGoogleAuthUser(payload: Profile, authUser: User | null): Promise<{ - status: string, - message: string, - user: Partial; - access_token: string; - }> { - try { - - let user: User; - let profile: UserProfile; - if (!authUser) { - user = new User(); - profile = new UserProfile() - } - else { - user = authUser; - profile = user.profile; - } - - user.name = payload.displayName; - user.email = payload.email; - user.google_id = payload.id; - user.otp = 1234; - user.isverified = true; - user.otp_expires_at = new Date(Date.now()); - profile.phone = ""; - profile.first_name = payload.given_name; - profile.last_name = payload.family_name; - profile.avatarUrl = payload.picture; - user.profile = profile + public async handleGoogleAuthUser( + payload: Profile, + authUser: User | null, + ): Promise<{ + status: string; + message: string; + user: Partial; + access_token: string; + }> { + try { + let user: User; + let profile: UserProfile; + if (!authUser) { + user = new User(); + profile = new UserProfile(); + } else { + user = authUser; + profile = user.profile; + } - const createdUser = await AppDataSource.manager.save(user); - const access_token = jwt.sign( - { userId: createdUser.id }, - config.TOKEN_SECRET, - { - expiresIn: "1d", - } - ); - - const { password: _, ...rest } = createdUser; - - return { - status: "success", - message: "User successfully authenticated", - access_token, - user: rest, - }; - } catch (error) { - if (error instanceof HttpError) { - throw error; - } - throw new HttpError(error.status || 500, error.message || error); + user.name = payload.displayName; + user.email = payload.email; + user.google_id = payload.id; + user.otp = 1234; + user.isverified = true; + user.otp_expires_at = new Date(Date.now()); + profile.phone = ""; + profile.first_name = payload.given_name; + profile.last_name = payload.family_name; + profile.avatarUrl = payload.picture; + user.profile = profile; + + const createdUser = await AppDataSource.manager.save(user); + const access_token = jwt.sign( + { userId: createdUser.id }, + config.TOKEN_SECRET, + { + expiresIn: "1d", + }, + ); + + const { password: _, ...rest } = createdUser; + + return { + status: "success", + message: "User successfully authenticated", + access_token, + user: rest, + }; + } catch (error) { + if (error instanceof HttpError) { + throw error; } + throw new HttpError(error.status || 500, error.message || error); } + } - public async getUserByGoogleId(google_id: string): Promise { - try { - return AppDataSource.getRepository(User).findOne({ where: { google_id }, relations: ["profile"] }); - } catch(error) { - if (error instanceof HttpError) { - throw error; - } - throw new HttpError(error.status || 500, error.message || error); - } - + public async getUserByGoogleId(google_id: string): Promise { + try { + return AppDataSource.getRepository(User).findOne({ + where: { google_id }, + relations: ["profile"], + }); + } catch (error) { + if (error instanceof HttpError) { + throw error; + } + throw new HttpError(error.status || 500, error.message || error); } -} \ No newline at end of file + } +} diff --git a/src/services/product.services.ts b/src/services/product.services.ts index 476bf4cb..879ba370 100644 --- a/src/services/product.services.ts +++ b/src/services/product.services.ts @@ -13,9 +13,7 @@ export class ProductService { } private productRepository = AppDataSource.getRepository(Product); - public async getProductPagination( - query: any, - ): Promise<{ + public async getProductPagination(query: any): Promise<{ page: number; limit: number; totalProducts: number; @@ -57,4 +55,13 @@ export class ProductService { throw new Error(err.message); } } + async getOneProduct(id: string): Promise { + + const product = await this.productRepository.findOneBy({id}); + + return product; + + + } + } diff --git a/src/services/sms.services.ts b/src/services/sms.services.ts index d331696d..a62bb656 100644 --- a/src/services/sms.services.ts +++ b/src/services/sms.services.ts @@ -5,7 +5,7 @@ import { Sms } from "../models/sms"; import { User } from "../models"; class SmsService { - private twilioClient: Twilio.Twilio;; + private twilioClient: Twilio.Twilio; constructor() { this.twilioClient = Twilio(config.TWILIO_SID, config.TWILIO_AUTH_TOKEN); @@ -14,7 +14,7 @@ class SmsService { public async sendSms( sender: User, phoneNumber: string, - message: string + message: string, ): Promise { await this.twilioClient.messages.create({ body: message, diff --git a/src/services/user.services.ts b/src/services/user.services.ts index ab968db2..ef431913 100644 --- a/src/services/user.services.ts +++ b/src/services/user.services.ts @@ -1,24 +1,34 @@ // src/services/UserService.ts import { User } from "../models/user"; +import { Profile } from "../models/profile"; import { IUserService } from "../types"; import { HttpError } from "../middleware"; -import { Repository, UpdateResult } from 'typeorm'; -import AppDataSource from '../data-source'; +import { Repository, UpdateResult } from "typeorm"; +import AppDataSource from "../data-source"; +import { cloudinary } from "../config/multer"; -export class UserService { +interface IUserProfileUpdate { + first_name: string; + last_name: string; + phone: string; + avatarUrl: string; +} + +export class UserService { private userRepository: Repository; constructor() { this.userRepository = AppDataSource.getRepository(User); } - + static async getUserById(id: string): Promise { const userRepository = AppDataSource.getRepository(User); - return userRepository.findOne({ + const user = userRepository.findOne({ where: { id }, relations: ["profile"], withDeleted: true, }); + return user; } public async getAllUsers(): Promise { @@ -28,16 +38,96 @@ export class UserService { return users; } - public async softDeleteUser(id:string):Promise { - const user = await this.userRepository.findOne({where: {id}}); + public async softDeleteUser(id: string): Promise { + const user = await this.userRepository.findOne({ where: { id } }); if (!user) { throw new HttpError(404, "User Not Found"); } - - user.is_deleted = true; + + user.is_deleted = true; await this.userRepository.save(user); - const deletedUser = await this.userRepository.softDelete({id}); + const deletedUser = await this.userRepository.softDelete({ id }); return deletedUser; } + + public async updateUserProfile( + id: string, + payload: IUserProfileUpdate, + file?: Express.Multer.File, + ): Promise { + try { + const userRepository = AppDataSource.getRepository(User); + const profileRepository = AppDataSource.getRepository(Profile); + + const user = await userRepository.findOne({ + where: { id }, + relations: ["profile"], + }); + + if (!user) { + throw new Error("User not found"); + } + + const profile: Partial = { + first_name: payload.first_name, + last_name: payload.last_name, + phone: payload.phone, + avatarUrl: file ? file.path : undefined, + }; + + const userProfile = await profileRepository.findOne({ + where: { id: user.profile.id }, + }); + + if (!userProfile) { + throw new Error("Profile not found"); + } + + if (file) { + // delete old image from cloudinary + const oldImage = userProfile.avatarUrl; + if (oldImage) { + const publicId = oldImage.split("/").pop()?.split(".")[0]; + await cloudinary.uploader.destroy(publicId); + } + + // upload new image to cloudinary + const { path } = file; + const result = await cloudinary.uploader.upload(path); + userProfile.avatarUrl = result.secure_url; + } + + await profileRepository.update(userProfile.id, profile); + + user.profile = userProfile; + + await userRepository.update(user.id, user); + + if (profile.first_name || profile.last_name) { + const updatedProfile = await profileRepository.findOne({ + where: { id: user.profile.id }, + }); + if (updatedProfile) { + user.name = `${updatedProfile.first_name} ${updatedProfile.last_name}`; + } + // user.name = `${payload.first_name} ${payload.last_name}`; + await userRepository.update(user.id, user); + } + + const updatedUser = await userRepository.findOne({ + where: { id }, + relations: ["profile"], + }); + + // remove password from response + if (updatedUser) { + delete updatedUser.password; + } + + return updatedUser; + } catch (error) { + throw new Error(error.message); + } + } } diff --git a/src/test/product.spec.ts b/src/test/product.spec.ts new file mode 100644 index 00000000..bf5d5741 --- /dev/null +++ b/src/test/product.spec.ts @@ -0,0 +1,84 @@ +import { Repository } from "typeorm"; +import AppDataSource from "../data-source"; +import { Product } from "../models/product"; +import { User } from "../models"; +import { ProductService } from "../services"; +import { describe, expect, it, beforeEach, afterEach } from "@jest/globals"; + +jest.mock("../data-source", () => ({ + __esModule: true, + default: { + getRepository: jest.fn(), + initialize: jest.fn(), + isInitialized: false, + }, +})); + +describe("ProductService", () => { + let productService: ProductService; + let mockRepository: jest.Mocked>; + + beforeEach(() => { + mockRepository = { + findOneBy: jest.fn(), + // Add other methods if needed + } as any; + + (AppDataSource.getRepository as jest.Mock).mockReturnValue(mockRepository); + + productService = new ProductService(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("fetchProductById", () => { + it("should return the product if it exists", async () => { + const productId = "123"; + const user: User = { + id: 'user-123', + name: 'John Doe', + // Add any other necessary properties + } as User; + const product = { + id: "123", + name: "Product 1", + description: "Product is robust", + price: 19, + category: "Gadgets", + user:user + + } as Product; + + mockRepository.findOneBy.mockResolvedValue(product); + + const result = await productService.getOneProduct(productId); + expect(result).toEqual(product); + expect(mockRepository.findOneBy).toHaveBeenCalledWith({ id: productId }); + }); + + it("should return null if the product does not exist", async () => { + const productId = "non-existing-id"; + + mockRepository.findOneBy.mockResolvedValue(null); + + const result = await productService.getOneProduct(productId); + + expect(result).toBeNull(); + expect(mockRepository.findOneBy).toHaveBeenCalledWith({ id: productId }); + }); + + it("should throw an error if there is an issue with fetching the product", async () => { + const productId = "123"; + const error = new Error("Error fetching product"); + + mockRepository.findOneBy.mockRejectedValue(error); + + await expect(productService.getOneProduct(productId)).rejects.toThrow( + "Error fetching product", + ); + expect(mockRepository.findOneBy).toHaveBeenCalledWith({ id: productId }); + }); + }); +}); diff --git a/src/test/products.spec.ts b/src/test/products.spec.ts index d76808aa..260a174e 100644 --- a/src/test/products.spec.ts +++ b/src/test/products.spec.ts @@ -1,10 +1,10 @@ -import { ProductService } from '../services/product.services'; -import productController from '../controllers/ProductController'; -import { Product } from '../models/product'; -import AppDataSource from '../data-source'; -import { mock, MockProxy } from 'jest-mock-extended'; +import { ProductService } from "../services/product.services"; +import productController from "../controllers/ProductController"; +import { Product } from "../models/product"; +import AppDataSource from "../data-source"; +import { mock, MockProxy } from "jest-mock-extended"; -describe('Product Tests', () => { +describe("Product Tests", () => { let productService: ProductService; let productRepository: MockProxy; @@ -14,67 +14,72 @@ describe('Product Tests', () => { productService = new ProductService(); }); - it('should return paginated products', async () => { + it("should return paginated products", async () => { const products = [ - { id: 1, name: 'Product 1' }, - { id: 2, name: 'Product 2' } + { id: 1, name: "Product 1" }, + { id: 2, name: "Product 2" }, ]; productRepository.find.mockResolvedValue(products); productRepository.count.mockResolvedValue(2); - const result = await productService.getProductPagination({ page: '1', limit: '2' }); + const result = await productService.getProductPagination({ + page: "1", + limit: "2", + }); expect(result).toEqual({ page: 1, limit: 2, totalProducts: 2, - products + products, }); expect(productRepository.find).toHaveBeenCalledWith({ skip: 0, take: 2 }); expect(productRepository.count).toHaveBeenCalled(); }); - it('should throw an error for invalid page/limit values', async () => { - await expect(productService.getProductPagination({ page: '-1', limit: '2' })) - .rejects - .toThrow("Page and limit must be positive integers."); + it("should throw an error for invalid page/limit values", async () => { + await expect( + productService.getProductPagination({ page: "-1", limit: "2" }), + ).rejects.toThrow("Page and limit must be positive integers."); - await expect(productService.getProductPagination({ page: '1', limit: '0' })) - .rejects - .toThrow("Page and limit must be positive integers."); + await expect( + productService.getProductPagination({ page: "1", limit: "0" }), + ).rejects.toThrow("Page and limit must be positive integers."); }); - it('should throw an error for out-of-range pages', async () => { + it("should throw an error for out-of-range pages", async () => { productRepository.find.mockResolvedValue([]); productRepository.count.mockResolvedValue(2); - await expect(productService.getProductPagination({ page: '2', limit: '2' })) - .rejects - .toThrow("The requested page is out of range. Please adjust the page number."); + await expect( + productService.getProductPagination({ page: "2", limit: "2" }), + ).rejects.toThrow( + "The requested page is out of range. Please adjust the page number.", + ); }); - it('should throw an error for invalid limit', async () => { - await expect(productService.getProductPagination({ page: '1', limit: '-1' })) - .rejects - .toThrow("Page and limit must be positive integers."); + it("should throw an error for invalid limit", async () => { + await expect( + productService.getProductPagination({ page: "1", limit: "-1" }), + ).rejects.toThrow("Page and limit must be positive integers."); }); - it('should handle empty products', async () => { + it("should handle empty products", async () => { productRepository.find.mockResolvedValue([]); productRepository.count.mockResolvedValue(0); - const result = await productService.getProductPagination({ page: '1', limit: '2' }); + const result = await productService.getProductPagination({ + page: "1", + limit: "2", + }); expect(result).toEqual({ page: 1, limit: 2, totalProducts: 0, - products: [] + products: [], }); }); }); - - - // import { ProductService } from '../services/product.services'; // import { Product } from '../models/product'; // import AppDataSource from '../data-source'; diff --git a/src/test/users.spec.ts b/src/test/users.spec.ts index 493223c4..0a80f725 100644 --- a/src/test/users.spec.ts +++ b/src/test/users.spec.ts @@ -1,28 +1,49 @@ // @ts-nocheck - import { UserService } from "../services"; +import { UserController } from "../controllers/UserController"; import { User } from "../models"; -import { Repository } from 'typeorm'; +import { Repository } from "typeorm"; import AppDataSource from "../data-source"; import { HttpError } from "../middleware"; +import { validate as mockUuidValidate } from "uuid"; +import { Request, Response, NextFunction } from "express"; -jest.mock('../data-source', () => ({ +jest.mock("../data-source", () => ({ getRepository: jest.fn(), })); -describe('UserService', () => { +jest.mock("uuid", () => ({ + validate: jest.fn(), +})); + +describe("UserService", () => { let userService: UserService; let userRepositoryMock: jest.Mocked>; + let req: Partial; + let res: Partial; + let next: NextFunction; beforeEach(() => { userRepositoryMock = { findOne: jest.fn(), save: jest.fn(), softDelete: jest.fn(), - ...jest.requireActual('typeorm').Repository.prototype, + ...jest.requireActual("typeorm").Repository.prototype, } as jest.Mocked>; - (AppDataSource.getRepository as jest.Mock).mockReturnValue(userRepositoryMock); + req = { + user: { id: "0863d5e0-7f92-4c18-bdd9-6eaa81e73529" }, + }; + res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + next = jest.fn(); + mockUuidValidate.mockReturnValue(true); + + (AppDataSource.getRepository as jest.Mock).mockReturnValue( + userRepositoryMock, + ); userService = new UserService(); }); @@ -30,28 +51,131 @@ describe('UserService', () => { jest.clearAllMocks(); }); - describe('softDeleteUser', () => { - it('should soft delete a user', async () => { - const user: User = { id: '123', is_deleted: false } as User; - userRepositoryMock.findOne.mockResolvedValue(user); - userRepositoryMock.softDelete.mockResolvedValue({ affected: 1 } as any); + describe("Unit Test For Users", () => { + describe("softDeleteUser", () => { + it("should soft delete a user", async () => { + const user: User = { id: "123", is_deleted: false } as User; + userRepositoryMock.findOne.mockResolvedValue(user); + userRepositoryMock.softDelete.mockResolvedValue({ affected: 1 } as any); + + const result = await userService.softDeleteUser("123"); - const result = await userService.softDeleteUser('123'); + expect(userRepositoryMock.findOne).toHaveBeenCalledWith({ + where: { id: "123" }, + }); + expect(userRepositoryMock.save).toHaveBeenCalledWith({ + ...user, + is_deleted: true, + }); + expect(userRepositoryMock.softDelete).toHaveBeenCalledWith({ + id: "123", + }); + expect(result).toEqual({ affected: 1 }); + }); - expect(userRepositoryMock.findOne).toHaveBeenCalledWith({ where: { id: '123' } }); - expect(userRepositoryMock.save).toHaveBeenCalledWith({ ...user, is_deleted: true }); - expect(userRepositoryMock.softDelete).toHaveBeenCalledWith({ id: '123' }); - expect(result).toEqual({ affected: 1 }); + it("should throw a 404 error if user not found", async () => { + userRepositoryMock.findOne.mockResolvedValue(null); + + await expect(userService.softDeleteUser("123")).rejects.toThrow( + new HttpError(404, "User Not Found"), + ); + + expect(userRepositoryMock.findOne).toHaveBeenCalledWith({ + where: { id: "123" }, + }); + expect(userRepositoryMock.save).not.toHaveBeenCalled(); + expect(userRepositoryMock.softDelete).not.toHaveBeenCalled(); + }); }); - it('should throw a 404 error if user not found', async () => { - userRepositoryMock.findOne.mockResolvedValue(null); + describe("getProfile for authenticated user", () => { + it("should return 400 if user id format is invalid", async () => { + mockUuidValidate.mockReturnValue(false); + + await UserController.getProfile(req as Request, res as Response, next); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ + status_code: 400, + error: "Unauthorized! Invalid User Id Format", + }); + }); + + it("should return 404 if user is not found", async () => { + userRepositoryMock.findOne.mockResolvedValue(null); + + await UserController.getProfile(req as Request, res as Response, next); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ + status_code: 404, + error: "User Not Found!", + }); + }); + + it("should return 404 if user is soft deleted", async () => { + const user: User = { + id: "0863d5e0-7f92-4c18-bdd9-6eaa81e73529", + is_deleted: true, + } as User; + userRepositoryMock.findOne.mockResolvedValue(user); + + await UserController.getProfile(req as Request, res as Response, next); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ + status_code: 404, + error: "User not found! (soft deleted user)", + }); + }); + + it("should return user profile details", async () => { + const user: User = { + id: "0863d5e0-7f92-4c18-bdd9-6eaa81e73529", + name: "Test User", + email: "test@example.com", + role: "user", + profile: { + id: "profile-id", + first_name: "Test", + last_name: "User", + phone: "1234567890", + avatarUrl: "http://example.com/avatar.png", + }, + } as User; + userRepositoryMock.findOne.mockResolvedValue(user); + + await UserController.getProfile(req as Request, res as Response, next); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + status_code: 200, + message: "User profile details retrieved successfully", + data: { + id: user.id, + name: user.name, + email: user.email, + role: user.role, + profile_id: user.profile?.id, + first_name: user.profile?.first_name, + last_name: user.profile?.last_name, + phone: user.profile?.phone, + avatar_url: user.profile?.avatarUrl, + }, + }); + }); + + it("should return 500 if there is an internal server error", async () => { + userRepositoryMock.findOne.mockRejectedValue(new Error("Test Error")); - await expect(userService.softDeleteUser('123')).rejects.toThrow(new HttpError(404, "User Not Found")); + await UserController.getProfile(req as Request, res as Response, next); - expect(userRepositoryMock.findOne).toHaveBeenCalledWith({ where: { id: '123' } }); - expect(userRepositoryMock.save).not.toHaveBeenCalled(); - expect(userRepositoryMock.softDelete).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + status_code: 500, + error: "Internal Server Error", + }); + }); }); }); }); diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 8905c674..c6b09c12 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -54,7 +54,7 @@ export interface ICreateOrganisation { export interface IOrganisationService { createOrganisation( payload: ICreateOrganisation, - userId: string + userId: string, ): Promise; } diff --git a/yarn.lock b/yarn.lock index b0d51565..41301bb5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1061,6 +1061,13 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== +"@types/multer@^1": + version "1.4.11" + resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.11.tgz#c70792670513b4af1159a2b60bf48cc932af55c5" + integrity sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w== + dependencies: + "@types/express" "*" + "@types/node@*": version "20.14.12" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.12.tgz#129d7c3a822cb49fc7ff661235f19cfefd422b49" @@ -1292,6 +1299,11 @@ app-root-path@^3.1.0: resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.1.0.tgz#5971a2fc12ba170369a7a1ef018c71e6e47c2e86" integrity sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA== +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== + arg@^4.1.0: version "4.1.3" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" @@ -1555,6 +1567,13 @@ bull@*, bull@^4.15.1: semver "^7.5.2" uuid "^8.3.0" +busboy@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + bytes@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" @@ -1707,6 +1726,14 @@ clone@^2.1.2: resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== +cloudinary@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/cloudinary/-/cloudinary-2.3.0.tgz#48d74ea574cddb8a69e84b9bef03ed68401004b2" + integrity sha512-QBa/ePVVfVcVOB1Vut236rjAbTZAArzOm0e2IWUkQJSZFS65Sjf+i3DyRGen4QX8GZzrcbzvKI9b8BTHAv1zqQ== + dependencies: + lodash "^4.17.21" + q "^1.5.1" + cluster-key-slot@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" @@ -1796,6 +1823,16 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + config@^3.3.12: version "3.3.12" resolved "https://registry.yarnpkg.com/config/-/config-3.3.12.tgz#a10ae66efcc3e48c1879fbb657c86c4ef6c7b25e" @@ -1859,6 +1896,11 @@ cookiejar@^2.1.4: resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + cors@^2.8.5: version "2.8.5" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" @@ -2782,7 +2824,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -3008,6 +3050,11 @@ isarray@^2.0.5: resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -3884,6 +3931,13 @@ minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== +mkdirp@^0.5.4: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" @@ -3930,6 +3984,24 @@ msgpackr@^1.10.1: optionalDependencies: msgpackr-extract "^3.0.2" +multer-storage-cloudinary@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/multer-storage-cloudinary/-/multer-storage-cloudinary-4.0.0.tgz#afc9e73c353668c57dda5b73b7bb84bae6635f6f" + integrity sha512-25lm9R6o5dWrHLqLvygNX+kBOxprzpmZdnVKH4+r68WcfCt8XV6xfQaMuAg+kUE5Xmr8mJNA4gE0AcBj9FJyWA== + +multer@^1.4.5-lts.1: + version "1.4.5-lts.1" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.5-lts.1.tgz#803e24ad1984f58edffbc79f56e305aec5cfd1ac" + integrity sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ== + dependencies: + append-field "^1.0.0" + busboy "^1.0.0" + concat-stream "^1.5.2" + mkdirp "^0.5.4" + object-assign "^4.1.1" + type-is "^1.6.4" + xtend "^4.0.0" + mz@^2.4.0: version "2.7.0" resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" @@ -4007,7 +4079,7 @@ oauth@0.10.x: resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.10.0.tgz#3551c4c9b95c53ea437e1e21e46b649482339c58" integrity sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q== -object-assign@^4, object-assign@^4.0.1: +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== @@ -4432,6 +4504,11 @@ pretty-format@^29.0.0, pretty-format@^29.7.0: ansi-styles "^5.0.0" react-is "^18.0.0" +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + process-warning@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-3.0.0.tgz#96e5b88884187a1dce6f5c3166d611132058710b" @@ -4476,6 +4553,11 @@ pure-rand@^6.0.0: resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== +q@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== + qs@6.11.0: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" @@ -4515,6 +4597,19 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== +readable-stream@^2.2.2: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + readable-stream@^4.0.0: version "4.5.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" @@ -4648,6 +4743,11 @@ safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-stable-stringify@^2.3.1: version "2.4.3" resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" @@ -4862,6 +4962,11 @@ stop-iteration-iterator@^1.0.0: dependencies: internal-slot "^1.0.4" +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + string-argv@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" @@ -4875,16 +4980,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -4918,14 +5014,14 @@ string_decoder@^1.3.0: dependencies: safe-buffer "~5.2.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== dependencies: - ansi-regex "^5.0.1" + safe-buffer "~5.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -5157,26 +5253,7 @@ ts-node-dev@^2.0.0: ts-node "^10.4.0" tsconfig "^7.0.0" -ts-node@^10.4.0: - version "10.9.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" - integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== - dependencies: - "@cspotcode/source-map-support" "^0.8.0" - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.2" - acorn "^8.4.1" - acorn-walk "^8.1.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - v8-compile-cache-lib "^3.0.1" - yn "3.1.1" - -ts-node@^10.9.2: +ts-node@^10.4.0, ts-node@^10.9.2: version "10.9.2" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== @@ -5233,7 +5310,7 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== -type-is@~1.6.18: +type-is@^1.6.4, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== @@ -5241,6 +5318,11 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + typeorm@^0.3.20: version "0.3.20" resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.20.tgz#4b61d737c6fed4e9f63006f88d58a5e54816b7ab" @@ -5321,11 +5403,21 @@ update-browserslist-db@^1.1.0: escalade "^3.1.2" picocolors "^1.0.1" +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + utils-merge@1.0.1, utils-merge@1.x.x, utils-merge@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +uuid@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" + integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== + uuid@^8.3.0: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -5411,16 +5503,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==