From 58d2be80a65fb3363d829ff4012d48bcad9734f8 Mon Sep 17 00:00:00 2001 From: UmeshaJayakody Date: Thu, 7 Aug 2025 00:00:10 +0530 Subject: [PATCH 1/8] change to typescript --- index.js => index.ts | 8 +- package-lock.json | 177 +++++- package.json | 16 +- prisma/{seed.js => seed.ts} | 0 ...y.controller.js => catagory.controller.ts} | 35 +- src/controllers/category.controller.ts | 272 ++++++++ src/controllers/company.controller.js | 45 -- src/controllers/company.controller.ts | 46 ++ src/controllers/provider.controller.js | 43 -- src/controllers/provider.controller.ts | 44 ++ ...s.controller.js => services.controller.ts} | 31 +- ...{user.controller.js => user.controller.ts} | 23 +- ...{auth.middleware.js => auth.middleware.ts} | 7 +- ...middleware.js => validation.middleware.ts} | 6 +- .../{catagory.route.js => catagory.route.ts} | 4 +- src/routes/category.route.ts | 121 ++++ .../{company.route.js => company.route.ts} | 0 .../{provider.route.js => provider.route.ts} | 0 .../{services.route.js => services.route.ts} | 0 src/routes/{user.route.js => user.route.ts} | 0 ...atagory.service.js => catagory.service.ts} | 98 ++- src/services/category.service.ts | 594 ++++++++++++++++++ ...{company.service.js => company.service.ts} | 29 +- ...rovider.service.js => provider.service.ts} | 27 +- ...ervices.service.js => services.service.ts} | 46 +- .../{user.service.js => user.service.ts} | 44 +- src/utils/hash.js | 9 - src/utils/hash.ts | 9 + ...ory.validator.js => category.validator.ts} | 0 ...pany.validator.js => company.validator.ts} | 0 ...der.validator.js => provider.validator.ts} | 0 ...ces.validator.js => services.validator.ts} | 0 .../{user.validator.js => user.validator.ts} | 0 tsconfig.json | 44 ++ 34 files changed, 1555 insertions(+), 223 deletions(-) rename index.js => index.ts (86%) rename prisma/{seed.js => seed.ts} (100%) rename src/controllers/{catagory.controller.js => catagory.controller.ts} (78%) create mode 100644 src/controllers/category.controller.ts delete mode 100644 src/controllers/company.controller.js create mode 100644 src/controllers/company.controller.ts delete mode 100644 src/controllers/provider.controller.js create mode 100644 src/controllers/provider.controller.ts rename src/controllers/{services.controller.js => services.controller.ts} (63%) rename src/controllers/{user.controller.js => user.controller.ts} (56%) rename src/middlewares/{auth.middleware.js => auth.middleware.ts} (61%) rename src/middlewares/{validation.middleware.js => validation.middleware.ts} (88%) rename src/routes/{catagory.route.js => catagory.route.ts} (97%) create mode 100644 src/routes/category.route.ts rename src/routes/{company.route.js => company.route.ts} (100%) rename src/routes/{provider.route.js => provider.route.ts} (100%) rename src/routes/{services.route.js => services.route.ts} (100%) rename src/routes/{user.route.js => user.route.ts} (100%) rename src/services/{catagory.service.js => catagory.service.ts} (85%) create mode 100644 src/services/category.service.ts rename src/services/{company.service.js => company.service.ts} (79%) rename src/services/{provider.service.js => provider.service.ts} (89%) rename src/services/{services.service.js => services.service.ts} (84%) rename src/services/{user.service.js => user.service.ts} (75%) delete mode 100644 src/utils/hash.js create mode 100644 src/utils/hash.ts rename src/validators/{catagory.validator.js => category.validator.ts} (100%) rename src/validators/{company.validator.js => company.validator.ts} (100%) rename src/validators/{provider.validator.js => provider.validator.ts} (100%) rename src/validators/{services.validator.js => services.validator.ts} (100%) rename src/validators/{user.validator.js => user.validator.ts} (100%) create mode 100644 tsconfig.json diff --git a/index.js b/index.ts similarity index 86% rename from index.js rename to index.ts index 5be8525..0f2fa37 100644 --- a/index.js +++ b/index.ts @@ -12,17 +12,17 @@ try { import { PrismaClient } from '@prisma/client'; import { withAccelerate } from '@prisma/extension-accelerate'; -import express from 'express'; +import express, { type Application } from 'express'; import cors from 'cors'; import userRoutes from './src/routes/user.route.js'; import providerRoutes from './src/routes/provider.route.js'; import companyRoutes from './src/routes/company.route.js'; import servicesRoutes from './src/routes/services.route.js'; -import categoryRoutes from './src/routes/catagory.route.js'; +import categoryRoutes from './src/routes/category.route.js'; const prisma = new PrismaClient().$extends(withAccelerate()); -const app = express(); +const app: Application = express(); // CORS configuration app.use(cors({ @@ -39,5 +39,5 @@ app.use('/api/companies', companyRoutes); app.use('/api/services', servicesRoutes); app.use('/api/categories', categoryRoutes); -const PORT = process.env.PORT || 3000; +const PORT: number = parseInt(process.env.PORT || '3000', 10); app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); diff --git a/package-lock.json b/package-lock.json index 036a584..d412559 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,8 +21,14 @@ "prisma": "^6.12.0" }, "devDependencies": { + "@types/bcrypt": "^6.0.0", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/joi": "^17.2.2", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^24.2.0", "ts-node": "^10.9.2", - "typescript": "^5.8.3" + "typescript": "^5.9.2" } }, "node_modules/@cspotcode/source-map-support": { @@ -217,15 +223,159 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", + "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/joi": { + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/@types/joi/-/joi-17.2.2.tgz", + "integrity": "sha512-vPvPwxn0Y4pQyqkEcMCJYxXCMYcrHqdfFX4SpF4zcqYioYexmDyxtM3OK+m/ZwGBS8/dooJ0il9qCwAdd6KFtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "joi": "*" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { - "version": "24.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", - "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz", + "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "undici-types": "~7.8.0" + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" } }, "node_modules/accepts": { @@ -1644,9 +1794,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -1664,12 +1814,11 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/unpipe": { "version": "1.0.0", diff --git a/package.json b/package.json index 47de208..3adaedd 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "backend", "version": "1.0.0", - "main": "index.js", + "main": "index.ts", "type": "module", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "dev": "nodemon index.js", - "start": "nodemon index.js" + "build": "tsc", + "start": "ts-node index.ts", + "dev": "npm run build && node dist/index.js" }, "repository": { "type": "git", @@ -35,7 +35,13 @@ "prisma": "^6.12.0" }, "devDependencies": { + "@types/bcrypt": "^6.0.0", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/joi": "^17.2.2", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^24.2.0", "ts-node": "^10.9.2", - "typescript": "^5.8.3" + "typescript": "^5.9.2" } } diff --git a/prisma/seed.js b/prisma/seed.ts similarity index 100% rename from prisma/seed.js rename to prisma/seed.ts diff --git a/src/controllers/catagory.controller.js b/src/controllers/catagory.controller.ts similarity index 78% rename from src/controllers/catagory.controller.js rename to src/controllers/catagory.controller.ts index ddbf870..d95f141 100644 --- a/src/controllers/catagory.controller.js +++ b/src/controllers/catagory.controller.ts @@ -1,9 +1,10 @@ -import * as categoryService from '../services/catagory.service.js'; +import type { Request, Response, NextFunction } from 'express'; +import * as categoryService from '../services/category.service.js'; /** * Create a new category */ -export const createCategory = async (req, res, next) => { +export const createCategory = async (req: Request, res: Response, next: NextFunction) => { try { const categoryData = req.body; const newCategory = await categoryService.createCategory(categoryData); @@ -21,10 +22,10 @@ export const createCategory = async (req, res, next) => { /** * Get all categories with optional filtering */ -export const getCategories = async (req, res, next) => { +export const getCategories = async (req: Request, res: Response, next: NextFunction) => { try { const filters = { - parentId: req.query.parentId, + parentId: req.query.parentId as string | null, includeChildren: req.query.includeChildren !== 'false', includeParent: req.query.includeParent !== 'false', includeServices: req.query.includeServices === 'true' @@ -45,7 +46,7 @@ export const getCategories = async (req, res, next) => { /** * Get category by ID */ -export const getCategoryById = async (req, res, next) => { +export const getCategoryById = async (req: Request, res: Response, next: NextFunction) => { try { const { id } = req.params; const options = { @@ -54,7 +55,7 @@ export const getCategoryById = async (req, res, next) => { includeServices: req.query.includeServices === 'true' }; - const category = await categoryService.getCategoryById(id, options); + const category = await categoryService.getCategoryById(id!, options); if (!category) { return res.status(404).json({ @@ -76,7 +77,7 @@ export const getCategoryById = async (req, res, next) => { /** * Get category by slug */ -export const getCategoryBySlug = async (req, res, next) => { +export const getCategoryBySlug = async (req: Request, res: Response, next: NextFunction) => { try { const { slug } = req.params; const options = { @@ -85,7 +86,7 @@ export const getCategoryBySlug = async (req, res, next) => { includeServices: req.query.includeServices === 'true' }; - const category = await categoryService.getCategoryBySlug(slug, options); + const category = await categoryService.getCategoryBySlug(slug!, options); if (!category) { return res.status(404).json({ @@ -107,12 +108,12 @@ export const getCategoryBySlug = async (req, res, next) => { /** * Update category */ -export const updateCategory = async (req, res, next) => { +export const updateCategory = async (req: Request, res: Response, next: NextFunction) => { try { const { id } = req.params; const updateData = req.body; - const updatedCategory = await categoryService.updateCategory(id, updateData); + const updatedCategory = await categoryService.updateCategory(id!, updateData); res.status(200).json({ success: true, @@ -127,14 +128,14 @@ export const updateCategory = async (req, res, next) => { /** * Delete category */ -export const deleteCategory = async (req, res, next) => { +export const deleteCategory = async (req: Request, res: Response, next: NextFunction) => { try { const { id } = req.params; const options = { force: req.query.force === 'true' }; - const deletedCategory = await categoryService.deleteCategory(id, options); + const deletedCategory = await categoryService.deleteCategory(id!, options); res.status(200).json({ success: true, @@ -149,7 +150,7 @@ export const deleteCategory = async (req, res, next) => { /** * Get root categories (categories with no parent) */ -export const getRootCategories = async (req, res, next) => { +export const getRootCategories = async (req: Request, res: Response, next: NextFunction) => { try { const options = { includeChildren: req.query.includeChildren !== 'false' @@ -170,11 +171,11 @@ export const getRootCategories = async (req, res, next) => { /** * Get category hierarchy */ -export const getCategoryHierarchy = async (req, res, next) => { +export const getCategoryHierarchy = async (req: Request, res: Response, next: NextFunction) => { try { const { id } = req.params; - const hierarchy = await categoryService.getCategoryHierarchy(id); + const hierarchy = await categoryService.getCategoryHierarchy(id!); if (!hierarchy) { return res.status(404).json({ @@ -196,7 +197,7 @@ export const getCategoryHierarchy = async (req, res, next) => { /** * Search categories */ -export const searchCategories = async (req, res, next) => { +export const searchCategories = async (req: Request, res: Response, next: NextFunction) => { try { const { q: searchTerm } = req.query; @@ -212,7 +213,7 @@ export const searchCategories = async (req, res, next) => { includeParent: req.query.includeParent !== 'false' }; - const categories = await categoryService.searchCategories(searchTerm, options); + const categories = await categoryService.searchCategories(searchTerm as string, options); res.status(200).json({ success: true, diff --git a/src/controllers/category.controller.ts b/src/controllers/category.controller.ts new file mode 100644 index 0000000..dfec133 --- /dev/null +++ b/src/controllers/category.controller.ts @@ -0,0 +1,272 @@ +import type { Request, Response, NextFunction } from 'express'; +import * as categoryService from '../services/category.service.js'; + +/** + * Create a new category + */ +export const createCategory = async (req: Request, res: Response, next: NextFunction) => { + try { + const categoryData = req.body; + const newCategory = await categoryService.createCategory(categoryData); + + res.status(201).json({ + success: true, + message: 'Category created successfully', + data: newCategory + }); + } catch (error) { + next(error); + } +}; + +/** + * Get categories with optional filtering + */ +export const getCategories = async (req: Request, res: Response, next: NextFunction) => { + try { + const { parentId, includeChildren, includeParent, includeServices } = req.query; + + const filters = { + ...(parentId && { parentId: parentId as string }), + ...(includeChildren !== undefined && { includeChildren: includeChildren === 'true' }), + ...(includeParent !== undefined && { includeParent: includeParent === 'true' }), + ...(includeServices !== undefined && { includeServices: includeServices === 'true' }) + }; + + const categories = await categoryService.getAllCategories(filters); + + res.status(200).json({ + success: true, + message: 'Categories fetched successfully', + data: categories + }); + } catch (error) { + next(error); + } +}; + +/** + * Get category by ID + */ +export const getCategoryById = async (req: Request, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + const { includeChildren, includeParent, includeServices } = req.query; + + if (!id) { + return res.status(400).json({ + success: false, + message: 'Category ID is required' + }); + } + + const options = { + ...(includeChildren !== undefined && { includeChildren: includeChildren === 'true' }), + ...(includeParent !== undefined && { includeParent: includeParent === 'true' }), + ...(includeServices !== undefined && { includeServices: includeServices === 'true' }) + }; + + const category = await categoryService.getCategoryById(id, options); + + if (!category) { + return res.status(404).json({ + success: false, + message: 'Category not found' + }); + } + + res.status(200).json({ + success: true, + message: 'Category fetched successfully', + data: category + }); + } catch (error) { + next(error); + } +}; + +/** + * Get category by slug + */ +export const getCategoryBySlug = async (req: Request, res: Response, next: NextFunction) => { + try { + const { slug } = req.params; + const { includeChildren, includeParent, includeServices } = req.query; + + if (!slug) { + return res.status(400).json({ + success: false, + message: 'Category slug is required' + }); + } + + const options = { + ...(includeChildren !== undefined && { includeChildren: includeChildren === 'true' }), + ...(includeParent !== undefined && { includeParent: includeParent === 'true' }), + ...(includeServices !== undefined && { includeServices: includeServices === 'true' }) + }; + + const category = await categoryService.getCategoryBySlug(slug, options); + + if (!category) { + return res.status(404).json({ + success: false, + message: 'Category not found' + }); + } + + res.status(200).json({ + success: true, + message: 'Category fetched successfully', + data: category + }); + } catch (error) { + next(error); + } +}; + +/** + * Update category + */ +export const updateCategory = async (req: Request, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + const updateData = req.body; + + if (!id) { + return res.status(400).json({ + success: false, + message: 'Category ID is required' + }); + } + + const updatedCategory = await categoryService.updateCategory(id, updateData); + + res.status(200).json({ + success: true, + message: 'Category updated successfully', + data: updatedCategory + }); + } catch (error) { + next(error); + } +}; + +/** + * Delete category + */ +export const deleteCategory = async (req: Request, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + const { force } = req.query; + + if (!id) { + return res.status(400).json({ + success: false, + message: 'Category ID is required' + }); + } + + const options = { + ...(force !== undefined && { force: force === 'true' }) + }; + + const deletedCategory = await categoryService.deleteCategory(id, options); + + res.status(200).json({ + success: true, + message: 'Category deleted successfully', + data: deletedCategory + }); + } catch (error) { + next(error); + } +}; + +/** + * Get root categories + */ +export const getRootCategories = async (req: Request, res: Response, next: NextFunction) => { + try { + const { includeChildren } = req.query; + + const options = { + ...(includeChildren !== undefined && { includeChildren: includeChildren === 'true' }) + }; + + const categories = await categoryService.getRootCategories(options); + + res.status(200).json({ + success: true, + message: 'Root categories fetched successfully', + data: categories + }); + } catch (error) { + next(error); + } +}; + +/** + * Get category hierarchy + */ +export const getCategoryHierarchy = async (req: Request, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + + if (!id) { + return res.status(400).json({ + success: false, + message: 'Category ID is required' + }); + } + + const hierarchy = await categoryService.getCategoryHierarchy(id); + + if (!hierarchy) { + return res.status(404).json({ + success: false, + message: 'Category not found' + }); + } + + res.status(200).json({ + success: true, + message: 'Category hierarchy fetched successfully', + data: hierarchy + }); + } catch (error) { + next(error); + } +}; + +/** + * Search categories + */ +export const searchCategories = async (req: Request, res: Response, next: NextFunction) => { + try { + const { q: searchTerm } = req.query; + const { includeChildren, includeParent } = req.query; + + if (!searchTerm || typeof searchTerm !== 'string') { + return res.status(400).json({ + success: false, + message: 'Search term is required' + }); + } + + const options = { + ...(includeChildren !== undefined && { includeChildren: includeChildren === 'true' }), + ...(includeParent !== undefined && { includeParent: includeParent === 'true' }) + }; + + const categories = await categoryService.searchCategories(searchTerm, options); + + res.status(200).json({ + success: true, + message: 'Categories search completed', + data: categories + }); + } catch (error) { + next(error); + } +}; diff --git a/src/controllers/company.controller.js b/src/controllers/company.controller.js deleted file mode 100644 index ed59e28..0000000 --- a/src/controllers/company.controller.js +++ /dev/null @@ -1,45 +0,0 @@ -import * as companyService from '../services/company.service.js'; - -export const createCompany = async (req, res, next) => { - try { - const company = await companyService.createCompany(req.user.id, req.body); - res.status(201).json({ - message: 'Company created successfully', - company - }); - } catch (err) { - next(err); - } -}; - -export const updateCompany = async (req, res, next) => { - try { - const company = await companyService.updateCompany(req.user.id, req.params.companyId, req.body); - res.status(200).json({ - message: 'Company updated successfully', - company - }); - } catch (err) { - next(err); - } -}; - -export const deleteCompany = async (req, res, next) => { - try { - await companyService.deleteCompany(req.user.id, req.params.companyId); - res.status(200).json({ - message: 'Company deleted successfully' - }); - } catch (err) { - next(err); - } -}; - -export const getCompanies = async (req, res, next) => { - try { - const companies = await companyService.getCompanies(req.user.id); - res.status(200).json(companies); - } catch (err) { - next(err); - } -}; diff --git a/src/controllers/company.controller.ts b/src/controllers/company.controller.ts new file mode 100644 index 0000000..bc46088 --- /dev/null +++ b/src/controllers/company.controller.ts @@ -0,0 +1,46 @@ +import type { Request, Response, NextFunction } from 'express'; +import * as companyService from '../services/company.service.js'; + +export const createCompany = async (req: Request, res: Response, next: NextFunction) => { + try { + const company = await companyService.createCompany((req as any).user.id, req.body); + res.status(201).json({ + message: 'Company created successfully', + company + }); + } catch (err) { + next(err); + } +}; + +export const updateCompany = async (req: Request, res: Response, next: NextFunction) => { + try { + const company = await companyService.updateCompany((req as any).user.id, req.params.companyId!, req.body); + res.status(200).json({ + message: 'Company updated successfully', + company + }); + } catch (err) { + next(err); + } +}; + +export const deleteCompany = async (req: Request, res: Response, next: NextFunction) => { + try { + await companyService.deleteCompany((req as any).user.id, req.params.companyId!); + res.status(200).json({ + message: 'Company deleted successfully' + }); + } catch (err) { + next(err); + } +}; + +export const getCompanies = async (req: Request, res: Response, next: NextFunction) => { + try { + const companies = await companyService.getCompanies((req as any).user.id); + res.status(200).json(companies); + } catch (err) { + next(err); + } +}; diff --git a/src/controllers/provider.controller.js b/src/controllers/provider.controller.js deleted file mode 100644 index 2760dd2..0000000 --- a/src/controllers/provider.controller.js +++ /dev/null @@ -1,43 +0,0 @@ -import * as providerService from '../services/provider.service.js'; - -export const createProvider = async (req, res, next) => { - try { - const provider = await providerService.createProvider(req.user.id, req.body); - res.status(201).json({ - message: 'Service provider profile created successfully', - provider - }); - } catch (err) { - next(err); - } -}; - -export const updateProvider = async (req, res, next) => { - try { - const provider = await providerService.updateProvider(req.user.id, req.body); - res.status(200).json({ - message: 'Service provider profile updated successfully', - provider - }); - } catch (err) { - next(err); - } -}; - -export const deleteProvider = async (req, res, next) => { - try { - const result = await providerService.deleteProvider(req.user.id); - res.status(200).json(result); - } catch (err) { - next(err); - } -}; - -export const getProviderProfile = async (req, res, next) => { - try { - const provider = await providerService.getProviderProfile(req.user.id); - res.status(200).json(provider); - } catch (err) { - next(err); - } -}; diff --git a/src/controllers/provider.controller.ts b/src/controllers/provider.controller.ts new file mode 100644 index 0000000..fbf72cd --- /dev/null +++ b/src/controllers/provider.controller.ts @@ -0,0 +1,44 @@ +import type { Request, Response, NextFunction } from 'express'; +import * as providerService from '../services/provider.service.js'; + +export const createProvider = async (req: Request, res: Response, next: NextFunction) => { + try { + const provider = await providerService.createProvider((req as any).user.id, req.body); + res.status(201).json({ + message: 'Service provider profile created successfully', + provider + }); + } catch (err) { + next(err); + } +}; + +export const updateProvider = async (req: Request, res: Response, next: NextFunction) => { + try { + const provider = await providerService.updateProvider((req as any).user.id, req.body); + res.status(200).json({ + message: 'Service provider profile updated successfully', + provider + }); + } catch (err) { + next(err); + } +}; + +export const deleteProvider = async (req: Request, res: Response, next: NextFunction) => { + try { + const result = await providerService.deleteProvider((req as any).user.id); + res.status(200).json(result); + } catch (err) { + next(err); + } +}; + +export const getProviderProfile = async (req: Request, res: Response, next: NextFunction) => { + try { + const provider = await providerService.getProviderProfile((req as any).user.id); + res.status(200).json(provider); + } catch (err) { + next(err); + } +}; diff --git a/src/controllers/services.controller.js b/src/controllers/services.controller.ts similarity index 63% rename from src/controllers/services.controller.js rename to src/controllers/services.controller.ts index 88884a2..2573735 100644 --- a/src/controllers/services.controller.js +++ b/src/controllers/services.controller.ts @@ -1,9 +1,10 @@ +import type { Request, Response, NextFunction } from 'express'; import * as serviceService from '../services/services.service.js'; /** * Create a new service */ -export const createService = async (req, res, next) => { +export const createService = async (req: Request, res: Response, next: NextFunction) => { try { const serviceData = req.body; const newService = await serviceService.createService(serviceData); @@ -21,15 +22,17 @@ export const createService = async (req, res, next) => { /** * Get all services with optional filtering */ -export const getServices = async (req, res, next) => { +export const getServices = async (req: Request, res: Response, next: NextFunction) => { try { - const filters = { - providerId: req.query.providerId, - categoryId: req.query.categoryId, - isActive: req.query.isActive ? req.query.isActive === 'true' : undefined, - skip: req.query.skip ? parseInt(req.query.skip) : 0, - take: req.query.take ? parseInt(req.query.take) : 10 + const filters: any = { + providerId: req.query.providerId as string, + categoryId: req.query.categoryId as string, + skip: req.query.skip ? parseInt(req.query.skip as string) : 0, + take: req.query.take ? parseInt(req.query.take as string) : 10 }; + if (typeof req.query.isActive !== 'undefined') { + filters.isActive = req.query.isActive === 'true'; + } const services = await serviceService.getServices(filters); @@ -50,10 +53,10 @@ export const getServices = async (req, res, next) => { /** * Get a single service by ID */ -export const getServiceById = async (req, res, next) => { +export const getServiceById = async (req: Request, res: Response, next: NextFunction) => { try { const { id } = req.params; - const service = await serviceService.getServiceById(id); + const service = await serviceService.getServiceById(id!); if (!service) { return res.status(404).json({ @@ -75,12 +78,12 @@ export const getServiceById = async (req, res, next) => { /** * Update a service */ -export const updateService = async (req, res, next) => { +export const updateService = async (req: Request, res: Response, next: NextFunction) => { try { const { id } = req.params; const updateData = req.body; - const updatedService = await serviceService.updateService(id, updateData); + const updatedService = await serviceService.updateService(id!, updateData); res.status(200).json({ success: true, @@ -95,11 +98,11 @@ export const updateService = async (req, res, next) => { /** * Delete a service */ -export const deleteService = async (req, res, next) => { +export const deleteService = async (req: Request, res: Response, next: NextFunction) => { try { const { id } = req.params; - await serviceService.deleteService(id); + await serviceService.deleteService(id!); res.status(200).json({ success: true, diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.ts similarity index 56% rename from src/controllers/user.controller.js rename to src/controllers/user.controller.ts index 21d3ca8..050e3e6 100644 --- a/src/controllers/user.controller.js +++ b/src/controllers/user.controller.ts @@ -1,6 +1,7 @@ +import type { Request, Response, NextFunction } from 'express'; import * as userService from '../services/user.service.js'; -export const createUser = async (req, res, next) => { +export const createUser = async (req: Request, res: Response, next: NextFunction) => { try { const { email, firstName, lastName, password, imageUrl, location, address, phone, socialmedia } = req.body; const user = await userService.register({ email, firstName, lastName, password, imageUrl, location, address, phone, socialmedia }); @@ -10,49 +11,49 @@ export const createUser = async (req, res, next) => { } }; -export const checkEmailExists = async (req, res, next) => { +export const checkEmailExists = async (req: Request, res: Response, next: NextFunction) => { try { const { email } = req.query; if (!email) { return res.status(400).json({ message: 'Email is required' }); } - const exists = await userService.checkEmailExists(email); + const exists = await userService.checkEmailExists(email as string); res.status(200).json({ exists }); } catch (err) { next(err); } }; -export const loginUser = async (req, res) => { +export const loginUser = async (req: Request, res: Response) => { try { const result = await userService.login(req.body); res.status(200).json({ message: 'Login successful', ...result }); - } catch (err) { + } catch (err: any) { res.status(401).json({ message: err.message }); } }; -export const getUserProfile = async (req, res, next) => { +export const getUserProfile = async (req: Request, res: Response, next: NextFunction) => { try { - const user = await userService.getProfile(req.user.id); + const user = await userService.getProfile((req as any).user.id); res.status(200).json(user); } catch (err) { next(err); } }; -export const updateUserProfile = async (req, res, next) => { +export const updateUserProfile = async (req: Request, res: Response, next: NextFunction) => { try { - const updatedUser = await userService.updateProfile(req.user.id, req.body); + const updatedUser = await userService.updateProfile((req as any).user.id, req.body); res.status(200).json({ message: 'Profile updated', user: updatedUser }); } catch (err) { next(err); } }; -export const deleteUserProfile = async (req, res, next) => { +export const deleteUserProfile = async (req: Request, res: Response, next: NextFunction) => { try { - await userService.deleteProfile(req.user.id); + await userService.deleteProfile((req as any).user.id); res.status(200).json({ message: 'Profile deleted' }); } catch (err) { next(err); diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.ts similarity index 61% rename from src/middlewares/auth.middleware.js rename to src/middlewares/auth.middleware.ts index b2e7d66..d49958b 100644 --- a/src/middlewares/auth.middleware.js +++ b/src/middlewares/auth.middleware.ts @@ -1,7 +1,8 @@ +import type { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; const { verify } = jwt; -export default (req, res, next) => { +export default (req: Request, res: Response, next: NextFunction) => { const authHeader = req.headers['authorization']; if (!authHeader || !authHeader.startsWith('Bearer ')) { @@ -11,8 +12,8 @@ export default (req, res, next) => { const token = authHeader.split(' ')[1]; try { - const decoded = verify(token, process.env.JWT_SECRET); - req.user = decoded; // Add user info to request + const decoded = verify(token, process.env.JWT_SECRET!); + (req as any).user = decoded; // Add user info to request next(); } catch (err) { return res.status(401).json({ message: 'Unauthorized: Token invalid' }); diff --git a/src/middlewares/validation.middleware.js b/src/middlewares/validation.middleware.ts similarity index 88% rename from src/middlewares/validation.middleware.js rename to src/middlewares/validation.middleware.ts index 7929a94..855164e 100644 --- a/src/middlewares/validation.middleware.js +++ b/src/middlewares/validation.middleware.ts @@ -1,9 +1,11 @@ +import type { Request, Response, NextFunction } from 'express'; + /** * Validation middleware that can validate different parts of the request * @param {Object} schema - Joi validation schema * @param {string} source - Source to validate: 'body', 'query', 'params' (default: 'body') */ -export default (schema, source = 'body') => (req, res, next) => { +export default (schema: any, source = 'body') => (req: Request, res: Response, next: NextFunction) => { console.log(`=== Validation Middleware (${source}) ===`); let dataToValidate; @@ -35,7 +37,7 @@ export default (schema, source = 'body') => (req, res, next) => { return res.status(400).json({ success: false, message: 'Validation failed', - errors: error.details.map(detail => ({ + errors: error.details.map((detail: any) => ({ field: detail.path.join('.'), message: detail.message })) diff --git a/src/routes/catagory.route.js b/src/routes/catagory.route.ts similarity index 97% rename from src/routes/catagory.route.js rename to src/routes/catagory.route.ts index e9c554e..7994a63 100644 --- a/src/routes/catagory.route.js +++ b/src/routes/catagory.route.ts @@ -11,7 +11,7 @@ import { getRootCategories, getCategoryHierarchy, searchCategories -} from '../controllers/catagory.controller.js'; +} from '../controllers/category.controller.js'; import validate from '../middlewares/validation.middleware.js'; import { @@ -21,7 +21,7 @@ import { categorySlugSchema, searchCategoriesSchema, categoryQuerySchema -} from '../validators/catagory.validator.js'; +} from '../validators/category.validator.js'; import authMiddleware from '../middlewares/auth.middleware.js'; diff --git a/src/routes/category.route.ts b/src/routes/category.route.ts new file mode 100644 index 0000000..7994a63 --- /dev/null +++ b/src/routes/category.route.ts @@ -0,0 +1,121 @@ +import { Router } from 'express'; +const router = Router(); + +import { + createCategory, + getCategories, + getCategoryById, + getCategoryBySlug, + updateCategory, + deleteCategory, + getRootCategories, + getCategoryHierarchy, + searchCategories +} from '../controllers/category.controller.js'; + +import validate from '../middlewares/validation.middleware.js'; +import { + createCategorySchema, + updateCategorySchema, + categoryIdSchema, + categorySlugSchema, + searchCategoriesSchema, + categoryQuerySchema +} from '../validators/category.validator.js'; + +import authMiddleware from '../middlewares/auth.middleware.js'; + +// Public routes (no authentication required) +/** + * @route GET /api/categories + * @desc Get all categories with optional filtering + * @access Public + * @query parentId - Filter by parent category ID + * @query includeChildren - Include children categories (default: true) + * @query includeParent - Include parent category info (default: true) + * @query includeServices - Include services count (default: false) + */ +router.get('/', validate(categoryQuerySchema, 'query'), getCategories); + +/** + * @route GET /api/categories/roots + * @desc Get all root categories (categories with no parent) + * @access Public + * @query includeChildren - Include children categories (default: true) + */ +router.get('/roots', validate(categoryQuerySchema, 'query'), getRootCategories); + +/** + * @route GET /api/categories/search + * @desc Search categories by name or description + * @access Public + * @query q - Search term (required) + * @query includeChildren - Include children categories (default: true) + * @query includeParent - Include parent category info (default: true) + */ +router.get('/search', validate(searchCategoriesSchema, 'query'), searchCategories); + +/** + * @route GET /api/categories/id/:id + * @desc Get category by ID + * @access Public + * @param id - Category ID + * @query includeChildren - Include children categories (default: true) + * @query includeParent - Include parent category info (default: true) + * @query includeServices - Include services (default: false) + */ +router.get('/id/:id', validate(categoryIdSchema, 'params'), getCategoryById); + +/** + * @route GET /api/categories/slug/:slug + * @desc Get category by slug + * @access Public + * @param slug - Category slug + * @query includeChildren - Include children categories (default: true) + * @query includeParent - Include parent category info (default: true) + * @query includeServices - Include services (default: false) + */ +router.get('/slug/:slug', validate(categorySlugSchema, 'params'), getCategoryBySlug); + +/** + * @route GET /api/categories/:id/hierarchy + * @desc Get category hierarchy (full tree starting from category) + * @access Public + * @param id - Category ID + */ +router.get('/:id/hierarchy', validate(categoryIdSchema, 'params'), getCategoryHierarchy); + +// Protected routes (authentication required) +/** + * @route POST /api/categories + * @desc Create a new category + * @access Public (For Testing) + * @body name - Category name (optional) + * @body slug - Category slug (required, unique) + * @body description - Category description (optional) + * @body parentId - Parent category ID (optional) + */ +router.post('/', validate(createCategorySchema), createCategory); + +/** + * @route PUT /api/categories/:id + * @desc Update a category + * @access Private (Admin/Provider) + * @param id - Category ID + * @body name - Category name (optional) + * @body slug - Category slug (optional) + * @body description - Category description (optional) + * @body parentId - Parent category ID (optional) + */ +router.put('/:id', authMiddleware, validate(categoryIdSchema, 'params'), validate(updateCategorySchema), updateCategory); + +/** + * @route DELETE /api/categories/:id + * @desc Delete a category + * @access Private (Admin only) + * @param id - Category ID + * @query force - Force delete even if category has children or services (default: false) + */ +router.delete('/:id', authMiddleware, validate(categoryIdSchema, 'params'), deleteCategory); + +export default router; diff --git a/src/routes/company.route.js b/src/routes/company.route.ts similarity index 100% rename from src/routes/company.route.js rename to src/routes/company.route.ts diff --git a/src/routes/provider.route.js b/src/routes/provider.route.ts similarity index 100% rename from src/routes/provider.route.js rename to src/routes/provider.route.ts diff --git a/src/routes/services.route.js b/src/routes/services.route.ts similarity index 100% rename from src/routes/services.route.js rename to src/routes/services.route.ts diff --git a/src/routes/user.route.js b/src/routes/user.route.ts similarity index 100% rename from src/routes/user.route.js rename to src/routes/user.route.ts diff --git a/src/services/catagory.service.js b/src/services/catagory.service.ts similarity index 85% rename from src/services/catagory.service.js rename to src/services/catagory.service.ts index 6fe1535..525609b 100644 --- a/src/services/catagory.service.js +++ b/src/services/catagory.service.ts @@ -2,16 +2,69 @@ import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); +// Type definitions +interface CategoryCreateData { + name?: string; + slug: string; + description?: string; + parentId?: string; +} + +interface CategoryUpdateData { + name?: string; + slug?: string; + description?: string; + parentId?: string; +} + +interface CategoryFilters { + parentId?: string | null; + includeChildren?: boolean; + includeParent?: boolean; + includeServices?: boolean; +} + +interface CategoryOptions { + includeChildren?: boolean; + includeParent?: boolean; + includeServices?: boolean; +} + +interface DeleteOptions { + force?: boolean; +} + +interface SearchOptions { + includeChildren?: boolean; + includeParent?: boolean; +} + +interface RootCategoryOptions { + includeChildren?: boolean; +} + +// Custom error class for better error handling +class CustomError extends Error { + status?: number; + + constructor(message: string, status?: number, name = 'Error') { + super(message); + this.status = status; + this.name = name; + } +} + +// Extend Error interface to include status property +interface ErrorWithStatus extends Error { + status?: number; +} + /** * Create a new category - * @param {Object} categoryData - The category data - * @param {string} [categoryData.name] - Category name - * @param {string} categoryData.slug - Unique slug for the category - * @param {string} [categoryData.description] - Category description - * @param {string} [categoryData.parentId] - Parent category ID for hierarchical structure + * @param {CategoryCreateData} categoryData - The category data * @returns {Promise} Created category object */ -export const createCategory = async (categoryData) => { +export const createCategory = async (categoryData: CategoryCreateData) => { try { const { name, slug, description, parentId } = categoryData; @@ -25,7 +78,7 @@ export const createCategory = async (categoryData) => { where: { slug } }); if (existingCategory) { - const err = new Error('Category with this slug already exists'); + const err = new Error('Category with this slug already exists') as ErrorWithStatus; err.name = 'BadRequestError'; err.status = 400; throw err; @@ -74,7 +127,7 @@ export const createCategory = async (categoryData) => { return newCategory; } catch (error) { - throw new Error(`Failed to create category: ${error.message}`); + throw new Error(`Failed to create category: ${error instanceof Error ? error.message : 'Unknown error'}`); } }; @@ -87,7 +140,14 @@ export const createCategory = async (categoryData) => { * @param {boolean} [filters.includeServices=false] - Include services count * @returns {Promise} Array of category objects */ -export const getAllCategories = async (filters = {}) => { +interface CategoryFilters { + parentId?: string | null; + includeChildren?: boolean; + includeParent?: boolean; + includeServices?: boolean; +} + +export const getAllCategories = async (filters: CategoryFilters = {}) => { try { const { parentId, @@ -96,7 +156,7 @@ export const getAllCategories = async (filters = {}) => { includeServices = false } = filters; - const whereClause = {}; + const whereClause: any = {}; if (parentId !== undefined) { whereClause.parentId = parentId; } @@ -145,7 +205,7 @@ export const getAllCategories = async (filters = {}) => { * @param {boolean} [options.includeServices=false] - Include services * @returns {Promise} Category object or null if not found */ -export const getCategoryById = async (id, options = {}) => { +export const getCategoryById = async (id: string, options: CategoryOptions = {}) => { try { const { includeChildren = true, @@ -205,7 +265,7 @@ export const getCategoryById = async (id, options = {}) => { * @param {boolean} [options.includeServices=false] - Include services * @returns {Promise} Category object or null if not found */ -export const getCategoryBySlug = async (slug, options = {}) => { +export const getCategoryBySlug = async (slug: string, options: CategoryOptions = {}) => { try { const { includeChildren = true, @@ -266,7 +326,7 @@ export const getCategoryBySlug = async (slug, options = {}) => { * @param {string} [updateData.parentId] - Parent category ID * @returns {Promise} Updated category object */ -export const updateCategory = async (id, updateData) => { +export const updateCategory = async (id: string, updateData: CategoryUpdateData) => { try { const { name, slug, description, parentId } = updateData; @@ -284,7 +344,7 @@ export const updateCategory = async (id, updateData) => { where: { slug } }); if (slugExists) { - const err = new Error('Category with this slug already exists'); + const err = new Error('Category with this slug already exists') as ErrorWithStatus; err.name = 'BadRequestError'; err.status = 400; throw err; @@ -356,7 +416,7 @@ export const updateCategory = async (id, updateData) => { * @param {boolean} [options.force=false] - Force delete even if category has children or services * @returns {Promise} Deleted category object */ -export const deleteCategory = async (id, options = {}) => { +export const deleteCategory = async (id: string, options: DeleteOptions = {}) => { try { const { force = false } = options; @@ -416,7 +476,7 @@ export const deleteCategory = async (id, options = {}) => { * @param {boolean} [options.includeChildren=true] - Include children categories * @returns {Promise} Array of root category objects */ -export const getRootCategories = async (options = {}) => { +export const getRootCategories = async (options: RootCategoryOptions = {}) => { try { const { includeChildren = true } = options; @@ -436,7 +496,7 @@ export const getRootCategories = async (options = {}) => { * @param {string} categoryId - Starting category ID * @returns {Promise} Category with full hierarchy */ -export const getCategoryHierarchy = async (categoryId) => { +export const getCategoryHierarchy = async (categoryId: string) => { try { const category = await prisma.category.findUnique({ where: { id: categoryId }, @@ -474,7 +534,7 @@ export const getCategoryHierarchy = async (categoryId) => { * @param {string} newParentId - New parent category ID * @returns {Promise} True if circular reference would be created */ -const checkCircularReference = async (categoryId, newParentId) => { +const checkCircularReference = async (categoryId: string, newParentId: string): Promise => { let currentParentId = newParentId; while (currentParentId) { @@ -502,7 +562,7 @@ const checkCircularReference = async (categoryId, newParentId) => { * @param {boolean} [options.includeParent=true] - Include parent category info * @returns {Promise} Array of matching categories */ -export const searchCategories = async (searchTerm, options = {}) => { +export const searchCategories = async (searchTerm: string, options: SearchOptions = {}) => { try { const { includeChildren = true, includeParent = true } = options; diff --git a/src/services/category.service.ts b/src/services/category.service.ts new file mode 100644 index 0000000..a1326c2 --- /dev/null +++ b/src/services/category.service.ts @@ -0,0 +1,594 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +// Type definitions +interface CategoryCreateData { + name?: string; + slug: string; + description?: string; + parentId?: string; +} + +interface CategoryUpdateData { + name?: string; + slug?: string; + description?: string; + parentId?: string; +} + +interface CategoryFilters { + parentId?: string | null; + includeChildren?: boolean; + includeParent?: boolean; + includeServices?: boolean; +} + +interface CategoryOptions { + includeChildren?: boolean; + includeParent?: boolean; + includeServices?: boolean; +} + +interface DeleteOptions { + force?: boolean; +} + +interface SearchOptions { + includeChildren?: boolean; + includeParent?: boolean; +} + +interface RootCategoryOptions { + includeChildren?: boolean; +} + +// Custom error class for better error handling +class CategoryError extends Error { + status: number | undefined; + + constructor(message: string, status?: number, name = 'CategoryError') { + super(message); + if (status !== undefined) { + this.status = status; + } + this.name = name; + } +} + +/** + * Create a new category + * @param categoryData - The category data + * @returns Created category object + */ +export const createCategory = async (categoryData: CategoryCreateData) => { + try { + const { name, slug, description, parentId } = categoryData; + + // Validate required fields + if (!slug) { + throw new CategoryError('Slug is required', 400); + } + + // Check if slug already exists + const existingCategory = await prisma.category.findUnique({ + where: { slug } + }); + if (existingCategory) { + throw new CategoryError('Category with this slug already exists', 400); + } + + // If parentId is provided, validate that parent exists + if (parentId) { + const parentCategory = await prisma.category.findUnique({ + where: { id: parentId } + }); + if (!parentCategory) { + throw new CategoryError('Parent category not found', 404); + } + } + + // Create data object with only defined properties + const createData: any = { slug }; + if (name !== undefined) createData.name = name; + if (description !== undefined) createData.description = description; + if (parentId !== undefined) createData.parentId = parentId; + + // Create the category + const newCategory = await prisma.category.create({ + data: createData, + include: { + parent: { + select: { + id: true, + name: true, + slug: true + } + }, + children: { + select: { + id: true, + name: true, + slug: true + } + }, + _count: { + select: { + services: true + } + } + } + }); + + return newCategory; + } catch (error) { + if (error instanceof CategoryError) { + throw error; + } + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + throw new CategoryError(`Failed to create category: ${errorMessage}`); + } +}; + +/** + * Get all categories with optional filtering + * @param filters - Optional filters + * @returns Array of category objects + */ +export const getAllCategories = async (filters: CategoryFilters = {}) => { + try { + const { + parentId, + includeChildren = true, + includeParent = true, + includeServices = false + } = filters; + + const whereClause: any = {}; + if (parentId !== undefined) { + whereClause.parentId = parentId; + } + + const categories = await prisma.category.findMany({ + where: whereClause, + include: { + parent: includeParent ? { + select: { + id: true, + name: true, + slug: true + } + } : false, + children: includeChildren ? { + select: { + id: true, + name: true, + slug: true, + description: true + } + } : false, + _count: includeServices ? { + select: { + services: true + } + } : false + }, + orderBy: { + name: 'asc' + } + }); + + return categories; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + throw new CategoryError(`Failed to fetch categories: ${errorMessage}`); + } +}; + +/** + * Get category by ID + * @param id - Category ID + * @param options - Additional options + * @returns Category object or null if not found + */ +export const getCategoryById = async (id: string, options: CategoryOptions = {}) => { + try { + const { + includeChildren = true, + includeParent = true, + includeServices = false + } = options; + + const category = await prisma.category.findUnique({ + where: { id }, + include: { + parent: includeParent ? { + select: { + id: true, + name: true, + slug: true, + description: true + } + } : false, + children: includeChildren ? { + select: { + id: true, + name: true, + slug: true, + description: true + } + } : false, + services: includeServices ? { + select: { + id: true, + title: true, + description: true, + price: true, + currency: true, + isActive: true + } + } : false, + _count: { + select: { + services: true + } + } + } + }); + + return category; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + throw new CategoryError(`Failed to fetch category: ${errorMessage}`); + } +}; + +/** + * Get category by slug + * @param slug - Category slug + * @param options - Additional options + * @returns Category object or null if not found + */ +export const getCategoryBySlug = async (slug: string, options: CategoryOptions = {}) => { + try { + const { + includeChildren = true, + includeParent = true, + includeServices = false + } = options; + + const category = await prisma.category.findUnique({ + where: { slug }, + include: { + parent: includeParent ? { + select: { + id: true, + name: true, + slug: true, + description: true + } + } : false, + children: includeChildren ? { + select: { + id: true, + name: true, + slug: true, + description: true + } + } : false, + services: includeServices ? { + select: { + id: true, + title: true, + description: true, + price: true, + currency: true, + isActive: true + } + } : false, + _count: { + select: { + services: true + } + } + } + }); + + return category; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + throw new CategoryError(`Failed to fetch category: ${errorMessage}`); + } +}; + +/** + * Update a category + * @param id - Category ID + * @param updateData - Data to update + * @returns Updated category object + */ +export const updateCategory = async (id: string, updateData: CategoryUpdateData) => { + try { + const { name, slug, description, parentId } = updateData; + + // Check if category exists + const existingCategory = await prisma.category.findUnique({ + where: { id } + }); + if (!existingCategory) { + throw new CategoryError('Category not found', 404); + } + + // If slug is being updated, check if new slug already exists + if (slug && slug !== existingCategory.slug) { + const slugExists = await prisma.category.findUnique({ + where: { slug } + }); + if (slugExists) { + throw new CategoryError('Category with this slug already exists', 400); + } + } + + // If parentId is being updated, validate that parent exists and prevent circular references + if (parentId && parentId !== existingCategory.parentId) { + if (parentId === id) { + throw new CategoryError('Category cannot be its own parent', 400); + } + + const parentCategory = await prisma.category.findUnique({ + where: { id: parentId } + }); + if (!parentCategory) { + throw new CategoryError('Parent category not found', 404); + } + + // Check for circular reference by checking if the current category is an ancestor of the new parent + const isCircularReference = await checkCircularReference(id, parentId); + if (isCircularReference) { + throw new CategoryError('Cannot create circular reference in category hierarchy', 400); + } + } + + // Create update data object with only defined properties + const updateDataObj: any = {}; + if (name !== undefined) updateDataObj.name = name; + if (slug !== undefined) updateDataObj.slug = slug; + if (description !== undefined) updateDataObj.description = description; + if (parentId !== undefined) updateDataObj.parentId = parentId; + + // Update the category + const updatedCategory = await prisma.category.update({ + where: { id }, + data: updateDataObj, + include: { + parent: { + select: { + id: true, + name: true, + slug: true + } + }, + children: { + select: { + id: true, + name: true, + slug: true + } + }, + _count: { + select: { + services: true + } + } + } + }); + + return updatedCategory; + } catch (error) { + if (error instanceof CategoryError) { + throw error; + } + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + throw new CategoryError(`Failed to update category: ${errorMessage}`); + } +}; + +/** + * Delete a category + * @param id - Category ID + * @param options - Delete options + * @returns Deleted category object + */ +export const deleteCategory = async (id: string, options: DeleteOptions = {}) => { + try { + const { force = false } = options; + + // Check if category exists + const existingCategory = await prisma.category.findUnique({ + where: { id }, + include: { + children: true, + _count: { + select: { + services: true + } + } + } + }); + + if (!existingCategory) { + throw new CategoryError('Category not found', 404); + } + + // Check if category has children or services and force is not enabled + if (!force) { + if (existingCategory.children.length > 0) { + throw new CategoryError('Cannot delete category with child categories. Use force option or delete children first.', 400); + } + if (existingCategory._count.services > 0) { + throw new CategoryError('Cannot delete category with associated services. Use force option or remove services first.', 400); + } + } + + // If force delete, handle children and services + if (force) { + // Set children's parentId to null (make them root categories) + await prisma.category.updateMany({ + where: { parentId: id }, + data: { parentId: null } + }); + + // Note: Services will be orphaned but not deleted + // You might want to handle this differently based on business requirements + } + + // Delete the category + const deletedCategory = await prisma.category.delete({ + where: { id } + }); + + return deletedCategory; + } catch (error) { + if (error instanceof CategoryError) { + throw error; + } + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + throw new CategoryError(`Failed to delete category: ${errorMessage}`); + } +}; + +/** + * Get root categories (categories with no parent) + * @param options - Additional options + * @returns Array of root category objects + */ +export const getRootCategories = async (options: RootCategoryOptions = {}) => { + try { + const { includeChildren = true } = options; + + return await getAllCategories({ + parentId: null, + includeChildren, + includeParent: false, + includeServices: true + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + throw new CategoryError(`Failed to fetch root categories: ${errorMessage}`); + } +}; + +/** + * Get category hierarchy starting from a specific category + * @param categoryId - Starting category ID + * @returns Category with full hierarchy + */ +export const getCategoryHierarchy = async (categoryId: string) => { + try { + const category = await prisma.category.findUnique({ + where: { id: categoryId }, + include: { + parent: { + include: { + parent: { + include: { + parent: true // Up to 3 levels up + } + } + } + }, + children: { + include: { + children: { + include: { + children: true // Up to 3 levels down + } + } + } + } + } + }); + + return category; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + throw new CategoryError(`Failed to fetch category hierarchy: ${errorMessage}`); + } +}; + +/** + * Helper function to check for circular references in category hierarchy + * @param categoryId - Current category ID + * @param newParentId - New parent category ID + * @returns True if circular reference would be created + */ +const checkCircularReference = async (categoryId: string, newParentId: string): Promise => { + let currentParentId: string | null = newParentId; + + while (currentParentId) { + if (currentParentId === categoryId) { + return true; // Circular reference found + } + + const parent: { parentId: string | null } | null = await prisma.category.findUnique({ + where: { id: currentParentId }, + select: { parentId: true } + }); + + if (!parent) break; + currentParentId = parent.parentId; + } + + return false; +}; + +/** + * Search categories by name or description + * @param searchTerm - Search term + * @param options - Search options + * @returns Array of matching categories + */ +export const searchCategories = async (searchTerm: string, options: SearchOptions = {}) => { + try { + const { includeChildren = true, includeParent = true } = options; + + const categories = await prisma.category.findMany({ + where: { + OR: [ + { name: { contains: searchTerm, mode: 'insensitive' } }, + { description: { contains: searchTerm, mode: 'insensitive' } } + ] + }, + include: { + parent: includeParent ? { + select: { + id: true, + name: true, + slug: true + } + } : false, + children: includeChildren ? { + select: { + id: true, + name: true, + slug: true, + description: true + } + } : false, + _count: { + select: { + services: true + } + } + }, + orderBy: { + name: 'asc' + } + }); + + return categories; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + throw new CategoryError(`Failed to search categories: ${errorMessage}`); + } +}; diff --git a/src/services/company.service.js b/src/services/company.service.ts similarity index 79% rename from src/services/company.service.js rename to src/services/company.service.ts index d44648b..2dbec16 100644 --- a/src/services/company.service.js +++ b/src/services/company.service.ts @@ -2,7 +2,26 @@ import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); -export const createCompany = async (userId, companyData) => { +// Type definitions +interface CompanyCreateData { + name: string; + description?: string; + logo?: string; + address?: string; + contact?: string; + socialmedia?: any; +} + +interface CompanyUpdateData { + name?: string; + description?: string; + logo?: string; + address?: string; + contact?: string; + socialmedia?: any; +} + +export const createCompany = async (userId: string, companyData: CompanyCreateData) => { // Check if user is a verified provider const provider = await prisma.serviceProvider.findUnique({ where: { userId }, @@ -32,7 +51,7 @@ export const createCompany = async (userId, companyData) => { return company; }; -export const updateCompany = async (userId, companyId, companyData) => { +export const updateCompany = async (userId: string, companyId: string, companyData: CompanyUpdateData) => { // Check if user owns this company const provider = await prisma.serviceProvider.findUnique({ where: { userId }, @@ -51,7 +70,7 @@ export const updateCompany = async (userId, companyId, companyData) => { throw new Error('Company not found or you do not have permission to update it'); } - const updatedData = {}; + const updatedData: any = {}; if (companyData.name !== undefined) updatedData.name = companyData.name; if (companyData.description !== undefined) updatedData.description = companyData.description; if (companyData.logo !== undefined) updatedData.logo = companyData.logo; @@ -67,7 +86,7 @@ export const updateCompany = async (userId, companyId, companyData) => { return company; }; -export const deleteCompany = async (userId, companyId) => { +export const deleteCompany = async (userId: string, companyId: string) => { // Check if user owns this company const provider = await prisma.serviceProvider.findUnique({ where: { userId }, @@ -91,7 +110,7 @@ export const deleteCompany = async (userId, companyId) => { }); }; -export const getCompanies = async (userId) => { +export const getCompanies = async (userId: string) => { const provider = await prisma.serviceProvider.findUnique({ where: { userId }, include: { diff --git a/src/services/provider.service.js b/src/services/provider.service.ts similarity index 89% rename from src/services/provider.service.js rename to src/services/provider.service.ts index f2f3b5f..f516922 100644 --- a/src/services/provider.service.js +++ b/src/services/provider.service.ts @@ -2,7 +2,24 @@ import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); -export const createProvider = async (userId, providerData) => { +// Type definitions +interface ProviderCreateData { + bio?: string; + skills?: string[]; + qualifications?: string[]; + logoUrl?: string; + IDCardUrl?: string; +} + +interface ProviderUpdateData { + bio?: string; + skills?: string[]; + qualifications?: string[]; + logoUrl?: string; + IDCardUrl?: string; +} + +export const createProvider = async (userId: string, providerData: ProviderCreateData) => { // Check if user exists and doesn't already have a provider profile const user = await prisma.user.findUnique({ where: { id: userId }, @@ -50,7 +67,7 @@ export const createProvider = async (userId, providerData) => { return newProvider; }; -export const updateProvider = async (userId, providerData) => { +export const updateProvider = async (userId: string, providerData: ProviderUpdateData) => { // Check if user has a provider profile const existingProvider = await prisma.serviceProvider.findUnique({ where: { userId } @@ -60,7 +77,7 @@ export const updateProvider = async (userId, providerData) => { throw new Error('Service provider profile not found'); } - const updatedData = {}; + const updatedData: any = {}; if (providerData.bio !== undefined) updatedData.bio = providerData.bio; if (providerData.skills !== undefined) updatedData.skills = providerData.skills; if (providerData.qualifications !== undefined) updatedData.qualifications = providerData.qualifications; @@ -117,7 +134,7 @@ export const updateProvider = async (userId, providerData) => { return updatedProvider; }; -export const deleteProvider = async (userId) => { +export const deleteProvider = async (userId: string) => { // Check if user has a provider profile const existingProvider = await prisma.serviceProvider.findUnique({ where: { userId }, @@ -158,7 +175,7 @@ export const deleteProvider = async (userId) => { return { message: 'Service provider profile deleted successfully' }; }; -export const getProviderProfile = async (userId) => { +export const getProviderProfile = async (userId: string) => { const provider = await prisma.serviceProvider.findUnique({ where: { userId }, include: { diff --git a/src/services/services.service.js b/src/services/services.service.ts similarity index 84% rename from src/services/services.service.js rename to src/services/services.service.ts index 244f42e..1099ab6 100644 --- a/src/services/services.service.js +++ b/src/services/services.service.ts @@ -2,22 +2,34 @@ import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); +// Type definitions +interface ServiceCreateData { + providerId: string; + categoryId: string; + title?: string; + description?: string; + price: number; + currency?: string; + tags?: string[]; + images?: string[]; + isActive?: boolean; + workingTime?: string[]; +} + +interface ServiceFilters { + providerId?: string; + categoryId?: string; + isActive?: boolean; + skip?: number; + take?: number; +} + /** * Create a new service - * @param {Object} serviceData - The service data - * @param {string} serviceData.providerId - ID of the service provider - * @param {string} serviceData.categoryId - ID of the service category - * @param {string} [serviceData.title] - Service title - * @param {string} [serviceData.description] - Service description - * @param {number} serviceData.price - Service price - * @param {string} [serviceData.currency="USD"] - Currency (defaults to USD) - * @param {string[]} [serviceData.tags=[]] - Array of tags - * @param {string[]} [serviceData.images=[]] - Array of image URLs - * @param {boolean} [serviceData.isActive=true] - Service active status - * @param {string[]} [serviceData.workingTime=[]] - Array of working time slots + * @param {ServiceCreateData} serviceData - The service data * @returns {Promise} Created service object */ -export const createService = async (serviceData) => { +export const createService = async (serviceData: ServiceCreateData) => { try { const { providerId, @@ -105,7 +117,7 @@ export const createService = async (serviceData) => { * @param {number} [filters.take=10] - Number of records to take for pagination * @returns {Promise} Array of service objects */ -export const getServices = async (filters = {}) => { +export const getServices = async (filters: ServiceFilters = {}) => { try { const { providerId, @@ -115,7 +127,7 @@ export const getServices = async (filters = {}) => { take = 10 } = filters; - const whereClause = {}; + const whereClause: any = {}; if (providerId) whereClause.providerId = providerId; if (categoryId) whereClause.categoryId = categoryId; @@ -165,7 +177,7 @@ export const getServices = async (filters = {}) => { * @param {string} serviceId - The service ID * @returns {Promise} Service object or null if not found */ -export const getServiceById = async (serviceId) => { +export const getServiceById = async (serviceId: string) => { try { const service = await prisma.service.findUnique({ where: { id: serviceId }, @@ -209,7 +221,7 @@ export const getServiceById = async (serviceId) => { * @param {Object} updateData - Data to update * @returns {Promise} Updated service object */ -export const updateService = async (serviceId, updateData) => { +export const updateService = async (serviceId: string, updateData: Partial) => { try { const service = await prisma.service.findUnique({ where: { id: serviceId } @@ -249,7 +261,7 @@ export const updateService = async (serviceId, updateData) => { * @param {string} serviceId - The service ID * @returns {Promise} Deleted service object */ -export const deleteService = async (serviceId) => { +export const deleteService = async (serviceId: string) => { try { const service = await prisma.service.findUnique({ where: { id: serviceId } diff --git a/src/services/user.service.js b/src/services/user.service.ts similarity index 75% rename from src/services/user.service.js rename to src/services/user.service.ts index 38e47cd..e8f4c53 100644 --- a/src/services/user.service.js +++ b/src/services/user.service.ts @@ -5,10 +5,38 @@ import { comparePassword, hashPassword } from '../utils/hash.js'; const prisma = new PrismaClient(); -export const register = async ({ email, firstName, lastName, password, imageUrl, location, address, phone, socialmedia }) => { +// Type definitions +interface UserRegistrationData { + email: string; + firstName: string; + lastName: string; + password: string; + imageUrl?: string; + location?: string; + address?: string; + phone?: string; + socialmedia?: any; +} + +interface UserUpdateData { + firstName?: string; + lastName?: string; + imageUrl?: string; + location?: string; + address?: string; + phone?: string; + socialmedia?: any; +} + +// Extend Error interface to include status property +interface ErrorWithStatus extends Error { + status?: number; +} + +export const register = async ({ email, firstName, lastName, password, imageUrl, location, address, phone, socialmedia }: UserRegistrationData) => { const existingUser = await prisma.user.findUnique({ where: { email } }); if (existingUser) { - const err = new Error('Email already exists. Please use a different email address.'); + const err = new Error('Email already exists. Please use a different email address.') as ErrorWithStatus; err.name = 'BadRequestError'; err.status = 400; throw err; @@ -29,7 +57,7 @@ export const register = async ({ email, firstName, lastName, password, imageUrl, }); }; -export const login = async ({ email, password }) => { +export const login = async ({ email, password }: { email: string; password: string }) => { const user = await prisma.user.findUnique({ where: { email } }); if (!user) throw new Error('User not found'); @@ -40,7 +68,7 @@ export const login = async ({ email, password }) => { return { token, user }; }; -export const getProfile = async (userId) => { +export const getProfile = async (userId: string) => { const user = await prisma.user.findUnique({ where: { id: userId }, select: { @@ -103,8 +131,8 @@ export const getProfile = async (userId) => { return user; } -export const updateProfile = async (userId, data) => { - const updatedData = {}; +export const updateProfile = async (userId: string, data: UserUpdateData) => { + const updatedData: any = {}; if (data.firstName) updatedData.firstName = data.firstName; if (data.lastName) updatedData.lastName = data.lastName; if (data.imageUrl) updatedData.imageUrl = data.imageUrl; @@ -119,13 +147,13 @@ export const updateProfile = async (userId, data) => { }); } -export const deleteProfile = async (userId) => { +export const deleteProfile = async (userId: string) => { await prisma.user.delete({ where: { id: userId }, }); } -export const checkEmailExists = async (email) => { +export const checkEmailExists = async (email: string) => { const user = await prisma.user.findUnique({ where: { email } }); return !!user; } \ No newline at end of file diff --git a/src/utils/hash.js b/src/utils/hash.js deleted file mode 100644 index 96d80b5..0000000 --- a/src/utils/hash.js +++ /dev/null @@ -1,9 +0,0 @@ -import { hash as _hash, compare } from 'bcrypt'; - -export async function hashPassword(plainText) { - return await _hash(plainText, 10); -} - -export async function comparePassword(plainText, hash) { - return await compare(plainText, hash); -} diff --git a/src/utils/hash.ts b/src/utils/hash.ts new file mode 100644 index 0000000..8232d9f --- /dev/null +++ b/src/utils/hash.ts @@ -0,0 +1,9 @@ +import { hash as _hash, compare } from 'bcrypt'; + +export async function hashPassword(plainText: string): Promise { + return await _hash(plainText, 10); +} + +export async function comparePassword(plainText: string, hash: string): Promise { + return await compare(plainText, hash); +} diff --git a/src/validators/catagory.validator.js b/src/validators/category.validator.ts similarity index 100% rename from src/validators/catagory.validator.js rename to src/validators/category.validator.ts diff --git a/src/validators/company.validator.js b/src/validators/company.validator.ts similarity index 100% rename from src/validators/company.validator.js rename to src/validators/company.validator.ts diff --git a/src/validators/provider.validator.js b/src/validators/provider.validator.ts similarity index 100% rename from src/validators/provider.validator.js rename to src/validators/provider.validator.ts diff --git a/src/validators/services.validator.js b/src/validators/services.validator.ts similarity index 100% rename from src/validators/services.validator.js rename to src/validators/services.validator.ts diff --git a/src/validators/user.validator.js b/src/validators/user.validator.ts similarity index 100% rename from src/validators/user.validator.js rename to src/validators/user.validator.ts diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c8ec273 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,44 @@ +{ + // Visit https://aka.ms/tsconfig to read more about this file + "compilerOptions": { + // File Layout + // "rootDir": "./src", + "outDir": "./dist", + + // Environment Settings + // See also https://aka.ms/tsconfig/module + "module": "ESNext", + "target": "ESNext", + "moduleResolution": "node", + "types": ["node"], + "lib": ["esnext"], + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + + // Other Outputs + "sourceMap": true, + "declaration": true, + "declarationMap": true, + + // Stricter Typechecking Options + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": false, + + // Style Options + // "noImplicitReturns": true, + // "noImplicitOverride": true, + // "noUnusedLocals": true, + // "noUnusedParameters": true, + // "noFallthroughCasesInSwitch": true, + // "noPropertyAccessFromIndexSignature": true, + + // Recommended Options + "strict": false, + "jsx": "react-jsx", + "verbatimModuleSyntax": false, + "isolatedModules": true, + "noUncheckedSideEffectImports": false, + "moduleDetection": "force", + "skipLibCheck": true, + } +} From 64e097dceb6db8a25b5ee304e794ef45a8bbe5b9 Mon Sep 17 00:00:00 2001 From: UmeshaJayakody Date: Thu, 7 Aug 2025 01:03:45 +0530 Subject: [PATCH 2/8] refactor: update Dockerfile to install TypeScript and run transpiled JavaScript --- dockerfile | 12 ++++++++---- tsconfig.json | 3 +++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/dockerfile b/dockerfile index 74638c3..7ba1329 100644 --- a/dockerfile +++ b/dockerfile @@ -1,5 +1,3 @@ - - FROM node:18-alpine # App directory @@ -17,8 +15,14 @@ RUN npx prisma generate # Rest of the source COPY . . +# Install TypeScript globally +RUN npm install -g typescript + +# Transpile TypeScript to JavaScript +RUN npx tsc + # Expose the service port EXPOSE 3000 -# Start the API -CMD ["npm", "start"] +# Run the transpiled JavaScript file +CMD ["node", "dist/index.js"] diff --git a/tsconfig.json b/tsconfig.json index c8ec273..59405e6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -40,5 +40,8 @@ "noUncheckedSideEffectImports": false, "moduleDetection": "force", "skipLibCheck": true, + }, + "ts-node": { + "esm": true } } From 7d1c1221d26b6b0a5ca9cf2ecd5fbd89a111e0e8 Mon Sep 17 00:00:00 2001 From: Yasith Imalka <157888522+Yasith763@users.noreply.github.com> Date: Sat, 9 Aug 2025 23:19:42 +0530 Subject: [PATCH 3/8] feat: add getProviderById endpoint and validation schema --- src/controllers/provider.controller.ts | 9 +++++ src/routes/provider.route.ts | 6 ++- src/services/provider.service.ts | 54 ++++++++++++++++++++++++++ src/validators/provider.validator.ts | 4 ++ 4 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/controllers/provider.controller.ts b/src/controllers/provider.controller.ts index fbf72cd..13aa916 100644 --- a/src/controllers/provider.controller.ts +++ b/src/controllers/provider.controller.ts @@ -42,3 +42,12 @@ export const getProviderProfile = async (req: Request, res: Response, next: Next next(err); } }; + +export const getProviderById = async (req: Request, res: Response, next: NextFunction) => { + try { + const provider = await providerService.getProviderById(req.params.id); + res.status(200).json(provider); + } catch (err) { + next(err); + } +}; diff --git a/src/routes/provider.route.ts b/src/routes/provider.route.ts index c27f414..604ad35 100644 --- a/src/routes/provider.route.ts +++ b/src/routes/provider.route.ts @@ -4,15 +4,17 @@ import { createProvider, updateProvider, deleteProvider, - getProviderProfile + getProviderProfile, + getProviderById } from '../controllers/provider.controller.js'; import validate from '../middlewares/validation.middleware.js'; -import { createProviderSchema, updateProviderSchema } from '../validators/provider.validator.js'; +import { createProviderSchema, updateProviderSchema, providerParamsSchema } from '../validators/provider.validator.js'; import authMiddleware from '../middlewares/auth.middleware.js'; router.post('/', authMiddleware, validate(createProviderSchema), createProvider); router.get('/profile', authMiddleware, getProviderProfile); router.put('/profile', authMiddleware, validate(updateProviderSchema), updateProvider); router.delete('/profile', authMiddleware, deleteProvider); +router.get('/:id', validate(providerParamsSchema, 'params'), getProviderById); export default router; diff --git a/src/services/provider.service.ts b/src/services/provider.service.ts index f516922..2a0d03d 100644 --- a/src/services/provider.service.ts +++ b/src/services/provider.service.ts @@ -243,3 +243,57 @@ export const getProviderProfile = async (userId: string) => { return provider; }; + +export const getProviderById = async (id: string) => { + const provider = await prisma.serviceProvider.findUnique({ + where: { id }, + include: { + user: { + select: { + id: true, + email: true, + firstName: true, + lastName: true, + imageUrl: true, + role: true + } + }, + services: { + select: { + id: true, + title: true, + description: true, + price: true, + currency: true, + images: true, + isActive: true + } + }, + reviews: { + select: { + id: true, + rating: true, + comment: true, + createdAt: true, + reviewer: { + select: { + firstName: true, + lastName: true, + imageUrl: true + } + } + }, + orderBy: { + createdAt: 'desc' + }, + take: 10 + } + } + }); + + if (!provider) { + throw new Error('Service provider not found'); + } + + return provider; +}; diff --git a/src/validators/provider.validator.ts b/src/validators/provider.validator.ts index 8ba21b6..74d3823 100644 --- a/src/validators/provider.validator.ts +++ b/src/validators/provider.validator.ts @@ -15,3 +15,7 @@ export const updateProviderSchema = Joi.object({ logoUrl: Joi.string().uri().optional(), IDCardUrl: Joi.string().uri().optional() // Optional for updates }); + +export const providerParamsSchema = Joi.object({ + id: Joi.string().required() +}); From 487731481462b9b4ab61f2c663e06a7d6487429c Mon Sep 17 00:00:00 2001 From: indunil-k Date: Sun, 10 Aug 2025 18:18:13 +0530 Subject: [PATCH 4/8] added userSearch api for chat purposes --- .dockerignore | 0 .gitignore | 0 CATAGORY_API.md | 0 README.md | 0 SERVICES_API.md | 0 category_dataset.json | 0 docker-compose.yml | 0 dockerfile | 0 index.ts | 0 package-lock.json | 0 package.json | 0 .../20250728103456_init/migration.sql | 0 .../20250728111351_add_address/migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../20250731084030_fix/migration.sql | 0 .../20250731092201_init/migration.sql | 0 .../20250731092602_doi/migration.sql | 0 .../20250731093947_ff/migration.sql | 0 .../20250803042313_init/migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 prisma/migrations/migration_lock.toml | 0 prisma/schema.prisma | 0 prisma/seed.ts | 0 src/controllers/catagory.controller.ts | 0 src/controllers/category.controller.ts | 0 src/controllers/company.controller.ts | 0 src/controllers/provider.controller.ts | 0 src/controllers/services.controller.ts | 0 src/controllers/user.controller.ts | 29 ++++++++++++++----- src/middlewares/auth.middleware.ts | 0 src/middlewares/validation.middleware.ts | 0 src/routes/catagory.route.ts | 0 src/routes/category.route.ts | 0 src/routes/company.route.ts | 0 src/routes/provider.route.ts | 0 src/routes/services.route.ts | 0 src/routes/user.route.ts | 5 ++-- src/services/catagory.service.ts | 0 src/services/category.service.ts | 0 src/services/company.service.ts | 0 src/services/provider.service.ts | 0 src/services/services.service.ts | 0 src/services/user.service.ts | 21 ++++++++++++++ src/utils/hash.ts | 0 src/validators/category.validator.ts | 0 src/validators/company.validator.ts | 0 src/validators/provider.validator.ts | 0 src/validators/services.validator.ts | 0 src/validators/user.validator.ts | 0 tsconfig.json | 0 53 files changed, 45 insertions(+), 10 deletions(-) mode change 100644 => 100755 .dockerignore mode change 100644 => 100755 .gitignore mode change 100644 => 100755 CATAGORY_API.md mode change 100644 => 100755 README.md mode change 100644 => 100755 SERVICES_API.md mode change 100644 => 100755 category_dataset.json mode change 100644 => 100755 docker-compose.yml mode change 100644 => 100755 dockerfile mode change 100644 => 100755 index.ts mode change 100644 => 100755 package-lock.json mode change 100644 => 100755 package.json mode change 100644 => 100755 prisma/migrations/20250728103456_init/migration.sql mode change 100644 => 100755 prisma/migrations/20250728111351_add_address/migration.sql mode change 100644 => 100755 prisma/migrations/20250730071048_add_banner_url/migration.sql mode change 100644 => 100755 prisma/migrations/20250730071403_add_gig_table/migration.sql mode change 100644 => 100755 prisma/migrations/20250731084030_fix/migration.sql mode change 100644 => 100755 prisma/migrations/20250731092201_init/migration.sql mode change 100644 => 100755 prisma/migrations/20250731092602_doi/migration.sql mode change 100644 => 100755 prisma/migrations/20250731093947_ff/migration.sql mode change 100644 => 100755 prisma/migrations/20250803042313_init/migration.sql mode change 100644 => 100755 prisma/migrations/20250803044427_make_idcard_required/migration.sql mode change 100644 => 100755 prisma/migrations/20250803044751_make_idcard_optional/migration.sql mode change 100644 => 100755 prisma/migrations/20250803045109_make_idcard_required/migration.sql mode change 100644 => 100755 prisma/migrations/migration_lock.toml mode change 100644 => 100755 prisma/schema.prisma mode change 100644 => 100755 prisma/seed.ts mode change 100644 => 100755 src/controllers/catagory.controller.ts mode change 100644 => 100755 src/controllers/category.controller.ts mode change 100644 => 100755 src/controllers/company.controller.ts mode change 100644 => 100755 src/controllers/provider.controller.ts mode change 100644 => 100755 src/controllers/services.controller.ts mode change 100644 => 100755 src/controllers/user.controller.ts mode change 100644 => 100755 src/middlewares/auth.middleware.ts mode change 100644 => 100755 src/middlewares/validation.middleware.ts mode change 100644 => 100755 src/routes/catagory.route.ts mode change 100644 => 100755 src/routes/category.route.ts mode change 100644 => 100755 src/routes/company.route.ts mode change 100644 => 100755 src/routes/provider.route.ts mode change 100644 => 100755 src/routes/services.route.ts mode change 100644 => 100755 src/routes/user.route.ts mode change 100644 => 100755 src/services/catagory.service.ts mode change 100644 => 100755 src/services/category.service.ts mode change 100644 => 100755 src/services/company.service.ts mode change 100644 => 100755 src/services/provider.service.ts mode change 100644 => 100755 src/services/services.service.ts mode change 100644 => 100755 src/services/user.service.ts mode change 100644 => 100755 src/utils/hash.ts mode change 100644 => 100755 src/validators/category.validator.ts mode change 100644 => 100755 src/validators/company.validator.ts mode change 100644 => 100755 src/validators/provider.validator.ts mode change 100644 => 100755 src/validators/services.validator.ts mode change 100644 => 100755 src/validators/user.validator.ts mode change 100644 => 100755 tsconfig.json diff --git a/.dockerignore b/.dockerignore old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/CATAGORY_API.md b/CATAGORY_API.md old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/SERVICES_API.md b/SERVICES_API.md old mode 100644 new mode 100755 diff --git a/category_dataset.json b/category_dataset.json old mode 100644 new mode 100755 diff --git a/docker-compose.yml b/docker-compose.yml old mode 100644 new mode 100755 diff --git a/dockerfile b/dockerfile old mode 100644 new mode 100755 diff --git a/index.ts b/index.ts old mode 100644 new mode 100755 diff --git a/package-lock.json b/package-lock.json old mode 100644 new mode 100755 diff --git a/package.json b/package.json old mode 100644 new mode 100755 diff --git a/prisma/migrations/20250728103456_init/migration.sql b/prisma/migrations/20250728103456_init/migration.sql old mode 100644 new mode 100755 diff --git a/prisma/migrations/20250728111351_add_address/migration.sql b/prisma/migrations/20250728111351_add_address/migration.sql old mode 100644 new mode 100755 diff --git a/prisma/migrations/20250730071048_add_banner_url/migration.sql b/prisma/migrations/20250730071048_add_banner_url/migration.sql old mode 100644 new mode 100755 diff --git a/prisma/migrations/20250730071403_add_gig_table/migration.sql b/prisma/migrations/20250730071403_add_gig_table/migration.sql old mode 100644 new mode 100755 diff --git a/prisma/migrations/20250731084030_fix/migration.sql b/prisma/migrations/20250731084030_fix/migration.sql old mode 100644 new mode 100755 diff --git a/prisma/migrations/20250731092201_init/migration.sql b/prisma/migrations/20250731092201_init/migration.sql old mode 100644 new mode 100755 diff --git a/prisma/migrations/20250731092602_doi/migration.sql b/prisma/migrations/20250731092602_doi/migration.sql old mode 100644 new mode 100755 diff --git a/prisma/migrations/20250731093947_ff/migration.sql b/prisma/migrations/20250731093947_ff/migration.sql old mode 100644 new mode 100755 diff --git a/prisma/migrations/20250803042313_init/migration.sql b/prisma/migrations/20250803042313_init/migration.sql old mode 100644 new mode 100755 diff --git a/prisma/migrations/20250803044427_make_idcard_required/migration.sql b/prisma/migrations/20250803044427_make_idcard_required/migration.sql old mode 100644 new mode 100755 diff --git a/prisma/migrations/20250803044751_make_idcard_optional/migration.sql b/prisma/migrations/20250803044751_make_idcard_optional/migration.sql old mode 100644 new mode 100755 diff --git a/prisma/migrations/20250803045109_make_idcard_required/migration.sql b/prisma/migrations/20250803045109_make_idcard_required/migration.sql old mode 100644 new mode 100755 diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml old mode 100644 new mode 100755 diff --git a/prisma/schema.prisma b/prisma/schema.prisma old mode 100644 new mode 100755 diff --git a/prisma/seed.ts b/prisma/seed.ts old mode 100644 new mode 100755 diff --git a/src/controllers/catagory.controller.ts b/src/controllers/catagory.controller.ts old mode 100644 new mode 100755 diff --git a/src/controllers/category.controller.ts b/src/controllers/category.controller.ts old mode 100644 new mode 100755 diff --git a/src/controllers/company.controller.ts b/src/controllers/company.controller.ts old mode 100644 new mode 100755 diff --git a/src/controllers/provider.controller.ts b/src/controllers/provider.controller.ts old mode 100644 new mode 100755 diff --git a/src/controllers/services.controller.ts b/src/controllers/services.controller.ts old mode 100644 new mode 100755 diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts old mode 100644 new mode 100755 index 050e3e6..74dac5f --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -1,23 +1,23 @@ import type { Request, Response, NextFunction } from 'express'; -import * as userService from '../services/user.service.js'; +import { register, login, getProfile, updateProfile, deleteProfile, checkEmailExists, searchUsers } from '../services/user.service.js'; export const createUser = async (req: Request, res: Response, next: NextFunction) => { try { const { email, firstName, lastName, password, imageUrl, location, address, phone, socialmedia } = req.body; - const user = await userService.register({ email, firstName, lastName, password, imageUrl, location, address, phone, socialmedia }); + const user = await register({ email, firstName, lastName, password, imageUrl, location, address, phone, socialmedia }); res.status(201).json({ message: 'User registered', user }); } catch (err) { next(err); } }; -export const checkEmailExists = async (req: Request, res: Response, next: NextFunction) => { +export const checkEmailExistsController = async (req: Request, res: Response, next: NextFunction) => { try { const { email } = req.query; if (!email) { return res.status(400).json({ message: 'Email is required' }); } - const exists = await userService.checkEmailExists(email as string); + const exists = await checkEmailExists(email as string); res.status(200).json({ exists }); } catch (err) { next(err); @@ -26,7 +26,7 @@ export const checkEmailExists = async (req: Request, res: Response, next: NextFu export const loginUser = async (req: Request, res: Response) => { try { - const result = await userService.login(req.body); + const result = await login(req.body); res.status(200).json({ message: 'Login successful', ...result }); } catch (err: any) { res.status(401).json({ message: err.message }); @@ -35,7 +35,7 @@ export const loginUser = async (req: Request, res: Response) => { export const getUserProfile = async (req: Request, res: Response, next: NextFunction) => { try { - const user = await userService.getProfile((req as any).user.id); + const user = await getProfile((req as any).user.id); res.status(200).json(user); } catch (err) { next(err); @@ -44,7 +44,7 @@ export const getUserProfile = async (req: Request, res: Response, next: NextFunc export const updateUserProfile = async (req: Request, res: Response, next: NextFunction) => { try { - const updatedUser = await userService.updateProfile((req as any).user.id, req.body); + const updatedUser = await updateProfile((req as any).user.id, req.body); res.status(200).json({ message: 'Profile updated', user: updatedUser }); } catch (err) { next(err); @@ -53,9 +53,22 @@ export const updateUserProfile = async (req: Request, res: Response, next: NextF export const deleteUserProfile = async (req: Request, res: Response, next: NextFunction) => { try { - await userService.deleteProfile((req as any).user.id); + await deleteProfile((req as any).user.id); res.status(200).json({ message: 'Profile deleted' }); } catch (err) { next(err); } +}; + +export const searchUsersController = async (req: Request, res: Response, next: NextFunction) => { + try { + const { q } = req.query; + if (!q) { + return res.status(400).json({ message: 'Search query is required' }); + } + const users = await searchUsers(q as string); + res.status(200).json(users); + } catch (err) { + next(err); + } }; \ No newline at end of file diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts old mode 100644 new mode 100755 diff --git a/src/middlewares/validation.middleware.ts b/src/middlewares/validation.middleware.ts old mode 100644 new mode 100755 diff --git a/src/routes/catagory.route.ts b/src/routes/catagory.route.ts old mode 100644 new mode 100755 diff --git a/src/routes/category.route.ts b/src/routes/category.route.ts old mode 100644 new mode 100755 diff --git a/src/routes/company.route.ts b/src/routes/company.route.ts old mode 100644 new mode 100755 diff --git a/src/routes/provider.route.ts b/src/routes/provider.route.ts old mode 100644 new mode 100755 diff --git a/src/routes/services.route.ts b/src/routes/services.route.ts old mode 100644 new mode 100755 diff --git a/src/routes/user.route.ts b/src/routes/user.route.ts old mode 100644 new mode 100755 index d1ad645..6f1b1a1 --- a/src/routes/user.route.ts +++ b/src/routes/user.route.ts @@ -1,11 +1,12 @@ import { Router } from 'express'; const router = Router(); -import { createUser,loginUser,getUserProfile,updateUserProfile,deleteUserProfile,checkEmailExists} from '../controllers/user.controller.js'; +import { createUser,loginUser,getUserProfile,updateUserProfile,deleteUserProfile,checkEmailExistsController,searchUsersController} from '../controllers/user.controller.js'; import validate from '../middlewares/validation.middleware.js'; import { registerSchema,loginSchema,updateProfileSchema } from '../validators/user.validator.js'; import authMiddleware from '../middlewares/auth.middleware.js'; -router.get('/check-email', checkEmailExists); +router.get('/check-email', checkEmailExistsController); +router.get('/search', authMiddleware, searchUsersController); router.post('/register', validate(registerSchema), createUser); router.post('/login', validate(loginSchema), loginUser); router.get('/profile', authMiddleware, getUserProfile); diff --git a/src/services/catagory.service.ts b/src/services/catagory.service.ts old mode 100644 new mode 100755 diff --git a/src/services/category.service.ts b/src/services/category.service.ts old mode 100644 new mode 100755 diff --git a/src/services/company.service.ts b/src/services/company.service.ts old mode 100644 new mode 100755 diff --git a/src/services/provider.service.ts b/src/services/provider.service.ts old mode 100644 new mode 100755 diff --git a/src/services/services.service.ts b/src/services/services.service.ts old mode 100644 new mode 100755 diff --git a/src/services/user.service.ts b/src/services/user.service.ts old mode 100644 new mode 100755 index e8f4c53..4231740 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -156,4 +156,25 @@ export const deleteProfile = async (userId: string) => { export const checkEmailExists = async (email: string) => { const user = await prisma.user.findUnique({ where: { email } }); return !!user; +} + +export const searchUsers = async (query: string) => { + const users = await prisma.user.findMany({ + where: { + OR: [ + { firstName: { contains: query, mode: 'insensitive' } }, + { lastName: { contains: query, mode: 'insensitive' } }, + { email: { contains: query, mode: 'insensitive' } }, + ], + }, + select: { + id: true, + email: true, + firstName: true, + lastName: true, + imageUrl: true, + }, + take: 20, // Limit results to prevent performance issues + }); + return users; } \ No newline at end of file diff --git a/src/utils/hash.ts b/src/utils/hash.ts old mode 100644 new mode 100755 diff --git a/src/validators/category.validator.ts b/src/validators/category.validator.ts old mode 100644 new mode 100755 diff --git a/src/validators/company.validator.ts b/src/validators/company.validator.ts old mode 100644 new mode 100755 diff --git a/src/validators/provider.validator.ts b/src/validators/provider.validator.ts old mode 100644 new mode 100755 diff --git a/src/validators/services.validator.ts b/src/validators/services.validator.ts old mode 100644 new mode 100755 diff --git a/src/validators/user.validator.ts b/src/validators/user.validator.ts old mode 100644 new mode 100755 diff --git a/tsconfig.json b/tsconfig.json old mode 100644 new mode 100755 From b74ec8a3ae3e4b3a377056b8f65c95cc9ab5aad9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 Aug 2025 16:33:02 +0000 Subject: [PATCH 5/8] Initial plan From eb30224eb007ae5ff77839b749be57760ffeb285 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 Aug 2025 16:37:32 +0000 Subject: [PATCH 6/8] Initial analysis - identify duplicate category files for consolidation Co-authored-by: indunil-k <161734455+indunil-k@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index d412559..f42c1de 100755 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "@types/express": "^5.0.3", "@types/joi": "^17.2.2", "@types/jsonwebtoken": "^9.0.10", - "@types/node": "^24.2.0", + "@types/node": "^24.3.0", "ts-node": "^10.9.2", "typescript": "^5.9.2" } @@ -332,9 +332,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz", - "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 3adaedd..43cb5a5 100755 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@types/express": "^5.0.3", "@types/joi": "^17.2.2", "@types/jsonwebtoken": "^9.0.10", - "@types/node": "^24.2.0", + "@types/node": "^24.3.0", "ts-node": "^10.9.2", "typescript": "^5.9.2" } From b4d51b04908558b4f40153344192895fd2e18761 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 Aug 2025 16:41:54 +0000 Subject: [PATCH 7/8] Remove duplicate misspelled category files to centralize database structure Co-authored-by: indunil-k <161734455+indunil-k@users.noreply.github.com> --- src/controllers/catagory.controller.ts | 227 --------- src/routes/catagory.route.ts | 121 ----- src/services/catagory.service.ts | 607 ------------------------- 3 files changed, 955 deletions(-) delete mode 100755 src/controllers/catagory.controller.ts delete mode 100755 src/routes/catagory.route.ts delete mode 100755 src/services/catagory.service.ts diff --git a/src/controllers/catagory.controller.ts b/src/controllers/catagory.controller.ts deleted file mode 100755 index d95f141..0000000 --- a/src/controllers/catagory.controller.ts +++ /dev/null @@ -1,227 +0,0 @@ -import type { Request, Response, NextFunction } from 'express'; -import * as categoryService from '../services/category.service.js'; - -/** - * Create a new category - */ -export const createCategory = async (req: Request, res: Response, next: NextFunction) => { - try { - const categoryData = req.body; - const newCategory = await categoryService.createCategory(categoryData); - - res.status(201).json({ - success: true, - message: 'Category created successfully', - data: newCategory - }); - } catch (error) { - next(error); - } -}; - -/** - * Get all categories with optional filtering - */ -export const getCategories = async (req: Request, res: Response, next: NextFunction) => { - try { - const filters = { - parentId: req.query.parentId as string | null, - includeChildren: req.query.includeChildren !== 'false', - includeParent: req.query.includeParent !== 'false', - includeServices: req.query.includeServices === 'true' - }; - - const categories = await categoryService.getAllCategories(filters); - - res.status(200).json({ - success: true, - message: 'Categories retrieved successfully', - data: categories - }); - } catch (error) { - next(error); - } -}; - -/** - * Get category by ID - */ -export const getCategoryById = async (req: Request, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const options = { - includeChildren: req.query.includeChildren !== 'false', - includeParent: req.query.includeParent !== 'false', - includeServices: req.query.includeServices === 'true' - }; - - const category = await categoryService.getCategoryById(id!, options); - - if (!category) { - return res.status(404).json({ - success: false, - message: 'Category not found' - }); - } - - res.status(200).json({ - success: true, - message: 'Category retrieved successfully', - data: category - }); - } catch (error) { - next(error); - } -}; - -/** - * Get category by slug - */ -export const getCategoryBySlug = async (req: Request, res: Response, next: NextFunction) => { - try { - const { slug } = req.params; - const options = { - includeChildren: req.query.includeChildren !== 'false', - includeParent: req.query.includeParent !== 'false', - includeServices: req.query.includeServices === 'true' - }; - - const category = await categoryService.getCategoryBySlug(slug!, options); - - if (!category) { - return res.status(404).json({ - success: false, - message: 'Category not found' - }); - } - - res.status(200).json({ - success: true, - message: 'Category retrieved successfully', - data: category - }); - } catch (error) { - next(error); - } -}; - -/** - * Update category - */ -export const updateCategory = async (req: Request, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const updateData = req.body; - - const updatedCategory = await categoryService.updateCategory(id!, updateData); - - res.status(200).json({ - success: true, - message: 'Category updated successfully', - data: updatedCategory - }); - } catch (error) { - next(error); - } -}; - -/** - * Delete category - */ -export const deleteCategory = async (req: Request, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const options = { - force: req.query.force === 'true' - }; - - const deletedCategory = await categoryService.deleteCategory(id!, options); - - res.status(200).json({ - success: true, - message: 'Category deleted successfully', - data: deletedCategory - }); - } catch (error) { - next(error); - } -}; - -/** - * Get root categories (categories with no parent) - */ -export const getRootCategories = async (req: Request, res: Response, next: NextFunction) => { - try { - const options = { - includeChildren: req.query.includeChildren !== 'false' - }; - - const rootCategories = await categoryService.getRootCategories(options); - - res.status(200).json({ - success: true, - message: 'Root categories retrieved successfully', - data: rootCategories - }); - } catch (error) { - next(error); - } -}; - -/** - * Get category hierarchy - */ -export const getCategoryHierarchy = async (req: Request, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - - const hierarchy = await categoryService.getCategoryHierarchy(id!); - - if (!hierarchy) { - return res.status(404).json({ - success: false, - message: 'Category not found' - }); - } - - res.status(200).json({ - success: true, - message: 'Category hierarchy retrieved successfully', - data: hierarchy - }); - } catch (error) { - next(error); - } -}; - -/** - * Search categories - */ -export const searchCategories = async (req: Request, res: Response, next: NextFunction) => { - try { - const { q: searchTerm } = req.query; - - if (!searchTerm) { - return res.status(400).json({ - success: false, - message: 'Search term is required' - }); - } - - const options = { - includeChildren: req.query.includeChildren !== 'false', - includeParent: req.query.includeParent !== 'false' - }; - - const categories = await categoryService.searchCategories(searchTerm as string, options); - - res.status(200).json({ - success: true, - message: 'Search completed successfully', - data: categories, - searchTerm - }); - } catch (error) { - next(error); - } -}; diff --git a/src/routes/catagory.route.ts b/src/routes/catagory.route.ts deleted file mode 100755 index 7994a63..0000000 --- a/src/routes/catagory.route.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { Router } from 'express'; -const router = Router(); - -import { - createCategory, - getCategories, - getCategoryById, - getCategoryBySlug, - updateCategory, - deleteCategory, - getRootCategories, - getCategoryHierarchy, - searchCategories -} from '../controllers/category.controller.js'; - -import validate from '../middlewares/validation.middleware.js'; -import { - createCategorySchema, - updateCategorySchema, - categoryIdSchema, - categorySlugSchema, - searchCategoriesSchema, - categoryQuerySchema -} from '../validators/category.validator.js'; - -import authMiddleware from '../middlewares/auth.middleware.js'; - -// Public routes (no authentication required) -/** - * @route GET /api/categories - * @desc Get all categories with optional filtering - * @access Public - * @query parentId - Filter by parent category ID - * @query includeChildren - Include children categories (default: true) - * @query includeParent - Include parent category info (default: true) - * @query includeServices - Include services count (default: false) - */ -router.get('/', validate(categoryQuerySchema, 'query'), getCategories); - -/** - * @route GET /api/categories/roots - * @desc Get all root categories (categories with no parent) - * @access Public - * @query includeChildren - Include children categories (default: true) - */ -router.get('/roots', validate(categoryQuerySchema, 'query'), getRootCategories); - -/** - * @route GET /api/categories/search - * @desc Search categories by name or description - * @access Public - * @query q - Search term (required) - * @query includeChildren - Include children categories (default: true) - * @query includeParent - Include parent category info (default: true) - */ -router.get('/search', validate(searchCategoriesSchema, 'query'), searchCategories); - -/** - * @route GET /api/categories/id/:id - * @desc Get category by ID - * @access Public - * @param id - Category ID - * @query includeChildren - Include children categories (default: true) - * @query includeParent - Include parent category info (default: true) - * @query includeServices - Include services (default: false) - */ -router.get('/id/:id', validate(categoryIdSchema, 'params'), getCategoryById); - -/** - * @route GET /api/categories/slug/:slug - * @desc Get category by slug - * @access Public - * @param slug - Category slug - * @query includeChildren - Include children categories (default: true) - * @query includeParent - Include parent category info (default: true) - * @query includeServices - Include services (default: false) - */ -router.get('/slug/:slug', validate(categorySlugSchema, 'params'), getCategoryBySlug); - -/** - * @route GET /api/categories/:id/hierarchy - * @desc Get category hierarchy (full tree starting from category) - * @access Public - * @param id - Category ID - */ -router.get('/:id/hierarchy', validate(categoryIdSchema, 'params'), getCategoryHierarchy); - -// Protected routes (authentication required) -/** - * @route POST /api/categories - * @desc Create a new category - * @access Public (For Testing) - * @body name - Category name (optional) - * @body slug - Category slug (required, unique) - * @body description - Category description (optional) - * @body parentId - Parent category ID (optional) - */ -router.post('/', validate(createCategorySchema), createCategory); - -/** - * @route PUT /api/categories/:id - * @desc Update a category - * @access Private (Admin/Provider) - * @param id - Category ID - * @body name - Category name (optional) - * @body slug - Category slug (optional) - * @body description - Category description (optional) - * @body parentId - Parent category ID (optional) - */ -router.put('/:id', authMiddleware, validate(categoryIdSchema, 'params'), validate(updateCategorySchema), updateCategory); - -/** - * @route DELETE /api/categories/:id - * @desc Delete a category - * @access Private (Admin only) - * @param id - Category ID - * @query force - Force delete even if category has children or services (default: false) - */ -router.delete('/:id', authMiddleware, validate(categoryIdSchema, 'params'), deleteCategory); - -export default router; diff --git a/src/services/catagory.service.ts b/src/services/catagory.service.ts deleted file mode 100755 index 525609b..0000000 --- a/src/services/catagory.service.ts +++ /dev/null @@ -1,607 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -// Type definitions -interface CategoryCreateData { - name?: string; - slug: string; - description?: string; - parentId?: string; -} - -interface CategoryUpdateData { - name?: string; - slug?: string; - description?: string; - parentId?: string; -} - -interface CategoryFilters { - parentId?: string | null; - includeChildren?: boolean; - includeParent?: boolean; - includeServices?: boolean; -} - -interface CategoryOptions { - includeChildren?: boolean; - includeParent?: boolean; - includeServices?: boolean; -} - -interface DeleteOptions { - force?: boolean; -} - -interface SearchOptions { - includeChildren?: boolean; - includeParent?: boolean; -} - -interface RootCategoryOptions { - includeChildren?: boolean; -} - -// Custom error class for better error handling -class CustomError extends Error { - status?: number; - - constructor(message: string, status?: number, name = 'Error') { - super(message); - this.status = status; - this.name = name; - } -} - -// Extend Error interface to include status property -interface ErrorWithStatus extends Error { - status?: number; -} - -/** - * Create a new category - * @param {CategoryCreateData} categoryData - The category data - * @returns {Promise} Created category object - */ -export const createCategory = async (categoryData: CategoryCreateData) => { - try { - const { name, slug, description, parentId } = categoryData; - - // Validate required fields - if (!slug) { - throw new Error('Slug is required'); - } - - // Check if slug already exists - const existingCategory = await prisma.category.findUnique({ - where: { slug } - }); - if (existingCategory) { - const err = new Error('Category with this slug already exists') as ErrorWithStatus; - err.name = 'BadRequestError'; - err.status = 400; - throw err; - } - - // If parentId is provided, validate that parent exists - if (parentId) { - const parentCategory = await prisma.category.findUnique({ - where: { id: parentId } - }); - if (!parentCategory) { - throw new Error('Parent category not found'); - } - } - - // Create the category - const newCategory = await prisma.category.create({ - data: { - name, - slug, - description, - parentId - }, - include: { - parent: { - select: { - id: true, - name: true, - slug: true - } - }, - children: { - select: { - id: true, - name: true, - slug: true - } - }, - _count: { - select: { - services: true - } - } - } - }); - - return newCategory; - } catch (error) { - throw new Error(`Failed to create category: ${error instanceof Error ? error.message : 'Unknown error'}`); - } -}; - -/** - * Get all categories with optional filtering - * @param {Object} filters - Optional filters - * @param {string} [filters.parentId] - Filter by parent category ID - * @param {boolean} [filters.includeChildren=true] - Include children categories - * @param {boolean} [filters.includeParent=true] - Include parent category info - * @param {boolean} [filters.includeServices=false] - Include services count - * @returns {Promise} Array of category objects - */ -interface CategoryFilters { - parentId?: string | null; - includeChildren?: boolean; - includeParent?: boolean; - includeServices?: boolean; -} - -export const getAllCategories = async (filters: CategoryFilters = {}) => { - try { - const { - parentId, - includeChildren = true, - includeParent = true, - includeServices = false - } = filters; - - const whereClause: any = {}; - if (parentId !== undefined) { - whereClause.parentId = parentId; - } - - const categories = await prisma.category.findMany({ - where: whereClause, - include: { - parent: includeParent ? { - select: { - id: true, - name: true, - slug: true - } - } : false, - children: includeChildren ? { - select: { - id: true, - name: true, - slug: true, - description: true - } - } : false, - _count: includeServices ? { - select: { - services: true - } - } : false - }, - orderBy: { - name: 'asc' - } - }); - - return categories; - } catch (error) { - throw new Error(`Failed to fetch categories: ${error.message}`); - } -}; - -/** - * Get category by ID - * @param {string} id - Category ID - * @param {Object} options - Additional options - * @param {boolean} [options.includeChildren=true] - Include children categories - * @param {boolean} [options.includeParent=true] - Include parent category info - * @param {boolean} [options.includeServices=false] - Include services - * @returns {Promise} Category object or null if not found - */ -export const getCategoryById = async (id: string, options: CategoryOptions = {}) => { - try { - const { - includeChildren = true, - includeParent = true, - includeServices = false - } = options; - - const category = await prisma.category.findUnique({ - where: { id }, - include: { - parent: includeParent ? { - select: { - id: true, - name: true, - slug: true, - description: true - } - } : false, - children: includeChildren ? { - select: { - id: true, - name: true, - slug: true, - description: true - } - } : false, - services: includeServices ? { - select: { - id: true, - title: true, - description: true, - price: true, - currency: true, - isActive: true - } - } : false, - _count: { - select: { - services: true - } - } - } - }); - - return category; - } catch (error) { - throw new Error(`Failed to fetch category: ${error.message}`); - } -}; - -/** - * Get category by slug - * @param {string} slug - Category slug - * @param {Object} options - Additional options - * @param {boolean} [options.includeChildren=true] - Include children categories - * @param {boolean} [options.includeParent=true] - Include parent category info - * @param {boolean} [options.includeServices=false] - Include services - * @returns {Promise} Category object or null if not found - */ -export const getCategoryBySlug = async (slug: string, options: CategoryOptions = {}) => { - try { - const { - includeChildren = true, - includeParent = true, - includeServices = false - } = options; - - const category = await prisma.category.findUnique({ - where: { slug }, - include: { - parent: includeParent ? { - select: { - id: true, - name: true, - slug: true, - description: true - } - } : false, - children: includeChildren ? { - select: { - id: true, - name: true, - slug: true, - description: true - } - } : false, - services: includeServices ? { - select: { - id: true, - title: true, - description: true, - price: true, - currency: true, - isActive: true - } - } : false, - _count: { - select: { - services: true - } - } - } - }); - - return category; - } catch (error) { - throw new Error(`Failed to fetch category: ${error.message}`); - } -}; - -/** - * Update a category - * @param {string} id - Category ID - * @param {Object} updateData - Data to update - * @param {string} [updateData.name] - Category name - * @param {string} [updateData.slug] - Category slug - * @param {string} [updateData.description] - Category description - * @param {string} [updateData.parentId] - Parent category ID - * @returns {Promise} Updated category object - */ -export const updateCategory = async (id: string, updateData: CategoryUpdateData) => { - try { - const { name, slug, description, parentId } = updateData; - - // Check if category exists - const existingCategory = await prisma.category.findUnique({ - where: { id } - }); - if (!existingCategory) { - throw new Error('Category not found'); - } - - // If slug is being updated, check if new slug already exists - if (slug && slug !== existingCategory.slug) { - const slugExists = await prisma.category.findUnique({ - where: { slug } - }); - if (slugExists) { - const err = new Error('Category with this slug already exists') as ErrorWithStatus; - err.name = 'BadRequestError'; - err.status = 400; - throw err; - } - } - - // If parentId is being updated, validate that parent exists and prevent circular references - if (parentId && parentId !== existingCategory.parentId) { - if (parentId === id) { - throw new Error('Category cannot be its own parent'); - } - - const parentCategory = await prisma.category.findUnique({ - where: { id: parentId } - }); - if (!parentCategory) { - throw new Error('Parent category not found'); - } - - // Check for circular reference by checking if the current category is an ancestor of the new parent - const isCircularReference = await checkCircularReference(id, parentId); - if (isCircularReference) { - throw new Error('Cannot create circular reference in category hierarchy'); - } - } - - // Update the category - const updatedCategory = await prisma.category.update({ - where: { id }, - data: { - ...(name !== undefined && { name }), - ...(slug !== undefined && { slug }), - ...(description !== undefined && { description }), - ...(parentId !== undefined && { parentId }) - }, - include: { - parent: { - select: { - id: true, - name: true, - slug: true - } - }, - children: { - select: { - id: true, - name: true, - slug: true - } - }, - _count: { - select: { - services: true - } - } - } - }); - - return updatedCategory; - } catch (error) { - throw new Error(`Failed to update category: ${error.message}`); - } -}; - -/** - * Delete a category - * @param {string} id - Category ID - * @param {Object} options - Delete options - * @param {boolean} [options.force=false] - Force delete even if category has children or services - * @returns {Promise} Deleted category object - */ -export const deleteCategory = async (id: string, options: DeleteOptions = {}) => { - try { - const { force = false } = options; - - // Check if category exists - const existingCategory = await prisma.category.findUnique({ - where: { id }, - include: { - children: true, - _count: { - select: { - services: true - } - } - } - }); - - if (!existingCategory) { - throw new Error('Category not found'); - } - - // Check if category has children or services and force is not enabled - if (!force) { - if (existingCategory.children.length > 0) { - throw new Error('Cannot delete category with child categories. Use force option or delete children first.'); - } - if (existingCategory._count.services > 0) { - throw new Error('Cannot delete category with associated services. Use force option or remove services first.'); - } - } - - // If force delete, handle children and services - if (force) { - // Set children's parentId to null (make them root categories) - await prisma.category.updateMany({ - where: { parentId: id }, - data: { parentId: null } - }); - - // Note: Services will be orphaned but not deleted - // You might want to handle this differently based on business requirements - } - - // Delete the category - const deletedCategory = await prisma.category.delete({ - where: { id } - }); - - return deletedCategory; - } catch (error) { - throw new Error(`Failed to delete category: ${error.message}`); - } -}; - -/** - * Get root categories (categories with no parent) - * @param {Object} options - Additional options - * @param {boolean} [options.includeChildren=true] - Include children categories - * @returns {Promise} Array of root category objects - */ -export const getRootCategories = async (options: RootCategoryOptions = {}) => { - try { - const { includeChildren = true } = options; - - return await getAllCategories({ - parentId: null, - includeChildren, - includeParent: false, - includeServices: true - }); - } catch (error) { - throw new Error(`Failed to fetch root categories: ${error.message}`); - } -}; - -/** - * Get category hierarchy starting from a specific category - * @param {string} categoryId - Starting category ID - * @returns {Promise} Category with full hierarchy - */ -export const getCategoryHierarchy = async (categoryId: string) => { - try { - const category = await prisma.category.findUnique({ - where: { id: categoryId }, - include: { - parent: { - include: { - parent: { - include: { - parent: true // Up to 3 levels up - } - } - } - }, - children: { - include: { - children: { - include: { - children: true // Up to 3 levels down - } - } - } - } - } - }); - - return category; - } catch (error) { - throw new Error(`Failed to fetch category hierarchy: ${error.message}`); - } -}; - -/** - * Helper function to check for circular references in category hierarchy - * @param {string} categoryId - Current category ID - * @param {string} newParentId - New parent category ID - * @returns {Promise} True if circular reference would be created - */ -const checkCircularReference = async (categoryId: string, newParentId: string): Promise => { - let currentParentId = newParentId; - - while (currentParentId) { - if (currentParentId === categoryId) { - return true; // Circular reference found - } - - const parent = await prisma.category.findUnique({ - where: { id: currentParentId }, - select: { parentId: true } - }); - - if (!parent) break; - currentParentId = parent.parentId; - } - - return false; -}; - -/** - * Search categories by name or description - * @param {string} searchTerm - Search term - * @param {Object} options - Search options - * @param {boolean} [options.includeChildren=true] - Include children categories - * @param {boolean} [options.includeParent=true] - Include parent category info - * @returns {Promise} Array of matching categories - */ -export const searchCategories = async (searchTerm: string, options: SearchOptions = {}) => { - try { - const { includeChildren = true, includeParent = true } = options; - - const categories = await prisma.category.findMany({ - where: { - OR: [ - { name: { contains: searchTerm, mode: 'insensitive' } }, - { description: { contains: searchTerm, mode: 'insensitive' } } - ] - }, - include: { - parent: includeParent ? { - select: { - id: true, - name: true, - slug: true - } - } : false, - children: includeChildren ? { - select: { - id: true, - name: true, - slug: true, - description: true - } - } : false, - _count: { - select: { - services: true - } - } - }, - orderBy: { - name: 'asc' - } - }); - - return categories; - } catch (error) { - throw new Error(`Failed to search categories: ${error.message}`); - } -}; From 825b210f3bc41edf7969a1ccf51bc3d1ff67f68e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 Aug 2025 16:44:05 +0000 Subject: [PATCH 8/8] Fix documentation filename to complete database centralization Co-authored-by: indunil-k <161734455+indunil-k@users.noreply.github.com> --- CATAGORY_API.md => CATEGORY_API.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename CATAGORY_API.md => CATEGORY_API.md (100%) diff --git a/CATAGORY_API.md b/CATEGORY_API.md similarity index 100% rename from CATAGORY_API.md rename to CATEGORY_API.md