From 7f05c74976a7c47b33ff87c939870d9ace44f340 Mon Sep 17 00:00:00 2001 From: ShamaKamina Date: Thu, 26 Sep 2024 10:11:09 +0200 Subject: [PATCH 01/12] changes --- .../controllers/commentController.test.js | 77 ++ .../controllers/issueController.test.js | 111 +++ .../controllers/locationController.test.js | 126 ++++ .../controllers/reactionController.test.js | 49 ++ .../controllers/reportsController.test.js | 109 +++ .../subscriptionsController.test.js | 139 ++++ .../controllers/userController.test.js | 93 +++ .../visualizationController.test.js | 75 ++ .../__tests__/middleware/middleware.test.js | 96 +++ .../__tests__/routes/commentRoutes.test.js | 88 +++ .../__tests__/routes/issueRoutes.test.js | 72 ++ .../__tests__/routes/locationRoutes.test.js | 61 ++ .../__tests__/routes/reactionRoutes.test.js | 38 + .../__tests__/routes/reportsRoutes.test.js | 67 ++ .../routes/subscriptionsRoutes.test.js | 92 +++ .../__tests__/routes/userRoutes.test.js | 94 +++ .../routes/visualizationRoutes.test.js | 58 ++ .../__tests__/services/commentService.test.js | 198 +++++ .../__tests__/services/issueService.test.js | 407 ++++++++++ .../services/locationService.test.js | 45 ++ .../services/reactionService.test.js | 105 +++ .../__tests__/services/reportsService.test.js | 251 ++++++ .../services/subscriptionsService.test.js | 87 +++ .../__tests__/services/userService.test.js | 143 ++++ .../services/visualizationService.test.js | 53 ++ .../__tests__/utilities/response.test.js | 32 + backend/public/app.js | 56 ++ backend/public/data/mockFile.js | 14 + backend/public/data/mockUser.js | 18 + backend/public/middleware/cacheMiddleware.js | 43 ++ backend/public/middleware/middleware.js | 67 ++ .../clusters/controllers/clusterController.js | 101 +++ .../repositories/clusterRepository.js | 331 ++++++++ .../modules/clusters/routes/clusterRoutes.js | 13 + .../clusters/services/clusterService.js | 306 ++++++++ .../comments/controllers/commentController.js | 63 ++ .../repositories/commentRepository.js | 153 ++++ .../modules/comments/routes/commentRoutes.js | 35 + .../comments/services/commentService.js | 140 ++++ .../issues/controllers/issueController.js | 275 +++++++ .../issues/repositories/categoryRepository.js | 38 + .../issues/repositories/issueRepository.js | 712 ++++++++++++++++++ .../modules/issues/routes/issueRoutes.js | 43 ++ .../modules/issues/services/issueService.js | 630 ++++++++++++++++ .../controllers/locationController.js | 41 + .../repositories/locationRepository.js | 126 ++++ .../locations/routes/locationRoutes.js | 11 + .../locations/services/locationService.js | 52 ++ .../controllers/organizationController.js | 359 +++++++++ .../repositories/organizationRepository.js | 548 ++++++++++++++ .../routes/organizationRoutes.js | 46 ++ .../services/organizationService.js | 631 ++++++++++++++++ .../points/controllers/pointsController.js | 53 ++ .../points/repositories/pointsRepository.js | 227 ++++++ .../modules/points/routes/pointsRoutes.js | 11 + .../modules/points/services/pointsService.js | 71 ++ .../controllers/reactionController.js | 31 + .../repositories/reactionRepository.js | 113 +++ .../reactions/routes/reactionRoutes.js | 12 + .../reactions/services/reactionService.js | 68 ++ .../reports/controllers/reportsController.js | 103 +++ .../reports/repositories/reportsRepository.js | 265 +++++++ .../modules/reports/routes/reportsRoutes.js | 38 + .../reports/services/reportsService.js | 131 ++++ .../repositories/resolutionRepository.js | 155 ++++ .../resolutionResponseRepository.js | 53 ++ .../resolutions/services/resolutionService.js | 184 +++++ .../public/modules/shared/models/cluster.js | 2 + .../public/modules/shared/models/comment.js | 2 + backend/public/modules/shared/models/issue.js | 2 + .../modules/shared/models/organization.js | 2 + .../public/modules/shared/models/reaction.js | 2 + .../public/modules/shared/models/reports.js | 2 + .../modules/shared/models/resolution.js | 2 + .../modules/shared/services/openAIService.js | 47 ++ .../modules/shared/services/redisClient.js | 20 + .../modules/shared/services/supabaseClient.js | 8 + .../controllers/subscriptionsController.js | 68 ++ .../repositories/subscriptionsRepository.js | 380 ++++++++++ .../routes/subscriptionsRoutes.js | 36 + .../services/subscriptionsService.js | 105 +++ .../users/controllers/userController.js | 63 ++ .../users/repositories/userRepository.js | 173 +++++ .../public/modules/users/routes/userRoutes.js | 16 + .../modules/users/services/userService.js | 280 +++++++ .../controllers/visualizationController.js | 27 + .../repositories/visualizationRepository.js | 59 ++ .../routes/visualizationRoutes.js | 30 + .../services/visualizationService.js | 30 + backend/public/types/comment.js | 2 + backend/public/types/issue.js | 2 + backend/public/types/pagination.js | 2 + backend/public/types/response.js | 11 + backend/public/types/shared.js | 2 + backend/public/types/subscriptions.js | 2 + backend/public/types/tempType.js | 2 + backend/public/types/users.js | 2 + backend/public/types/visualization.js | 2 + backend/public/utilities/cacheUtils.js | 26 + backend/public/utilities/response.js | 7 + backend/public/utilities/validators.js | 11 + 101 files changed, 10560 insertions(+) create mode 100644 backend/public/__tests__/controllers/commentController.test.js create mode 100644 backend/public/__tests__/controllers/issueController.test.js create mode 100644 backend/public/__tests__/controllers/locationController.test.js create mode 100644 backend/public/__tests__/controllers/reactionController.test.js create mode 100644 backend/public/__tests__/controllers/reportsController.test.js create mode 100644 backend/public/__tests__/controllers/subscriptionsController.test.js create mode 100644 backend/public/__tests__/controllers/userController.test.js create mode 100644 backend/public/__tests__/controllers/visualizationController.test.js create mode 100644 backend/public/__tests__/middleware/middleware.test.js create mode 100644 backend/public/__tests__/routes/commentRoutes.test.js create mode 100644 backend/public/__tests__/routes/issueRoutes.test.js create mode 100644 backend/public/__tests__/routes/locationRoutes.test.js create mode 100644 backend/public/__tests__/routes/reactionRoutes.test.js create mode 100644 backend/public/__tests__/routes/reportsRoutes.test.js create mode 100644 backend/public/__tests__/routes/subscriptionsRoutes.test.js create mode 100644 backend/public/__tests__/routes/userRoutes.test.js create mode 100644 backend/public/__tests__/routes/visualizationRoutes.test.js create mode 100644 backend/public/__tests__/services/commentService.test.js create mode 100644 backend/public/__tests__/services/issueService.test.js create mode 100644 backend/public/__tests__/services/locationService.test.js create mode 100644 backend/public/__tests__/services/reactionService.test.js create mode 100644 backend/public/__tests__/services/reportsService.test.js create mode 100644 backend/public/__tests__/services/subscriptionsService.test.js create mode 100644 backend/public/__tests__/services/userService.test.js create mode 100644 backend/public/__tests__/services/visualizationService.test.js create mode 100644 backend/public/__tests__/utilities/response.test.js create mode 100644 backend/public/app.js create mode 100644 backend/public/data/mockFile.js create mode 100644 backend/public/data/mockUser.js create mode 100644 backend/public/middleware/cacheMiddleware.js create mode 100644 backend/public/middleware/middleware.js create mode 100644 backend/public/modules/clusters/controllers/clusterController.js create mode 100644 backend/public/modules/clusters/repositories/clusterRepository.js create mode 100644 backend/public/modules/clusters/routes/clusterRoutes.js create mode 100644 backend/public/modules/clusters/services/clusterService.js create mode 100644 backend/public/modules/comments/controllers/commentController.js create mode 100644 backend/public/modules/comments/repositories/commentRepository.js create mode 100644 backend/public/modules/comments/routes/commentRoutes.js create mode 100644 backend/public/modules/comments/services/commentService.js create mode 100644 backend/public/modules/issues/controllers/issueController.js create mode 100644 backend/public/modules/issues/repositories/categoryRepository.js create mode 100644 backend/public/modules/issues/repositories/issueRepository.js create mode 100644 backend/public/modules/issues/routes/issueRoutes.js create mode 100644 backend/public/modules/issues/services/issueService.js create mode 100644 backend/public/modules/locations/controllers/locationController.js create mode 100644 backend/public/modules/locations/repositories/locationRepository.js create mode 100644 backend/public/modules/locations/routes/locationRoutes.js create mode 100644 backend/public/modules/locations/services/locationService.js create mode 100644 backend/public/modules/organizations/controllers/organizationController.js create mode 100644 backend/public/modules/organizations/repositories/organizationRepository.js create mode 100644 backend/public/modules/organizations/routes/organizationRoutes.js create mode 100644 backend/public/modules/organizations/services/organizationService.js create mode 100644 backend/public/modules/points/controllers/pointsController.js create mode 100644 backend/public/modules/points/repositories/pointsRepository.js create mode 100644 backend/public/modules/points/routes/pointsRoutes.js create mode 100644 backend/public/modules/points/services/pointsService.js create mode 100644 backend/public/modules/reactions/controllers/reactionController.js create mode 100644 backend/public/modules/reactions/repositories/reactionRepository.js create mode 100644 backend/public/modules/reactions/routes/reactionRoutes.js create mode 100644 backend/public/modules/reactions/services/reactionService.js create mode 100644 backend/public/modules/reports/controllers/reportsController.js create mode 100644 backend/public/modules/reports/repositories/reportsRepository.js create mode 100644 backend/public/modules/reports/routes/reportsRoutes.js create mode 100644 backend/public/modules/reports/services/reportsService.js create mode 100644 backend/public/modules/resolutions/repositories/resolutionRepository.js create mode 100644 backend/public/modules/resolutions/repositories/resolutionResponseRepository.js create mode 100644 backend/public/modules/resolutions/services/resolutionService.js create mode 100644 backend/public/modules/shared/models/cluster.js create mode 100644 backend/public/modules/shared/models/comment.js create mode 100644 backend/public/modules/shared/models/issue.js create mode 100644 backend/public/modules/shared/models/organization.js create mode 100644 backend/public/modules/shared/models/reaction.js create mode 100644 backend/public/modules/shared/models/reports.js create mode 100644 backend/public/modules/shared/models/resolution.js create mode 100644 backend/public/modules/shared/services/openAIService.js create mode 100644 backend/public/modules/shared/services/redisClient.js create mode 100644 backend/public/modules/shared/services/supabaseClient.js create mode 100644 backend/public/modules/subscriptions/controllers/subscriptionsController.js create mode 100644 backend/public/modules/subscriptions/repositories/subscriptionsRepository.js create mode 100644 backend/public/modules/subscriptions/routes/subscriptionsRoutes.js create mode 100644 backend/public/modules/subscriptions/services/subscriptionsService.js create mode 100644 backend/public/modules/users/controllers/userController.js create mode 100644 backend/public/modules/users/repositories/userRepository.js create mode 100644 backend/public/modules/users/routes/userRoutes.js create mode 100644 backend/public/modules/users/services/userService.js create mode 100644 backend/public/modules/visualizations/controllers/visualizationController.js create mode 100644 backend/public/modules/visualizations/repositories/visualizationRepository.js create mode 100644 backend/public/modules/visualizations/routes/visualizationRoutes.js create mode 100644 backend/public/modules/visualizations/services/visualizationService.js create mode 100644 backend/public/types/comment.js create mode 100644 backend/public/types/issue.js create mode 100644 backend/public/types/pagination.js create mode 100644 backend/public/types/response.js create mode 100644 backend/public/types/shared.js create mode 100644 backend/public/types/subscriptions.js create mode 100644 backend/public/types/tempType.js create mode 100644 backend/public/types/users.js create mode 100644 backend/public/types/visualization.js create mode 100644 backend/public/utilities/cacheUtils.js create mode 100644 backend/public/utilities/response.js create mode 100644 backend/public/utilities/validators.js diff --git a/backend/public/__tests__/controllers/commentController.test.js b/backend/public/__tests__/controllers/commentController.test.js new file mode 100644 index 00000000..a52332f1 --- /dev/null +++ b/backend/public/__tests__/controllers/commentController.test.js @@ -0,0 +1,77 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const commentService_1 = require("@/modules/comments/services/commentService"); +const response_1 = require("@/utilities/response"); +const commentController = __importStar(require("@/modules/comments/controllers/commentController")); +jest.mock("@/modules/comments/services/commentService"); +jest.mock("@/utilities/response"); +jest.mock('@/modules/shared/services/redisClient', () => ({ + __esModule: true, + default: { + on: jest.fn(), + get: jest.fn(), + setex: jest.fn(), + del: jest.fn(), + keys: jest.fn().mockResolvedValue([]), + }, +})); +describe("Comment Controller", () => { + let mockRequest; + let mockResponse; + let mockCommentService; + beforeEach(() => { + mockRequest = { body: {} }; + mockResponse = { json: jest.fn() }; + mockCommentService = { + getNumComments: jest.fn(), + getComments: jest.fn(), + addComment: jest.fn(), + deleteComment: jest.fn(), + }; + commentService_1.CommentService.mockImplementation(() => mockCommentService); + }); + const testCases = [ + { name: "getNumComments", method: commentController.getNumComments }, + { name: "getComments", method: commentController.getComments }, + { name: "addComment", method: commentController.addComment }, + { name: "deleteComment", method: commentController.deleteComment }, + ]; + testCases.forEach(({ name, method }) => { + it(`should call sendResponse for ${name}`, () => __awaiter(void 0, void 0, void 0, function* () { + yield method(mockRequest, mockResponse); + expect(response_1.sendResponse).toHaveBeenCalled(); + })); + }); +}); diff --git a/backend/public/__tests__/controllers/issueController.test.js b/backend/public/__tests__/controllers/issueController.test.js new file mode 100644 index 00000000..69bd9178 --- /dev/null +++ b/backend/public/__tests__/controllers/issueController.test.js @@ -0,0 +1,111 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const issueController = __importStar(require("@/modules/issues/controllers/issueController")); +const issueService_1 = __importDefault(require("@/modules/issues/services/issueService")); +const response_1 = require("@/utilities/response"); +const cacheMiddleware_1 = require("@/middleware/cacheMiddleware"); +jest.mock("@/modules/issues/services/issueService"); +jest.mock("@/utilities/response"); +jest.mock("@/middleware/cacheMiddleware"); +jest.mock("@/utilities/cacheUtils"); +jest.mock("@/modules/shared/services/redisClient", () => ({ + __esModule: true, + default: { + on: jest.fn(), + get: jest.fn(), + setex: jest.fn(), + del: jest.fn(), + keys: jest.fn().mockResolvedValue([]), + }, +})); +describe("Issue Controller", () => { + let mockRequest; + let mockResponse; + let mockNext; + let mockIssueService; + beforeEach(() => { + mockRequest = { body: {}, query: {} }; + mockResponse = { + json: jest.fn(), + status: jest.fn().mockReturnThis(), + }; + mockNext = jest.fn(); + mockIssueService = { + getIssues: jest.fn(), + getIssueById: jest.fn(), + createIssue: jest.fn(), + updateIssue: jest.fn(), + deleteIssue: jest.fn(), + resolveIssue: jest.fn(), + getUserIssues: jest.fn(), + getUserResolvedIssues: jest.fn(), + }; + issueService_1.default.mockImplementation(() => mockIssueService); + cacheMiddleware_1.cacheMiddleware.mockImplementation(() => (req, res, next) => next()); + }); + const testControllerMethod = (methodName) => __awaiter(void 0, void 0, void 0, function* () { + const controllerMethod = issueController[methodName]; + if (Array.isArray(controllerMethod)) { + // If it's an array of middleware, call the last function (actual controller) + const lastMiddleware = controllerMethod[controllerMethod.length - 1]; + if (typeof lastMiddleware === 'function') { + yield lastMiddleware(mockRequest, mockResponse, mockNext); + } + } + else if (typeof controllerMethod === 'function') { + yield controllerMethod(mockRequest, mockResponse); + } + expect(response_1.sendResponse).toHaveBeenCalled(); + }); + it("should handle getIssues", () => testControllerMethod("getIssues")); + it("should handle getIssueById", () => testControllerMethod("getIssueById")); + it("should handle updateIssue", () => testControllerMethod("updateIssue")); + it("should handle deleteIssue", () => testControllerMethod("deleteIssue")); + it("should handle resolveIssue", () => testControllerMethod("resolveIssue")); + it("should handle getUserIssues", () => testControllerMethod("getUserIssues")); + it("should handle getUserResolvedIssues", () => testControllerMethod("getUserResolvedIssues")); + it("should handle createIssue", () => __awaiter(void 0, void 0, void 0, function* () { + mockRequest.file = {}; + yield testControllerMethod("createIssue"); + })); + it("should handle errors", () => __awaiter(void 0, void 0, void 0, function* () { + mockIssueService.getIssues.mockRejectedValue(new Error("Test error")); + yield testControllerMethod("getIssues"); + expect(response_1.sendResponse).toHaveBeenCalled(); + })); +}); diff --git a/backend/public/__tests__/controllers/locationController.test.js b/backend/public/__tests__/controllers/locationController.test.js new file mode 100644 index 00000000..eb720ed6 --- /dev/null +++ b/backend/public/__tests__/controllers/locationController.test.js @@ -0,0 +1,126 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const locationController = __importStar(require("@/modules/locations/controllers/locationController")); +const locationService_1 = require("@/modules/locations/services/locationService"); +const response_1 = require("@/utilities/response"); +const cacheMiddleware_1 = require("@/middleware/cacheMiddleware"); +jest.mock("@/modules/locations/services/locationService"); +jest.mock("@/utilities/response"); +jest.mock("@/middleware/cacheMiddleware"); +jest.mock("@/utilities/cacheUtils"); +jest.mock("@/modules/shared/services/redisClient", () => ({ + __esModule: true, + default: { + on: jest.fn(), + get: jest.fn(), + setex: jest.fn(), + del: jest.fn(), + keys: jest.fn().mockResolvedValue([]), + }, +})); +describe("Location Controller", () => { + let mockRequest; + let mockResponse; + let mockNext; + let mockLocationService; + beforeEach(() => { + mockRequest = { body: {}, query: {}, params: {} }; + mockResponse = { + json: jest.fn(), + status: jest.fn().mockReturnThis(), + }; + mockNext = jest.fn(); + mockLocationService = { + getAllLocations: jest.fn(), + getLocationById: jest.fn(), + }; + locationService_1.LocationService.mockImplementation(() => mockLocationService); + cacheMiddleware_1.cacheMiddleware.mockImplementation(() => (req, res, next) => next()); + }); + const testControllerMethod = (methodName) => __awaiter(void 0, void 0, void 0, function* () { + const controllerMethod = locationController[methodName]; + if (Array.isArray(controllerMethod)) { + for (const middleware of controllerMethod) { + if (typeof middleware === 'function') { + yield middleware(mockRequest, mockResponse, mockNext); + } + } + } + else if (typeof controllerMethod === 'function') { + yield controllerMethod(mockRequest, mockResponse, mockNext); + } + else { + throw new Error(`Controller method ${methodName} not found or not a function`); + } + expect(response_1.sendResponse).toHaveBeenCalled(); + }); + it("should handle getAllLocations", () => __awaiter(void 0, void 0, void 0, function* () { + const mockAPIResponse = { + success: true, + code: 200, + data: [], + }; + mockLocationService.getAllLocations.mockResolvedValue(mockAPIResponse); + yield testControllerMethod("getAllLocations"); + })); + it("should handle getLocationById", () => __awaiter(void 0, void 0, void 0, function* () { + const mockAPIResponse = { + success: true, + code: 200, + data: { + location_id: 1, + province: "Test Province", + city: "Test City", + suburb: "Test Suburb", + district: "Test", + place_id: "4", + latitude: "0", + longitude: "0", + }, + }; + mockLocationService.getLocationById.mockResolvedValue(mockAPIResponse); + mockRequest.params = { id: "1" }; + yield testControllerMethod("getLocationById"); + })); + it("should handle errors in getAllLocations", () => __awaiter(void 0, void 0, void 0, function* () { + mockLocationService.getAllLocations.mockRejectedValue(new Error("Test error")); + yield testControllerMethod("getAllLocations"); + })); + it("should handle errors in getLocationById", () => __awaiter(void 0, void 0, void 0, function* () { + mockLocationService.getLocationById.mockRejectedValue(new Error("Test error")); + mockRequest.params = { id: "1" }; + yield testControllerMethod("getLocationById"); + })); +}); diff --git a/backend/public/__tests__/controllers/reactionController.test.js b/backend/public/__tests__/controllers/reactionController.test.js new file mode 100644 index 00000000..ed2ae96b --- /dev/null +++ b/backend/public/__tests__/controllers/reactionController.test.js @@ -0,0 +1,49 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const reactionController_1 = __importDefault(require("@/modules/reactions/controllers/reactionController")); +const reactionService_1 = __importDefault(require("@/modules/reactions/services/reactionService")); +const response_1 = require("@/utilities/response"); +jest.mock("@/modules/reactions/services/reactionService"); +jest.mock("@/utilities/response"); +jest.mock("@/modules/shared/services/redisClient", () => ({ + __esModule: true, + default: { + on: jest.fn(), + get: jest.fn(), + setex: jest.fn(), + del: jest.fn(), + keys: jest.fn().mockResolvedValue([]), + }, +})); +describe("Reaction Controller", () => { + let mockRequest; + let mockResponse; + let mockReactionService; + beforeEach(() => { + mockRequest = { body: {} }; + mockResponse = { json: jest.fn(), status: jest.fn().mockReturnThis() }; + mockReactionService = new reactionService_1.default(); + reactionService_1.default.mockImplementation(() => mockReactionService); + }); + it("should handle addOrRemoveReaction", () => __awaiter(void 0, void 0, void 0, function* () { + yield reactionController_1.default.addOrRemoveReaction(mockRequest, mockResponse); + expect(response_1.sendResponse).toHaveBeenCalled(); + })); + it("should handle errors in addOrRemoveReaction", () => __awaiter(void 0, void 0, void 0, function* () { + mockReactionService.addOrRemoveReaction.mockRejectedValue(new Error("Test error")); + yield reactionController_1.default.addOrRemoveReaction(mockRequest, mockResponse); + expect(response_1.sendResponse).toHaveBeenCalled(); + })); +}); diff --git a/backend/public/__tests__/controllers/reportsController.test.js b/backend/public/__tests__/controllers/reportsController.test.js new file mode 100644 index 00000000..7b834c08 --- /dev/null +++ b/backend/public/__tests__/controllers/reportsController.test.js @@ -0,0 +1,109 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const reportsController = __importStar(require("@/modules/reports/controllers/reportsController")); +const reportsService_1 = __importDefault(require("@/modules/reports/services/reportsService")); +const response_1 = require("@/utilities/response"); +const cacheMiddleware_1 = require("@/middleware/cacheMiddleware"); +jest.mock("@/modules/reports/services/reportsService"); +jest.mock("@/utilities/response"); +jest.mock("@/middleware/cacheMiddleware"); +jest.mock("@/utilities/cacheUtils"); +jest.mock("@/modules/shared/services/redisClient", () => ({ + __esModule: true, + default: { + on: jest.fn(), + get: jest.fn(), + setex: jest.fn(), + del: jest.fn(), + keys: jest.fn().mockResolvedValue([]), + }, +})); +describe("Reports Controller", () => { + let mockRequest; + let mockResponse; + let mockNext; + let mockReportsService; + beforeEach(() => { + mockRequest = { body: {}, query: {} }; + mockResponse = { + json: jest.fn(), + status: jest.fn().mockReturnThis(), + }; + mockNext = jest.fn(); + mockReportsService = { + getAllIssuesGroupedByResolutionStatus: jest.fn(), + getIssueCountsGroupedByResolutionStatus: jest.fn(), + getIssueCountsGroupedByResolutionAndCategory: jest.fn(), + getIssuesGroupedByCreatedAt: jest.fn(), + getIssuesGroupedByCategory: jest.fn(), + getIssuesCountGroupedByCategoryAndCreatedAt: jest.fn(), + groupedByPoliticalAssociation: jest.fn(), + }; + reportsService_1.default.mockImplementation(() => mockReportsService); + cacheMiddleware_1.cacheMiddleware.mockImplementation(() => (req, res, next) => next()); + }); + const testControllerMethod = (methodName) => __awaiter(void 0, void 0, void 0, function* () { + const controllerMethod = reportsController[methodName]; + if (Array.isArray(controllerMethod)) { + for (const middleware of controllerMethod) { + if (typeof middleware === 'function') { + yield middleware(mockRequest, mockResponse, mockNext); + } + } + } + else if (typeof controllerMethod === 'function') { + yield controllerMethod(mockRequest, mockResponse, mockNext); + } + else { + throw new Error(`Controller method ${methodName} not found or not a function`); + } + expect(response_1.sendResponse).toHaveBeenCalled(); + }); + it("should handle getAllIssuesGroupedByResolutionStatus", () => testControllerMethod("getAllIssuesGroupedByResolutionStatus")); + it("should handle getIssueCountsGroupedByResolutionStatus", () => testControllerMethod("getIssueCountsGroupedByResolutionStatus")); + it("should handle getIssueCountsGroupedByResolutionAndCategory", () => testControllerMethod("getIssueCountsGroupedByResolutionAndCategory")); + it("should handle getIssuesGroupedByCreatedAt", () => testControllerMethod("getIssuesGroupedByCreatedAt")); + it("should handle getIssuesGroupedByCategory", () => testControllerMethod("getIssuesGroupedByCategory")); + it("should handle getIssuesCountGroupedByCategoryAndCreatedAt", () => testControllerMethod("getIssuesCountGroupedByCategoryAndCreatedAt")); + it("should handle groupedByPoliticalAssociation", () => testControllerMethod("groupedByPoliticalAssociation")); + it("should handle errors", () => __awaiter(void 0, void 0, void 0, function* () { + mockReportsService.getAllIssuesGroupedByResolutionStatus.mockRejectedValue(new Error("Test error")); + yield testControllerMethod("getAllIssuesGroupedByResolutionStatus"); + expect(response_1.sendResponse).toHaveBeenCalled(); + })); +}); diff --git a/backend/public/__tests__/controllers/subscriptionsController.test.js b/backend/public/__tests__/controllers/subscriptionsController.test.js new file mode 100644 index 00000000..d4faa129 --- /dev/null +++ b/backend/public/__tests__/controllers/subscriptionsController.test.js @@ -0,0 +1,139 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const response_1 = require("@/utilities/response"); +const subscriptionsService_1 = __importDefault(require("@/modules/subscriptions/services/subscriptionsService")); +const subscriptionsController = __importStar(require("@/modules/subscriptions/controllers/subscriptionsController")); +jest.mock("@/utilities/response"); +jest.mock("@/modules/subscriptions/services/subscriptionsService"); +jest.mock("@/modules/shared/services/redisClient", () => ({ + __esModule: true, + default: { + on: jest.fn(), + get: jest.fn(), + setex: jest.fn(), + del: jest.fn(), + keys: jest.fn().mockResolvedValue([]), + }, +})); +const mockSubscriptionsService = subscriptionsService_1.default; +describe("Subscriptions Controller", () => { + let req; + let res; + beforeEach(() => { + req = { + body: {}, + }; + res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + jest.clearAllMocks(); + }); + describe("issueSubscriptions", () => { + it("should send a successful response", () => __awaiter(void 0, void 0, void 0, function* () { + const mockResponse = { success: true, code: 200, data: "Issue subscription successful" }; + mockSubscriptionsService.prototype.issueSubscriptions.mockResolvedValue(mockResponse); + yield subscriptionsController.issueSubscriptions(req, res); + expect(mockSubscriptionsService.prototype.issueSubscriptions).toHaveBeenCalledWith(req.body); + expect(response_1.sendResponse).toHaveBeenCalledWith(res, mockResponse); + })); + it("should send an error response", () => __awaiter(void 0, void 0, void 0, function* () { + const mockError = { success: false, error: "Issue subscription failed" }; + mockSubscriptionsService.prototype.issueSubscriptions.mockRejectedValue(mockError); + yield subscriptionsController.issueSubscriptions(req, res); + expect(mockSubscriptionsService.prototype.issueSubscriptions).toHaveBeenCalledWith(req.body); + expect(response_1.sendResponse).toHaveBeenCalledWith(res, mockError); + })); + }); + describe("categorySubscriptions", () => { + it("should send a successful response", () => __awaiter(void 0, void 0, void 0, function* () { + const mockResponse = { success: true, code: 200, data: "Category subscription successful" }; + mockSubscriptionsService.prototype.categorySubscriptions.mockResolvedValue(mockResponse); + yield subscriptionsController.categorySubscriptions(req, res); + expect(mockSubscriptionsService.prototype.categorySubscriptions).toHaveBeenCalledWith(req.body); + expect(response_1.sendResponse).toHaveBeenCalledWith(res, mockResponse); + })); + it("should send an error response", () => __awaiter(void 0, void 0, void 0, function* () { + const mockError = { success: false, error: "Category subscription failed" }; + mockSubscriptionsService.prototype.categorySubscriptions.mockRejectedValue(mockError); + yield subscriptionsController.categorySubscriptions(req, res); + expect(mockSubscriptionsService.prototype.categorySubscriptions).toHaveBeenCalledWith(req.body); + expect(response_1.sendResponse).toHaveBeenCalledWith(res, mockError); + })); + }); + describe("locationSubscriptions", () => { + it("should send a successful response", () => __awaiter(void 0, void 0, void 0, function* () { + const mockResponse = { success: true, code: 200, data: "Location subscription successful" }; + mockSubscriptionsService.prototype.locationSubscriptions.mockResolvedValue(mockResponse); + yield subscriptionsController.locationSubscriptions(req, res); + expect(mockSubscriptionsService.prototype.locationSubscriptions).toHaveBeenCalledWith(req.body); + expect(response_1.sendResponse).toHaveBeenCalledWith(res, mockResponse); + })); + it("should send an error response", () => __awaiter(void 0, void 0, void 0, function* () { + const mockError = { success: false, error: "Location subscription failed" }; + mockSubscriptionsService.prototype.locationSubscriptions.mockRejectedValue(mockError); + yield subscriptionsController.locationSubscriptions(req, res); + expect(mockSubscriptionsService.prototype.locationSubscriptions).toHaveBeenCalledWith(req.body); + expect(response_1.sendResponse).toHaveBeenCalledWith(res, mockError); + })); + }); + describe("getSubscriptions", () => { + it("should send a successful response", () => __awaiter(void 0, void 0, void 0, function* () { + const mockResponse = { + success: true, + code: 200, + data: { + issues: [], + categories: [], + locations: [], + } + }; + mockSubscriptionsService.prototype.getSubscriptions.mockResolvedValue(mockResponse); + yield subscriptionsController.getSubscriptions(req, res); + expect(mockSubscriptionsService.prototype.getSubscriptions).toHaveBeenCalledWith(req.body); + expect(response_1.sendResponse).toHaveBeenCalledWith(res, mockResponse); + })); + it("should send an error response", () => __awaiter(void 0, void 0, void 0, function* () { + const mockError = { success: false, error: "Get subscriptions failed" }; + mockSubscriptionsService.prototype.getSubscriptions.mockRejectedValue(mockError); + yield subscriptionsController.getSubscriptions(req, res); + expect(mockSubscriptionsService.prototype.getSubscriptions).toHaveBeenCalledWith(req.body); + expect(response_1.sendResponse).toHaveBeenCalledWith(res, mockError); + })); + }); +}); diff --git a/backend/public/__tests__/controllers/userController.test.js b/backend/public/__tests__/controllers/userController.test.js new file mode 100644 index 00000000..0ab28edb --- /dev/null +++ b/backend/public/__tests__/controllers/userController.test.js @@ -0,0 +1,93 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const userController = __importStar(require("@/modules/users/controllers/userController")); +const userService_1 = require("@/modules/users/services/userService"); +const response_1 = require("@/utilities/response"); +jest.mock("@/modules/users/services/userService"); +jest.mock("@/utilities/response"); +jest.mock("@/modules/shared/services/redisClient", () => ({ + __esModule: true, + default: { + on: jest.fn(), + get: jest.fn(), + setex: jest.fn(), + del: jest.fn(), + keys: jest.fn().mockResolvedValue([]), + }, +})); +describe("User Controller", () => { + let mockRequest; + let mockResponse; + let mockNext; + let mockUserService; + beforeEach(() => { + mockRequest = { body: {}, params: {}, query: {}, file: {} }; + mockResponse = { + json: jest.fn(), + status: jest.fn().mockReturnThis(), + }; + mockNext = jest.fn(); + mockUserService = { + getUserById: jest.fn(), + updateUserProfile: jest.fn(), + }; + userService_1.UserService.mockImplementation(() => mockUserService); + }); + const testControllerMethod = (methodName) => __awaiter(void 0, void 0, void 0, function* () { + const controllerMethod = userController[methodName]; + if (Array.isArray(controllerMethod)) { + const lastMiddleware = controllerMethod[controllerMethod.length - 1]; + if (typeof lastMiddleware === 'function') { + yield lastMiddleware(mockRequest, mockResponse, mockNext); + } + } + else if (typeof controllerMethod === 'function') { + yield controllerMethod(mockRequest, mockResponse); + } + expect(response_1.sendResponse).toHaveBeenCalled(); + }); + it("should handle getUserById", () => testControllerMethod("getUserById")); + it("should handle updateUserProfile", () => testControllerMethod("updateUserProfile")); + it("should handle errors in getUserById", () => __awaiter(void 0, void 0, void 0, function* () { + mockUserService.getUserById.mockRejectedValue(new Error("Test error")); + yield testControllerMethod("getUserById"); + expect(response_1.sendResponse).toHaveBeenCalled(); + })); + it("should handle errors in updateUserProfile", () => __awaiter(void 0, void 0, void 0, function* () { + mockUserService.updateUserProfile.mockRejectedValue(new Error("Test error")); + yield testControllerMethod("updateUserProfile"); + expect(response_1.sendResponse).toHaveBeenCalled(); + })); +}); diff --git a/backend/public/__tests__/controllers/visualizationController.test.js b/backend/public/__tests__/controllers/visualizationController.test.js new file mode 100644 index 00000000..c2dff866 --- /dev/null +++ b/backend/public/__tests__/controllers/visualizationController.test.js @@ -0,0 +1,75 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const visualizationService_1 = require("@/modules/visualizations/services/visualizationService"); +const response_1 = require("@/utilities/response"); +const visualizationController = __importStar(require("@/modules/visualizations/controllers/visualizationController")); +jest.mock("@/modules/visualizations/services/visualizationService"); +jest.mock("@/utilities/response"); +jest.mock("@/modules/shared/services/redisClient", () => ({ + __esModule: true, + default: { + on: jest.fn(), + get: jest.fn(), + setex: jest.fn(), + del: jest.fn(), + keys: jest.fn().mockResolvedValue([]), + }, +})); +describe("Visualization Controller", () => { + let mockRequest; + let mockResponse; + let mockVisualizationService; + beforeEach(() => { + mockRequest = {}; + mockResponse = { json: jest.fn() }; + mockVisualizationService = { + getVizData: jest.fn(), + }; + visualizationService_1.VisualizationService.mockImplementation(() => mockVisualizationService); + jest + .spyOn(visualizationService_1.VisualizationService.prototype, "getVizData") + .mockImplementation(mockVisualizationService.getVizData); + }); + it("should call sendResponse for getVizData", () => __awaiter(void 0, void 0, void 0, function* () { + yield visualizationController.getVizData(mockRequest, mockResponse); + expect(response_1.sendResponse).toHaveBeenCalled(); + })); + it("should handle errors in getVizData", () => __awaiter(void 0, void 0, void 0, function* () { + const error = new Error("Test error"); + mockVisualizationService.getVizData.mockRejectedValue(error); + yield visualizationController.getVizData(mockRequest, mockResponse); + expect(response_1.sendResponse).toHaveBeenCalledWith(mockResponse, expect.any(Error)); + })); +}); diff --git a/backend/public/__tests__/middleware/middleware.test.js b/backend/public/__tests__/middleware/middleware.test.js new file mode 100644 index 00000000..5debd0d1 --- /dev/null +++ b/backend/public/__tests__/middleware/middleware.test.js @@ -0,0 +1,96 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const supertest_1 = __importDefault(require("supertest")); +const express_1 = __importDefault(require("express")); +const supabaseClient_1 = __importDefault(require("@/modules/shared/services/supabaseClient")); +const middleware_1 = require("@/middleware/middleware"); +const response_1 = require("@/utilities/response"); +jest.mock("@/modules/shared/services/supabaseClient"); +jest.mock("@/utilities/response"); +const app = (0, express_1.default)(); +app.use(express_1.default.json()); +app.use(middleware_1.serverMiddleare); +app.get("/test", middleware_1.verifyAndGetUser, (req, res) => { + res.status(200).json({ message: "success", user_id: req.body.user_id }); +}); +describe("Middleware", () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(console, "error").mockImplementation(() => { }); + process.env.SUPABASE_SERVICE_ROLE_KEY = "test-service-role-key"; + }); + afterEach(() => { + console.error.mockRestore(); + delete process.env.SUPABASE_SERVICE_ROLE_KEY; + }); + it("should call next() if no authorization header", () => __awaiter(void 0, void 0, void 0, function* () { + const response = yield (0, supertest_1.default)(app).get("/test"); + expect(response.status).toBe(200); + expect(response.body.message).toBe("success"); + expect(response.body.user_id).toBeUndefined(); + expect(supabaseClient_1.default.auth.getUser).not.toHaveBeenCalled(); + })); + it("should call next() if service role key is provided", () => __awaiter(void 0, void 0, void 0, function* () { + const response = yield (0, supertest_1.default)(app) + .get("/test") + .set("x-service-role-key", "test-service-role-key"); + expect(response.status).toBe(200); + expect(response.body.message).toBe("success"); + expect(supabaseClient_1.default.auth.getUser).not.toHaveBeenCalled(); + })); + it("should call next() if token is valid", () => __awaiter(void 0, void 0, void 0, function* () { + const mockUser = { id: "123" }; + supabaseClient_1.default.auth.getUser.mockResolvedValue({ + data: { user: mockUser }, + error: null, + }); + const response = yield (0, supertest_1.default)(app) + .get("/test") + .set("Authorization", "Bearer validtoken"); + expect(response.status).toBe(200); + expect(response.body.message).toBe("success"); + expect(response.body.user_id).toBe("123"); + expect(supabaseClient_1.default.auth.getUser).toHaveBeenCalledWith("validtoken"); + })); + it("should send 403 error response if token is invalid", () => __awaiter(void 0, void 0, void 0, function* () { + supabaseClient_1.default.auth.getUser.mockResolvedValue({ + data: { user: null }, + error: new Error("Invalid token"), + }); + response_1.sendResponse.mockImplementation((res, data) => { + res.status(data.code).json(data); + }); + const response = yield (0, supertest_1.default)(app) + .get("/test") + .set("Authorization", "Bearer invalidtoken"); + expect(response.status).toBe(403); + expect(response.body.success).toBe(false); + expect(response.body.error).toBe("Invalid token"); + expect(supabaseClient_1.default.auth.getUser).toHaveBeenCalledWith("invalidtoken"); + })); + it("should send 500 error response if an unexpected error occurs", () => __awaiter(void 0, void 0, void 0, function* () { + supabaseClient_1.default.auth.getUser.mockRejectedValue(new Error("Unexpected error")); + response_1.sendResponse.mockImplementation((res, data) => { + res.status(data.code).json(data); + }); + const response = yield (0, supertest_1.default)(app) + .get("/test") + .set("Authorization", "Bearer validtoken"); + expect(response.status).toBe(500); + expect(response.body.success).toBe(false); + expect(response.body.error).toBe("An unexpected error occurred. Please try again later."); + expect(supabaseClient_1.default.auth.getUser).toHaveBeenCalledWith("validtoken"); + })); +}); diff --git a/backend/public/__tests__/routes/commentRoutes.test.js b/backend/public/__tests__/routes/commentRoutes.test.js new file mode 100644 index 00000000..2a29ab35 --- /dev/null +++ b/backend/public/__tests__/routes/commentRoutes.test.js @@ -0,0 +1,88 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const supertest_1 = __importDefault(require("supertest")); +const express_1 = __importDefault(require("express")); +const commentRoutes_1 = __importDefault(require("@/modules/comments/routes/commentRoutes")); +const commentController = __importStar(require("@/modules/comments/controllers/commentController")); +const middleware_1 = require("@/middleware/middleware"); +jest.mock("@/middleware/middleware"); +jest.mock("@/modules/comments/controllers/commentController"); +const app = (0, express_1.default)(); +app.use(express_1.default.json()); +app.use("/comments", commentRoutes_1.default); +describe("Comments Routes", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + describe("POST /comments/", () => { + it("should call getComments controller", () => __awaiter(void 0, void 0, void 0, function* () { + middleware_1.verifyAndGetUser.mockImplementation((req, res, next) => next()); + commentController.getComments.mockImplementation((req, res) => res.status(200).json({})); + const response = yield (0, supertest_1.default)(app).post("/comments/").send(); + expect(response.status).toBe(200); + expect(commentController.getComments).toHaveBeenCalled(); + })); + }); + describe("POST /comments/count", () => { + it("should call getNumComments controller", () => __awaiter(void 0, void 0, void 0, function* () { + middleware_1.verifyAndGetUser.mockImplementation((req, res, next) => next()); + commentController.getNumComments.mockImplementation((req, res) => res.status(200).json({})); + const response = yield (0, supertest_1.default)(app).post("/comments/count").send(); + expect(response.status).toBe(200); + expect(commentController.getNumComments).toHaveBeenCalled(); + })); + }); + describe("POST /comments/add", () => { + it("should call addComment controller", () => __awaiter(void 0, void 0, void 0, function* () { + middleware_1.verifyAndGetUser.mockImplementation((req, res, next) => next()); + commentController.addComment.mockImplementation((req, res) => res.status(200).json({})); + const response = yield (0, supertest_1.default)(app).post("/comments/add").send(); + expect(response.status).toBe(200); + expect(commentController.addComment).toHaveBeenCalled(); + })); + }); + describe("DELETE /comments/delete", () => { + it("should call deleteComment controller", () => __awaiter(void 0, void 0, void 0, function* () { + middleware_1.verifyAndGetUser.mockImplementation((req, res, next) => next()); + commentController.deleteComment.mockImplementation((req, res) => res.status(200).json({})); + const response = yield (0, supertest_1.default)(app).delete("/comments/delete").send(); + expect(response.status).toBe(200); + expect(commentController.deleteComment).toHaveBeenCalled(); + })); + }); +}); diff --git a/backend/public/__tests__/routes/issueRoutes.test.js b/backend/public/__tests__/routes/issueRoutes.test.js new file mode 100644 index 00000000..a477e0a9 --- /dev/null +++ b/backend/public/__tests__/routes/issueRoutes.test.js @@ -0,0 +1,72 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const supertest_1 = __importDefault(require("supertest")); +const express_1 = __importDefault(require("express")); +const issueRoutes_1 = __importDefault(require("@/modules/issues/routes/issueRoutes")); +const issueController = __importStar(require("@/modules/issues/controllers/issueController")); +const middleware_1 = require("@/middleware/middleware"); +jest.mock("@/middleware/middleware"); +jest.mock("@/modules/issues/controllers/issueController", () => ({ + getIssues: [jest.fn()], + getIssueById: [jest.fn()], + createIssue: [jest.fn()], + updateIssue: [jest.fn()], + deleteIssue: [jest.fn()], + getUserIssues: [jest.fn()], + getUserResolvedIssues: [jest.fn()], + createSelfResolution: [jest.fn()], + createExternalResolution: [jest.fn()], + respondToResolution: [jest.fn()], + getUserResolutions: [jest.fn()], + deleteResolution: [jest.fn()], +})); +const app = (0, express_1.default)(); +app.use(express_1.default.json()); +app.use("/issues", issueRoutes_1.default); +describe("Issue Routes", () => { + beforeEach(() => { + jest.clearAllMocks(); + middleware_1.verifyAndGetUser.mockImplementation((req, res, next) => next()); + }); + it("should call getIssues controller", () => __awaiter(void 0, void 0, void 0, function* () { + issueController.getIssues[0].mockImplementation((req, res) => res.status(200).json({})); + const response = yield (0, supertest_1.default)(app).post("/issues"); + expect(response.status).toBe(200); + expect(issueController.getIssues[0]).toHaveBeenCalled(); + })); +}); diff --git a/backend/public/__tests__/routes/locationRoutes.test.js b/backend/public/__tests__/routes/locationRoutes.test.js new file mode 100644 index 00000000..d40af2a9 --- /dev/null +++ b/backend/public/__tests__/routes/locationRoutes.test.js @@ -0,0 +1,61 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const supertest_1 = __importDefault(require("supertest")); +const express_1 = __importDefault(require("express")); +const locationRoutes_1 = __importDefault(require("@/modules/locations/routes/locationRoutes")); +const locationController = __importStar(require("@/modules/locations/controllers/locationController")); +jest.mock("@/modules/locations/controllers/locationController", () => ({ + getAllLocations: [jest.fn()], + getLocationById: [jest.fn()], +})); +const app = (0, express_1.default)(); +app.use(express_1.default.json()); +app.use("/locations", locationRoutes_1.default); +describe("Location Routes", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + describe("POST /locations/", () => { + it("should call getAllLocations controller", () => __awaiter(void 0, void 0, void 0, function* () { + locationController.getAllLocations[0].mockImplementation((req, res) => res.status(200).json({})); + const response = yield (0, supertest_1.default)(app).post("/locations/"); + expect(response.status).toBe(200); + expect(locationController.getAllLocations[0]).toHaveBeenCalled(); + })); + }); +}); diff --git a/backend/public/__tests__/routes/reactionRoutes.test.js b/backend/public/__tests__/routes/reactionRoutes.test.js new file mode 100644 index 00000000..1461b9f8 --- /dev/null +++ b/backend/public/__tests__/routes/reactionRoutes.test.js @@ -0,0 +1,38 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const supertest_1 = __importDefault(require("supertest")); +const express_1 = __importDefault(require("express")); +const reactionRoutes_1 = __importDefault(require("@/modules/reactions/routes/reactionRoutes")); +const reactionController_1 = __importDefault(require("@/modules/reactions/controllers/reactionController")); +const middleware_1 = require("@/middleware/middleware"); +jest.mock("@/middleware/middleware"); +jest.mock("@/modules/reactions/controllers/reactionController"); +const app = (0, express_1.default)(); +app.use(express_1.default.json()); +app.use("/reactions", reactionRoutes_1.default); +describe("Reaction Routes", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + describe("POST /reactions/", () => { + it("should call addOrRemoveReaction controller", () => __awaiter(void 0, void 0, void 0, function* () { + middleware_1.verifyAndGetUser.mockImplementation((req, res, next) => next()); + reactionController_1.default.addOrRemoveReaction.mockImplementation((req, res) => res.status(200).json({})); + const response = yield (0, supertest_1.default)(app).post("/reactions/").send(); + expect(response.status).toBe(200); + expect(reactionController_1.default.addOrRemoveReaction).toHaveBeenCalled(); + })); + }); +}); diff --git a/backend/public/__tests__/routes/reportsRoutes.test.js b/backend/public/__tests__/routes/reportsRoutes.test.js new file mode 100644 index 00000000..2a60d161 --- /dev/null +++ b/backend/public/__tests__/routes/reportsRoutes.test.js @@ -0,0 +1,67 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const supertest_1 = __importDefault(require("supertest")); +const express_1 = __importDefault(require("express")); +const reportsRoutes_1 = __importDefault(require("@/modules/reports/routes/reportsRoutes")); +const reportsController = __importStar(require("@/modules/reports/controllers/reportsController")); +const middleware_1 = require("@/middleware/middleware"); +jest.mock("@/middleware/middleware"); +jest.mock("@/modules/reports/controllers/reportsController", () => ({ + getAllIssuesGroupedByResolutionStatus: [jest.fn()], + getIssueCountsGroupedByResolutionStatus: [jest.fn()], + getIssueCountsGroupedByResolutionAndCategory: [jest.fn()], + getIssuesGroupedByCreatedAt: [jest.fn()], + getIssuesGroupedByCategory: [jest.fn()], + getIssuesCountGroupedByCategoryAndCreatedAt: [jest.fn()], + groupedByPoliticalAssociation: [jest.fn()], +})); +const app = (0, express_1.default)(); +app.use(express_1.default.json()); +app.use("/reports", reportsRoutes_1.default); +describe("Reports Routes", () => { + beforeEach(() => { + jest.clearAllMocks(); + middleware_1.verifyAndGetUser.mockImplementation((req, res, next) => next()); + }); + it("should call getAllIssuesGroupedByResolutionStatus controller", () => __awaiter(void 0, void 0, void 0, function* () { + reportsController.getAllIssuesGroupedByResolutionStatus[0].mockImplementation((req, res) => res.status(200).json({})); + const response = yield (0, supertest_1.default)(app).post("/reports/groupedResolutionStatus"); + expect(response.status).toBe(200); + expect(reportsController.getAllIssuesGroupedByResolutionStatus[0]).toHaveBeenCalled(); + })); +}); diff --git a/backend/public/__tests__/routes/subscriptionsRoutes.test.js b/backend/public/__tests__/routes/subscriptionsRoutes.test.js new file mode 100644 index 00000000..779edc24 --- /dev/null +++ b/backend/public/__tests__/routes/subscriptionsRoutes.test.js @@ -0,0 +1,92 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = __importDefault(require("express")); +const supertest_1 = __importDefault(require("supertest")); +const subscriptionsRoutes_1 = __importDefault(require("@/modules/subscriptions/routes/subscriptionsRoutes")); +const subscriptionsController = __importStar(require("@/modules/subscriptions/controllers/subscriptionsController")); +const middleware_1 = require("@/middleware/middleware"); +const app = (0, express_1.default)(); +app.use(express_1.default.json()); +app.use("/subscriptions", subscriptionsRoutes_1.default); +jest.mock("@/middleware/middleware"); +jest.mock("@/modules/subscriptions/controllers/subscriptionsController"); +describe("Subscription Routes", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + describe("POST /subscriptions/issue", () => { + it("should call issueSubscriptions controller", () => __awaiter(void 0, void 0, void 0, function* () { + middleware_1.verifyAndGetUser.mockImplementation((req, res, next) => next()); + subscriptionsController.issueSubscriptions.mockImplementation((req, res) => res.status(200).json({ message: "Issue subscription successful" })); + const response = yield (0, supertest_1.default)(app).post("/subscriptions/issue").send(); + expect(response.status).toBe(200); + expect(response.body.message).toBe("Issue subscription successful"); + expect(subscriptionsController.issueSubscriptions).toHaveBeenCalled(); + })); + }); + describe("POST /subscriptions/category", () => { + it("should call categorySubscriptions controller", () => __awaiter(void 0, void 0, void 0, function* () { + middleware_1.verifyAndGetUser.mockImplementation((req, res, next) => next()); + subscriptionsController.categorySubscriptions.mockImplementation((req, res) => res.status(200).json({ message: "Category subscription successful" })); + const response = yield (0, supertest_1.default)(app).post("/subscriptions/category").send(); + expect(response.status).toBe(200); + expect(response.body.message).toBe("Category subscription successful"); + expect(subscriptionsController.categorySubscriptions).toHaveBeenCalled(); + })); + }); + describe("POST /subscriptions/location", () => { + it("should call locationSubscriptions controller", () => __awaiter(void 0, void 0, void 0, function* () { + middleware_1.verifyAndGetUser.mockImplementation((req, res, next) => next()); + subscriptionsController.locationSubscriptions.mockImplementation((req, res) => res.status(200).json({ message: "Location subscription successful" })); + const response = yield (0, supertest_1.default)(app).post("/subscriptions/location").send(); + expect(response.status).toBe(200); + expect(response.body.message).toBe("Location subscription successful"); + expect(subscriptionsController.locationSubscriptions).toHaveBeenCalled(); + })); + }); + describe("POST /subscriptions/subscriptions", () => { + it("should call getSubscriptions controller", () => __awaiter(void 0, void 0, void 0, function* () { + middleware_1.verifyAndGetUser.mockImplementation((req, res, next) => next()); + subscriptionsController.getSubscriptions.mockImplementation((req, res) => res.status(200).json({ message: "Get subscriptions successful" })); + const response = yield (0, supertest_1.default)(app).post("/subscriptions/subscriptions").send(); + expect(response.status).toBe(200); + expect(response.body.message).toBe("Get subscriptions successful"); + expect(subscriptionsController.getSubscriptions).toHaveBeenCalled(); + })); + }); +}); diff --git a/backend/public/__tests__/routes/userRoutes.test.js b/backend/public/__tests__/routes/userRoutes.test.js new file mode 100644 index 00000000..f3b5fcf6 --- /dev/null +++ b/backend/public/__tests__/routes/userRoutes.test.js @@ -0,0 +1,94 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const supertest_1 = __importDefault(require("supertest")); +const express_1 = __importDefault(require("express")); +const userRoutes_1 = __importDefault(require("@/modules/users/routes/userRoutes")); +const middleware_1 = require("@/middleware/middleware"); +const userController = __importStar(require("@/modules/users/controllers/userController")); +// Mock the middleware and controllers +jest.mock("@/middleware/middleware"); +jest.mock("@/modules/users/controllers/userController", () => ({ + getUserById: [jest.fn()], + updateUserProfile: jest.fn(), + updateUsername: jest.fn(), + changePassword: jest.fn(), +})); +const app = (0, express_1.default)(); +app.use(express_1.default.json()); +app.use("/users", userRoutes_1.default); +describe("User Routes", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + describe("GET /users/:id", () => { + it("should call getUserById controller", () => __awaiter(void 0, void 0, void 0, function* () { + middleware_1.verifyAndGetUser.mockImplementation((req, res, next) => next()); + userController.getUserById[0].mockImplementation((req, res) => res.status(200).json({})); + const response = yield (0, supertest_1.default)(app).post("/users/1").send(); + expect(response.status).toBe(200); + expect(userController.getUserById[0]).toHaveBeenCalled(); + })); + }); + describe("PUT /users/:id", () => { + it("should call updateUserProfile controller", () => __awaiter(void 0, void 0, void 0, function* () { + middleware_1.verifyAndGetUser.mockImplementation((req, res, next) => next()); + userController.updateUserProfile.mockImplementation((req, res) => res.status(200).json({})); + const response = yield (0, supertest_1.default)(app).put("/users/1").send(); + expect(response.status).toBe(200); + expect(userController.updateUserProfile).toHaveBeenCalled(); + })); + }); + describe("PUT /users/:id/username", () => { + it("should call updateUsername controller", () => __awaiter(void 0, void 0, void 0, function* () { + middleware_1.verifyAndGetUser.mockImplementation((req, res, next) => next()); + userController.updateUsername.mockImplementation((req, res) => res.status(200).json({})); + const response = yield (0, supertest_1.default)(app).put("/users/1/username").send({ username: "newUsername" }); + expect(response.status).toBe(200); + expect(userController.updateUsername).toHaveBeenCalled(); + })); + }); + describe("PUT /users/:id/password", () => { + it("should call changePassword controller", () => __awaiter(void 0, void 0, void 0, function* () { + middleware_1.verifyAndGetUser.mockImplementation((req, res, next) => next()); + userController.changePassword.mockImplementation((req, res) => res.status(200).json({})); + const response = yield (0, supertest_1.default)(app).put("/users/1/password").send({ password: "newPassword" }); + expect(response.status).toBe(200); + expect(userController.changePassword).toHaveBeenCalled(); + })); + }); +}); diff --git a/backend/public/__tests__/routes/visualizationRoutes.test.js b/backend/public/__tests__/routes/visualizationRoutes.test.js new file mode 100644 index 00000000..343c6f9d --- /dev/null +++ b/backend/public/__tests__/routes/visualizationRoutes.test.js @@ -0,0 +1,58 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const supertest_1 = __importDefault(require("supertest")); +const express_1 = __importDefault(require("express")); +const visualizationRoutes_1 = __importDefault(require("@/modules/visualizations/routes/visualizationRoutes")); +const visualizationController = __importStar(require("@/modules/visualizations/controllers/visualizationController")); +jest.mock("@/modules/visualizations/controllers/visualizationController"); +const app = (0, express_1.default)(); +app.use(express_1.default.json()); +app.use("/visualizations", visualizationRoutes_1.default); +describe("Visualization Routes", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + describe("POST /visualizations/", () => { + it("should call getVizData controller", () => __awaiter(void 0, void 0, void 0, function* () { + visualizationController.getVizData.mockImplementation((req, res) => res.status(200).json({})); + const response = yield (0, supertest_1.default)(app).post("/visualizations/").send(); + expect(response.status).toBe(200); + expect(visualizationController.getVizData).toHaveBeenCalled(); + })); + }); +}); diff --git a/backend/public/__tests__/services/commentService.test.js b/backend/public/__tests__/services/commentService.test.js new file mode 100644 index 00000000..b599e775 --- /dev/null +++ b/backend/public/__tests__/services/commentService.test.js @@ -0,0 +1,198 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const commentService_1 = require("@/modules/comments/services/commentService"); +const commentRepository_1 = require("@/modules/comments/repositories/commentRepository"); +const response_1 = require("@/types/response"); +jest.mock("@/modules/comments/repositories/commentRepository"); +jest.mock("@/modules/points/services/pointsService"); +describe("CommentService", () => { + let commentService; + let commentRepository; + let mockPointsService; + beforeEach(() => { + commentRepository = new commentRepository_1.CommentRepository(); + mockPointsService = { + awardPoints: jest.fn().mockResolvedValue(100), + }; + commentService = new commentService_1.CommentService(); + commentService.setCommentRepository(commentRepository); + commentService.setPointsService(mockPointsService); + }); + describe("getNumComments", () => { + it("should return the number of comments for an issue", () => __awaiter(void 0, void 0, void 0, function* () { + const params = { + issue_id: 1, + }; + commentRepository.getNumComments.mockResolvedValue(10); + const response = yield commentService.getNumComments(params); + expect(response.data).toBe(10); + expect(commentRepository.getNumComments).toHaveBeenCalledWith(1, undefined); + expect(commentRepository.getNumComments).toHaveBeenCalledTimes(1); + })); + it("should throw an error when issue_id is missing", () => __awaiter(void 0, void 0, void 0, function* () { + const params = {}; + yield expect(commentService.getNumComments(params)).rejects.toEqual((0, response_1.APIError)({ + code: 400, + success: false, + error: "Missing required fields for getting number of comments", + })); + expect(commentRepository.getNumComments).not.toHaveBeenCalled(); + })); + }); + describe("getComments", () => { + it("should return comments for an issue", () => __awaiter(void 0, void 0, void 0, function* () { + const params = { + issue_id: 1, + from: 0, + amount: 10, + }; + const mockComments = [ + { + comment_id: 1, + issue_id: 1, + user_id: "1", + parent_id: null, + content: "Comment 1", + is_anonymous: false, + created_at: "2022-01-01", + user: { + user_id: "1", + email_address: "test@example.com", + username: "testuser", + fullname: "Test User", + image_url: "https://example.com/image.png", + is_owner: false, + total_issues: 10, + resolved_issues: 5, + user_score: 0, + location_id: null, + location: null, + }, + is_owner: false, + }, + ]; + commentRepository.getComments.mockResolvedValue(mockComments); + const response = yield commentService.getComments(params); + expect(response.data).toEqual(mockComments); + expect(commentRepository.getComments).toHaveBeenCalledWith(params); + expect(commentRepository.getComments).toHaveBeenCalledTimes(1); + })); + it("should throw an error when required fields are missing", () => __awaiter(void 0, void 0, void 0, function* () { + const params = { + issue_id: 1, + }; + yield expect(commentService.getComments(params)).rejects.toEqual((0, response_1.APIError)({ + code: 400, + success: false, + error: "Missing required fields for getting comments", + })); + expect(commentRepository.getComments).not.toHaveBeenCalled(); + })); + }); + describe("addComment", () => { + it("should add a new comment", () => __awaiter(void 0, void 0, void 0, function* () { + const newComment = { + issue_id: 1, + user_id: "1", + content: "New Comment", + is_anonymous: false, + }; + const addedComment = { + comment_id: 1, + issue_id: 1, + user_id: "1", + parent_id: null, + content: "New Comment", + is_anonymous: false, + created_at: "2022-01-01", + user: { + user_id: "1", + email_address: "test@example.com", + username: "testuser", + fullname: "Test User", + image_url: "https://example.com/image.png", + is_owner: true, + total_issues: 10, + resolved_issues: 5, + user_score: 0, + location_id: null, + location: null + }, + is_owner: true, + }; + commentRepository.addComment.mockResolvedValue(addedComment); + const response = yield commentService.addComment(newComment); + expect(mockPointsService.awardPoints).toHaveBeenCalledWith("1", 10, "Left a comment on an open issue"); + expect(response.data).toEqual(addedComment); + expect(commentRepository.addComment).toHaveBeenCalledWith(newComment); + expect(commentRepository.addComment).toHaveBeenCalledTimes(1); + })); + it("should throw an error when user_id is missing", () => __awaiter(void 0, void 0, void 0, function* () { + const newComment = { + issue_id: 1, + content: "New Comment", + is_anonymous: false, + }; + yield expect(commentService.addComment(newComment)).rejects.toEqual((0, response_1.APIError)({ + code: 401, + success: false, + error: "You need to be signed in to create a comment", + })); + expect(commentRepository.addComment).not.toHaveBeenCalled(); + })); + it("should throw an error when required fields are missing", () => __awaiter(void 0, void 0, void 0, function* () { + const newComment = { + user_id: "1", + }; + yield expect(commentService.addComment(newComment)).rejects.toEqual((0, response_1.APIError)({ + code: 400, + success: false, + error: "Missing required fields for creating a comment", + })); + expect(commentRepository.addComment).not.toHaveBeenCalled(); + })); + }); + describe("deleteComment", () => { + it("should delete a comment", () => __awaiter(void 0, void 0, void 0, function* () { + const deleteParams = { + comment_id: 1, + user_id: "1", + }; + commentRepository.deleteComment.mockResolvedValue(); + yield commentService.deleteComment(deleteParams); + expect(commentRepository.deleteComment).toHaveBeenCalledWith(1, "1"); + expect(commentRepository.deleteComment).toHaveBeenCalledTimes(1); + })); + it("should throw an error when user_id is missing", () => __awaiter(void 0, void 0, void 0, function* () { + const deleteParams = { + comment_id: 1, + }; + yield expect(commentService.deleteComment(deleteParams)).rejects.toEqual((0, response_1.APIError)({ + code: 401, + success: false, + error: "You need to be signed in to delete a comment", + })); + expect(commentRepository.deleteComment).not.toHaveBeenCalled(); + })); + it("should throw an error when comment_id is missing", () => __awaiter(void 0, void 0, void 0, function* () { + const deleteParams = { + user_id: "1", + }; + yield expect(commentService.deleteComment(deleteParams)).rejects.toEqual((0, response_1.APIError)({ + code: 400, + success: false, + error: "Missing required fields for deleting a comment", + })); + expect(commentRepository.deleteComment).not.toHaveBeenCalled(); + })); + }); +}); diff --git a/backend/public/__tests__/services/issueService.test.js b/backend/public/__tests__/services/issueService.test.js new file mode 100644 index 00000000..73188c0d --- /dev/null +++ b/backend/public/__tests__/services/issueService.test.js @@ -0,0 +1,407 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const issueService_1 = __importDefault(require("@/modules/issues/services/issueService")); +const issueRepository_1 = __importDefault(require("@/modules/issues/repositories/issueRepository")); +const locationRepository_1 = require("@/modules/locations/repositories/locationRepository"); +const response_1 = require("@/types/response"); +const openAIService_1 = require("@/modules/shared/services/openAIService"); +jest.mock("@/modules/issues/repositories/issueRepository"); +jest.mock("@/modules/locations/repositories/locationRepository"); +jest.mock("@/modules/points/services/pointsService"); +describe("IssueService", () => { + let issueService; + let issueRepository; + let locationRepository; + let mockPointsService; + let mockClusterService; + let mockOpenAIService; + beforeEach(() => { + issueRepository = new issueRepository_1.default(); + locationRepository = + new locationRepository_1.LocationRepository(); + issueService = new issueService_1.default(); + issueService.setIssueRepository(issueRepository); + issueService.setLocationRepository(locationRepository); + mockPointsService = { + awardPoints: jest.fn().mockResolvedValue(100), + getFirstTimeAction: jest.fn().mockResolvedValue(true), + }; + issueService.setPointsService(mockPointsService); + issueService.processIssueAsync = jest.fn().mockResolvedValue(undefined); + issueService.setClusterService(mockClusterService); + mockOpenAIService = new openAIService_1.OpenAIService(); + issueService.setOpenAIService(mockOpenAIService); + mockOpenAIService.getEmbedding = jest.fn().mockResolvedValue([0.1, 0.2, 0.3]); + }); + it("should get all issues", () => __awaiter(void 0, void 0, void 0, function* () { + const mockIssues = [ + { + issue_id: 1, + user_id: "1", + location_id: null, + location_data: null, + category_id: 1, + content: "Test Issue", + resolved_at: null, + is_anonymous: false, + created_at: "2024-06-01", + updated_at: "2024-06-01", + sentiment: "angry", + image_url: "https://example.com/image.png", + user: { + user_id: "1", + email_address: "test@example.com", + username: "testuser", + fullname: "Test User", + image_url: "https://example.com/image.png", + is_owner: true, + total_issues: 10, + resolved_issues: 5, + user_score: 0, + location_id: null, + location: null + }, + category: { + name: "Category 1", + }, + reactions: [], + user_reaction: null, + comment_count: 0, + is_owner: false, + profile_user_id: "0", + }, + ]; + issueRepository.getIssues.mockResolvedValue(mockIssues); + const response = yield issueService.getIssues({ from: 0, amount: 999 }); + expect(response.data).toEqual(mockIssues); + expect(issueRepository.getIssues).toHaveBeenCalledTimes(1); + })); + it("should get an issue by ID", () => __awaiter(void 0, void 0, void 0, function* () { + const mockIssue = { + issue_id: 1, + user_id: "1", + location_id: null, + location_data: null, + category_id: 1, + content: "Test Issue", + resolved_at: null, + is_anonymous: false, + created_at: "2022-01-01", + updated_at: "2022-01-01", + sentiment: "neutral", + image_url: "https://example.com/image.png", + user: { + user_id: "1", + email_address: "test@example.com", + username: "testuser", + fullname: "Test User", + image_url: "https://example.com/image.png", + is_owner: true, + total_issues: 10, + resolved_issues: 5, + user_score: 0, + location_id: null, + location: null + }, + category: { + name: "Category 1", + }, + reactions: [], + user_reaction: null, + comment_count: 0, + is_owner: false, + profile_user_id: "0", + }; + issueRepository.getIssueById.mockResolvedValue(mockIssue); + const response = yield issueService.getIssueById({ issue_id: 1 }); + expect(response.data).toEqual(mockIssue); + expect(issueRepository.getIssueById).toHaveBeenCalledWith(1, undefined); + expect(issueRepository.getIssueById).toHaveBeenCalledTimes(1); + })); + describe("createIssue", () => { + it("should create a new issue when all required fields are provided", () => __awaiter(void 0, void 0, void 0, function* () { + const newIssue = { + user_id: "1", + location_id: null, + location_data: { + province: "Province", + city: "City", + suburb: "Suburb", + district: "District", + place_id: "place_id", + }, + category_id: 1, + content: "New Issue", + resolved_at: null, + is_anonymous: false, + sentiment: "neutral", + image_url: null, + }; + const createdIssue = { + issue_id: 1, + user_id: "1", + location_id: null, + location_data: { + province: "Province", + city: "City", + suburb: "Suburb", + district: "District", + place_id: "place_id", + }, + category_id: 1, + content: "New Issue", + resolved_at: null, + is_anonymous: false, + created_at: "2022-01-01", + updated_at: "2022-01-01", + sentiment: "neutral", + image_url: "https://example.com/image.png", + user: { + user_id: "1", + email_address: "test@example.com", + username: "testuser", + fullname: "Test User", + image_url: "https://example.com/image.png", + is_owner: true, + total_issues: 10, + resolved_issues: 5, + user_score: 0, + location_id: null, + location: null + }, + category: { + name: "Category 1", + }, + reactions: [], + user_reaction: null, + comment_count: 0, + is_owner: true, + profile_user_id: "0", + }; + issueRepository.createIssue.mockResolvedValue(createdIssue); + jest.spyOn(issueService, "getIssueById").mockResolvedValue((0, response_1.APIData)({ + success: true, + code: 200, + data: createdIssue + })); + const response = yield issueService.createIssue(newIssue); + expect(response.data).toEqual(createdIssue); + expect(issueRepository.createIssue).toHaveBeenCalledWith(expect.objectContaining(newIssue)); + expect(issueRepository.createIssue).toHaveBeenCalledTimes(1); + // Check that processIssueAsync was called + expect(issueService.processIssueAsync).toHaveBeenCalledWith(createdIssue); + // Wait for any pending promises to resolve + yield new Promise(process.nextTick); + expect(mockPointsService.getFirstTimeAction).toHaveBeenCalledWith("1", "created first issue"); + expect(mockPointsService.awardPoints).toHaveBeenCalledWith("1", 50, "created first issue"); + expect(response.data).toEqual(createdIssue); + expect(issueRepository.createIssue).toHaveBeenCalledWith(newIssue); + expect(issueRepository.createIssue).toHaveBeenCalledTimes(1); + })); + it("should throw an error when required fields are missing", () => __awaiter(void 0, void 0, void 0, function* () { + const newIssue = { user_id: "1", content: "New Issue" }; + yield expect((() => __awaiter(void 0, void 0, void 0, function* () { + try { + yield issueService.createIssue(newIssue); + } + catch (error) { + throw new Error(error.error); + } + }))()).rejects.toThrow("Missing required fields for creating an issue"); + expect(issueRepository.createIssue).not.toHaveBeenCalled(); + })); + it("should throw an error when content exceeds the maximum length", () => __awaiter(void 0, void 0, void 0, function* () { + const newIssue = { + user_id: "1", + location_id: null, + location_data: { + province: "Province", + city: "City", + suburb: "Suburb", + district: "District", + place_id: "place_id", + }, + category_id: 1, + content: "A".repeat(501), + resolved_at: null, + is_anonymous: false, + sentiment: "neutral", + }; + yield expect((() => __awaiter(void 0, void 0, void 0, function* () { + try { + yield issueService.createIssue(newIssue); + } + catch (error) { + throw new Error(error.error); + } + }))()).rejects.toThrow("Issue content exceeds the maximum length of 500 characters"); + expect(issueRepository.createIssue).not.toHaveBeenCalled(); + })); + }); + it("should update an existing issue", () => __awaiter(void 0, void 0, void 0, function* () { + const updateData = { content: "Updated Issue" }; + const updatedIssue = { + issue_id: 1, + user_id: "1", + location_id: null, + location_data: null, + category_id: 1, + content: "Updated Issue", + resolved_at: null, + is_anonymous: false, + created_at: "2022-01-01", + updated_at: "2022-01-01", + sentiment: "neutral", + image_url: null, + user: { + user_id: "1", + email_address: "test@example.com", + username: "testuser", + fullname: "Test User", + image_url: "https://example.com/image.png", + is_owner: true, + total_issues: 10, + resolved_issues: 5, + user_score: 0, + location_id: null, + location: null + }, + category: { + name: "Category 1", + }, + reactions: [], + user_reaction: null, + comment_count: 0, + is_owner: true, + profile_user_id: "0", + }; + issueRepository.updateIssue.mockResolvedValue(updatedIssue); + const response = yield issueService.updateIssue(Object.assign({ issue_id: 1, user_id: "1" }, updateData)); + expect(response.data).toEqual(updatedIssue); + expect(issueRepository.updateIssue).toHaveBeenCalledWith(1, updateData, "1"); + expect(issueRepository.updateIssue).toHaveBeenCalledTimes(1); + })); + it("should delete an issue", () => __awaiter(void 0, void 0, void 0, function* () { + const mockIssue = { + issue_id: 1, + user_id: "1", + location_id: null, + location_data: null, + category_id: 1, + content: "Test Issue", + resolved_at: null, + is_anonymous: false, + created_at: "2022-01-01", + updated_at: "2022-01-01", + sentiment: "neutral", + image_url: "https://example.com/image.png", + user: { + user_id: "1", + email_address: "test@example.com", + username: "testuser", + fullname: "Test User", + image_url: "https://example.com/image.png", + is_owner: true, + total_issues: 10, + resolved_issues: 5, + user_score: 0, + location_id: null, + location: null + }, + category: { + name: "Category 1", + }, + reactions: [], + user_reaction: null, + comment_count: 0, + is_owner: true, + profile_user_id: "0", + }; + issueRepository.getIssueById.mockResolvedValue(mockIssue); + issueRepository.deleteIssue.mockResolvedValue(); + yield issueService.deleteIssue({ issue_id: 1, user_id: "1" }); + expect(issueRepository.getIssueById).toHaveBeenCalledWith(1, "1"); + expect(issueRepository.deleteIssue).toHaveBeenCalledWith(1, "1"); + expect(issueRepository.deleteIssue).toHaveBeenCalledTimes(1); + })); + it("should use existing location if it already exists", () => __awaiter(void 0, void 0, void 0, function* () { + const newIssue = { + user_id: "1", + location_id: 1, + location_data: { + province: "Province", + city: "City", + suburb: "Suburb", + district: "District", + place_id: "place_id", + }, + category_id: 1, + content: "New Issue", + is_anonymous: false, + sentiment: "neutral", + }; + const createdIssue = { + issue_id: 1, + user_id: "1", + location_id: 1, + location_data: { + province: "Province", + city: "City", + suburb: "Suburb", + district: "District", + place_id: "place_id", + }, + category_id: 1, + content: "New Issue", + is_anonymous: false, + sentiment: "neutral", + created_at: "2022-01-01", + updated_at: "2022-01-01", + user: { + user_id: "1", + email_address: "test@example.com", + username: "testuser", + fullname: "Test User", + image_url: "https://example.com/image.png", + is_owner: true, + total_issues: 10, + resolved_issues: 5, + user_score: 0, + location_id: null, + location: null + }, + category: { + name: "Category 1", + }, + reactions: [], + user_reaction: null, + comment_count: 0, + is_owner: true, + resolved_at: null, + image_url: "https://example.com/image.png", + profile_user_id: "0", + }; + issueRepository.createIssue.mockResolvedValue(createdIssue); + jest.spyOn(issueService, "getIssueById").mockResolvedValue((0, response_1.APIData)({ + success: true, + code: 200, + data: createdIssue + })); + const response = yield issueService.createIssue(newIssue); + expect(response.data).toEqual(createdIssue); + expect(locationRepository.createLocation).not.toHaveBeenCalled(); + expect(issueRepository.createIssue).toHaveBeenCalledTimes(1); + })); +}); diff --git a/backend/public/__tests__/services/locationService.test.js b/backend/public/__tests__/services/locationService.test.js new file mode 100644 index 00000000..e00eb022 --- /dev/null +++ b/backend/public/__tests__/services/locationService.test.js @@ -0,0 +1,45 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const locationService_1 = require("@/modules/locations/services/locationService"); +const locationRepository_1 = require("@/modules/locations/repositories/locationRepository"); +const response_1 = require("@/types/response"); +jest.mock("@/modules/locations/repositories/locationRepository"); +describe("LocationService", () => { + let locationService; + let locationRepositoryMock; + beforeEach(() => { + locationRepositoryMock = new locationRepository_1.LocationRepository(); + locationService = new locationService_1.LocationService(); + locationService["locationRepository"] = locationRepositoryMock; + }); + it("should return all locations with a 200 status code", () => __awaiter(void 0, void 0, void 0, function* () { + const mockLocations = [{ id: 1, name: "Location 1" }, { id: 2, name: "Location 2" }]; + locationRepositoryMock.getAllLocations.mockResolvedValue(mockLocations); + const result = yield locationService.getAllLocations(); + expect(result).toEqual((0, response_1.APIData)({ + code: 200, + success: true, + data: mockLocations, + })); + expect(locationRepositoryMock.getAllLocations).toHaveBeenCalled(); + })); + it("should handle empty locations array", () => __awaiter(void 0, void 0, void 0, function* () { + locationRepositoryMock.getAllLocations.mockResolvedValue([]); + const result = yield locationService.getAllLocations(); + expect(result).toEqual((0, response_1.APIData)({ + code: 200, + success: true, + data: [], + })); + expect(locationRepositoryMock.getAllLocations).toHaveBeenCalled(); + })); +}); diff --git a/backend/public/__tests__/services/reactionService.test.js b/backend/public/__tests__/services/reactionService.test.js new file mode 100644 index 00000000..b318328e --- /dev/null +++ b/backend/public/__tests__/services/reactionService.test.js @@ -0,0 +1,105 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const reactionService_1 = __importDefault(require("@/modules/reactions/services/reactionService")); +const reactionRepository_1 = __importDefault(require("@/modules/reactions/repositories/reactionRepository")); +jest.mock("@/modules/reactions/repositories/reactionRepository"); +jest.mock("@/modules/points/services/pointsService"); +describe("ReactionService", () => { + let reactionService; + let reactionRepository; + let mockPointsService; + beforeEach(() => { + reactionRepository = + new reactionRepository_1.default(); + reactionService = new reactionService_1.default(); + reactionService.setReactionRepository(reactionRepository); + mockPointsService = { + awardPoints: jest.fn().mockResolvedValue(100), + }; + reactionService.setPointsService(mockPointsService); + }); + describe("addOrRemoveReaction", () => { + it("should add a new reaction", () => __awaiter(void 0, void 0, void 0, function* () { + const newReaction = { + issue_id: 1, + user_id: "1", + emoji: "👍", + }; + const addedReaction = Object.assign(Object.assign({}, newReaction), { issue_id: 1, user_id: "1", reaction_id: 1, emoji: "👍", created_at: "2022-01-01" }); + reactionRepository.getReactionByUserAndIssue.mockResolvedValue(null); + reactionRepository.addReaction.mockResolvedValue(addedReaction); + const response = yield reactionService.addOrRemoveReaction(newReaction); + expect(mockPointsService.awardPoints).toHaveBeenCalledWith("1", 5, "reacted to an issue"); + expect(response.data).toEqual({ + added: "👍", + removed: undefined, + }); + expect(reactionRepository.getReactionByUserAndIssue).toHaveBeenCalledWith(1, "1"); + expect(reactionRepository.addReaction).toHaveBeenCalledWith(newReaction); + expect(reactionRepository.addReaction).toHaveBeenCalledTimes(1); + })); + it("should remove an existing reaction", () => __awaiter(void 0, void 0, void 0, function* () { + const reaction = { + issue_id: 1, + user_id: "1", + emoji: "👍", + }; + const existingReaction = Object.assign(Object.assign({}, reaction), { issue_id: 1, user_id: "1", reaction_id: 1, emoji: "👍", created_at: "2022-01-01" }); + reactionRepository.getReactionByUserAndIssue.mockResolvedValue(existingReaction); + reactionRepository.deleteReaction.mockResolvedValue(existingReaction); + const response = yield reactionService.addOrRemoveReaction(reaction); + expect(response.data).toEqual({ + added: undefined, + removed: "👍", + }); + expect(reactionRepository.getReactionByUserAndIssue).toHaveBeenCalledWith(1, "1"); + expect(reactionRepository.deleteReaction).toHaveBeenCalledWith(1, "1"); + expect(reactionRepository.deleteReaction).toHaveBeenCalledTimes(1); + })); + it("should throw an error when user_id is missing", () => __awaiter(void 0, void 0, void 0, function* () { + const reaction = { + issue_id: 1, + emoji: "👍", + }; + yield expect((() => __awaiter(void 0, void 0, void 0, function* () { + try { + yield reactionService.addOrRemoveReaction(reaction); + } + catch (error) { + throw new Error(error.error); + } + }))()).rejects.toThrow("You need to be signed in to react"); + expect(reactionRepository.getReactionByUserAndIssue).not.toHaveBeenCalled(); + expect(reactionRepository.addReaction).not.toHaveBeenCalled(); + expect(reactionRepository.deleteReaction).not.toHaveBeenCalled(); + })); + it("should throw an error when required fields are missing", () => __awaiter(void 0, void 0, void 0, function* () { + const reaction = { + user_id: "1", + }; + yield expect((() => __awaiter(void 0, void 0, void 0, function* () { + try { + yield reactionService.addOrRemoveReaction(reaction); + } + catch (error) { + throw new Error(error.error); + } + }))()).rejects.toThrow("Missing required fields for reacting"); + expect(reactionRepository.getReactionByUserAndIssue).not.toHaveBeenCalled(); + expect(reactionRepository.addReaction).not.toHaveBeenCalled(); + expect(reactionRepository.deleteReaction).not.toHaveBeenCalled(); + })); + }); +}); diff --git a/backend/public/__tests__/services/reportsService.test.js b/backend/public/__tests__/services/reportsService.test.js new file mode 100644 index 00000000..e8501cdb --- /dev/null +++ b/backend/public/__tests__/services/reportsService.test.js @@ -0,0 +1,251 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const reportsService_1 = __importDefault(require("@/modules/reports/services/reportsService")); +const reportsRepository_1 = __importDefault(require("@/modules/reports/repositories/reportsRepository")); +jest.mock("@/modules/reports/repositories/reportsRepository"); +describe("ReportsService", () => { + let reportsService; + let reportsRepository; + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(console, "error").mockImplementation(() => { }); + reportsRepository = + new reportsRepository_1.default(); + reportsService = new reportsService_1.default(); + reportsService.setReportsRepository(reportsRepository); + }); + afterEach(() => { + console.error.mockRestore(); + }); + describe("getAllIssuesGroupedByResolutionStatus", () => { + it("should return all issues grouped by resolution status", () => __awaiter(void 0, void 0, void 0, function* () { + const params = {}; + const mockData = { + resolved: [ + { + id: 1, + category: { + name: "Category for 2024-06-20", + }, + location: { + suburb: "Suburb for 2024-06-20", + city: "City for 2024-06-20", + province: "Province for 2024-06-20", + }, + }, + ], + unresolved: [ + { + id: 2, + category: { + name: "Category for 2024-06-19", + }, + location: { + suburb: "Suburb for 2024-06-19", + city: "City for 2024-06-19", + province: "Province for 2024-06-19", + }, + }, + ], + }; + reportsRepository.getAllIssuesGroupedByResolutionStatus.mockResolvedValue(mockData); + const response = yield reportsService.getAllIssuesGroupedByResolutionStatus(params); + expect(response.data).toEqual(mockData); + expect(reportsRepository.getAllIssuesGroupedByResolutionStatus).toHaveBeenCalledWith(params); + expect(reportsRepository.getAllIssuesGroupedByResolutionStatus).toHaveBeenCalledTimes(1); + })); + it("should throw an error when something goes wrong", () => __awaiter(void 0, void 0, void 0, function* () { + const params = {}; + reportsRepository.getAllIssuesGroupedByResolutionStatus.mockRejectedValue(new Error("Something went wrong")); + yield expect(reportsService.getAllIssuesGroupedByResolutionStatus(params)).rejects.toEqual(expect.objectContaining({ + code: 404, + success: false, + error: "GroupedByResolutionStatus: Something Went wrong", + })); + expect(reportsRepository.getAllIssuesGroupedByResolutionStatus).toHaveBeenCalledWith(params); + expect(reportsRepository.getAllIssuesGroupedByResolutionStatus).toHaveBeenCalledTimes(1); + })); + }); + describe("getIssueCountsGroupedByResolutionStatus", () => { + it("should return issue counts grouped by resolution status", () => __awaiter(void 0, void 0, void 0, function* () { + const params = {}; + const mockData = { resolved: 5, unresolved: 10 }; + reportsRepository.getIssueCountsGroupedByResolutionStatus.mockResolvedValue(mockData); + const response = yield reportsService.getIssueCountsGroupedByResolutionStatus(params); + expect(response.data).toEqual(mockData); + expect(reportsRepository.getIssueCountsGroupedByResolutionStatus).toHaveBeenCalledWith(params); + expect(reportsRepository.getIssueCountsGroupedByResolutionStatus).toHaveBeenCalledTimes(1); + })); + it("should throw an error when something goes wrong", () => __awaiter(void 0, void 0, void 0, function* () { + const params = {}; + reportsRepository.getIssueCountsGroupedByResolutionStatus.mockRejectedValue(new Error("Something went wrong")); + yield expect(reportsService.getIssueCountsGroupedByResolutionStatus(params)).rejects.toEqual(expect.objectContaining({ + code: 404, + success: false, + error: "CountsGroupedByResolutionStatus: Something Went wrong", + })); + expect(reportsRepository.getIssueCountsGroupedByResolutionStatus).toHaveBeenCalledWith(params); + expect(reportsRepository.getIssueCountsGroupedByResolutionStatus).toHaveBeenCalledTimes(1); + })); + }); + describe("getIssueCountsGroupedByResolutionAndCategory", () => { + it("should return issue counts grouped by resolution and category", () => __awaiter(void 0, void 0, void 0, function* () { + const params = {}; + const mockData = { + resolved: { "Public Safety": 2, "Healthcare Services": 1 }, + unresolved: { Water: 5, Electricity: 1 }, + }; + reportsRepository.getIssueCountsGroupedByResolutionAndCategory.mockResolvedValue(mockData); + const response = yield reportsService.getIssueCountsGroupedByResolutionAndCategory(params); + expect(response.data).toEqual(mockData); + expect(reportsRepository.getIssueCountsGroupedByResolutionAndCategory).toHaveBeenCalledWith(params); + expect(reportsRepository.getIssueCountsGroupedByResolutionAndCategory).toHaveBeenCalledTimes(1); + })); + it("should throw an error when something goes wrong", () => __awaiter(void 0, void 0, void 0, function* () { + const params = {}; + reportsRepository.getIssueCountsGroupedByResolutionAndCategory.mockRejectedValue(new Error("Something went wrong")); + yield expect(reportsService.getIssueCountsGroupedByResolutionAndCategory(params)).rejects.toEqual(expect.objectContaining({ + code: 404, + success: false, + error: "CountsGroupedByResolutionAndCategory: Something Went wrong", + })); + expect(reportsRepository.getIssueCountsGroupedByResolutionAndCategory).toHaveBeenCalledWith(params); + expect(reportsRepository.getIssueCountsGroupedByResolutionAndCategory).toHaveBeenCalledTimes(1); + })); + }); + describe("getIssuesGroupedByCreatedAt", () => { + it("should return issues grouped by creation date", () => __awaiter(void 0, void 0, void 0, function* () { + const params = {}; + const mockData = { + "2024-06-20": [ + { + id: 1, + category: { + name: "Category for 2024-06-20", + }, + location: { + suburb: "Suburb for 2024-06-20", + city: "City for 2024-06-20", + province: "Province for 2024-06-20", + }, + }, + ], + "2024-06-19": [ + { + id: 2, + category: { + name: "Category for 2024-06-19", + }, + location: { + suburb: "Suburb for 2024-06-19", + city: "City for 2024-06-19", + province: "Province for 2024-06-19", + }, + }, + ], + }; + reportsRepository.getIssuesGroupedByCreatedAt.mockResolvedValue(mockData); + const response = yield reportsService.getIssuesGroupedByCreatedAt(params); + expect(response.data).toEqual(mockData); + expect(reportsRepository.getIssuesGroupedByCreatedAt).toHaveBeenCalledWith(params); + expect(reportsRepository.getIssuesGroupedByCreatedAt).toHaveBeenCalledTimes(1); + })); + it("should throw an error when something goes wrong", () => __awaiter(void 0, void 0, void 0, function* () { + const params = {}; + reportsRepository.getIssuesGroupedByCreatedAt.mockRejectedValue(new Error("Something went wrong")); + yield expect(reportsService.getIssuesGroupedByCreatedAt(params)).rejects.toEqual(expect.objectContaining({ + code: 404, + success: false, + error: "GroupedByCreatedAt: Something Went wrong", + })); + expect(reportsRepository.getIssuesGroupedByCreatedAt).toHaveBeenCalledWith(params); + expect(reportsRepository.getIssuesGroupedByCreatedAt).toHaveBeenCalledTimes(1); + })); + }); + describe("getIssuesGroupedByCategory", () => { + it("should return issues grouped by category", () => __awaiter(void 0, void 0, void 0, function* () { + const params = {}; + const mockData = { + "Public Safety": [ + { + id: 1, + category: { + name: "Public Safety", + }, + location: { + suburb: "Suburb for 2024-06-20", + city: "City for 2024-06-20", + province: "Province for 2024-06-20", + }, + }, + ], + Water: [ + { + id: 1, + category: { + name: "Water", + }, + location: { + suburb: "Suburb for 2024-06-20", + city: "City for 2024-06-20", + province: "Province for 2024-06-20", + }, + }, + ], + }; + reportsRepository.getIssuesGroupedByCategory.mockResolvedValue(mockData); + const response = yield reportsService.getIssuesGroupedByCategory(params); + expect(response.data).toEqual(mockData); + expect(reportsRepository.getIssuesGroupedByCategory).toHaveBeenCalledWith(params); + expect(reportsRepository.getIssuesGroupedByCategory).toHaveBeenCalledTimes(1); + })); + it("should throw an error when something goes wrong", () => __awaiter(void 0, void 0, void 0, function* () { + const params = {}; + reportsRepository.getIssuesGroupedByCategory.mockRejectedValue(new Error("Something went wrong")); + yield expect(reportsService.getIssuesGroupedByCategory(params)).rejects.toEqual(expect.objectContaining({ + code: 404, + success: false, + error: "GroupedByCategory: Something Went wrong", + })); + expect(reportsRepository.getIssuesGroupedByCategory).toHaveBeenCalledWith(params); + expect(reportsRepository.getIssuesGroupedByCategory).toHaveBeenCalledTimes(1); + })); + }); + describe("getIssuesCountGroupedByCategoryAndCreatedAt", () => { + it("should return issue counts grouped by category and creation date", () => __awaiter(void 0, void 0, void 0, function* () { + const params = {}; + const mockData = { + "Public Safety": { "2024-06-20": 2, "2024-06-19": 1 }, + Water: { "2024-06-20": 0, "2024-06-19": 2 }, + }; + reportsRepository.getIssuesCountGroupedByCategoryAndCreatedAt.mockResolvedValue(mockData); + const response = yield reportsService.getIssuesCountGroupedByCategoryAndCreatedAt(params); + expect(response.data).toEqual(mockData); + expect(reportsRepository.getIssuesCountGroupedByCategoryAndCreatedAt).toHaveBeenCalledWith(params); + expect(reportsRepository.getIssuesCountGroupedByCategoryAndCreatedAt).toHaveBeenCalledTimes(1); + })); + it("should throw an error when something goes wrong", () => __awaiter(void 0, void 0, void 0, function* () { + const params = {}; + reportsRepository.getIssuesCountGroupedByCategoryAndCreatedAt.mockRejectedValue(new Error("Something went wrong")); + yield expect(reportsService.getIssuesCountGroupedByCategoryAndCreatedAt(params)).rejects.toEqual(expect.objectContaining({ + code: 404, + success: false, + error: "CountGroupedByCategoryAndCreatedAt: Something Went wrong", + })); + expect(reportsRepository.getIssuesCountGroupedByCategoryAndCreatedAt).toHaveBeenCalledWith(params); + expect(reportsRepository.getIssuesCountGroupedByCategoryAndCreatedAt).toHaveBeenCalledTimes(1); + })); + }); +}); diff --git a/backend/public/__tests__/services/subscriptionsService.test.js b/backend/public/__tests__/services/subscriptionsService.test.js new file mode 100644 index 00000000..21d9f1ff --- /dev/null +++ b/backend/public/__tests__/services/subscriptionsService.test.js @@ -0,0 +1,87 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const subscriptionsService_1 = __importDefault(require("@/modules/subscriptions/services/subscriptionsService")); +const subscriptionsRepository_1 = __importDefault(require("@/modules/subscriptions/repositories/subscriptionsRepository")); +const response_1 = require("@/types/response"); +jest.mock("@/modules/subscriptions/repositories/subscriptionsRepository"); +const mockSubscriptionsRepository = subscriptionsRepository_1.default; +describe("SubscriptionsService", () => { + let subscriptionsService; + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(console, "error").mockImplementation(() => { }); + subscriptionsService = new subscriptionsService_1.default(); + subscriptionsService.setSubscriptionsRepository(new mockSubscriptionsRepository()); + }); + afterEach(() => { + console.error.mockRestore(); + }); + describe("issueSubscriptions", () => { + it("should return a successful response", () => __awaiter(void 0, void 0, void 0, function* () { + const mockResponse = "Subscription successfully created!"; + mockSubscriptionsRepository.prototype.issueSubscriptions.mockResolvedValue(mockResponse); + const result = yield subscriptionsService.issueSubscriptions({}); + expect(mockSubscriptionsRepository.prototype.issueSubscriptions).toHaveBeenCalledWith({}); + expect(result).toEqual({ code: 200, success: true, data: mockResponse }); + })); + it("should throw an APIError on failure", () => __awaiter(void 0, void 0, void 0, function* () { + mockSubscriptionsRepository.prototype.issueSubscriptions.mockRejectedValue(new Error("Issue subscription failed")); + yield expect(subscriptionsService.issueSubscriptions({})).rejects.toEqual((0, response_1.APIError)({ code: 404, success: false, error: "Issue: Something Went wrong" })); + })); + }); + describe("categorySubscriptions", () => { + it("should return a successful response", () => __awaiter(void 0, void 0, void 0, function* () { + const mockResponse = "Subscription successfully created!"; + mockSubscriptionsRepository.prototype.categorySubscriptions.mockResolvedValue(mockResponse); + const result = yield subscriptionsService.categorySubscriptions({}); + expect(mockSubscriptionsRepository.prototype.categorySubscriptions).toHaveBeenCalledWith({}); + expect(result).toEqual({ code: 200, success: true, data: mockResponse }); + })); + it("should throw an APIError on failure", () => __awaiter(void 0, void 0, void 0, function* () { + mockSubscriptionsRepository.prototype.categorySubscriptions.mockRejectedValue(new Error("Category subscription failed")); + yield expect(subscriptionsService.categorySubscriptions({})).rejects.toEqual((0, response_1.APIError)({ code: 404, success: false, error: "Category: Something Went wrong" })); + })); + }); + describe("locationSubscriptions", () => { + it("should return a successful response", () => __awaiter(void 0, void 0, void 0, function* () { + const mockResponse = "Subscription successfully created!"; + mockSubscriptionsRepository.prototype.locationSubscriptions.mockResolvedValue(mockResponse); + const result = yield subscriptionsService.locationSubscriptions({}); + expect(mockSubscriptionsRepository.prototype.locationSubscriptions).toHaveBeenCalledWith({}); + expect(result).toEqual({ code: 200, success: true, data: mockResponse }); + })); + it("should throw an APIError on failure", () => __awaiter(void 0, void 0, void 0, function* () { + mockSubscriptionsRepository.prototype.locationSubscriptions.mockRejectedValue(new Error("Location subscription failed")); + yield expect(subscriptionsService.locationSubscriptions({})).rejects.toEqual((0, response_1.APIError)({ code: 404, success: false, error: "Location: Something Went wrong" })); + })); + }); + describe("getSubscriptions", () => { + it("should return a successful response", () => __awaiter(void 0, void 0, void 0, function* () { + const mockResponse = { + issues: [], + categories: [], + locations: [], + }; + mockSubscriptionsRepository.prototype.getSubscriptions.mockResolvedValue(mockResponse); + const result = yield subscriptionsService.getSubscriptions({}); + expect(mockSubscriptionsRepository.prototype.getSubscriptions).toHaveBeenCalledWith({}); + expect(result).toEqual({ code: 200, success: true, data: mockResponse }); + })); + it("should throw an APIError on failure", () => __awaiter(void 0, void 0, void 0, function* () { + mockSubscriptionsRepository.prototype.getSubscriptions.mockRejectedValue(new Error("Get subscriptions failed")); + yield expect(subscriptionsService.getSubscriptions({})).rejects.toEqual((0, response_1.APIError)({ code: 404, success: false, error: "Notifications: Something Went wrong" })); + })); + }); +}); diff --git a/backend/public/__tests__/services/userService.test.js b/backend/public/__tests__/services/userService.test.js new file mode 100644 index 00000000..a753d20b --- /dev/null +++ b/backend/public/__tests__/services/userService.test.js @@ -0,0 +1,143 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const userService_1 = require("@/modules/users/services/userService"); +const userRepository_1 = __importDefault(require("@/modules/users/repositories/userRepository")); +const supabaseClient_1 = __importDefault(require("@/modules/shared/services/supabaseClient")); +const mockUser_1 = __importDefault(require("@/data/mockUser")); +const mockFile_1 = __importDefault(require("@/data/mockFile")); +jest.mock("@/modules/users/repositories/userRepository"); +jest.mock("@/modules/shared/services/supabaseClient", () => ({ + storage: { + from: jest.fn(() => ({ + upload: jest.fn(), + getPublicUrl: jest.fn(), + remove: jest.fn(), + })), + }, +})); +describe("UserService", () => { + let userService; + let userRepository; + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(console, "error").mockImplementation(() => { }); + userRepository = new userRepository_1.default(); + userService = new userService_1.UserService(); + userService.setUserRepository(userRepository); + }); + afterEach(() => { + console.error.mockRestore(); + }); + describe("getUserById", () => { + it("should return a user by ID", () => __awaiter(void 0, void 0, void 0, function* () { + const userId = "1"; + const authenticatedUserId = "1"; + userRepository.getUserById.mockResolvedValue(mockUser_1.default); + const response = yield userService.getUserById(userId, authenticatedUserId); + expect(response.data).toEqual(Object.assign(Object.assign({}, mockUser_1.default), { is_owner: true })); + expect(userRepository.getUserById).toHaveBeenCalledWith(userId); + expect(userRepository.getUserById).toHaveBeenCalledTimes(1); + })); + it("should throw an error when user is not found", () => __awaiter(void 0, void 0, void 0, function* () { + const userId = "1"; + const authenticatedUserId = "1"; + userRepository.getUserById.mockResolvedValue(null); + yield expect(userService.getUserById(userId, authenticatedUserId)).rejects.toEqual(expect.objectContaining({ + code: 404, + success: false, + error: "User not found", + })); + expect(userRepository.getUserById).toHaveBeenCalledWith(userId); + expect(userRepository.getUserById).toHaveBeenCalledTimes(1); + })); + it("should set is_owner to false when authenticated user is different", () => __awaiter(void 0, void 0, void 0, function* () { + const userId = "1"; + const authenticatedUserId = "2"; + userRepository.getUserById.mockResolvedValue(mockUser_1.default); + const response = yield userService.getUserById(userId, authenticatedUserId); + expect(response.data).toEqual(Object.assign(Object.assign({}, mockUser_1.default), { is_owner: false })); + expect(userRepository.getUserById).toHaveBeenCalledWith(userId); + expect(userRepository.getUserById).toHaveBeenCalledTimes(1); + })); + }); + describe("updateUserProfile", () => { + const updateData = { fullname: "Updated User" }; + it("should update user profile without file", () => __awaiter(void 0, void 0, void 0, function* () { + userRepository.getUserById.mockResolvedValue(mockUser_1.default); + userRepository.updateUserProfile.mockResolvedValue(Object.assign(Object.assign({}, mockUser_1.default), updateData)); + const response = yield userService.updateUserProfile(mockUser_1.default.user_id || "", updateData); + expect(response.data).toEqual(Object.assign(Object.assign({}, mockUser_1.default), updateData)); + expect(userRepository.getUserById).toHaveBeenCalledWith(mockUser_1.default.user_id); + expect(userRepository.updateUserProfile).toHaveBeenCalledWith(mockUser_1.default.user_id, updateData); + })); + it("should throw an error when user is not found", () => __awaiter(void 0, void 0, void 0, function* () { + userRepository.getUserById.mockResolvedValue(null); + yield expect(userService.updateUserProfile(mockUser_1.default.user_id || "", updateData)).rejects.toEqual(expect.objectContaining({ + code: 404, + success: false, + error: "User not found", + })); + expect(userRepository.getUserById).toHaveBeenCalledWith(mockUser_1.default.user_id); + })); + it("should throw an error when file upload fails", () => __awaiter(void 0, void 0, void 0, function* () { + userRepository.getUserById.mockResolvedValue(mockUser_1.default); + supabaseClient_1.default.storage.from("user").upload.mockResolvedValue({ + error: new Error("Upload failed"), + }); + yield expect(userService.updateUserProfile(mockUser_1.default.user_id || "", updateData, mockFile_1.default)).rejects.toEqual(expect.objectContaining({ + code: 500, + success: false, + error: "Failed to delete old profile picture", + })); + expect(userRepository.getUserById).toHaveBeenCalledWith(mockUser_1.default.user_id); + expect(supabaseClient_1.default.storage.from("user").upload).not.toBe(null); + })); + it("should throw an error when deleting old profile picture fails", () => __awaiter(void 0, void 0, void 0, function* () { + userRepository.getUserById.mockResolvedValue(mockUser_1.default); + supabaseClient_1.default.storage.from("user").remove.mockResolvedValue({ + error: new Error("Delete failed"), + }); + yield expect(userService.updateUserProfile(mockUser_1.default.user_id || "", updateData, mockFile_1.default)).rejects.toEqual(expect.objectContaining({ + code: 500, + success: false, + error: "Failed to delete old profile picture", + })); + expect(userRepository.getUserById).toHaveBeenCalledWith(mockUser_1.default.user_id); + expect(supabaseClient_1.default.storage.from("user").remove).not.toBe(null); + })); + it('should upload new profile picture', () => __awaiter(void 0, void 0, void 0, function* () { + userRepository.getUserById.mockResolvedValue(mockUser_1.default); + userRepository.updateUserProfile.mockResolvedValue(mockUser_1.default); + supabaseClient_1.default.storage.from.mockReturnValue({ + remove: jest.fn().mockResolvedValue({ error: null }), + upload: jest.fn().mockResolvedValue({ error: null }), + getPublicUrl: jest.fn().mockReturnValue({ data: { publicUrl: 'new_url' } }), + }); + const file = { + fieldname: 'fieldname', + encoding: 'encoding', + mimetype: 'mimetype', + size: 0, + originalname: 'new_picture.png', + buffer: Buffer.from(''), + destination: 'destination', + filename: 'filename', + path: 'path', + }; + yield userService.updateUserProfile('1', {}, file); + expect(supabaseClient_1.default.storage.from('user').upload).toHaveBeenCalledWith(expect.stringContaining('profile_pictures/1-'), file.buffer); + })); + }); +}); diff --git a/backend/public/__tests__/services/visualizationService.test.js b/backend/public/__tests__/services/visualizationService.test.js new file mode 100644 index 00000000..8d6fc253 --- /dev/null +++ b/backend/public/__tests__/services/visualizationService.test.js @@ -0,0 +1,53 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const visualizationService_1 = require("@/modules/visualizations/services/visualizationService"); +const visualizationRepository_1 = require("@/modules/visualizations/repositories/visualizationRepository"); +const response_1 = require("@/types/response"); +jest.mock("@/modules/visualizations/repositories/visualizationRepository"); +describe("VisualizationService", () => { + let visualizationService; + let visualizationRepository; + beforeEach(() => { + jest.clearAllMocks(); + visualizationRepository = + new visualizationRepository_1.VisualizationRepository(); + visualizationService = new visualizationService_1.VisualizationService(); + visualizationService["visualizationRepository"] = visualizationRepository; + }); + describe("getVizData", () => { + it("should return visualization data successfully", () => __awaiter(void 0, void 0, void 0, function* () { + const mockVizData = [ + { $count: 1, "Chart 1": 3 }, + { $count: 2, "Chart 2": 3 }, + ]; + visualizationRepository.getVizData.mockResolvedValue(mockVizData[0]); + const response = yield visualizationService.getVizData(); + expect(response).toEqual((0, response_1.APIData)({ + code: 200, + success: true, + data: mockVizData[0], + })); + expect(visualizationRepository.getVizData).toHaveBeenCalledTimes(1); + })); + it("should handle an error when fetching visualization data", () => __awaiter(void 0, void 0, void 0, function* () { + const error = new Error("Failed to fetch data"); + visualizationRepository.getVizData.mockRejectedValue(error); + try { + yield visualizationService.getVizData(); + } + catch (e) { + expect(e).toBe(error); + } + expect(visualizationRepository.getVizData).toHaveBeenCalledTimes(1); + })); + }); +}); diff --git a/backend/public/__tests__/utilities/response.test.js b/backend/public/__tests__/utilities/response.test.js new file mode 100644 index 00000000..cf28b361 --- /dev/null +++ b/backend/public/__tests__/utilities/response.test.js @@ -0,0 +1,32 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const response_1 = require("@/utilities/response"); +describe("sendResponse", () => { + let res; + beforeEach(() => { + res = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + }; + }); + it("should set the status code and send the response as JSON", () => { + const response = { + code: 200, + success: true, + data: "Test data", + }; + (0, response_1.sendResponse)(res, response); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith(response); + }); + it("should handle different status codes", () => { + const response = { + code: 404, + success: false, + data: null, + }; + (0, response_1.sendResponse)(res, response); + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith(response); + }); +}); diff --git a/backend/public/app.js b/backend/public/app.js new file mode 100644 index 00000000..f48a2d7a --- /dev/null +++ b/backend/public/app.js @@ -0,0 +1,56 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = __importDefault(require("express")); +const issueRoutes_1 = __importDefault(require("@/modules/issues/routes/issueRoutes")); +const reactionRoutes_1 = __importDefault(require("@/modules/reactions/routes/reactionRoutes")); +const userRoutes_1 = __importDefault(require("@/modules/users/routes/userRoutes")); +const commentRoutes_1 = __importDefault(require("@/modules/comments/routes/commentRoutes")); +const visualizationRoutes_1 = __importDefault(require("@/modules/visualizations/routes/visualizationRoutes")); +const reportsRoutes_1 = __importDefault(require("@/modules/reports/routes/reportsRoutes")); +const locationRoutes_1 = __importDefault(require("@/modules/locations/routes/locationRoutes")); +const subscriptionsRoutes_1 = __importDefault(require("@/modules/subscriptions/routes/subscriptionsRoutes")); +const pointsRoutes_1 = __importDefault(require("@/modules/points/routes/pointsRoutes")); +const clusterRoutes_1 = __importDefault(require("@/modules/clusters/routes/clusterRoutes")); +const organizationRoutes_1 = __importDefault(require("@/modules/organizations/routes/organizationRoutes")); +const middleware_1 = require("@/middleware/middleware"); +const app = (0, express_1.default)(); +app.use(express_1.default.json()); +const allowedOrigins = ['http://localhost:3000', 'https://the-republic-six.vercel.app']; +app.use((req, res, next) => { + const origin = req.headers.origin; + if (origin && allowedOrigins.includes(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + } + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); + res.setHeader('Access-Control-Allow-Credentials', 'true'); + if (req.method === 'OPTIONS') { + res.sendStatus(200); + } + else { + next(); + } +}); +app.use(middleware_1.serverMiddleare); +app.use("/api/issues", issueRoutes_1.default); +app.use("/api/reactions", reactionRoutes_1.default); +app.use("/api/users", userRoutes_1.default); +app.use("/api/comments", commentRoutes_1.default); +app.use("/api/visualization", visualizationRoutes_1.default); +app.use("/api/reports", reportsRoutes_1.default); +app.use("/api/locations", locationRoutes_1.default); +app.use("/api/subscriptions", subscriptionsRoutes_1.default); +app.use("/api/points", pointsRoutes_1.default); +app.use('/api/clusters', clusterRoutes_1.default); +app.use("/api/organizations", organizationRoutes_1.default); +app.get("/", (req, res) => { + res.status(200).json({ + status: "success", + id: Math.floor(Math.random() * 500) + 1, + data: "Welcome to The-Republic Node-Express App", + }); +}); +exports.default = app; diff --git a/backend/public/data/mockFile.js b/backend/public/data/mockFile.js new file mode 100644 index 00000000..30f51c11 --- /dev/null +++ b/backend/public/data/mockFile.js @@ -0,0 +1,14 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const mockFile = { + fieldname: "image", + originalname: "profile.png", + encoding: "7bit", + mimetype: "image/png", + size: 1024, + destination: "uploads/", + filename: "profile.png", + path: "uploads/profile.png", + buffer: Buffer.from(""), +}; +exports.default = mockFile; diff --git a/backend/public/data/mockUser.js b/backend/public/data/mockUser.js new file mode 100644 index 00000000..06a7d58c --- /dev/null +++ b/backend/public/data/mockUser.js @@ -0,0 +1,18 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const mockUser = { + user_id: "user123", + email_address: "user@example.com", + username: "user123", + fullname: "User Fullname", + image_url: "http://example.com/image.jpg", + bio: "User biography", + is_owner: true, + total_issues: 10, + resolved_issues: 5, + access_token: "access_token_value", + user_score: 0, + location_id: null, + location: null +}; +exports.default = mockUser; diff --git a/backend/public/middleware/cacheMiddleware.js b/backend/public/middleware/cacheMiddleware.js new file mode 100644 index 00000000..02ed131a --- /dev/null +++ b/backend/public/middleware/cacheMiddleware.js @@ -0,0 +1,43 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.cacheMiddleware = void 0; +const redisClient_1 = __importDefault(require("@/modules/shared/services/redisClient")); +const cacheMiddleware = (duration) => { + return (req, res, next) => __awaiter(void 0, void 0, void 0, function* () { + if (!redisClient_1.default) { + return next(); + } + const key = `__express__${req.originalUrl || req.url}__${JSON.stringify(req.body)}`; + try { + const cachedBody = yield redisClient_1.default.get(key); + if (cachedBody) { + return res.send(JSON.parse(cachedBody)); + } + else { + const originalJson = res.json; + res.json = function (body) { + redisClient_1.default === null || redisClient_1.default === void 0 ? void 0 : redisClient_1.default.setex(key, duration, JSON.stringify(body)); + return originalJson.call(this, body); + }; + next(); + } + } + catch (error) { + console.error('Redis operation failed:', error); + next(); + } + }); +}; +exports.cacheMiddleware = cacheMiddleware; diff --git a/backend/public/middleware/middleware.js b/backend/public/middleware/middleware.js new file mode 100644 index 00000000..0e33dbc1 --- /dev/null +++ b/backend/public/middleware/middleware.js @@ -0,0 +1,67 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.verifyAndGetUser = exports.serverMiddleare = void 0; +const supabaseClient_1 = __importDefault(require("@/modules/shared/services/supabaseClient")); +const response_1 = require("@/utilities/response"); +const response_2 = require("@/types/response"); +const serverMiddleare = (req, res, next) => { + next(); +}; +exports.serverMiddleare = serverMiddleare; +const verifyAndGetUser = (req, res, next) => __awaiter(void 0, void 0, void 0, function* () { + const authHeader = req.headers.authorization; + const serviceRoleKey = req.headers["x-service-role-key"]; + if (serviceRoleKey === process.env.SUPABASE_SERVICE_ROLE_KEY) { + next(); + return; + } + req.body.user_id = undefined; + if (authHeader === undefined) { + next(); + return; + } + const jwt = authHeader.split(" ")[1]; + try { + const { data: { user }, error, } = yield supabaseClient_1.default.auth.getUser(jwt); + if (error) { + (0, response_1.sendResponse)(res, (0, response_2.APIError)({ + code: 403, + success: false, + error: "Invalid token", + })); + return; + } + if (user) { + req.body.user_id = user.id; + next(); + } + else { + (0, response_1.sendResponse)(res, (0, response_2.APIError)({ + code: 403, + success: false, + error: "Invalid token", + })); + } + } + catch (error) { + console.error(error); + (0, response_1.sendResponse)(res, (0, response_2.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + })); + } +}); +exports.verifyAndGetUser = verifyAndGetUser; diff --git a/backend/public/modules/clusters/controllers/clusterController.js b/backend/public/modules/clusters/controllers/clusterController.js new file mode 100644 index 00000000..2c2d3549 --- /dev/null +++ b/backend/public/modules/clusters/controllers/clusterController.js @@ -0,0 +1,101 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ClusterController = void 0; +const clusterService_1 = require("../services/clusterService"); +const response_1 = require("@/utilities/response"); +const response_2 = require("@/types/response"); +const cacheMiddleware_1 = require("@/middleware/cacheMiddleware"); +const cacheUtils_1 = require("@/utilities/cacheUtils"); +class ClusterController { + constructor() { + this.getClusters = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(this, void 0, void 0, function* () { + try { + const { categoryId, suburb, fromDate, toDate } = req.query; + if (!categoryId || !suburb) { + throw (0, response_2.APIError)({ + code: 400, + success: false, + error: "Category ID and Suburb are required", + }); + } + const clusters = yield this.clusterService.getClusters({ + categoryId: Number(categoryId), + suburb: String(suburb), + fromDate: fromDate ? new Date(fromDate) : undefined, + toDate: toDate ? new Date(toDate) : undefined, + }); + const response = { + code: 200, + success: true, + data: clusters, + }; + (0, response_1.sendResponse)(res, response); + } + catch (err) { + (0, response_1.sendResponse)(res, err); + } + }) + ]; + this.getClusterById = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(this, void 0, void 0, function* () { + try { + const { id } = req.params; + if (!id) { + throw (0, response_2.APIError)({ + code: 400, + success: false, + error: "Cluster ID is required", + }); + } + const cluster = yield this.clusterService.getClusterById(id); + const response = { + code: 200, + success: true, + data: cluster, + }; + (0, response_1.sendResponse)(res, response); + } + catch (err) { + (0, response_1.sendResponse)(res, err); + } + }) + ]; + this.assignCluster = (req, res) => __awaiter(this, void 0, void 0, function* () { + try { + const { issueId } = req.body; + if (!issueId) { + throw (0, response_2.APIError)({ + code: 400, + success: false, + error: "Issue ID is required", + }); + } + const clusterId = yield this.clusterService.assignClusterToIssue(issueId); + (0, cacheUtils_1.clearCachePattern)('__express__/api/clusters*'); + const response = { + code: 200, + success: true, + data: { clusterId }, + }; + (0, response_1.sendResponse)(res, response); + } + catch (err) { + (0, response_1.sendResponse)(res, err); + } + }); + this.clusterService = new clusterService_1.ClusterService(); + } +} +exports.ClusterController = ClusterController; diff --git a/backend/public/modules/clusters/repositories/clusterRepository.js b/backend/public/modules/clusters/repositories/clusterRepository.js new file mode 100644 index 00000000..2706aeea --- /dev/null +++ b/backend/public/modules/clusters/repositories/clusterRepository.js @@ -0,0 +1,331 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ClusterRepository = void 0; +const supabaseClient_1 = __importDefault(require("@/modules/shared/services/supabaseClient")); +const response_1 = require("@/types/response"); +class ClusterRepository { + createCluster(issue) { + return __awaiter(this, void 0, void 0, function* () { + let suburb; + if (!issue.location) { + const { data: locationData, error: locationError } = yield supabaseClient_1.default + .from('location') + .select('suburb') + .eq('location_id', issue.location_id) + .single(); + if (locationError) { + console.error('Error fetching location:', locationError); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching location data.", + }); + } + suburb = locationData.suburb; + } + else { + suburb = issue.location.suburb; + } + const { data, error } = yield supabaseClient_1.default + .from('cluster') + .insert({ + category_id: issue.category_id, + suburb: suburb, + issue_count: 1, + centroid_embedding: issue.content_embedding + }) + .select() + .single(); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while creating a new cluster.", + }); + } + return data; + }); + } + updateCluster(clusterId, newCentroid, newIssueCount) { + return __awaiter(this, void 0, void 0, function* () { + const { error } = yield supabaseClient_1.default + .from('cluster') + .update({ + centroid_embedding: newCentroid, + issue_count: newIssueCount, + last_modified: new Date().toISOString() + }) + .eq('cluster_id', clusterId); + if (error) { + console.error('Error updating cluster:', error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while updating cluster.", + }); + } + }); + } + findSimilarClusters(issue_1) { + return __awaiter(this, arguments, void 0, function* (issue, threshold = 0.7) { + let suburb; + if (!issue.location) { + const { data: locationData, error: locationError } = yield supabaseClient_1.default + .from('location') + .select('suburb') + .eq('location_id', issue.location_id) + .single(); + if (locationError) { + console.error('Error fetching location:', locationError); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching location data.", + }); + } + suburb = locationData.suburb; + } + else { + suburb = issue.location.suburb; + } + const { data, error } = yield supabaseClient_1.default + .rpc('find_similar_clusters', { + query_embedding: issue.content_embedding, + similarity_threshold: threshold, + category_id_param: issue.category_id, + suburb_param: suburb, + current_time_param: new Date().toISOString() + }); + if (error) { + console.error('Error finding similar clusters:', error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while finding similar clusters.", + }); + } + return data; + }); + } + findSimilarClustersForIssues(issues_1) { + return __awaiter(this, arguments, void 0, function* (issues, threshold = 0.8) { + var _a, _b; + if (issues.length === 0) { + return []; + } + const categoryId = issues[0].category_id; + const suburb = ((_a = issues[0].location) === null || _a === void 0 ? void 0 : _a.suburb) || ((_b = issues[0].location_data) === null || _b === void 0 ? void 0 : _b.suburb); + if (!suburb) { + console.error('No suburb information found for the issue'); + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Suburb information is missing for the issue.", + }); + } + const embeddings = issues.map(issue => issue.content_embedding); + const averageEmbedding = this.calculateAverageEmbedding(embeddings); + const { data, error } = yield supabaseClient_1.default + .rpc('find_similar_clusters', { + query_embedding: averageEmbedding, + similarity_threshold: threshold, + category_id_param: categoryId, + suburb_param: suburb, + current_time_param: new Date().toISOString() + }); + if (error) { + console.error('Error finding similar clusters:', error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while finding similar clusters.", + }); + } + return data; + }); + } + calculateAverageEmbedding(embeddings) { + let sumEmbedding = []; + let validEmbeddingsCount = 0; + for (const embedding of embeddings) { + let embeddingArray = []; + if (typeof embedding === 'string') { + try { + embeddingArray = JSON.parse(embedding); + } + catch (error) { + console.error(`Error parsing embedding:`, error); + continue; + } + } + else if (Array.isArray(embedding)) { + embeddingArray = embedding; + } + else { + console.error(`Invalid embedding type:`, typeof embedding); + continue; + } + if (embeddingArray.length > 0) { + if (sumEmbedding.length === 0) { + sumEmbedding = new Array(embeddingArray.length).fill(0); + } + for (let i = 0; i < embeddingArray.length; i++) { + sumEmbedding[i] += embeddingArray[i]; + } + validEmbeddingsCount++; + } + } + return sumEmbedding.map(val => val / validEmbeddingsCount); + } + getClusters(params) { + return __awaiter(this, void 0, void 0, function* () { + let query = supabaseClient_1.default + .from('cluster') + .select('*') + .eq('category_id', params.categoryId) + .eq('suburb', params.suburb); + if (params.fromDate) { + query = query.gte('created_at', params.fromDate.toISOString()); + } + if (params.toDate) { + query = query.lte('created_at', params.toDate.toISOString()); + } + const { data, error } = yield query; + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching clusters.", + }); + } + return data; + }); + } + getClusterById(clusterId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from('cluster') + .select('*') + .eq('cluster_id', clusterId) + .single(); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching the cluster.", + }); + } + return data; + }); + } + deleteCluster(clusterId) { + return __awaiter(this, void 0, void 0, function* () { + const { error } = yield supabaseClient_1.default + .from('cluster') + .delete() + .eq('cluster_id', clusterId); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while deleting the cluster.", + }); + } + }); + } + getIssuesInCluster(clusterId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from('issue') + .select(` + *, + cluster:cluster_id ( + centroid_embedding + ) + `) + .eq('cluster_id', clusterId); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching issues in the cluster.", + }); + } + return data; + }); + } + getIssueEmbeddingsInCluster(clusterId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from('issue_embeddings') + .select(` + issue_id, + content_embedding, + ...issue!inner ( + cluster_id + ) + `) + .eq("issue.cluster_id", clusterId); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching issue embeddings for cluster.", + }); + } + return data; + }); + } + updateIssueCluster(issueId, newClusterId) { + return __awaiter(this, void 0, void 0, function* () { + const { error } = yield supabaseClient_1.default + .from('issue') + .update({ cluster_id: newClusterId }) + .eq('issue_id', issueId); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while updating the issue's cluster.", + }); + } + }); + } + getClusterSize(clusterId) { + return __awaiter(this, void 0, void 0, function* () { + const { count, error } = yield supabaseClient_1.default + .from('issue') + .select('issue_id', { count: 'exact' }) + .eq('cluster_id', clusterId); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while getting the cluster size.", + }); + } + return count || 0; + }); + } +} +exports.ClusterRepository = ClusterRepository; diff --git a/backend/public/modules/clusters/routes/clusterRoutes.js b/backend/public/modules/clusters/routes/clusterRoutes.js new file mode 100644 index 00000000..019ea571 --- /dev/null +++ b/backend/public/modules/clusters/routes/clusterRoutes.js @@ -0,0 +1,13 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = __importDefault(require("express")); +const clusterController_1 = require("../controllers/clusterController"); +const router = express_1.default.Router(); +const clusterController = new clusterController_1.ClusterController(); +router.get('/', clusterController.getClusters); +router.get('/:id', clusterController.getClusterById); +router.post('/assign', clusterController.assignCluster); +exports.default = router; diff --git a/backend/public/modules/clusters/services/clusterService.js b/backend/public/modules/clusters/services/clusterService.js new file mode 100644 index 00000000..0f9a9da9 --- /dev/null +++ b/backend/public/modules/clusters/services/clusterService.js @@ -0,0 +1,306 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ClusterService = void 0; +const clusterRepository_1 = require("../repositories/clusterRepository"); +const issueRepository_1 = __importDefault(require("@/modules/issues/repositories/issueRepository")); +const openAIService_1 = require("@/modules/shared/services/openAIService"); +const response_1 = require("@/types/response"); +class ClusterService { + constructor() { + this.clusterRepository = new clusterRepository_1.ClusterRepository(); + this.issueRepository = new issueRepository_1.default(); + this.openAIService = new openAIService_1.OpenAIService(); + } + getClusters(params) { + return __awaiter(this, void 0, void 0, function* () { + return this.clusterRepository.getClusters(params); + }); + } + getClusterById(clusterId) { + return __awaiter(this, void 0, void 0, function* () { + const cluster = yield this.clusterRepository.getClusterById(clusterId); + if (!cluster) { + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "Cluster not found", + }); + } + return cluster; + }); + } + assignClusterToIssue(issue) { + return __awaiter(this, void 0, void 0, function* () { + if (!issue.content_embedding) { + issue.content_embedding = yield this.openAIService.getEmbedding(issue.content); + yield this.issueRepository.setIssueEmbedding(issue.issue_id, issue.content_embedding); + } + const similarClusters = yield this.clusterRepository.findSimilarClusters(issue, 0.9); + let assignedCluster; + if (similarClusters.length > 0) { + assignedCluster = similarClusters[0]; + const newEmbedding = Array.isArray(issue.content_embedding) ? issue.content_embedding : JSON.parse(issue.content_embedding); + yield this.updateCluster(assignedCluster, newEmbedding); + } + else { + assignedCluster = yield this.clusterRepository.createCluster(issue); + } + yield this.issueRepository.updateIssueCluster(issue.issue_id, assignedCluster.cluster_id); + return assignedCluster.cluster_id; + }); + } + updateCluster(cluster, newEmbedding) { + return __awaiter(this, void 0, void 0, function* () { + let centroidEmbedding; + if (typeof cluster.centroid_embedding === 'string') { + try { + centroidEmbedding = JSON.parse(cluster.centroid_embedding); + } + catch (error) { + console.error('Error parsing centroid_embedding:', error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "Invalid centroid_embedding format", + }); + } + } + else if (Array.isArray(cluster.centroid_embedding)) { + centroidEmbedding = cluster.centroid_embedding; + } + else { + console.error('Unexpected centroid_embedding type:', typeof cluster.centroid_embedding); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "Invalid centroid_embedding format", + }); + } + const newIssueCount = cluster.issue_count + 1; + const newCentroid = centroidEmbedding.map((val, i) => (val * cluster.issue_count + newEmbedding[i]) / newIssueCount); + yield this.clusterRepository.updateCluster(cluster.cluster_id, JSON.stringify(newCentroid), newIssueCount); + }); + } + removeIssueFromCluster(issueId, clusterId) { + return __awaiter(this, void 0, void 0, function* () { + try { + const cluster = yield this.clusterRepository.getClusterById(clusterId); + if (!cluster) { + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "Cluster not found", + }); + } + //console.log('Cluster:', cluster); + if (cluster.issue_count <= 1) { + yield this.clusterRepository.deleteCluster(clusterId); + } + else { + const updatedIssueCount = cluster.issue_count - 1; + const issues = yield this.clusterRepository.getIssueEmbeddingsInCluster(clusterId); + const updatedCentroid = this.recalculateCentroid(issues, issueId); + const formattedCentroid = this.formatCentroidForDatabase(updatedCentroid); + yield this.clusterRepository.updateCluster(clusterId, formattedCentroid, updatedIssueCount); + } + } + catch (error) { + console.error('Error in removeIssueFromCluster:', error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: `An error occurred while removing issue from cluster: ${error}`, + }); + } + }); + } + recalculateCentroid(issues, excludeIssueId) { + const relevantIssues = issues.filter(issue => issue.issue_id !== excludeIssueId); + if (relevantIssues.length === 0) { + throw new Error("No issues left in cluster after exclusion"); + } + //console.log('Relevant issues:', relevantIssues); + let embeddingLength = 0; + let sumEmbedding = []; + let validEmbeddingsCount = 0; + for (const issue of relevantIssues) { + let embeddingArray = []; + if (typeof issue.content_embedding === 'string') { + try { + embeddingArray = JSON.parse(issue.content_embedding); + } + catch (error) { + console.error(`Error parsing embedding for issue ${issue.issue_id}:`, error); + continue; + } + } + else if (Array.isArray(issue.content_embedding)) { + embeddingArray = issue.content_embedding; + } + else { + console.error(`Invalid embedding type for issue ${issue.issue_id}:`, typeof issue.content_embedding); + continue; + } + if (embeddingArray.length > 0) { + if (embeddingLength === 0) { + embeddingLength = embeddingArray.length; + sumEmbedding = new Array(embeddingLength).fill(0); + } + if (embeddingArray.length === embeddingLength) { + for (let i = 0; i < embeddingLength; i++) { + const value = embeddingArray[i]; + if (typeof value === 'number' && !isNaN(value) && isFinite(value)) { + sumEmbedding[i] += value; + } + } + validEmbeddingsCount++; + } + } + } + if (validEmbeddingsCount === 0) { + console.error('No valid embeddings found in cluster'); + return this.getDefaultEmbedding(relevantIssues); + } + return sumEmbedding.map(val => val / validEmbeddingsCount); + } + getDefaultEmbedding(issues) { + for (const issue of issues) { + if (issue.cluster && typeof issue.cluster.centroid_embedding === 'string') { + try { + return JSON.parse(issue.cluster.centroid_embedding); + } + catch (error) { + console.error('Error parsing cluster centroid:', error); + } + } + } + const embeddingLength = 1536; + return new Array(embeddingLength).fill(0); + } + moveAcceptedMembersToNewCluster(issueId, acceptedUserIds) { + return __awaiter(this, void 0, void 0, function* () { + const issue = yield this.issueRepository.getIssueById(issueId); + if (!issue) { + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "Issue not found", + }); + } + if (!issue.cluster_id) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Issue is not associated with a cluster", + }); + } + const oldClusterId = issue.cluster_id; + const clusterIssues = yield this.clusterRepository.getIssuesInCluster(oldClusterId); + // Find accepted issues + const acceptedIssues = clusterIssues.filter(clusterIssue => clusterIssue.issue_id !== issueId && acceptedUserIds.includes(clusterIssue.user_id)); + let newClusterId = ''; + if (acceptedIssues.length > 0) { + // Check for similar clusters among accepted issues + const similarClusters = yield this.clusterRepository.findSimilarClustersForIssues(acceptedIssues, 0.9); + if (similarClusters.length > 0) { + // Move accepted issues to the most similar cluster + const mostSimilarCluster = similarClusters[0]; + newClusterId = mostSimilarCluster.cluster_id; + for (const acceptedIssue of acceptedIssues) { + yield this.clusterRepository.updateIssueCluster(acceptedIssue.issue_id, newClusterId); + } + } + else { + // Create a new cluster with the first accepted issue + const newCluster = yield this.clusterRepository.createCluster(acceptedIssues[0]); + newClusterId = newCluster.cluster_id; + // Move accepted issues to the new cluster + for (const acceptedIssue of acceptedIssues) { + yield this.clusterRepository.updateIssueCluster(acceptedIssue.issue_id, newClusterId); + } + } + } + else { + // Create a new cluster with the accepted issue + const newCluster = yield this.clusterRepository.createCluster(issue); + newClusterId = newCluster.cluster_id; + } + // Move the accepted issue to the new cluster + yield this.clusterRepository.updateIssueCluster(issueId, newClusterId); + // Recalculate centroids for both old and new clusters + yield this.recalculateClusterCentroid(oldClusterId); + yield this.recalculateClusterCentroid(newClusterId); + // Check if the old cluster is empty and delete if necessary + const oldClusterSize = yield this.clusterRepository.getClusterSize(oldClusterId); + if (oldClusterSize === 0) { + yield this.clusterRepository.deleteCluster(oldClusterId); + } + return newClusterId; + }); + } + recalculateClusterCentroid(clusterId) { + return __awaiter(this, void 0, void 0, function* () { + const issues = yield this.clusterRepository.getIssuesInCluster(clusterId); + if (issues.length === 0) { + return; // Cluster is empty, no need to recalculate + } + const newCentroid = this.calculateAverageEmbedding(issues); + const formattedCentroid = this.formatCentroidForDatabase(newCentroid); + yield this.clusterRepository.updateCluster(clusterId, formattedCentroid, issues.length); + }); + } + calculateAverageEmbedding(issues) { + let sumEmbedding = []; + let validEmbeddingsCount = 0; + for (const issue of issues) { + let embeddingArray = []; + if (typeof issue.content_embedding === 'string') { + try { + embeddingArray = JSON.parse(issue.content_embedding); + } + catch (error) { + console.error(`Error parsing embedding for issue ${issue.issue_id}:`, error); + continue; + } + } + else if (Array.isArray(issue.content_embedding)) { + embeddingArray = issue.content_embedding; + } + else { + console.error(`Invalid embedding type for issue ${issue.issue_id}:`, typeof issue.content_embedding); + continue; + } + if (embeddingArray.length > 0) { + if (sumEmbedding.length === 0) { + sumEmbedding = new Array(embeddingArray.length).fill(0); + } + for (let i = 0; i < embeddingArray.length; i++) { + sumEmbedding[i] += embeddingArray[i]; + } + validEmbeddingsCount++; + } + } + return sumEmbedding.map(val => val / validEmbeddingsCount); + } + formatCentroidForDatabase(centroid) { + return `[${centroid.map(val => { + if (isNaN(val) || !isFinite(val)) { + return '0'; + } + return val.toFixed(6); + }).join(',')}]`; + } +} +exports.ClusterService = ClusterService; diff --git a/backend/public/modules/comments/controllers/commentController.js b/backend/public/modules/comments/controllers/commentController.js new file mode 100644 index 00000000..07e8dfea --- /dev/null +++ b/backend/public/modules/comments/controllers/commentController.js @@ -0,0 +1,63 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.deleteComment = exports.addComment = exports.getComments = exports.getNumComments = void 0; +const commentService_1 = require("@/modules/comments/services/commentService"); +const response_1 = require("@/utilities/response"); +const commentService = new commentService_1.CommentService(); +function getNumComments(req, res) { + return __awaiter(this, void 0, void 0, function* () { + try { + const response = yield commentService.getNumComments(req.body); + (0, response_1.sendResponse)(res, response); + } + catch (error) { + (0, response_1.sendResponse)(res, error); + } + }); +} +exports.getNumComments = getNumComments; +function getComments(req, res) { + return __awaiter(this, void 0, void 0, function* () { + try { + const response = yield commentService.getComments(req.body); + (0, response_1.sendResponse)(res, response); + } + catch (error) { + (0, response_1.sendResponse)(res, error); + } + }); +} +exports.getComments = getComments; +function addComment(req, res) { + return __awaiter(this, void 0, void 0, function* () { + try { + const response = yield commentService.addComment(req.body); + (0, response_1.sendResponse)(res, response); + } + catch (error) { + (0, response_1.sendResponse)(res, error); + } + }); +} +exports.addComment = addComment; +function deleteComment(req, res) { + return __awaiter(this, void 0, void 0, function* () { + try { + const response = yield commentService.deleteComment(req.body); + (0, response_1.sendResponse)(res, response); + } + catch (error) { + (0, response_1.sendResponse)(res, error); + } + }); +} +exports.deleteComment = deleteComment; diff --git a/backend/public/modules/comments/repositories/commentRepository.js b/backend/public/modules/comments/repositories/commentRepository.js new file mode 100644 index 00000000..2d162f06 --- /dev/null +++ b/backend/public/modules/comments/repositories/commentRepository.js @@ -0,0 +1,153 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CommentRepository = void 0; +const supabaseClient_1 = __importDefault(require("@/modules/shared/services/supabaseClient")); +const response_1 = require("@/types/response"); +class CommentRepository { + getNumComments(issue_id, parent_id) { + return __awaiter(this, void 0, void 0, function* () { + let query = supabaseClient_1.default + .from("comment") + .select("*", { + count: "exact", + head: true, + }) + .eq("issue_id", issue_id); + query = !parent_id + ? query.is("parent_id", null) + : query.eq("parent_id", parent_id); + const { count, error } = yield query; + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + return count; + }); + } + getComments(_a) { + return __awaiter(this, arguments, void 0, function* ({ issue_id, user_id, from, amount, parent_id }) { + let query = supabaseClient_1.default + .from("comment") + .select(` + *, + user: user_id ( + user_id, + email_address, + username, + fullname, + image_url, + user_score + ) + `) + .eq("issue_id", issue_id) + .order("created_at", { ascending: false }) + .range(from, from + amount - 1); + query = !parent_id + ? query.is("parent_id", null) + : query.eq("parent_id", parent_id); + const { data, error } = yield query; + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + const comments = data.map((comment) => { + const isOwner = comment.is_anonymous + ? comment.user_id === user_id + : comment.user_id === user_id; + return Object.assign(Object.assign({}, comment), { is_owner: isOwner, user: comment.is_anonymous + ? { + user_id: null, + email_address: null, + username: "Anonymous", + fullname: "Anonymous", + image_url: null, + } + : comment.user }); + }); + return comments; + }); + } + addComment(comment) { + return __awaiter(this, void 0, void 0, function* () { + comment.created_at = new Date().toISOString(); + const { data, error } = yield supabaseClient_1.default + .from("comment") + .insert(comment) + .select(` + *, + user: user_id ( + user_id, + email_address, + username, + fullname, + image_url + ) + `) + .single(); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + return Object.assign(Object.assign({}, data), { is_owner: true, user: data.is_anonymous + ? { + user_id: null, + email_address: null, + username: "Anonymous", + fullname: "Anonymous", + image_url: null, + } + : data.user }); + }); + } + deleteComment(comment_id, user_id) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("comment") + .delete() + .eq("comment_id", comment_id) + .eq("user_id", user_id) + .select() + .maybeSingle(); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + if (!data) { + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "Comment does not exist", + }); + } + }); + } +} +exports.CommentRepository = CommentRepository; diff --git a/backend/public/modules/comments/routes/commentRoutes.js b/backend/public/modules/comments/routes/commentRoutes.js new file mode 100644 index 00000000..60adace4 --- /dev/null +++ b/backend/public/modules/comments/routes/commentRoutes.js @@ -0,0 +1,35 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const commentController = __importStar(require("@/modules/comments/controllers/commentController")); +const middleware_1 = require("@/middleware/middleware"); +const router = (0, express_1.Router)(); +router.use(middleware_1.verifyAndGetUser); +router.post("/", commentController.getComments); +router.post("/count", commentController.getNumComments); +router.post("/add", commentController.addComment); +router.delete("/delete", commentController.deleteComment); +exports.default = router; diff --git a/backend/public/modules/comments/services/commentService.js b/backend/public/modules/comments/services/commentService.js new file mode 100644 index 00000000..fe82b814 --- /dev/null +++ b/backend/public/modules/comments/services/commentService.js @@ -0,0 +1,140 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CommentService = void 0; +const commentRepository_1 = require("@/modules/comments/repositories/commentRepository"); +const response_1 = require("@/types/response"); +const pointsService_1 = require("@/modules/points/services/pointsService"); +class CommentService { + constructor() { + this.commentRepository = new commentRepository_1.CommentRepository(); + this.pointsService = new pointsService_1.PointsService(); + } + setCommentRepository(commentRepository) { + this.commentRepository = commentRepository; + } + setPointsService(pointsService) { + this.pointsService = pointsService; + } + getNumComments(_a) { + return __awaiter(this, arguments, void 0, function* ({ issue_id, parent_id }) { + if (!issue_id) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Missing required fields for getting number of comments", + }); + } + const count = yield this.commentRepository.getNumComments(issue_id, parent_id); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: count, + }); + }); + } + getComments(params) { + return __awaiter(this, void 0, void 0, function* () { + if (!params.issue_id || !params.amount || params.from === undefined) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Missing required fields for getting comments", + }); + } + const comments = yield this.commentRepository.getComments(params); + const commentsWithUserInfo = comments.map((comment) => { + const isOwner = comment.user_id === params.user_id; + if (comment.is_anonymous) { + comment.user = { + user_id: null, + email_address: null, + username: "Anonymous", + fullname: "Anonymous", + image_url: null, + is_owner: isOwner, + total_issues: null, + resolved_issues: null, + user_score: 0, + location_id: null, + location: null + }; + } + else { + comment.user.is_owner = isOwner; + } + return Object.assign(Object.assign({}, comment), { is_owner: isOwner }); + }); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: commentsWithUserInfo, + }); + }); + } + addComment(comment) { + return __awaiter(this, void 0, void 0, function* () { + var _a; + if (!comment.user_id) { + throw (0, response_1.APIError)({ + code: 401, + success: false, + error: "You need to be signed in to create a comment", + }); + } + if (!comment.issue_id || + !comment.content || + comment.is_anonymous === undefined) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Missing required fields for creating a comment", + }); + } + (_a = comment.parent_id) !== null && _a !== void 0 ? _a : (comment.parent_id = null); + delete comment.comment_id; + const addedComment = yield this.commentRepository.addComment(comment); + // Award points for adding a comment, but only if it's a top-level comment + if (!comment.parent_id) { + yield this.pointsService.awardPoints(comment.user_id, 10, "Left a comment on an open issue"); + } + return (0, response_1.APIData)({ + code: 201, + success: true, + data: addedComment, + }); + }); + } + deleteComment(_a) { + return __awaiter(this, arguments, void 0, function* ({ comment_id, user_id }) { + if (!user_id) { + throw (0, response_1.APIError)({ + code: 401, + success: false, + error: "You need to be signed in to delete a comment", + }); + } + if (!comment_id) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Missing required fields for deleting a comment", + }); + } + yield this.commentRepository.deleteComment(comment_id, user_id); + return (0, response_1.APIData)({ + code: 204, + success: true, + }); + }); + } +} +exports.CommentService = CommentService; diff --git a/backend/public/modules/issues/controllers/issueController.js b/backend/public/modules/issues/controllers/issueController.js new file mode 100644 index 00000000..ccf7fd5d --- /dev/null +++ b/backend/public/modules/issues/controllers/issueController.js @@ -0,0 +1,275 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getRelatedIssues = exports.deleteResolution = exports.getUserResolutions = exports.getUserIssueInCluster = exports.hasUserIssuesInCluster = exports.getResolutionsForIssue = exports.respondToResolution = exports.createExternalResolution = exports.createSelfResolution = exports.getUserResolvedIssues = exports.getUserIssues = exports.resolveIssue = exports.deleteIssue = exports.updateIssue = exports.createIssue = exports.getIssueById = exports.getIssues = void 0; +const issueService_1 = __importDefault(require("@/modules/issues/services/issueService")); +const response_1 = require("@/types/response"); +const response_2 = require("@/utilities/response"); +const multer_1 = __importDefault(require("multer")); +const cacheMiddleware_1 = require("@/middleware/cacheMiddleware"); +const cacheUtils_1 = require("@/utilities/cacheUtils"); +const issueService = new issueService_1.default(); +const upload = (0, multer_1.default)({ storage: multer_1.default.memoryStorage() }); +const handleError = (res, err) => { + console.error(err); + if (err instanceof Error) { + (0, response_2.sendResponse)(res, (0, response_1.APIError)({ + code: 500, + success: false, + error: err.message || "An unexpected error occurred", + })); + } + else { + (0, response_2.sendResponse)(res, err); + } +}; +exports.getIssues = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield issueService.getIssues(req.body); + (0, response_2.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } + }) +]; +exports.getIssueById = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield issueService.getIssueById(req.body); + (0, response_2.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } + }) +]; +exports.createIssue = [ + upload.single("image"), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const file = req.file; + const response = yield issueService.createIssue(req.body, file); + (0, cacheUtils_1.clearCachePattern)('__express__/api/issues*'); + (0, response_2.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } + }), +]; +const updateIssue = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield issueService.updateIssue(req.body); + (0, cacheUtils_1.clearCache)('/api/issues/single', { issueId: req.body.issueId }); + (0, cacheUtils_1.clearCachePattern)('__express__/api/issues*'); + (0, response_2.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } +}); +exports.updateIssue = updateIssue; +const deleteIssue = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield issueService.deleteIssue(req.body); + (0, cacheUtils_1.clearCachePattern)('__express__/api/issues*'); + (0, response_2.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } +}); +exports.deleteIssue = deleteIssue; +const resolveIssue = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const { issueId, userId } = req.body; + const response = yield issueService.createSelfResolution(issueId, userId, "Issue resolved by owner"); + (0, cacheUtils_1.clearCachePattern)('__express__/api/issues*'); + (0, response_2.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } +}); +exports.resolveIssue = resolveIssue; +exports.getUserIssues = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield issueService.getUserIssues(req.body); + (0, response_2.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } + }) +]; +exports.getUserResolvedIssues = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield issueService.getUserResolvedIssues(req.body); + (0, response_2.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } + }) +]; +exports.createSelfResolution = [ + upload.single("proofImage"), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const { issueId, userId, resolutionText } = req.body; + const response = yield issueService.createSelfResolution(parseInt(issueId), userId, resolutionText, req.file); + (0, cacheUtils_1.clearCachePattern)('__express__/api/issues*'); + (0, response_2.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } + }), +]; +exports.createExternalResolution = [ + upload.single("proofImage"), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const { issueId, userId, resolutionText, politicalAssociation, stateEntityAssociation, resolvedBy } = req.body; + const response = yield issueService.createExternalResolution(parseInt(issueId), userId, resolutionText, req.file, politicalAssociation, stateEntityAssociation, resolvedBy); + (0, cacheUtils_1.clearCachePattern)('__express__/api/issues*'); + (0, response_2.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } + }), +]; +const respondToResolution = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const { resolutionId, userId, accept } = req.body; + const response = yield issueService.respondToResolution(resolutionId, userId, accept); + (0, cacheUtils_1.clearCachePattern)('__express__/api/issues*'); + (0, response_2.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } +}); +exports.respondToResolution = respondToResolution; +exports.getResolutionsForIssue = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const { issueId } = req.body; + const response = yield issueService.getResolutionsForIssue(parseInt(issueId)); + (0, response_2.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } + }) +]; +exports.hasUserIssuesInCluster = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const { userId, clusterId } = req.body; + const response = yield issueService.hasUserIssuesInCluster(userId, clusterId); + (0, response_2.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } + }) +]; +exports.getUserIssueInCluster = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const { clusterId, user_id } = req.body; + if (!user_id) { + return res.status(401).json({ error: "Unauthorized" }); + } + const issue = yield issueService.getUserIssueInCluster(user_id, clusterId); + if (issue) { + return res.json({ issue }); + } + else { + return res.status(404).json({ error: "User issue not found in the cluster" }); + } + } + catch (error) { + console.error("Error fetching user's issue in cluster:", error); + return res.status(500).json({ error: "Internal server error" }); + } + }) +]; +exports.getUserResolutions = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const { userId } = req.body; + const resolutions = yield issueService.getUserResolutions(userId); + (0, response_2.sendResponse)(res, (0, response_1.APIData)({ + code: 200, + success: true, + data: resolutions, + })); + } + catch (err) { + handleError(res, err); + } + }) +]; +const deleteResolution = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const { resolutionId, userId } = req.body; + yield issueService.deleteResolution(resolutionId, userId); + (0, cacheUtils_1.clearCachePattern)('__express__/api/issues*'); + (0, response_2.sendResponse)(res, (0, response_1.APIData)({ + code: 200, + success: true + })); + } + catch (err) { + handleError(res, err); + } +}); +exports.deleteResolution = deleteResolution; +exports.getRelatedIssues = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + var _a; + try { + const { issueId } = req.body; + const userId = (_a = req.body) === null || _a === void 0 ? void 0 : _a.user_id; + if (!userId) { + return (0, response_2.sendResponse)(res, (0, response_1.APIError)({ + code: 401, + success: false, + error: "Unauthorized", + })); + } + const response = yield issueService.getRelatedIssues(parseInt(issueId), userId); + (0, response_2.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } + }) +]; diff --git a/backend/public/modules/issues/repositories/categoryRepository.js b/backend/public/modules/issues/repositories/categoryRepository.js new file mode 100644 index 00000000..ab266988 --- /dev/null +++ b/backend/public/modules/issues/repositories/categoryRepository.js @@ -0,0 +1,38 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CategoryRepository = void 0; +const supabaseClient_1 = __importDefault(require("@/modules/shared/services/supabaseClient")); +const response_1 = require("@/types/response"); +class CategoryRepository { + getCategoryId(name) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("category") + .select("category_id") + .eq("name", name) + .single(); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + return data.category_id; + }); + } +} +exports.CategoryRepository = CategoryRepository; diff --git a/backend/public/modules/issues/repositories/issueRepository.js b/backend/public/modules/issues/repositories/issueRepository.js new file mode 100644 index 00000000..de839698 --- /dev/null +++ b/backend/public/modules/issues/repositories/issueRepository.js @@ -0,0 +1,712 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const supabaseClient_1 = __importDefault(require("@/modules/shared/services/supabaseClient")); +const luxon_1 = require("luxon"); +const response_1 = require("@/types/response"); +const reactionRepository_1 = __importDefault(require("@/modules/reactions/repositories/reactionRepository")); +const categoryRepository_1 = require("@/modules/issues/repositories/categoryRepository"); +const commentRepository_1 = require("@/modules/comments/repositories/commentRepository"); +const locationRepository_1 = require("@/modules/locations/repositories/locationRepository"); +const reactionRepository = new reactionRepository_1.default(); +const categoryRepository = new categoryRepository_1.CategoryRepository(); +const commentRepository = new commentRepository_1.CommentRepository(); +class IssueRepository { + getIssues(_a) { + return __awaiter(this, arguments, void 0, function* ({ from, amount, category, mood, user_id, order_by = "created_at", ascending = false, location, }) { + let locationIds = []; + if (location) { + let locationQuery = supabaseClient_1.default.from("location").select("location_id"); + if (location.province) { + locationQuery = locationQuery.ilike("province", `%${location.province}%`); + } + if (location.city) { + locationQuery = locationQuery.ilike("city", `%${location.city}%`); + } + if (location.suburb) { + locationQuery = locationQuery.ilike("suburb", `%${location.suburb}%`); + } + if (location.district) { + locationQuery = locationQuery.ilike("district", `%${location.district}%`); + } + const { data: locationData, error: locationError } = yield locationQuery; + if (locationError) { + console.error("Error fetching locations:", locationError); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while fetching locations.", + }); + } + locationIds = locationData.map((loc) => loc.location_id); + } + let query = supabaseClient_1.default + .from("issue") + .select(` + *, + user: user_id ( + user_id, + email_address, + username, + fullname, + image_url, + user_score + ), + category: category_id ( + name + ), + location: location_id ( + province, + city, + suburb, + district, + latitude, + longitude + ), + comment_count + `) + .order(order_by, { ascending }) + .order("created_at", { ascending }) + .range(from, from + amount - 1); + if (locationIds.length > 0) { + query = query.in("location_id", locationIds); + } + if (category) { + const categoryId = yield categoryRepository.getCategoryId(category); + query = query.eq("category_id", categoryId); + } + if (mood) { + query = query.eq("sentiment", mood); + } + const { data, error } = yield query; + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + const issues = yield Promise.all(data.map((issue) => __awaiter(this, void 0, void 0, function* () { + var _b, _c; + const reactions = yield reactionRepository.getReactionCountsByIssueId(issue.issue_id); + const userReaction = user_id + ? yield reactionRepository.getReactionByUserAndIssue(issue.issue_id, user_id) + : null; + const pendingResolution = yield this.getPendingResolutionForIssue(issue.issue_id); + const resolutions = yield this.getResolutionsForIssue(issue.issue_id); + const userHasIssueInCluster = user_id ? yield this.userHasIssueInCluster(user_id, (_b = issue.cluster_id) !== null && _b !== void 0 ? _b : null) : false; + const { issues: relatedIssues, totalCount: relatedIssuesCount } = yield this.getRelatedIssues((_c = issue.cluster_id) !== null && _c !== void 0 ? _c : null, issue.issue_id); + return Object.assign(Object.assign({}, issue), { reactions, user_reaction: (userReaction === null || userReaction === void 0 ? void 0 : userReaction.emoji) || null, is_owner: issue.user_id === user_id, user: issue.is_anonymous + ? { + user_id: null, + email_address: null, + username: "Anonymous", + fullname: "Anonymous", + image_url: null, + } + : issue.user, hasPendingResolution: !!pendingResolution, pendingResolutionId: (pendingResolution === null || pendingResolution === void 0 ? void 0 : pendingResolution.resolution_id) || null, resolutions, + relatedIssuesCount, + userHasIssueInCluster, + relatedIssues }); + }))); + return issues; + }); + } + getIssueById(issueId, user_id) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("issue") + .select(` + *, + user: user_id ( + user_id, + email_address, + username, + fullname, + image_url, + user_score + ), + category: category_id ( + name + ), + location: location_id ( + suburb, + city, + province, + latitude, + longitude + ), + cluster_id + `) + .eq("issue_id", issueId) + .maybeSingle(); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + if (!data) { + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "Issue does not exist", + }); + } + const reactions = yield reactionRepository.getReactionCountsByIssueId(data.issue_id); + const userReaction = user_id + ? yield reactionRepository.getReactionByUserAndIssue(data.issue_id, user_id) + : null; + const commentCount = yield commentRepository.getNumComments(data.issue_id); + const pendingResolution = yield this.getPendingResolutionForIssue(data.issue_id); + const resolutions = yield this.getResolutionsForIssue(data.issue_id); + const { issues: relatedIssues, totalCount: relatedIssuesCount } = yield this.getRelatedIssues(data.cluster_id, data.issue_id); + const userHasIssueInCluster = user_id ? yield this.userHasIssueInCluster(user_id, data.cluster_id) : false; + return Object.assign(Object.assign({}, data), { reactions, user_reaction: (userReaction === null || userReaction === void 0 ? void 0 : userReaction.emoji) || null, comment_count: commentCount, is_owner: data.user_id === user_id, user: data.is_anonymous + ? { + user_id: null, + email_address: null, + username: "Anonymous", + fullname: "Anonymous", + image_url: null, + } + : data.user, hasPendingResolution: !!pendingResolution, pendingResolutionId: (pendingResolution === null || pendingResolution === void 0 ? void 0 : pendingResolution.resolution_id) || null, resolutions, + relatedIssuesCount, + userHasIssueInCluster, + relatedIssues }); + }); + } + getRelatedIssues(clusterId, currentIssueId) { + return __awaiter(this, void 0, void 0, function* () { + if (!clusterId) + return { issues: [], totalCount: 0 }; + const { data, error, count } = yield supabaseClient_1.default + .from('issue') + .select(` + *, + user: user_id ( + user_id, + email_address, + username, + fullname, + image_url, + user_score + ), + category: category_id ( + name + ), + location: location_id ( + suburb, + city, + province, + latitude, + longitude + ) + `, { count: 'exact' }) + .eq('cluster_id', clusterId) + .neq('issue_id', currentIssueId) + .limit(3); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching related issues.", + }); + } + return { issues: data, totalCount: (count || 0) - 1 }; + }); + } + createIssue(issue) { + return __awaiter(this, void 0, void 0, function* () { + issue.created_at = new Date().toISOString(); + let locationId = null; + if (issue.location_data) { + let locationDataObj; + try { + locationDataObj = + typeof issue.location_data === "string" + ? JSON.parse(issue.location_data) + : issue.location_data; + const locationRepository = new locationRepository_1.LocationRepository(); + const existingLocations = yield locationRepository.getLocationByPlacesId(locationDataObj.place_id); + if (existingLocations.length > 0) { + // If locations exist, use the first one + locationId = existingLocations[0].location_id; + } + else { + // If no location exists, create a new one + const newLocation = yield locationRepository.createLocation({ + place_id: locationDataObj.place_id, + province: locationDataObj.province, + city: locationDataObj.city, + suburb: locationDataObj.suburb, + district: locationDataObj.district, + latitude: locationDataObj.lat, + longitude: locationDataObj.lng + }); + locationId = newLocation.location_id; + } + } + catch (error) { + console.error("Error processing location data:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: `An error occurred while processing location data: ${error instanceof Error ? error.message : JSON.stringify(error)}`, + }); + } + } + try { + const { data, error } = yield supabaseClient_1.default + .from("issue") + .insert({ + user_id: issue.user_id, + category_id: issue.category_id, + content: issue.content, + sentiment: issue.sentiment, + is_anonymous: issue.is_anonymous, + location_id: locationId, + created_at: issue.created_at, + image_url: issue.image_url || null, + updated_at: new Date().toISOString() + }) + .select(` + *, + user: user_id ( + user_id, + email_address, + username, + fullname, + image_url, + user_score + ), + category: category_id ( + name + ), + location: location_id ( + suburb, + city, + province, + latitude, + longitude + ), + cluster_id + `) + .single(); + if (error) { + console.error("Error inserting issue:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: `An error occurred while inserting the issue: ${error.message}`, + }); + } + return Object.assign(Object.assign({}, data), { reactions: [], user_reaction: null, comment_count: 0, is_owner: true, user: data.is_anonymous + ? { + user_id: null, + email_address: null, + username: "Anonymous", + fullname: "Anonymous", + image_url: null, + total_issues: null, + resolved_issues: null, + user_score: 0, + location_id: null, + location: null + } + : data.user, hasPendingResolution: false, pendingResolutionId: null, resolutions: [], relatedIssuesCount: 0, userHasIssueInCluster: false }); + } + catch (error) { + console.error("Unexpected error in createIssue:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: `An unexpected error occurred: ${error}`, + }); + } + }); + } + updateIssueCluster(issueId, clusterId) { + return __awaiter(this, void 0, void 0, function* () { + const { error } = yield supabaseClient_1.default + .from('issue') + .update({ + cluster_id: clusterId, + updated_at: new Date().toISOString() + }) + .eq('issue_id', issueId); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while updating issue cluster.", + }); + } + }); + } + setIssueEmbedding(issueId, embedding) { + return __awaiter(this, void 0, void 0, function* () { + const { error } = yield supabaseClient_1.default + .from('issue_embeddings') + .insert({ + issue_id: issueId, + content_embedding: embedding, + }); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while updating issue embedding.", + }); + } + }); + } + updateIssue(issueId, issue, user_id) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("issue") + .update(issue) + .eq("issue_id", issueId) + .eq("user_id", user_id) + .select() + .maybeSingle(); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + if (!data) { + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "Issue does not exist", + }); + } + const reactions = yield reactionRepository.getReactionCountsByIssueId(data.issue_id); + return Object.assign(Object.assign({}, data), { reactions, is_owner: true, user: data.is_anonymous + ? { + user_id: null, + email_address: null, + username: "Anonymous", + fullname: "Anonymous", + image_url: null, + } + : data.user }); + }); + } + deleteIssue(issueId, user_id) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("issue") + .delete() + .eq("issue_id", issueId) + .eq("user_id", user_id) + .select() + .maybeSingle(); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + if (!data) { + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "Issue does not exist", + }); + } + }); + } + resolveIssue(issueId, user_id) { + return __awaiter(this, void 0, void 0, function* () { + const resolvedAt = luxon_1.DateTime.now().setZone("UTC+2").toISO(); + const { data, error } = yield supabaseClient_1.default + .from("issue") + .update({ resolved_at: resolvedAt }) + .eq("issue_id", issueId) + .eq("user_id", user_id) + .select() + .maybeSingle(); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + if (!data) { + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "Issue does not exist", + }); + } + return data; + }); + } + getUserIssues(userId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("issue") + .select(` + *, + user: user_id ( + user_id, + email_address, + username, + fullname, + image_url, + user_score + ), + category: category_id ( + name + ), + location: location_id ( + suburb, + city, + province + ) + `) + .eq("user_id", userId) + .order("created_at", { ascending: false }); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + const issues = yield Promise.all(data.map((issue) => __awaiter(this, void 0, void 0, function* () { + const reactions = yield reactionRepository.getReactionCountsByIssueId(issue.issue_id); + const userReaction = yield reactionRepository.getReactionByUserAndIssue(issue.issue_id, userId); + const commentCount = yield commentRepository.getNumComments(issue.issue_id); + return Object.assign(Object.assign({}, issue), { reactions, user_reaction: (userReaction === null || userReaction === void 0 ? void 0 : userReaction.emoji) || null, comment_count: commentCount, is_owner: issue.user_id === userId, user: issue.is_anonymous + ? { + user_id: null, + email_address: null, + username: "Anonymous", + fullname: "Anonymous", + image_url: null, + } + : issue.user }); + }))); + return issues; + }); + } + getUserResolvedIssues(userId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("issue") + .select(` + *, + user: user_id ( + user_id, + email_address, + username, + fullname, + image_url, + user_score + ), + category: category_id ( + name + ), + location: location_id ( + suburb, + city, + province, + latitude, + longitude + ) + `) + .eq("user_id", userId) + .not("resolved_at", "is", null) + .order("created_at", { ascending: false }); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + const issues = yield Promise.all(data.map((issue) => __awaiter(this, void 0, void 0, function* () { + const reactions = yield reactionRepository.getReactionCountsByIssueId(issue.issue_id); + const userReaction = yield reactionRepository.getReactionByUserAndIssue(issue.issue_id, userId); + const commentCount = yield commentRepository.getNumComments(issue.issue_id); + return Object.assign(Object.assign({}, issue), { reactions, user_reaction: (userReaction === null || userReaction === void 0 ? void 0 : userReaction.emoji) || null, comment_count: commentCount, is_owner: issue.user_id === userId, user: issue.is_anonymous + ? { + user_id: null, + email_address: null, + username: "Anonymous", + fullname: "Anonymous", + image_url: null, + } + : issue.user }); + }))); + return issues; + }); + } + updateIssueResolutionStatus(issueId, resolved) { + return __awaiter(this, void 0, void 0, function* () { + const resolvedAt = luxon_1.DateTime.now().setZone("UTC+2").toISO(); + const { error } = yield supabaseClient_1.default + .from('issue') + .update({ + resolved_at: resolved ? resolvedAt : null, + updated_at: new Date().toISOString() + }) + .eq('issue_id', issueId); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while updating the issue resolution status.", + }); + } + }); + } + isIssueResolved(issueId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from('issue') + .select('resolved_at') + .eq('issue_id', issueId) + .single(); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while checking the issue resolution status.", + }); + } + return data.resolved_at !== null; + }); + } + getPendingResolutionForIssue(issueId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from('resolution') + .select('*') + .eq('issue_id', issueId) + .eq('status', 'pending') + .single(); + if (error && error.code !== 'PGRST116') { // PGRST116 is the error code for no rows returned + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while checking for pending resolutions.", + }); + } + return data || null; + }); + } + getResolutionsForIssue(issueId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from('resolution') + .select('*') + .eq('issue_id', issueId) + .order('created_at', { ascending: false }); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching resolutions for the issue.", + }); + } + return data; + }); + } + userHasIssueInCluster(userId, clusterId) { + return __awaiter(this, void 0, void 0, function* () { + if (!clusterId) + return false; + const { count } = yield supabaseClient_1.default + .from('issue') + .select('*', { count: 'exact', head: true }) + .eq('user_id', userId) + .eq('cluster_id', clusterId); + return (count || 0) > 0; + }); + } + hasUserIssuesInCluster(userId, clusterId) { + return __awaiter(this, void 0, void 0, function* () { + const { count, error } = yield supabaseClient_1.default + .from('issue') + .select('*', { count: 'exact', head: true }) + .eq('user_id', userId) + .eq('cluster_id', clusterId); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while checking user issues in the cluster.", + }); + } + return count !== null && count > 0; + }); + } + getUserIssueInCluster(userId, clusterId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("issue") + .select("*") + .eq("user_id", userId) + .eq("cluster_id", clusterId) + .single(); + if (error) { + console.error("Error fetching user's issue in cluster:", error); + throw error; + } + return data; + }); + } + getIssuesInCluster(clusterId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from('issue') + .select('*') + .eq('cluster_id', clusterId); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching issues in the cluster.", + }); + } + return data; + }); + } +} +exports.default = IssueRepository; diff --git a/backend/public/modules/issues/routes/issueRoutes.js b/backend/public/modules/issues/routes/issueRoutes.js new file mode 100644 index 00000000..dcc99bef --- /dev/null +++ b/backend/public/modules/issues/routes/issueRoutes.js @@ -0,0 +1,43 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const issueController = __importStar(require("@/modules/issues/controllers/issueController")); +const middleware_1 = require("@/middleware/middleware"); +const router = (0, express_1.Router)(); +router.use(middleware_1.verifyAndGetUser); +router.post("/", issueController.getIssues); +router.post("/single", issueController.getIssueById); +router.post("/create", issueController.createIssue); +router.put("/", issueController.updateIssue); +router.delete("/", issueController.deleteIssue); +router.post("/user", issueController.getUserIssues); +router.post("/user/resolved", issueController.getUserResolvedIssues); +router.post("/self-resolution", issueController.createSelfResolution); +router.post("/external-resolution", issueController.createExternalResolution); +router.post("/respond-resolution", issueController.respondToResolution); +router.post("/user-resolutions", issueController.getUserResolutions); +router.post("/delete-resolution", issueController.deleteResolution); +exports.default = router; diff --git a/backend/public/modules/issues/services/issueService.js b/backend/public/modules/issues/services/issueService.js new file mode 100644 index 00000000..d68b41cb --- /dev/null +++ b/backend/public/modules/issues/services/issueService.js @@ -0,0 +1,630 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const issueRepository_1 = __importDefault(require("@/modules/issues/repositories/issueRepository")); +const response_1 = require("@/types/response"); +const locationRepository_1 = require("@/modules/locations/repositories/locationRepository"); +const supabaseClient_1 = __importDefault(require("@/modules/shared/services/supabaseClient")); +const pointsService_1 = require("@/modules/points/services/pointsService"); +const clusterService_1 = require("@/modules/clusters/services/clusterService"); +const openAIService_1 = require("@/modules/shared/services/openAIService"); +const resolutionService_1 = require("@/modules/resolutions/services/resolutionService"); +const reactionRepository_1 = __importDefault(require("@/modules/reactions/repositories/reactionRepository")); +const commentRepository_1 = require("@/modules/comments/repositories/commentRepository"); +class IssueService { + constructor() { + this.issueRepository = new issueRepository_1.default(); + this.locationRepository = new locationRepository_1.LocationRepository(); + this.pointsService = new pointsService_1.PointsService(); + this.clusterService = new clusterService_1.ClusterService(); + this.openAIService = new openAIService_1.OpenAIService(); + this.resolutionService = new resolutionService_1.ResolutionService(); + this.reactionRepository = new reactionRepository_1.default(); + this.commentRepository = new commentRepository_1.CommentRepository(); + } + setIssueRepository(issueRepository) { + this.issueRepository = issueRepository; + } + setLocationRepository(locationRepository) { + this.locationRepository = locationRepository; + } + setPointsService(pointsService) { + this.pointsService = pointsService; + } + setClusterService(clusterService) { + this.clusterService = clusterService; + } + setOpenAIService(openAIService) { + this.openAIService = openAIService; + } + setResolutionService(resolutionService) { + this.resolutionService = resolutionService; + } + setReactionRepository(reactionRepository) { + this.reactionRepository = reactionRepository; + } + setCommentRepository(commentRepository) { + this.commentRepository = commentRepository; + } + getIssues(params) { + return __awaiter(this, void 0, void 0, function* () { + if (params.from === undefined || !params.amount) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Missing required fields for getting issues", + }); + } + const issues = yield this.issueRepository.getIssues(params); + const issuesWithUserInfo = issues.map((issue) => { + const isOwner = issue.user_id === params.user_id; + if (issue.is_anonymous) { + issue.user = { + user_id: null, + email_address: null, + username: "Anonymous", + fullname: "Anonymous", + image_url: null, + is_owner: false, + total_issues: null, + resolved_issues: null, + user_score: 0, + location_id: null, + location: null + }; + } + return Object.assign(Object.assign({}, issue), { is_owner: isOwner }); + }); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: issuesWithUserInfo, + }); + }); + } + getIssueById(issue) { + return __awaiter(this, void 0, void 0, function* () { + const issue_id = issue.issue_id; + if (!issue_id) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Missing required fields for getting an issue", + }); + } + const resIssue = yield this.issueRepository.getIssueById(issue_id, issue.user_id); + const isOwner = resIssue.user_id === issue.user_id; + if (resIssue.cluster_id) { + const clusterInfo = yield this.clusterService.getClusterById(resIssue.cluster_id); + resIssue.cluster = clusterInfo; + } + if (resIssue.is_anonymous) { + resIssue.user = { + user_id: null, + email_address: null, + username: "Anonymous", + fullname: "Anonymous", + image_url: null, + is_owner: false, + total_issues: null, + resolved_issues: null, + user_score: 0, + location_id: null, + location: null + }; + } + return (0, response_1.APIData)({ + code: 200, + success: true, + data: Object.assign(Object.assign({}, resIssue), { is_owner: isOwner }), + }); + }); + } + createIssue(issue, image) { + return __awaiter(this, void 0, void 0, function* () { + if (!issue.user_id) { + throw (0, response_1.APIError)({ + code: 401, + success: false, + error: "You need to be signed in to create an issue", + }); + } + if (!issue.category_id || !issue.content) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Missing required fields for creating an issue", + }); + } + if (issue.content.length > 500) { + throw (0, response_1.APIError)({ + code: 413, + success: false, + error: "Issue content exceeds the maximum length of 500 characters", + }); + } + let imageUrl = null; + if (image) { + const fileName = `${issue.user_id}_${Date.now()}-${image.originalname}`; + const { error } = yield supabaseClient_1.default.storage + .from("issues") + .upload(fileName, image.buffer); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while uploading the image. Please try again.", + }); + } + const { data: urlData } = supabaseClient_1.default.storage + .from("issues") + .getPublicUrl(fileName); + imageUrl = urlData.publicUrl; + } + delete issue.issue_id; + const createdIssue = yield this.issueRepository.createIssue(Object.assign(Object.assign({}, issue), { image_url: imageUrl })); + this.processIssueAsync(createdIssue); + const isFirstIssue = yield this.pointsService.getFirstTimeAction(issue.user_id, "created first issue"); + const points = isFirstIssue ? 50 : 20; + yield this.pointsService.awardPoints(issue.user_id, points, isFirstIssue ? "created first issue" : "created an issue"); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: createdIssue, + }); + }); + } + processIssueAsync(issue) { + return __awaiter(this, void 0, void 0, function* () { + try { + const embedding = yield this.openAIService.getEmbedding(issue.content); + yield this.issueRepository.setIssueEmbedding(issue.issue_id, embedding); + issue.content_embedding = embedding; + yield this.clusterService.assignClusterToIssue(issue); + } + catch (error) { + console.error(`Error processing issue ${issue}:`, error); + } + }); + } + updateIssue(issue) { + return __awaiter(this, void 0, void 0, function* () { + const user_id = issue.user_id; + if (!user_id) { + throw (0, response_1.APIError)({ + code: 401, + success: false, + error: "You need to be signed in to update an issue", + }); + } + const issue_id = issue.issue_id; + if (!issue_id) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Missing required fields for updating an issue", + }); + } + if (issue.created_at || issue.resolved_at) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Cannot change the time an issue was created or resolved", + }); + } + delete issue.user_id; + delete issue.issue_id; + const updatedIssue = yield this.issueRepository.updateIssue(issue_id, issue, user_id); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: updatedIssue, + }); + }); + } + deleteIssue(issue) { + return __awaiter(this, void 0, void 0, function* () { + const user_id = issue.user_id; + if (!user_id) { + throw (0, response_1.APIError)({ + code: 401, + success: false, + error: "You need to be signed in to delete an issue", + }); + } + const issue_id = issue.issue_id; + if (!issue_id) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Missing required fields for deleting an issue", + }); + } + const issueToDelete = yield this.issueRepository.getIssueById(issue_id, user_id); + if (issueToDelete.image_url) { + const imageName = issueToDelete.image_url.split("/").slice(-1)[0]; + const { error } = yield supabaseClient_1.default.storage + .from("issues") + .remove([imageName]); + if (error) { + console.error("Failed to delete image from storage:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while deleting the image. Please try again.", + }); + } + } + if (issueToDelete.cluster_id) { + // No await so it runs without blocking + this.clusterService.removeIssueFromCluster(issue_id, issueToDelete.cluster_id); + } + yield this.issueRepository.deleteIssue(issue_id, user_id); + return (0, response_1.APIData)({ + code: 204, + success: true, + }); + }); + } + resolveIssue(issue) { + return __awaiter(this, void 0, void 0, function* () { + const user_id = issue.user_id; + const issue_id = issue.issue_id; + if (!user_id || !issue_id) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Missing required fields for resolving an issue", + }); + } + return this.createSelfResolution(issue_id, user_id, "Issue resolved by owner"); + }); + } + createSelfResolution(issueId, userId, resolutionText, proofImage) { + return __awaiter(this, void 0, void 0, function* () { + try { + //console.log(`Starting createSelfResolution for issue ${issueId} by user ${userId}`); + const issue = yield this.issueRepository.getIssueById(issueId); + if (issue.resolved_at) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "This issue has already been resolved.", + }); + } + if (issue.user_id !== userId) { + throw (0, response_1.APIError)({ + code: 403, + success: false, + error: "You can only create a self-resolution for your own issues.", + }); + } + let numClusterMembers = 1; + if (issue.cluster_id) { + const cluster = yield this.clusterService.getClusterById(issue.cluster_id); + numClusterMembers = cluster.issue_count; + } + let imageUrl = null; + if (proofImage) { + //console.log(`Uploading proof image`); + const fileName = `${userId}_${Date.now()}-${proofImage.originalname}`; + const { error } = yield supabaseClient_1.default.storage + .from("resolutions") + .upload(fileName, proofImage.buffer); + if (error) { + console.error("Image upload error:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while uploading the image. Please try again.", + }); + } + const { data: urlData } = supabaseClient_1.default.storage + .from("resolutions") + .getPublicUrl(fileName); + imageUrl = urlData.publicUrl; + //console.log(`Image uploaded successfully. URL: ${imageUrl}`); + } + //console.log(`Creating resolution`); + const resolution = yield this.resolutionService.createResolution({ + issue_id: issueId, + resolver_id: userId, + resolution_text: resolutionText, + proof_image: imageUrl, + resolution_source: 'self', + num_cluster_members: numClusterMembers, + political_association: null, + state_entity_association: null, + resolved_by: null + }); + //console.log(`Returning successful response`); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: resolution, + }); + } + catch (error) { + console.error("Error in createSelfResolution:", error); + if (error instanceof response_1.APIError) { + throw error; + } + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while creating a self-resolution.", + }); + } + }); + } + createExternalResolution(issueId, userId, resolutionText, proofImage, politicalAssociation, stateEntityAssociation, resolvedBy) { + return __awaiter(this, void 0, void 0, function* () { + try { + const issue = yield this.issueRepository.getIssueById(issueId); + if (issue.resolved_at) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "This issue has already been resolved.", + }); + } + let numClusterMembers = 1; + if (issue.cluster_id) { + const cluster = yield this.clusterService.getClusterById(issue.cluster_id); + numClusterMembers = cluster.issue_count; + } + let imageUrl = null; + if (proofImage) { + const fileName = `${userId}_${Date.now()}-${proofImage.originalname}`; + const { error } = yield supabaseClient_1.default.storage + .from("resolutions") + .upload(fileName, proofImage.buffer); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while uploading the image. Please try again.", + }); + } + const { data: urlData } = supabaseClient_1.default.storage + .from("resolutions") + .getPublicUrl(fileName); + imageUrl = urlData.publicUrl; + } + const resolution = yield this.resolutionService.createResolution({ + issue_id: issueId, + resolver_id: userId, + resolution_text: resolutionText, + proof_image: imageUrl, + resolution_source: resolvedBy ? 'other' : 'unknown', + num_cluster_members: numClusterMembers, + political_association: politicalAssociation || null, + state_entity_association: stateEntityAssociation || null, + resolved_by: resolvedBy || null + }); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: resolution, + }); + } + catch (error) { + if (error instanceof response_1.APIError) { + throw error; + } + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while creating an external resolution.", + }); + } + }); + } + respondToResolution(resolutionId, userId, accept) { + return __awaiter(this, void 0, void 0, function* () { + try { + const resolution = yield this.resolutionService.updateResolutionStatus(resolutionId, accept ? 'accepted' : 'declined', userId); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: resolution, + }); + } + catch (error) { + if (error instanceof response_1.APIError) { + throw error; + } + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while responding to the resolution.", + }); + } + }); + } + getUserIssues(issue) { + return __awaiter(this, void 0, void 0, function* () { + const userId = issue.profile_user_id; + if (!userId) { + throw (0, response_1.APIError)({ + code: 401, + success: false, + error: "Missing profile user ID", + }); + } + const issues = yield this.issueRepository.getUserIssues(userId); + const issuesWithUserInfo = issues.map((issue) => { + const isOwner = issue.user_id === userId; + if (issue.is_anonymous) { + issue.user = { + user_id: null, + email_address: null, + username: "Anonymous", + fullname: "Anonymous", + image_url: null, + is_owner: false, + total_issues: null, + resolved_issues: null, + user_score: 0, + location_id: null, + location: null + }; + } + return Object.assign(Object.assign({}, issue), { is_owner: isOwner }); + }); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: issuesWithUserInfo, + }); + }); + } + getUserResolvedIssues(issue) { + return __awaiter(this, void 0, void 0, function* () { + const userId = issue.profile_user_id; + if (!userId) { + throw (0, response_1.APIError)({ + code: 401, + success: false, + error: "Missing profile user ID", + }); + } + const resolvedIssues = yield this.issueRepository.getUserResolvedIssues(userId); + const issuesWithUserInfo = resolvedIssues.map((issue) => { + const isOwner = issue.user_id === userId; + if (issue.is_anonymous) { + issue.user = { + user_id: null, + email_address: null, + username: "Anonymous", + fullname: "Anonymous", + image_url: null, + is_owner: false, + total_issues: null, + resolved_issues: null, + user_score: 0, + location_id: null, + location: null + }; + } + return Object.assign(Object.assign({}, issue), { is_owner: isOwner }); + }); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: issuesWithUserInfo, + }); + }); + } + hasUserIssuesInCluster(userId, clusterId) { + return __awaiter(this, void 0, void 0, function* () { + try { + const hasIssues = yield this.issueRepository.hasUserIssuesInCluster(userId, clusterId); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: hasIssues, + }); + } + catch (error) { + if (error instanceof response_1.APIError) { + throw error; + } + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while checking user issues in the cluster.", + }); + } + }); + } + getResolutionsForIssue(issueId) { + return __awaiter(this, void 0, void 0, function* () { + try { + const resolutions = yield this.issueRepository.getResolutionsForIssue(issueId); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: resolutions, + }); + } + catch (error) { + if (error instanceof response_1.APIError) { + throw error; + } + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching resolutions for the issue.", + }); + } + }); + } + getUserIssueInCluster(user_id, clusterId) { + return __awaiter(this, void 0, void 0, function* () { + const issue = yield this.issueRepository.getUserIssueInCluster(user_id, clusterId); + return issue; + }); + } + getUserResolutions(userId) { + return __awaiter(this, void 0, void 0, function* () { + return this.resolutionService.getUserResolutions(userId); + }); + } + deleteResolution(resolutionId, userId) { + return __awaiter(this, void 0, void 0, function* () { + yield this.resolutionService.deleteResolution(resolutionId, userId); + }); + } + getRelatedIssues(issueId, userId) { + return __awaiter(this, void 0, void 0, function* () { + try { + const issue = yield this.issueRepository.getIssueById(issueId); + if (!issue.cluster_id) { + return (0, response_1.APIData)({ + code: 200, + success: true, + data: [], + }); + } + const relatedIssues = yield this.issueRepository.getIssuesInCluster(issue.cluster_id); + const processedIssues = yield Promise.all(relatedIssues + .filter(relatedIssue => relatedIssue.issue_id !== issueId) + .map((relatedIssue) => __awaiter(this, void 0, void 0, function* () { + // Use the existing getIssueById method to get full issue details + const fullIssue = yield this.issueRepository.getIssueById(relatedIssue.issue_id, userId); + // Add any additional properties not included in getIssueById + const processedIssue = Object.assign(Object.assign({}, fullIssue), { is_owner: fullIssue.user_id === userId }); + return processedIssue; + }))); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: processedIssues, + }); + } + catch (error) { + console.error("Error in getRelatedIssues:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching related issues.", + }); + } + }); + } +} +exports.default = IssueService; diff --git a/backend/public/modules/locations/controllers/locationController.js b/backend/public/modules/locations/controllers/locationController.js new file mode 100644 index 00000000..12571d6c --- /dev/null +++ b/backend/public/modules/locations/controllers/locationController.js @@ -0,0 +1,41 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getLocationById = exports.getAllLocations = void 0; +const locationService_1 = require("@/modules/locations/services/locationService"); +const response_1 = require("@/utilities/response"); +const cacheMiddleware_1 = require("@/middleware/cacheMiddleware"); +const locationService = new locationService_1.LocationService(); +exports.getAllLocations = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield locationService.getAllLocations(); + (0, response_1.sendResponse)(res, response); + } + catch (err) { + (0, response_1.sendResponse)(res, err); + } + }) +]; +exports.getLocationById = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const locationId = parseInt(req.params.id); + const response = yield locationService.getLocationById(locationId); + (0, response_1.sendResponse)(res, response); + } + catch (err) { + (0, response_1.sendResponse)(res, err); + } + }) +]; diff --git a/backend/public/modules/locations/repositories/locationRepository.js b/backend/public/modules/locations/repositories/locationRepository.js new file mode 100644 index 00000000..bc233ce9 --- /dev/null +++ b/backend/public/modules/locations/repositories/locationRepository.js @@ -0,0 +1,126 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.LocationRepository = void 0; +const supabaseClient_1 = __importDefault(require("@/modules/shared/services/supabaseClient")); +const response_1 = require("@/types/response"); +class LocationRepository { + getAllLocations() { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("location") + .select("*") + .order("province", { ascending: true }) + .order("city", { ascending: true }) + .order("suburb", { ascending: true }); + if (error) { + console.error("Error fetching locations:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching locations.", + }); + } + const uniqueLocations = Array.from(new Set(data.map((loc) => JSON.stringify({ + province: loc.province, + city: loc.city, + suburb: loc.suburb, + })))).map((strLoc) => { + const parsedLoc = JSON.parse(strLoc); + return data.find((loc) => loc.province === parsedLoc.province && + loc.city === parsedLoc.city && + loc.suburb === parsedLoc.suburb); + }); + return uniqueLocations; + }); + } + getLocationByPlacesId(placesId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("location") + .select("*") + .eq("place_id", placesId); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching location.", + }); + } + return data; + }); + } + createLocation(location) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("location") + .insert(Object.assign(Object.assign({}, location), { latitude: location.latitude, longitude: location.longitude })) + .select() + .single(); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + return data; + }); + } + getLocationById(locationId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("location") + .select("*") + .eq("location_id", locationId) + .single(); + if (error) { + console.error("Error fetching location by ID:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching the location.", + }); + } + return data; + }); + } + getLocationIds(filter) { + return __awaiter(this, void 0, void 0, function* () { + let query = supabaseClient_1.default.from("location").select("location_id"); + if (filter.province) { + query = query.eq("province", filter.province); + } + if (filter.city) { + query = query.eq("city", filter.city); + } + if (filter.suburb) { + query = query.eq("suburb", filter.suburb); + } + const { data, error } = yield query; + if (error) { + console.error("Error fetching location IDs:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching location IDs.", + }); + } + return data.map(loc => loc.location_id); + }); + } +} +exports.LocationRepository = LocationRepository; diff --git a/backend/public/modules/locations/routes/locationRoutes.js b/backend/public/modules/locations/routes/locationRoutes.js new file mode 100644 index 00000000..3739376f --- /dev/null +++ b/backend/public/modules/locations/routes/locationRoutes.js @@ -0,0 +1,11 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = __importDefault(require("express")); +const locationController_1 = require("@/modules/locations/controllers/locationController"); +const router = express_1.default.Router(); +router.post("/", locationController_1.getAllLocations); +router.get("/:id", locationController_1.getLocationById); +exports.default = router; diff --git a/backend/public/modules/locations/services/locationService.js b/backend/public/modules/locations/services/locationService.js new file mode 100644 index 00000000..54f02f6f --- /dev/null +++ b/backend/public/modules/locations/services/locationService.js @@ -0,0 +1,52 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.LocationService = void 0; +const locationRepository_1 = require("@/modules/locations/repositories/locationRepository"); +const response_1 = require("@/types/response"); +class LocationService { + constructor() { + this.locationRepository = new locationRepository_1.LocationRepository(); + } + getAllLocations() { + return __awaiter(this, void 0, void 0, function* () { + const locations = yield this.locationRepository.getAllLocations(); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: locations, + }); + }); + } + getLocationById(locationId) { + return __awaiter(this, void 0, void 0, function* () { + const location = yield this.locationRepository.getLocationById(locationId); + if (!location) { + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "Location not found", + }); + } + return (0, response_1.APIData)({ + code: 200, + success: true, + data: location, + }); + }); + } + getLocationIds(filter) { + return __awaiter(this, void 0, void 0, function* () { + return this.locationRepository.getLocationIds(filter); + }); + } +} +exports.LocationService = LocationService; diff --git a/backend/public/modules/organizations/controllers/organizationController.js b/backend/public/modules/organizations/controllers/organizationController.js new file mode 100644 index 00000000..6008727f --- /dev/null +++ b/backend/public/modules/organizations/controllers/organizationController.js @@ -0,0 +1,359 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.searchOrganizations = exports.deleteJoinRequest = exports.leaveOrganization = exports.getUserOrganizations = exports.generateReport = exports.getOrganizationById = exports.getOrganizations = exports.removeMember = exports.handleJoinRequest = exports.getJoinRequests = exports.setJoinPolicy = exports.joinOrganization = exports.deleteOrganization = exports.updateOrganization = exports.createOrganization = void 0; +const organizationService_1 = require("../services/organizationService"); +const response_1 = require("@/utilities/response"); +const response_2 = require("@/types/response"); +const multer_1 = __importDefault(require("multer")); +const cacheMiddleware_1 = require("@/middleware/cacheMiddleware"); +const cacheUtils_1 = require("@/utilities/cacheUtils"); +const organizationService = new organizationService_1.OrganizationService(); +const upload = (0, multer_1.default)({ storage: multer_1.default.memoryStorage() }); +const createOrganization = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const userId = req.body.user_id; + if (!userId) { + return (0, response_1.sendResponse)(res, (0, response_2.APIError)({ + code: 401, + success: false, + error: "Unauthorized: User ID is missing", + })); + } + const response = yield organizationService.createOrganization(req.body, userId); + (0, cacheUtils_1.clearCachePattern)('__express__/api/organizations*'); + (0, response_1.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } +}); +exports.createOrganization = createOrganization; +exports.updateOrganization = [ + upload.single("profilePhoto"), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const userId = req.body.user_id; + const organizationId = req.params.id; + if (!userId) { + return (0, response_1.sendResponse)(res, (0, response_2.APIError)({ + code: 401, + success: false, + error: "Unauthorized: User ID is missing", + })); + } + const response = yield organizationService.updateOrganization(organizationId, req.body, userId, req.file); + (0, cacheUtils_1.clearCachePattern)(`__express__/api/organizations/${organizationId}`); + (0, response_1.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } + }) +]; +const deleteOrganization = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const userId = req.body.user_id; + const organizationId = req.params.id; + if (!userId) { + return (0, response_1.sendResponse)(res, (0, response_2.APIError)({ + code: 401, + success: false, + error: "Unauthorized: User ID is missing", + })); + } + const response = yield organizationService.deleteOrganization(organizationId, userId); + (0, cacheUtils_1.clearCachePattern)(`__express__/api/organizations/${organizationId}`); + (0, response_1.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } +}); +exports.deleteOrganization = deleteOrganization; +const joinOrganization = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const userId = req.body.user_id; + const organizationId = req.params.id; + if (!userId) { + return (0, response_1.sendResponse)(res, (0, response_2.APIError)({ + code: 401, + success: false, + error: "Unauthorized: User ID is missing", + })); + } + const response = yield organizationService.joinOrganization(organizationId, userId); + (0, cacheUtils_1.clearCachePattern)(`__express__/api/organizations/${organizationId}/join-requests`); + (0, response_1.sendResponse)(res, response); + } + catch (err) { + console.error("Error in joinOrganization controller:", err); + handleError(res, err); + } +}); +exports.joinOrganization = joinOrganization; +const setJoinPolicy = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const userId = req.body.user_id; + const organizationId = req.params.id; + const { joinPolicy } = req.body; + if (!userId) { + return (0, response_1.sendResponse)(res, (0, response_2.APIError)({ + code: 401, + success: false, + error: "Unauthorized: User ID is missing", + })); + } + const response = yield organizationService.setJoinPolicy(organizationId, joinPolicy, userId); + (0, cacheUtils_1.clearCachePattern)(`__express__/api/organizations/${organizationId}`); + (0, response_1.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } +}); +exports.setJoinPolicy = setJoinPolicy; +exports.getJoinRequests = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const userId = req.body.user_id; + const organizationId = req.params.id; + const { offset, limit } = req.query; + if (!userId) { + return (0, response_1.sendResponse)(res, (0, response_2.APIError)({ + code: 401, + success: false, + error: "Unauthorized: User ID is missing", + })); + } + const paginationParams = { + offset: Number(offset) || 0, + limit: Number(limit) || 10 + }; + const response = yield organizationService.getJoinRequests(organizationId, userId, paginationParams); + (0, response_1.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } + }) +]; +const handleJoinRequest = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const userId = req.body.user_id; + const organizationId = req.params.id; + const requestId = parseInt(req.params.requestId, 10); + const { accept } = req.body; + if (!userId) { + return (0, response_1.sendResponse)(res, (0, response_2.APIError)({ + code: 401, + success: false, + error: "Unauthorized: User ID is missing", + })); + } + if (isNaN(requestId)) { + return (0, response_1.sendResponse)(res, (0, response_2.APIError)({ + code: 400, + success: false, + error: "Invalid request ID", + })); + } + if (typeof accept !== 'boolean') { + return (0, response_1.sendResponse)(res, (0, response_2.APIError)({ + code: 400, + success: false, + error: "Accept must be a boolean value", + })); + } + const response = yield organizationService.handleJoinRequest(organizationId, requestId, accept, userId); + (0, cacheUtils_1.clearCachePattern)(`__express__/api/organizations/${organizationId}/join-requests`); + (0, response_1.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } +}); +exports.handleJoinRequest = handleJoinRequest; +const removeMember = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const adminId = req.body.user_id; + const organizationId = req.params.id; + const memberUserId = req.params.userId; + if (!adminId) { + return (0, response_1.sendResponse)(res, (0, response_2.APIError)({ + code: 401, + success: false, + error: "Unauthorized: User ID is missing", + })); + } + const response = yield organizationService.removeMember(organizationId, memberUserId, adminId); + (0, cacheUtils_1.clearCachePattern)(`__express__/api/organizations/${organizationId}`); + (0, response_1.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } +}); +exports.removeMember = removeMember; +exports.getOrganizations = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const { offset, limit } = req.query; + const paginationParams = { + offset: Number(offset) || 0, + limit: Number(limit) || 10 + }; + const response = yield organizationService.getOrganizations(paginationParams); + (0, response_1.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } + }) +]; +exports.getOrganizationById = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const organizationId = req.params.id; + const response = yield organizationService.getOrganizationById(organizationId); + (0, response_1.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } + }) +]; +const generateReport = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const userId = req.body.user_id; + const organizationId = req.params.id; + if (!userId) { + return (0, response_1.sendResponse)(res, (0, response_2.APIError)({ + code: 401, + success: false, + error: "Unauthorized: User ID is missing", + })); + } + const response = yield organizationService.generateReport(organizationId, userId); + (0, response_1.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } +}); +exports.generateReport = generateReport; +exports.getUserOrganizations = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const userId = req.body.user_id; + if (!userId) { + return (0, response_1.sendResponse)(res, (0, response_2.APIError)({ + code: 401, + success: false, + error: "Unauthorized: User ID is missing", + })); + } + const response = yield organizationService.getUserOrganizations(userId); + (0, response_1.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } + }) +]; +const leaveOrganization = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const userId = req.body.user_id; + const organizationId = req.params.id; + if (!userId) { + return (0, response_1.sendResponse)(res, (0, response_2.APIError)({ + code: 401, + success: false, + error: "Unauthorized: User ID is missing", + })); + } + const response = yield organizationService.leaveOrganization(organizationId, userId); + (0, cacheUtils_1.clearCachePattern)(`__express__/api/organizations/${organizationId}`); + (0, response_1.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } +}); +exports.leaveOrganization = leaveOrganization; +const deleteJoinRequest = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const userId = req.body.user_id; + const requestId = Number(req.params.requestId); + if (!userId) { + return (0, response_1.sendResponse)(res, (0, response_2.APIError)({ + code: 401, + success: false, + error: "Unauthorized: User ID is missing", + })); + } + const response = yield organizationService.deleteJoinRequest(requestId, userId); + (0, cacheUtils_1.clearCachePattern)(`__express__/api/organizations/*/join-requests`); + (0, response_1.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } +}); +exports.deleteJoinRequest = deleteJoinRequest; +const handleError = (res, err) => { + console.error("Handling error:", err); + if (err instanceof Error) { + (0, response_1.sendResponse)(res, (0, response_2.APIData)({ + code: 500, + success: false, + error: err.message || "An unexpected error occurred", + })); + } + else { + (0, response_1.sendResponse)(res, (0, response_2.APIData)({ + code: 500, + success: false, + error: "An unexpected error occurred", + })); + } +}; +exports.searchOrganizations = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const { searchTerm, offset, limit } = req.query; + if (typeof searchTerm !== 'string') { + return (0, response_1.sendResponse)(res, (0, response_2.APIError)({ + code: 400, + success: false, + error: "Search term is required and must be a string", + })); + } + const paginationParams = { + offset: Number(offset) || 0, + limit: Number(limit) || 10 + }; + const response = yield organizationService.searchOrganizations(searchTerm, paginationParams); + (0, response_1.sendResponse)(res, response); + } + catch (err) { + handleError(res, err); + } + }) +]; diff --git a/backend/public/modules/organizations/repositories/organizationRepository.js b/backend/public/modules/organizations/repositories/organizationRepository.js new file mode 100644 index 00000000..577e7bbc --- /dev/null +++ b/backend/public/modules/organizations/repositories/organizationRepository.js @@ -0,0 +1,548 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.OrganizationRepository = void 0; +const supabaseClient_1 = __importDefault(require("@/modules/shared/services/supabaseClient")); +const response_1 = require("@/types/response"); +class OrganizationRepository { + createOrganization(organization) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("organizations") + .insert({ + name: organization.name, + username: organization.username, + bio: organization.bio, + website_url: organization.website_url, + join_policy: organization.join_policy, + created_at: new Date().toISOString(), + verified_status: false, + points: 0 + }) + .select() + .single(); + if (error) { + console.error("Error creating organization:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while creating the organization.", + }); + } + return data; + }); + } + updateOrganization(id, updates, profilePhoto) { + return __awaiter(this, void 0, void 0, function* () { + const safeUpdates = Object.assign({}, updates); + delete safeUpdates.id; + delete safeUpdates.created_at; + delete safeUpdates.verified_status; + delete safeUpdates.points; + if (profilePhoto) { + const fileName = `${id}_${Date.now()}-${profilePhoto.originalname}`; + const { error: uploadError } = yield supabaseClient_1.default.storage + .from("organizations") + .upload(fileName, profilePhoto.buffer); + if (uploadError) { + console.error("Error uploading profile photo:", uploadError); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while uploading the profile photo.", + }); + } + const { data: urlData } = supabaseClient_1.default.storage + .from("organizations") + .getPublicUrl(fileName); + safeUpdates.profile_photo = urlData.publicUrl; + } + const { data, error } = yield supabaseClient_1.default + .from("organizations") + .update(safeUpdates) + .eq('id', id) + .select() + .single(); + if (error) { + console.error("Error updating organization:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while updating the organization.", + }); + } + return data; + }); + } + deleteOrganization(id) { + return __awaiter(this, void 0, void 0, function* () { + const { error } = yield supabaseClient_1.default + .from("organizations") + .delete() + .eq('id', id); + if (error) { + console.error("Error deleting organization:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while deleting the organization.", + }); + } + }); + } + isUserAdmin(organizationId, userId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("organization_members") + .select('role') + .eq('organization_id', organizationId) + .eq('user_id', userId) + .single(); + if (error) { + if (error.code === 'PGRST116') { + return null; + } + console.error("Error checking user role:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while checking user role.", + }); + } + return (data === null || data === void 0 ? void 0 : data.role) === 'admin'; + }); + } + addOrganizationMember(member) { + return __awaiter(this, void 0, void 0, function* () { + const { count, error: countError } = yield supabaseClient_1.default + .from("organization_members") + .select("*", { count: "exact", head: true }) + .eq("user_id", member.user_id); + if (countError) { + console.error("Error checking user's organization count:", countError); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while checking user's organization count.", + }); + } + if (count && count >= 5) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "You have reached the maximum number of organizations you can join (5).", + }); + } + const { data, error } = yield supabaseClient_1.default + .from("organization_members") + .insert(member) + .select() + .single(); + if (error) { + console.error("Error adding organization member:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while adding the organization member.", + }); + } + return data; + }); + } + getOrganizationJoinPolicy(organizationId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("organizations") + .select('join_policy') + .eq('id', organizationId) + .single(); + if (error) { + console.error("Error getting organization join policy:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while getting the organization join policy.", + }); + } + return data.join_policy; + }); + } + updateOrganizationJoinPolicy(organizationId, joinPolicy) { + return __awaiter(this, void 0, void 0, function* () { + const { error } = yield supabaseClient_1.default + .from("organizations") + .update({ join_policy: joinPolicy }) + .eq('id', organizationId); + if (error) { + console.error("Error updating organization join policy:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while updating the organization join policy.", + }); + } + }); + } + removeMember(organizationId, userId) { + return __awaiter(this, void 0, void 0, function* () { + const { error } = yield supabaseClient_1.default + .from("organization_members") + .delete() + .eq('organization_id', organizationId) + .eq('user_id', userId); + if (error) { + console.error("Error removing member:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while removing the member.", + }); + } + }); + } + getOrganizationById(id) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("organizations") + .select('*') + .eq('id', id) + .single(); + if (error) { + console.error("Error getting organization:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while getting the organization.", + }); + } + return data; + }); + } + getOrganizationMembers(organizationId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("organization_members") + .select('*') + .eq('organization_id', organizationId); + if (error) { + console.error("Error getting organization members:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while getting organization members.", + }); + } + return data; + }); + } + isMember(organizationId, userId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("organization_members") + .select('id') + .eq('organization_id', organizationId) + .eq('user_id', userId) + .single(); + if (error && error.code !== 'PGRST116') { + console.error("Error checking membership:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while checking membership.", + }); + } + return !!data; + }); + } + createJoinRequest(organizationId, userId) { + return __awaiter(this, void 0, void 0, function* () { + const { count, error: countError } = yield supabaseClient_1.default + .from("join_requests") + .select("*", { count: "exact", head: true }) + .eq("user_id", userId) + .eq("status", "pending"); + if (countError) { + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while checking existing join requests.", + }); + } + if (count && count >= 5) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "You have reached the maximum number of pending join requests.", + }); + } + const { data, error } = yield supabaseClient_1.default + .from("join_requests") + .insert({ + organization_id: organizationId, + user_id: userId, + status: "pending", + created_at: new Date().toISOString(), + }) + .select() + .single(); + if (error) { + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while creating the join request.", + }); + } + return data; + }); + } + getJoinRequests(organizationId, params) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error, count } = yield supabaseClient_1.default + .from("join_requests") + .select("*", { count: "exact" }) + .eq("organization_id", organizationId) + .eq("status", "pending") + .range(params.offset, params.offset + params.limit - 1); + if (error) { + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while fetching join requests.", + }); + } + return { data: data, total: count || 0 }; + }); + } + getJoinRequestById(requestId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("join_requests") + .select('*') + .eq('id', requestId) + .single(); + if (error) { + console.error("Error getting join request:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while getting the join request.", + }); + } + return data; + }); + } + updateJoinRequestStatus(requestId, status) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("join_requests") + .update({ status, updated_at: new Date().toISOString() }) + .eq("id", requestId) + .select() + .single(); + if (error) { + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while updating the join request status.", + }); + } + return data; + }); + } + deleteJoinRequest(requestId, userId) { + return __awaiter(this, void 0, void 0, function* () { + const { error } = yield supabaseClient_1.default + .from("join_requests") + .delete() + .eq("id", requestId) + .eq("user_id", userId); + if (error) { + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while deleting the join request.", + }); + } + }); + } + getUserOrganizations(userId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("organization_members") + .select(` + organizations ( + id, + created_at, + name, + username, + bio, + website_url, + verified_status, + join_policy, + points, + profile_photo + ) + `) + .eq("user_id", userId); + if (error) { + console.error("Error fetching user organizations:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while fetching user organizations.", + }); + } + const organizations = data.flatMap(item => item.organizations); + return organizations; + }); + } + getOrganizations(params) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error, count } = yield supabaseClient_1.default + .from("organizations") + .select("*", { count: "exact" }) + .range(params.offset, params.offset + params.limit - 1); + if (error) { + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while fetching organizations.", + }); + } + return { data: data, total: count || 0 }; + }); + } + isOrganizationNameUnique(name) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("organizations") + .select("id") + .eq("name", name) + .single(); + if (error && error.code !== "PGRST116") { + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while checking organization name uniqueness.", + }); + } + return !data; + }); + } + isOrganizationUsernameUnique(username) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("organizations") + .select("id") + .eq("username", username) + .single(); + if (error && error.code !== "PGRST116") { + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while checking organization username uniqueness.", + }); + } + return !data; + }); + } + getJoinRequestsByUser(userId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("join_requests") + .select('*') + .eq('user_id', userId); + if (error) { + console.error("Error getting user's join requests:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while getting user's join requests.", + }); + } + return data; + }); + } + updateMemberRole(organizationId, userId, role) { + return __awaiter(this, void 0, void 0, function* () { + const { error } = yield supabaseClient_1.default + .from("organization_members") + .update({ role }) + .eq('organization_id', organizationId) + .eq('user_id', userId); + if (error) { + console.error("Error updating member role:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while updating member role.", + }); + } + }); + } + getOrganizationMemberCount(organizationId) { + return __awaiter(this, void 0, void 0, function* () { + const { count, error } = yield supabaseClient_1.default + .from("organization_members") + .select('*', { count: 'exact', head: true }) + .eq('organization_id', organizationId); + if (error) { + console.error("Error getting organization member count:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while getting organization member count.", + }); + } + return count || 0; + }); + } + getOrganizationAdminCount(organizationId) { + return __awaiter(this, void 0, void 0, function* () { + const { count, error } = yield supabaseClient_1.default + .from("organization_members") + .select('*', { count: 'exact', head: true }) + .eq('organization_id', organizationId) + .eq('role', 'admin'); + if (error) { + console.error("Error getting organization admin count:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while getting organization admin count.", + }); + } + return count || 0; + }); + } + searchOrganizations(searchTerm, params) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error, count } = yield supabaseClient_1.default + .from("organizations") + .select("*", { count: "exact" }) + .or(`name.ilike.%${searchTerm}%,username.ilike.%${searchTerm}%,bio.ilike.%${searchTerm}%`) + .range(params.offset, params.offset + params.limit - 1); + if (error) { + console.error("Error searching organizations:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while searching organizations.", + }); + } + return { data: data, total: count || 0 }; + }); + } +} +exports.OrganizationRepository = OrganizationRepository; diff --git a/backend/public/modules/organizations/routes/organizationRoutes.js b/backend/public/modules/organizations/routes/organizationRoutes.js new file mode 100644 index 00000000..4fd87b7c --- /dev/null +++ b/backend/public/modules/organizations/routes/organizationRoutes.js @@ -0,0 +1,46 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const organizationController = __importStar(require("../controllers/organizationController")); +const middleware_1 = require("@/middleware/middleware"); +const router = (0, express_1.Router)(); +router.use(middleware_1.verifyAndGetUser); +router.post("/create", organizationController.createOrganization); +router.get("/", organizationController.getOrganizations); +router.get("/:id", organizationController.getOrganizationById); +router.put("/:id", organizationController.updateOrganization); +router.delete("/:id", organizationController.deleteOrganization); +router.post("/:id/join", organizationController.joinOrganization); +router.post("/:id/leave", organizationController.leaveOrganization); +router.put("/:id/join-policy", organizationController.setJoinPolicy); +router.get("/:id/join-requests", organizationController.getJoinRequests); +router.post("/:id/join-requests/:requestId", organizationController.handleJoinRequest); +router.delete("/join-requests/:requestId", organizationController.deleteJoinRequest); +router.delete("/:id/members/:userId", organizationController.removeMember); +router.get("/:id/report", organizationController.generateReport); +router.get("/user/organizations", organizationController.getUserOrganizations); +router.get("/search", organizationController.searchOrganizations); +exports.default = router; diff --git a/backend/public/modules/organizations/services/organizationService.js b/backend/public/modules/organizations/services/organizationService.js new file mode 100644 index 00000000..dc0b4a29 --- /dev/null +++ b/backend/public/modules/organizations/services/organizationService.js @@ -0,0 +1,631 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.OrganizationService = void 0; +const organizationRepository_1 = require("../repositories/organizationRepository"); +const response_1 = require("@/types/response"); +const validators_1 = require("@/utilities/validators"); +class OrganizationService { + constructor() { + this.organizationRepository = new organizationRepository_1.OrganizationRepository(); + } + createOrganization(organization, userId) { + return __awaiter(this, void 0, void 0, function* () { + try { + if (!organization.name || !organization.username || !organization.join_policy) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Name, username, and join policy are required fields.", + }); + } + if (!(0, validators_1.validateOrganizationName)(organization.name)) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Invalid organization name format.", + }); + } + if (!(0, validators_1.validateOrganizationUsername)(organization.username)) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Invalid organization username format.", + }); + } + const isNameUnique = yield this.organizationRepository.isOrganizationNameUnique(organization.name); + if (!isNameUnique) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Organization name is already taken.", + }); + } + const isUsernameUnique = yield this.organizationRepository.isOrganizationUsernameUnique(organization.username); + if (!isUsernameUnique) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Organization username is already taken.", + }); + } + const createdOrganization = yield this.organizationRepository.createOrganization(organization); + yield this.organizationRepository.addOrganizationMember({ + organization_id: createdOrganization.id, + user_id: userId, + role: 'admin', + joined_at: new Date().toISOString() + }); + return (0, response_1.APIData)({ + code: 201, + success: true, + data: createdOrganization, + }); + } + catch (error) { + if (error instanceof response_1.APIError) { + throw error; + } + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while creating the organization.", + }); + } + }); + } + updateOrganization(id, updates, userId, profilePhoto) { + return __awaiter(this, void 0, void 0, function* () { + try { + const isAdmin = yield this.organizationRepository.isUserAdmin(id, userId); + if (!isAdmin) { + throw (0, response_1.APIError)({ + code: 403, + success: false, + error: "You do not have permission to update this organization.", + }); + } + const updatedOrganization = yield this.organizationRepository.updateOrganization(id, updates, profilePhoto); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: updatedOrganization, + }); + } + catch (error) { + if (error instanceof response_1.APIError) { + throw error; + } + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while updating the organization.", + }); + } + }); + } + deleteOrganization(id, userId) { + return __awaiter(this, void 0, void 0, function* () { + try { + const isAdmin = yield this.organizationRepository.isUserAdmin(id, userId); + if (!isAdmin) { + throw (0, response_1.APIError)({ + code: 403, + success: false, + error: "You do not have permission to delete this organization.", + }); + } + yield this.organizationRepository.deleteOrganization(id); + return (0, response_1.APIData)({ + code: 204, + success: true, + data: null, + }); + } + catch (error) { + if (error instanceof response_1.APIError) { + throw error; + } + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while deleting the organization.", + }); + } + }); + } + joinOrganization(organizationId, userId) { + return __awaiter(this, void 0, void 0, function* () { + try { + const isMember = yield this.organizationRepository.isMember(organizationId, userId); + if (isMember) { + return (0, response_1.APIData)({ + code: 400, + success: false, + error: "You are already a member of this organization.", + }); + } + const joinPolicy = yield this.organizationRepository.getOrganizationJoinPolicy(organizationId); + if (joinPolicy === 'open') { + const member = yield this.organizationRepository.addOrganizationMember({ + organization_id: organizationId, + user_id: userId, + role: 'member', + joined_at: new Date().toISOString() + }); + return (0, response_1.APIData)({ + code: 201, + success: true, + data: member, + }); + } + else { + const joinRequest = yield this.organizationRepository.createJoinRequest(organizationId, userId); + return (0, response_1.APIData)({ + code: 201, + success: true, + data: joinRequest, + }); + } + } + catch (error) { + console.error("Error in joinOrganization:", error); + if (error instanceof Error) { + return (0, response_1.APIData)({ + code: 500, + success: false, + error: error.message || "An unexpected error occurred while joining the organization.", + }); + } + return (0, response_1.APIData)({ + code: 500, + success: false, + error: "An unexpected error occurred while joining the organization.", + }); + } + }); + } + getUserOrganizations(userId) { + return __awaiter(this, void 0, void 0, function* () { + try { + const organizations = yield this.organizationRepository.getUserOrganizations(userId); + if (organizations.length === 0) { + return (0, response_1.APIData)({ + code: 200, + success: true, + data: [], + error: "User is not a member of any organizations." + }); + } + return (0, response_1.APIData)({ + code: 200, + success: true, + data: organizations, + }); + } + catch (error) { + console.error("Error in getUserOrganizations:", error); + if (error instanceof response_1.APIError) { + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching user organizations.", + }); + } + return (0, response_1.APIData)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching user organizations.", + }); + } + }); + } + leaveOrganization(organizationId, userId) { + return __awaiter(this, void 0, void 0, function* () { + try { + const isAdmin = yield this.organizationRepository.isUserAdmin(organizationId, userId); + if (isAdmin === null) { + return (0, response_1.APIData)({ + code: 400, + success: false, + error: "You are not a member of this organization.", + }); + } + const memberCount = yield this.organizationRepository.getOrganizationMemberCount(organizationId); + const adminCount = yield this.organizationRepository.getOrganizationAdminCount(organizationId); + if (memberCount === 1) { + return (0, response_1.APIData)({ + code: 400, + success: false, + error: "You are the last member. Please delete the organization instead.", + }); + } + if (isAdmin && adminCount === 1) { + return (0, response_1.APIData)({ + code: 400, + success: false, + error: "You are the only admin. Please appoint another admin before leaving.", + }); + } + yield this.organizationRepository.removeMember(organizationId, userId); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: null, + }); + } + catch (error) { + console.error("Error in leaveOrganization:", error); + if (error instanceof response_1.APIError) { + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred.", + }); + } + return (0, response_1.APIData)({ + code: 500, + success: false, + error: "An unexpected error occurred while leaving the organization.", + }); + } + }); + } + addAdmin(organizationId, adminId, newAdminId) { + return __awaiter(this, void 0, void 0, function* () { + try { + const isAdmin = yield this.organizationRepository.isUserAdmin(organizationId, adminId); + if (!isAdmin) { + throw (0, response_1.APIError)({ + code: 403, + success: false, + error: "You do not have permission to add admins.", + }); + } + yield this.organizationRepository.updateMemberRole(organizationId, newAdminId, 'admin'); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: null, + }); + } + catch (error) { + if (error instanceof response_1.APIError) { + throw error; + } + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while adding an admin.", + }); + } + }); + } + setJoinPolicy(organizationId, joinPolicy, userId) { + return __awaiter(this, void 0, void 0, function* () { + try { + const isAdmin = yield this.organizationRepository.isUserAdmin(organizationId, userId); + if (!isAdmin) { + throw (0, response_1.APIError)({ + code: 403, + success: false, + error: "You do not have permission to change the join policy.", + }); + } + if (joinPolicy !== 'open' && joinPolicy !== 'request') { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Invalid join policy. Must be 'open' or 'request'.", + }); + } + yield this.organizationRepository.updateOrganizationJoinPolicy(organizationId, joinPolicy); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: null, + }); + } + catch (error) { + if (error instanceof response_1.APIError) { + throw error; + } + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while setting the join policy.", + }); + } + }); + } + getJoinRequests(organizationId, userId, params) { + return __awaiter(this, void 0, void 0, function* () { + try { + const isAdmin = yield this.organizationRepository.isUserAdmin(organizationId, userId); + if (!isAdmin) { + throw (0, response_1.APIError)({ + code: 403, + success: false, + error: "You do not have permission to view join requests.", + }); + } + const joinRequests = yield this.organizationRepository.getJoinRequests(organizationId, params); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: joinRequests, + }); + } + catch (error) { + if (error instanceof response_1.APIError) { + throw error; + } + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while getting join requests.", + }); + } + }); + } + handleJoinRequest(organizationId, requestId, accept, userId) { + return __awaiter(this, void 0, void 0, function* () { + try { + const isAdmin = yield this.organizationRepository.isUserAdmin(organizationId, userId); + if (isAdmin !== true) { + return (0, response_1.APIData)({ + code: 403, + success: false, + error: "You do not have permission to handle join requests.", + }); + } + const joinRequest = yield this.organizationRepository.getJoinRequestById(requestId); + if (!joinRequest) { + return (0, response_1.APIData)({ + code: 404, + success: false, + error: "Join request not found.", + }); + } + if (joinRequest.organization_id !== organizationId) { + return (0, response_1.APIData)({ + code: 400, + success: false, + error: "Join request does not belong to this organization.", + }); + } + if (joinRequest.status !== 'pending') { + return (0, response_1.APIData)({ + code: 400, + success: false, + error: "This join request has already been processed.", + }); + } + if (accept) { + // Update the join request status + yield this.organizationRepository.updateJoinRequestStatus(requestId, "accepted"); + // Add the user to the organization_members table + yield this.organizationRepository.addOrganizationMember({ + organization_id: organizationId, + user_id: joinRequest.user_id, + role: 'member', + joined_at: new Date().toISOString() + }); + } + else { + // If not accepting, just update the join request status to rejected + yield this.organizationRepository.updateJoinRequestStatus(requestId, "rejected"); + } + return (0, response_1.APIData)({ + code: 200, + success: true, + data: null, + }); + } + catch (error) { + console.error("Error in handleJoinRequest:", error); + if (error instanceof response_1.APIError) { + return (0, response_1.APIData)({ + code: 500, + success: false, + error: "An unexpected error occurred while handling the join request.", + }); + } + return (0, response_1.APIData)({ + code: 500, + success: false, + error: "An unexpected error occurred while handling the join request.", + }); + } + }); + } + removeMember(organizationId, memberUserId, adminUserId) { + return __awaiter(this, void 0, void 0, function* () { + try { + const isAdmin = yield this.organizationRepository.isUserAdmin(organizationId, adminUserId); + if (!isAdmin) { + throw (0, response_1.APIError)({ + code: 403, + success: false, + error: "You do not have permission to remove members.", + }); + } + if (memberUserId === adminUserId) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "You cannot remove yourself from the organization.", + }); + } + yield this.organizationRepository.removeMember(organizationId, memberUserId); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: null, + }); + } + catch (error) { + if (error instanceof response_1.APIError) { + throw error; + } + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while removing the member.", + }); + } + }); + } + getOrganizations(params) { + return __awaiter(this, void 0, void 0, function* () { + try { + const organizations = yield this.organizationRepository.getOrganizations(params); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: organizations, + }); + } + catch (error) { + if (error instanceof response_1.APIError) { + throw error; + } + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching organizations.", + }); + } + }); + } + getOrganizationById(id) { + return __awaiter(this, void 0, void 0, function* () { + try { + const organization = yield this.organizationRepository.getOrganizationById(id); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: organization, + }); + } + catch (error) { + if (error instanceof response_1.APIError) { + throw error; + } + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching the organization.", + }); + } + }); + } + generateReport(organizationId, userId) { + return __awaiter(this, void 0, void 0, function* () { + try { + const isAdmin = yield this.organizationRepository.isUserAdmin(organizationId, userId); + if (!isAdmin) { + throw (0, response_1.APIError)({ + code: 403, + success: false, + error: "You do not have permission to generate reports.", + }); + } + // Placeholder for future implementation + // Example: Fetch organization and member details, generate report + // const organization = await this.organizationRepository.getOrganizationById(organizationId); + // const members = await this.organizationRepository.getOrganizationMembers(organizationId); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: null, // Replace with the actual report object in the future + }); + } + catch (error) { + if (error instanceof response_1.APIError) { + throw error; + } + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while generating the report.", + }); + } + }); + } + deleteJoinRequest(requestId, userId) { + return __awaiter(this, void 0, void 0, function* () { + try { + yield this.organizationRepository.deleteJoinRequest(requestId, userId); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: null, + }); + } + catch (error) { + if (error instanceof response_1.APIError) { + throw error; + } + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while deleting the join request.", + }); + } + }); + } + getJoinRequestsByUser(userId) { + return __awaiter(this, void 0, void 0, function* () { + try { + const joinRequests = yield this.organizationRepository.getJoinRequestsByUser(userId); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: joinRequests, + }); + } + catch (error) { + if (error instanceof response_1.APIError) { + throw error; + } + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching user's join requests.", + }); + } + }); + } + searchOrganizations(searchTerm, params) { + return __awaiter(this, void 0, void 0, function* () { + try { + const result = yield this.organizationRepository.searchOrganizations(searchTerm, params); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: result, + }); + } + catch (error) { + if (error instanceof response_1.APIError) { + throw error; + } + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while searching organizations.", + }); + } + }); + } +} +exports.OrganizationService = OrganizationService; diff --git a/backend/public/modules/points/controllers/pointsController.js b/backend/public/modules/points/controllers/pointsController.js new file mode 100644 index 00000000..9db103d0 --- /dev/null +++ b/backend/public/modules/points/controllers/pointsController.js @@ -0,0 +1,53 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.PointsController = void 0; +const pointsService_1 = require("../services/pointsService"); +const locationService_1 = require("../../locations/services/locationService"); +const response_1 = require("@/utilities/response"); +const response_2 = require("@/types/response"); +const cacheMiddleware_1 = require("@/middleware/cacheMiddleware"); +class PointsController { + constructor() { + this.getLeaderboard = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(this, void 0, void 0, function* () { + try { + const { userId, province, city, suburb } = req.body; + if (!userId) { + throw (0, response_2.APIError)({ + code: 400, + success: false, + error: "User ID is required", + }); + } + const leaderboard = yield this.pointsService.getLeaderboard({ province, city, suburb }); + const userPosition = yield this.pointsService.getUserPosition(userId, { province, city, suburb }); + const response = { + code: 200, + success: true, + data: { + userPosition, + leaderboard, + }, + }; + (0, response_1.sendResponse)(res, response); + } + catch (err) { + (0, response_1.sendResponse)(res, err); + } + }) + ]; + this.pointsService = new pointsService_1.PointsService(); + this.locationService = new locationService_1.LocationService(); + } +} +exports.PointsController = PointsController; diff --git a/backend/public/modules/points/repositories/pointsRepository.js b/backend/public/modules/points/repositories/pointsRepository.js new file mode 100644 index 00000000..184f1e17 --- /dev/null +++ b/backend/public/modules/points/repositories/pointsRepository.js @@ -0,0 +1,227 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.PointsRepository = void 0; +const supabaseClient_1 = __importDefault(require("@/modules/shared/services/supabaseClient")); +const response_1 = require("@/types/response"); +class PointsRepository { + updateUserScore(userId, points) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .rpc('increment_score', { input_user_id: userId, score_increment: points }); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while updating user score.", + }); + } + return data; + }); + } + logPointsTransaction(userId, points, reason) { + return __awaiter(this, void 0, void 0, function* () { + const { error } = yield supabaseClient_1.default.from("points_history").insert({ + user_id: userId, + points: points, + action: reason, + created_at: new Date().toISOString(), + }); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while logging points transaction.", + }); + } + }); + } + getUserScore(userId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("user") + .select("user_score") + .eq("user_id", userId) + .single(); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching user score.", + }); + } + return data.user_score; + }); + } + suspendUserFromResolving(userId) { + return __awaiter(this, void 0, void 0, function* () { + const suspensionEnd = new Date(); + suspensionEnd.setHours(suspensionEnd.getHours() + 24); + const { error } = yield supabaseClient_1.default + .from("user") + .update({ resolve_suspension_end: suspensionEnd.toISOString() }) + .eq("user_id", userId); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while suspending user from resolving.", + }); + } + }); + } + blockUser(userId) { + return __awaiter(this, void 0, void 0, function* () { + const { error } = yield supabaseClient_1.default + .from("user") + .update({ is_blocked: true }) + .eq("user_id", userId); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while blocking user.", + }); + } + }); + } + getLeaderboard(locationFilter) { + return __awaiter(this, void 0, void 0, function* () { + let query = supabaseClient_1.default + .from('user') + .select(` + user_id, + username, + fullname, + image_url, + user_score, + location:location_id ( + location_id, + province, + city, + suburb, + district + ) + `) + .order('user_score', { ascending: false }) + .limit(10); + if (locationFilter.province || locationFilter.city || locationFilter.suburb) { + query = query.not('location_id', 'is', null); + if (locationFilter.province) + query = query.eq('location.province', locationFilter.province); + if (locationFilter.city) + query = query.eq('location.city', locationFilter.city); + if (locationFilter.suburb) + query = query.eq('location.suburb', locationFilter.suburb); + } + const { data, error } = yield query; + if (error) { + console.error("Error fetching leaderboard:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching the leaderboard.", + }); + } + const filteredData = locationFilter.province || locationFilter.city || locationFilter.suburb + ? data.filter(user => user.location !== null) + : data; + return filteredData; + }); + } + getUserPosition(userId, locationFilter) { + return __awaiter(this, void 0, void 0, function* () { + const { data: userData, error: userError } = yield supabaseClient_1.default + .from('user') + .select(` + user_id, + username, + fullname, + email_address, + image_url, + user_score, + location_id, + location:location_id ( + location_id, + province, + city, + suburb, + district + ) + `) + .eq('user_id', userId) + .single(); + if (userError || !userData) { + console.error("Error fetching user:", userError); + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "User not found.", + }); + } + const databaseUser = Object.assign(Object.assign({}, userData), { location: userData.location ? userData.location[0] : null }); + const user = Object.assign(Object.assign({}, databaseUser), { is_owner: true, total_issues: 0, resolved_issues: 0, location_id: userData.location_id || null, location: databaseUser.location || null }); + let query = supabaseClient_1.default + .from('user') + .select('user_id', { count: 'exact' }) + .gt('user_score', user.user_score); + let locationMessage = ""; + if (locationFilter.province || locationFilter.city || locationFilter.suburb) { + query = query.not('location_id', 'is', null); + const locationParts = []; + if (locationFilter.suburb) + locationParts.push(locationFilter.suburb); + if (locationFilter.city) + locationParts.push(locationFilter.city); + if (locationFilter.province) + locationParts.push(locationFilter.province); + locationMessage = `in ${locationParts.join(", ")}`; + if (!this.userMatchesLocationFilter(user, locationFilter)) { + return Object.assign(Object.assign({}, user), { position: null, message: `User not found ${locationMessage}.` }); + } + } + else { + locationMessage = "nationwide"; + } + const { count, error: countError } = yield query; + if (countError) { + console.error("Error fetching user count:", countError); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while calculating user position.", + }); + } + const position = (count !== null) ? count + 1 : null; + return Object.assign(Object.assign({}, user), { position: position !== null ? position.toString() : null, message: `User ranked ${position} ${locationMessage}.` }); + }); + } + userMatchesLocationFilter(user, locationFilter) { + if (!user.location) + return false; + if (locationFilter.province && user.location.province !== locationFilter.province) + return false; + if (locationFilter.city && user.location.city !== locationFilter.city) + return false; + if (locationFilter.suburb && user.location.suburb !== locationFilter.suburb) + return false; + return true; + } +} +exports.PointsRepository = PointsRepository; diff --git a/backend/public/modules/points/routes/pointsRoutes.js b/backend/public/modules/points/routes/pointsRoutes.js new file mode 100644 index 00000000..c3a2ea2d --- /dev/null +++ b/backend/public/modules/points/routes/pointsRoutes.js @@ -0,0 +1,11 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = __importDefault(require("express")); +const pointsController_1 = require("../controllers/pointsController"); +const router = express_1.default.Router(); +const pointsController = new pointsController_1.PointsController(); +router.post('/leaderboard', pointsController.getLeaderboard); +exports.default = router; diff --git a/backend/public/modules/points/services/pointsService.js b/backend/public/modules/points/services/pointsService.js new file mode 100644 index 00000000..fc478b2f --- /dev/null +++ b/backend/public/modules/points/services/pointsService.js @@ -0,0 +1,71 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.PointsService = void 0; +const pointsRepository_1 = require("./../repositories/pointsRepository"); +const supabaseClient_1 = __importDefault(require("@/modules/shared/services/supabaseClient")); +class PointsService { + constructor() { + this.pointsRepository = new pointsRepository_1.PointsRepository(); + } + awardPoints(userId, points, reason) { + return __awaiter(this, void 0, void 0, function* () { + const newScore = yield this.pointsRepository.updateUserScore(userId, points); + yield this.pointsRepository.logPointsTransaction(userId, points, reason); + if (newScore < -150) { + yield this.pointsRepository.blockUser(userId); + } + return newScore; + }); + } + penalizeUser(userId, points, reason) { + return __awaiter(this, void 0, void 0, function* () { + const newScore = yield this.pointsRepository.updateUserScore(userId, -points); + yield this.pointsRepository.logPointsTransaction(userId, -points, reason); + if (reason === "Falsely resolving someone else's issue") { + yield this.pointsRepository.suspendUserFromResolving(userId); + } + if (newScore < -150) { + yield this.pointsRepository.blockUser(userId); + } + return newScore; + }); + } + getFirstTimeAction(userId, action) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("points_history") + .select("id") + .eq("user_id", userId) + .eq("action", action) + .limit(1); + if (error) { + console.error(error); + throw new Error("Failed to check first time action"); + } + return data.length === 0; + }); + } + getLeaderboard(locationFilter) { + return __awaiter(this, void 0, void 0, function* () { + return this.pointsRepository.getLeaderboard(locationFilter); + }); + } + getUserPosition(userId, locationFilter) { + return __awaiter(this, void 0, void 0, function* () { + return this.pointsRepository.getUserPosition(userId, locationFilter); + }); + } +} +exports.PointsService = PointsService; diff --git a/backend/public/modules/reactions/controllers/reactionController.js b/backend/public/modules/reactions/controllers/reactionController.js new file mode 100644 index 00000000..ea4cbe14 --- /dev/null +++ b/backend/public/modules/reactions/controllers/reactionController.js @@ -0,0 +1,31 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const reactionService_1 = __importDefault(require("@/modules/reactions/services/reactionService")); +const response_1 = require("@/utilities/response"); +const cacheUtils_1 = require("@/utilities/cacheUtils"); +const reactionService = new reactionService_1.default(); +const addOrRemoveReaction = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield reactionService.addOrRemoveReaction(req.body); + (0, cacheUtils_1.clearCachePattern)('__express__/api/reactions*'); + (0, response_1.sendResponse)(res, response); + } + catch (error) { + (0, response_1.sendResponse)(res, error); + } +}); +exports.default = { + addOrRemoveReaction, +}; diff --git a/backend/public/modules/reactions/repositories/reactionRepository.js b/backend/public/modules/reactions/repositories/reactionRepository.js new file mode 100644 index 00000000..4465506c --- /dev/null +++ b/backend/public/modules/reactions/repositories/reactionRepository.js @@ -0,0 +1,113 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const supabaseClient_1 = __importDefault(require("@/modules/shared/services/supabaseClient")); +const response_1 = require("@/types/response"); +class ReactionRepository { + addReaction(reaction) { + return __awaiter(this, void 0, void 0, function* () { + reaction.created_at = new Date().toISOString(); + const { data, error } = yield supabaseClient_1.default + .from("reaction") + .insert(reaction) + .select() + .single(); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + return data; + }); + } + deleteReaction(issueId, userId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("reaction") + .delete() + .eq("issue_id", issueId) + .eq("user_id", userId) + .select() + .maybeSingle(); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + if (!data) { + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "Reaction does not exist", + }); + } + return data; + }); + } + getReactionByUserAndIssue(issueId, userId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("reaction") + .select("*") + .eq("issue_id", issueId) + .eq("user_id", userId) + .maybeSingle(); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + return data; + }); + } + getReactionCountsByIssueId(issueId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("reaction") + .select("emoji") + .eq("issue_id", issueId); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + // Aggregate the counts manually + const reactionCounts = {}; + data.forEach((reaction) => { + if (!reactionCounts[reaction.emoji]) { + reactionCounts[reaction.emoji] = 0; + } + reactionCounts[reaction.emoji]++; + }); + // Convert the aggregated counts into an array + return Object.keys(reactionCounts).map((emoji) => ({ + emoji, + count: reactionCounts[emoji], + })); + }); + } +} +exports.default = ReactionRepository; diff --git a/backend/public/modules/reactions/routes/reactionRoutes.js b/backend/public/modules/reactions/routes/reactionRoutes.js new file mode 100644 index 00000000..9faac400 --- /dev/null +++ b/backend/public/modules/reactions/routes/reactionRoutes.js @@ -0,0 +1,12 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const reactionController_1 = __importDefault(require("@/modules/reactions/controllers/reactionController")); +const middleware_1 = require("@/middleware/middleware"); +const router = (0, express_1.Router)(); +router.use(middleware_1.verifyAndGetUser); +router.post("/", reactionController_1.default.addOrRemoveReaction); +exports.default = router; diff --git a/backend/public/modules/reactions/services/reactionService.js b/backend/public/modules/reactions/services/reactionService.js new file mode 100644 index 00000000..a7da3acc --- /dev/null +++ b/backend/public/modules/reactions/services/reactionService.js @@ -0,0 +1,68 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const reactionRepository_1 = __importDefault(require("@/modules/reactions/repositories/reactionRepository")); +const response_1 = require("@/types/response"); +const pointsService_1 = require("@/modules/points/services/pointsService"); +class ReactionService { + constructor() { + this.reactionRepository = new reactionRepository_1.default(); + this.pointsService = new pointsService_1.PointsService(); + } + setReactionRepository(reactionRepository) { + this.reactionRepository = reactionRepository; + } + setPointsService(pointsService) { + this.pointsService = pointsService; + } + addOrRemoveReaction(reaction) { + return __awaiter(this, void 0, void 0, function* () { + if (!reaction.user_id) { + throw (0, response_1.APIError)({ + code: 401, + success: false, + error: "You need to be signed in to react", + }); + } + if (!reaction.issue_id || !reaction.emoji) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Missing required fields for reacting", + }); + } + let added; + let removed; + const existingReaction = yield this.reactionRepository.getReactionByUserAndIssue(reaction.issue_id, reaction.user_id); + if (existingReaction) { + const removedReaction = yield this.reactionRepository.deleteReaction(reaction.issue_id, reaction.user_id); + removed = removedReaction.emoji; + } + if (reaction.emoji !== removed) { + const addedReaction = yield this.reactionRepository.addReaction(reaction); + added = addedReaction.emoji; + yield this.pointsService.awardPoints(reaction.user_id, 5, "reacted to an issue"); + } + return (0, response_1.APIData)({ + code: 200, + success: true, + data: { + added, + removed, + }, + }); + }); + } +} +exports.default = ReactionService; diff --git a/backend/public/modules/reports/controllers/reportsController.js b/backend/public/modules/reports/controllers/reportsController.js new file mode 100644 index 00000000..a1ce0e9d --- /dev/null +++ b/backend/public/modules/reports/controllers/reportsController.js @@ -0,0 +1,103 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.groupedByPoliticalAssociation = exports.getIssuesCountGroupedByCategoryAndCreatedAt = exports.getIssuesGroupedByCategory = exports.getIssuesGroupedByCreatedAt = exports.getIssueCountsGroupedByResolutionAndCategory = exports.getIssueCountsGroupedByResolutionStatus = exports.getAllIssuesGroupedByResolutionStatus = void 0; +const response_1 = require("@/utilities/response"); +const reportsService_1 = __importDefault(require("@/modules/reports/services/reportsService")); +const cacheMiddleware_1 = require("@/middleware/cacheMiddleware"); +const reportsService = new reportsService_1.default(); +exports.getAllIssuesGroupedByResolutionStatus = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield reportsService.getAllIssuesGroupedByResolutionStatus(req.body); + (0, response_1.sendResponse)(res, response); + } + catch (error) { + (0, response_1.sendResponse)(res, error); + } + }) +]; +exports.getIssueCountsGroupedByResolutionStatus = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield reportsService.getIssueCountsGroupedByResolutionStatus(req.body); + (0, response_1.sendResponse)(res, response); + } + catch (error) { + (0, response_1.sendResponse)(res, error); + } + }) +]; +exports.getIssueCountsGroupedByResolutionAndCategory = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield reportsService.getIssueCountsGroupedByResolutionAndCategory(req.body); + (0, response_1.sendResponse)(res, response); + } + catch (error) { + (0, response_1.sendResponse)(res, error); + } + }) +]; +exports.getIssuesGroupedByCreatedAt = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield reportsService.getIssuesGroupedByCreatedAt(req.body); + (0, response_1.sendResponse)(res, response); + } + catch (error) { + (0, response_1.sendResponse)(res, error); + } + }) +]; +exports.getIssuesGroupedByCategory = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield reportsService.getIssuesGroupedByCategory(req.body); + (0, response_1.sendResponse)(res, response); + } + catch (error) { + (0, response_1.sendResponse)(res, error); + } + }) +]; +exports.getIssuesCountGroupedByCategoryAndCreatedAt = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield reportsService.getIssuesCountGroupedByCategoryAndCreatedAt(req.body); + (0, response_1.sendResponse)(res, response); + } + catch (error) { + (0, response_1.sendResponse)(res, error); + } + }) +]; +exports.groupedByPoliticalAssociation = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (_, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield reportsService.groupedByPoliticalAssociation(); + (0, response_1.sendResponse)(res, response); + } + catch (error) { + (0, response_1.sendResponse)(res, error); + } + }) +]; diff --git a/backend/public/modules/reports/repositories/reportsRepository.js b/backend/public/modules/reports/repositories/reportsRepository.js new file mode 100644 index 00000000..4e7604cb --- /dev/null +++ b/backend/public/modules/reports/repositories/reportsRepository.js @@ -0,0 +1,265 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const supabaseClient_1 = __importDefault(require("@/modules/shared/services/supabaseClient")); +const response_1 = require("@/types/response"); +class ReportsRepository { + getAllIssuesGroupedByResolutionStatus(_a) { + return __awaiter(this, arguments, void 0, function* ({ from, amount, }) { + const { data, error } = yield supabaseClient_1.default + .from("issue") + .select(` + *, + category: category_id ( + name + ), + location: location_id ( + suburb, + city, + province + ) + `) + .order("created_at", { ascending: false }) + .range(from, from + amount - 1); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + const groupedIssues = data.reduce((acc, issue) => { + const key = issue.resolved_at ? "resolved" : "unresolved"; + if (!acc[key]) { + acc[key] = []; + } + acc[key].push(Object.assign(Object.assign({}, issue), { user: undefined })); + return acc; + }, { resolved: [], unresolved: [] }); + return groupedIssues; + }); + } + getIssueCountsGroupedByResolutionStatus(_a) { + return __awaiter(this, arguments, void 0, function* ({ from, amount, }) { + const { data, error } = yield supabaseClient_1.default + .from("issue") + .select(` + resolved_at + `) + .order("created_at", { ascending: false }) + .range(from, from + amount - 1); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + const counts = data.reduce((acc, issue) => { + const key = issue.resolved_at ? "resolved" : "unresolved"; + acc[key] = (acc[key] || 0) + 1; + return acc; + }, { resolved: 0, unresolved: 0 }); + return counts; + }); + } + getIssueCountsGroupedByResolutionAndCategory(_a) { + return __awaiter(this, arguments, void 0, function* ({ from, amount, }) { + const { data, error } = yield supabaseClient_1.default + .from("issue") + .select(` + *, + category: category_id ( + name + ) + `) + .order("created_at", { ascending: false }) + .range(from, from + amount - 1); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + const groupedIssues = data.reduce((acc, issue) => { + const resolutionKey = issue.resolved_at ? "resolved" : "unresolved"; + const categoryKey = issue.category.name; + if (!acc[resolutionKey][categoryKey]) { + acc[resolutionKey][categoryKey] = 0; + } + acc[resolutionKey][categoryKey] += 1; + return acc; + }, { resolved: {}, unresolved: {} }); + return groupedIssues; + }); + } + getIssuesGroupedByCreatedAt(_a) { + return __awaiter(this, arguments, void 0, function* ({ from, amount, }) { + const { data, error } = yield supabaseClient_1.default + .from("issue") + .select(` + *, + category: category_id ( + name + ) + `) + .order("created_at", { ascending: false }) + .range(from, from + amount - 1); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + const groupedByCreatedAt = data.reduce((acc, issue) => { + const createdAtDate = issue.created_at.split("T")[0]; + if (!acc[createdAtDate]) { + acc[createdAtDate] = []; + } + acc[createdAtDate].push(issue); + return acc; + }, {}); + return groupedByCreatedAt; + }); + } + getIssuesGroupedByCategory(_a) { + return __awaiter(this, arguments, void 0, function* ({ from, amount }) { + const { data, error } = yield supabaseClient_1.default + .from("issue") + .select(` + *, + category: category_id ( + name + ) + `) + .order("created_at", { ascending: false }) + .range(from, from + amount - 1); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + const groupedByCategory = data.reduce((acc, issue) => { + const categoryName = issue.category.name; + if (!acc[categoryName]) { + acc[categoryName] = []; + } + acc[categoryName].push(issue); + return acc; + }, {}); + return groupedByCategory; + }); + } + getIssuesCountGroupedByCategoryAndCreatedAt(_a) { + return __awaiter(this, arguments, void 0, function* ({ from, amount, }) { + const { data, error } = yield supabaseClient_1.default + .from("issue") + .select(` + *, + category: category_id ( + name + ), + created_at + `) + .order("created_at", { ascending: false }) + .range(from, from + amount - 1); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + const groupedByCategory = data.reduce((acc, issue) => { + const categoryName = issue.category.name; + if (!acc[categoryName]) { + acc[categoryName] = []; + } + acc[categoryName].push(issue); + return acc; + }, {}); + const groupedAndCounted = Object.keys(groupedByCategory).reduce((acc, categoryName) => { + const issues = groupedByCategory[categoryName]; + const countsByCreatedAt = issues.reduce((counts, issue) => { + const createdAt = issue.created_at.split("T")[0]; + counts[createdAt] = (counts[createdAt] || 0) + 1; + return counts; + }, {}); + acc[categoryName] = countsByCreatedAt; + return acc; + }, {}); + const allDates = new Set(); + Object.values(groupedAndCounted).forEach((countsByCreatedAt) => { + Object.keys(countsByCreatedAt).forEach((date) => allDates.add(date)); + }); + const normalizedCounts = Object.keys(groupedAndCounted).reduce((acc, categoryName) => { + const categoryCounts = groupedAndCounted[categoryName]; + const normalizedCategoryCounts = {}; + allDates.forEach((date) => { + normalizedCategoryCounts[date] = categoryCounts[date] || 0; + }); + acc[categoryName] = normalizedCategoryCounts; + return acc; + }, {}); + return normalizedCounts; + }); + } + groupedByPoliticalAssociation() { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("resolution") + .select(` + name: political_association, + value: num_cluster_members_accepted.sum() + `); + if (error) { + console.log(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later." + }); + } + return data.reduce((newData, current) => { + var _a; + if (["NONE", null].includes(current.name)) { + if (((_a = newData[0]) === null || _a === void 0 ? void 0 : _a.name) === "No party") { + newData[0].value += current.value; + } + else { + newData.unshift({ + name: "No party", + value: current.value, + }); + } + } + else { + newData.push(current); + } + return newData; + }, []); + }); + } +} +exports.default = ReportsRepository; diff --git a/backend/public/modules/reports/routes/reportsRoutes.js b/backend/public/modules/reports/routes/reportsRoutes.js new file mode 100644 index 00000000..f8d86166 --- /dev/null +++ b/backend/public/modules/reports/routes/reportsRoutes.js @@ -0,0 +1,38 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const reportsController = __importStar(require("@/modules/reports/controllers/reportsController")); +const middleware_1 = require("@/middleware/middleware"); +const router = (0, express_1.Router)(); +router.use(middleware_1.verifyAndGetUser); +router.post("/groupedResolutionStatus", reportsController.getAllIssuesGroupedByResolutionStatus); +router.post("/countResolutionStatus", reportsController.getIssueCountsGroupedByResolutionStatus); +router.post("/groupedResolutionAndCategory", reportsController.getIssueCountsGroupedByResolutionAndCategory); +router.post("/groupedCreatedAt", reportsController.getIssuesGroupedByCreatedAt); +router.post("/groupedCategory", reportsController.getIssuesGroupedByCategory); +router.post("/groupedCategoryAndCreatedAt", reportsController.getIssuesCountGroupedByCategoryAndCreatedAt); +router.post("/groupedPoliticalAssociation", reportsController.groupedByPoliticalAssociation); +exports.default = router; diff --git a/backend/public/modules/reports/services/reportsService.js b/backend/public/modules/reports/services/reportsService.js new file mode 100644 index 00000000..adc45288 --- /dev/null +++ b/backend/public/modules/reports/services/reportsService.js @@ -0,0 +1,131 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const reportsRepository_1 = __importDefault(require("@/modules/reports/repositories/reportsRepository")); +const response_1 = require("@/types/response"); +class ReportsService { + constructor() { + this.ReportsRepository = new reportsRepository_1.default(); + } + setReportsRepository(ReportsRepository) { + this.ReportsRepository = ReportsRepository; + } + getAllIssuesGroupedByResolutionStatus(params) { + return __awaiter(this, void 0, void 0, function* () { + try { + const data = yield this.ReportsRepository.getAllIssuesGroupedByResolutionStatus(params); + return { code: 200, success: true, data }; + } + catch (error) { + console.error("Error: ", error); + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "GroupedByResolutionStatus: Something Went wrong", + }); + } + }); + } + getIssueCountsGroupedByResolutionStatus(params) { + return __awaiter(this, void 0, void 0, function* () { + try { + const data = yield this.ReportsRepository.getIssueCountsGroupedByResolutionStatus(params); + return { code: 200, success: true, data }; + } + catch (error) { + console.error("Error: ", error); + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "CountsGroupedByResolutionStatus: Something Went wrong", + }); + } + }); + } + getIssueCountsGroupedByResolutionAndCategory(params) { + return __awaiter(this, void 0, void 0, function* () { + try { + const data = yield this.ReportsRepository.getIssueCountsGroupedByResolutionAndCategory(params); + return { code: 200, success: true, data }; + } + catch (error) { + console.error("Error: ", error); + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "CountsGroupedByResolutionAndCategory: Something Went wrong", + }); + } + }); + } + getIssuesGroupedByCreatedAt(params) { + return __awaiter(this, void 0, void 0, function* () { + try { + const data = yield this.ReportsRepository.getIssuesGroupedByCreatedAt(params); + return { code: 200, success: true, data }; + } + catch (error) { + console.error("Error: ", error); + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "GroupedByCreatedAt: Something Went wrong", + }); + } + }); + } + getIssuesGroupedByCategory(params) { + return __awaiter(this, void 0, void 0, function* () { + try { + const data = yield this.ReportsRepository.getIssuesGroupedByCategory(params); + return { code: 200, success: true, data }; + } + catch (error) { + console.error("Error: ", error); + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "GroupedByCategory: Something Went wrong", + }); + } + }); + } + getIssuesCountGroupedByCategoryAndCreatedAt(params) { + return __awaiter(this, void 0, void 0, function* () { + try { + const data = yield this.ReportsRepository.getIssuesCountGroupedByCategoryAndCreatedAt(params); + return { code: 200, success: true, data }; + } + catch (error) { + console.error("Error: ", error); + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "CountGroupedByCategoryAndCreatedAt: Something Went wrong", + }); + } + }); + } + groupedByPoliticalAssociation() { + return __awaiter(this, void 0, void 0, function* () { + const data = yield this.ReportsRepository.groupedByPoliticalAssociation(); + return (0, response_1.APIData)({ + code: 200, + success: true, + data + }); + }); + } +} +exports.default = ReportsService; diff --git a/backend/public/modules/resolutions/repositories/resolutionRepository.js b/backend/public/modules/resolutions/repositories/resolutionRepository.js new file mode 100644 index 00000000..ed7e6a4f --- /dev/null +++ b/backend/public/modules/resolutions/repositories/resolutionRepository.js @@ -0,0 +1,155 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ResolutionRepository = void 0; +const supabaseClient_1 = __importDefault(require("@/modules/shared/services/supabaseClient")); +const response_1 = require("@/types/response"); +class ResolutionRepository { + createResolution(resolution) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from('resolution') + .insert(resolution) + .select() + .single(); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while creating a resolution.", + }); + } + return data; + }); + } + getResolutionById(resolutionId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from('resolution') + .select('*') + .eq('resolution_id', resolutionId) + .single(); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching the resolution.", + }); + } + return data; + }); + } + updateResolution(resolutionId, updates) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from('resolution') + .update(Object.assign(Object.assign({}, updates), { updated_at: new Date().toISOString() })) + .eq('resolution_id', resolutionId) + .select() + .single(); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while updating the resolution.", + }); + } + return data; + }); + } + getResolutionsByIssueId(issueId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from('resolution') + .select('*') + .eq('issue_id', issueId); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching resolutions for the issue.", + }); + } + return data; + }); + } + getUserResolutions(userId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from('resolution') + .select('*') + .eq('resolver_id', userId) + .order('created_at', { ascending: false }); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching user resolutions.", + }); + } + return data; + }); + } + deleteResolution(resolutionId, userId) { + return __awaiter(this, void 0, void 0, function* () { + const { data: resolution, error: fetchError } = yield supabaseClient_1.default + .from('resolution') + .select('*') + .eq('resolution_id', resolutionId) + .eq('resolver_id', userId) + .single(); + if (fetchError) { + console.error(fetchError); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching the resolution.", + }); + } + if (!resolution) { + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "Resolution not found or you don't have permission to delete it.", + }); + } + if (resolution.status !== 'pending') { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Only pending resolutions can be deleted.", + }); + } + const { error: deleteError } = yield supabaseClient_1.default + .from('resolution') + .delete() + .eq('resolution_id', resolutionId) + .eq('resolver_id', userId); + if (deleteError) { + console.error(deleteError); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while deleting the resolution.", + }); + } + }); + } +} +exports.ResolutionRepository = ResolutionRepository; diff --git a/backend/public/modules/resolutions/repositories/resolutionResponseRepository.js b/backend/public/modules/resolutions/repositories/resolutionResponseRepository.js new file mode 100644 index 00000000..3b8ef8a2 --- /dev/null +++ b/backend/public/modules/resolutions/repositories/resolutionResponseRepository.js @@ -0,0 +1,53 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ResolutionResponseRepository = void 0; +const supabaseClient_1 = __importDefault(require("@/modules/shared/services/supabaseClient")); +const response_1 = require("@/types/response"); +class ResolutionResponseRepository { + createResponse(resolutionId, userId, response) { + return __awaiter(this, void 0, void 0, function* () { + const { error } = yield supabaseClient_1.default + .from('resolution_responses') + .insert({ resolution_id: resolutionId, user_id: userId, response }); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while creating a resolution response.", + }); + } + }); + } + getAcceptedUsers(resolutionId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from('resolution_responses') + .select('user_id') + .eq('resolution_id', resolutionId) + .eq('response', 'accepted'); + if (error) { + console.error(error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching accepted users.", + }); + } + return data.map(row => row.user_id); + }); + } +} +exports.ResolutionResponseRepository = ResolutionResponseRepository; diff --git a/backend/public/modules/resolutions/services/resolutionService.js b/backend/public/modules/resolutions/services/resolutionService.js new file mode 100644 index 00000000..7615925b --- /dev/null +++ b/backend/public/modules/resolutions/services/resolutionService.js @@ -0,0 +1,184 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ResolutionService = void 0; +const resolutionRepository_1 = require("../repositories/resolutionRepository"); +const response_1 = require("@/types/response"); +const pointsService_1 = require("@/modules/points/services/pointsService"); +const clusterService_1 = require("@/modules/clusters/services/clusterService"); +const issueRepository_1 = __importDefault(require("@/modules/issues/repositories/issueRepository")); +const resolutionResponseRepository_1 = require("../repositories/resolutionResponseRepository"); +class ResolutionService { + constructor() { + this.resolutionRepository = new resolutionRepository_1.ResolutionRepository(); + this.pointsService = new pointsService_1.PointsService(); + this.clusterService = new clusterService_1.ClusterService(); + this.issueRepository = new issueRepository_1.default(); + this.ResolutionResponseRepository = new resolutionResponseRepository_1.ResolutionResponseRepository(); + } + createResolution(resolution) { + return __awaiter(this, void 0, void 0, function* () { + const isResolved = yield this.issueRepository.isIssueResolved(resolution.issue_id); + if (isResolved) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "This issue has already been resolved.", + }); + } + const pendingResolution = yield this.issueRepository.getPendingResolutionForIssue(resolution.issue_id); + if (pendingResolution) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "There is already a pending resolution for this issue.", + }); + } + const issue = yield this.issueRepository.getIssueById(resolution.issue_id); + const clusterId = issue.cluster_id; + let status = 'pending'; + if (resolution.resolution_source === 'self') { + status = 'accepted'; + yield this.issueRepository.updateIssueResolutionStatus(resolution.issue_id, true); + yield this.clusterService.moveAcceptedMembersToNewCluster(resolution.issue_id, [resolution.resolver_id]); + yield this.pointsService.awardPoints(resolution.resolver_id, 70, "self-resolution logged and accepted"); + } + const clusterIssues = clusterId ? yield this.issueRepository.getIssuesInCluster(clusterId) : [issue]; + const numClusterMembers = clusterIssues.length; + const createdResolution = yield this.resolutionRepository.createResolution(Object.assign(Object.assign({}, resolution), { status, num_cluster_members: numClusterMembers, num_cluster_members_accepted: resolution.resolution_source === 'self' ? 1 : 0, num_cluster_members_rejected: 0 })); + // Create resolutions for other issues in the cluster + if (clusterId) { + for (const clusterIssue of clusterIssues) { + if (clusterIssue.issue_id !== resolution.issue_id) { + yield this.resolutionRepository.createResolution(Object.assign(Object.assign({}, resolution), { issue_id: clusterIssue.issue_id, status: 'pending', num_cluster_members: numClusterMembers, num_cluster_members_accepted: 0, num_cluster_members_rejected: 0 })); + } + } + } + // Notify cluster members for both self and external resolutions + yield this.notifyClusterMembers(createdResolution, clusterIssues); + return createdResolution; + }); + } + notifyClusterMembers(resolution, clusterIssues) { + return __awaiter(this, void 0, void 0, function* () { + // Implement logic to notify cluster members about the new resolution + // For self-resolutions, inform them that the issue has been resolved but ask for their feedback + // For external resolutions, ask for their acceptance or rejection + // Only notify users who have issues in the cluster + for (const issue of clusterIssues) { + if (issue.user_id !== resolution.resolver_id) { + // Implement notification logic here + } + } + }); + } + updateResolutionStatus(resolutionId, status, userId) { + return __awaiter(this, void 0, void 0, function* () { + const resolution = yield this.resolutionRepository.getResolutionById(resolutionId); + if (resolution.status !== 'pending') { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "This resolution has already been processed.", + }); + } + const issue = yield this.issueRepository.getIssueById(resolution.issue_id); + if (issue.user_id !== userId) { + throw (0, response_1.APIError)({ + code: 403, + success: false, + error: "You don't have permission to respond to this resolution.", + }); + } + yield this.ResolutionResponseRepository.createResponse(resolutionId, userId, status); + const updatedResolution = yield this.resolutionRepository.updateResolution(resolutionId, { + num_cluster_members_accepted: status === 'accepted' ? resolution.num_cluster_members_accepted + 1 : resolution.num_cluster_members_accepted, + num_cluster_members_rejected: status === 'declined' ? resolution.num_cluster_members_rejected + 1 : resolution.num_cluster_members_rejected, + }); + const totalResponses = updatedResolution.num_cluster_members_accepted + updatedResolution.num_cluster_members_rejected; + // Only process the final decision if all members have responded + if (totalResponses === updatedResolution.num_cluster_members) { + if (updatedResolution.num_cluster_members_accepted > updatedResolution.num_cluster_members_rejected) { + yield this.finalizeResolution(updatedResolution); + } + else if (updatedResolution.num_cluster_members_accepted < updatedResolution.num_cluster_members_rejected) { + yield this.rejectResolution(updatedResolution); + } + else { + // In case of a tie, accept external resolutions and reject self-resolutions + if (updatedResolution.resolution_source !== 'self') { + yield this.finalizeResolution(updatedResolution); + } + else { + yield this.rejectResolution(updatedResolution); + } + } + } + return updatedResolution; + }); + } + finalizeResolution(resolution) { + return __awaiter(this, void 0, void 0, function* () { + yield this.resolutionRepository.updateResolution(resolution.resolution_id, { + status: 'accepted', + }); + yield this.issueRepository.updateIssueResolutionStatus(resolution.issue_id, true); + if (resolution.resolution_source === 'self') { + yield this.pointsService.awardPoints(resolution.resolver_id, 50, "self-resolution accepted"); + } + else { + yield this.pointsService.awardPoints(resolution.resolver_id, 100, "external resolution accepted"); + } + yield this.issueRepository.updateIssueResolutionStatus(resolution.issue_id, true); + const acceptedUsers = yield this.getAcceptedUsers(resolution.resolution_id); + // Move cluster members who ACCEPTED to a NEW CLUSTER + yield this.clusterService.moveAcceptedMembersToNewCluster(resolution.issue_id, acceptedUsers); + }); + } + rejectResolution(resolution) { + return __awaiter(this, void 0, void 0, function* () { + yield this.resolutionRepository.updateResolution(resolution.resolution_id, { + status: 'declined', + }); + if (resolution.resolution_source !== 'self') { + // Check if all members have responded or if a majority has rejected + const totalResponses = resolution.num_cluster_members_accepted + resolution.num_cluster_members_rejected; + const majority = Math.ceil(resolution.num_cluster_members / 2); + if (totalResponses === resolution.num_cluster_members || resolution.num_cluster_members_rejected > majority) { + yield this.pointsService.penalizeUser(resolution.resolver_id, 50, "external resolution rejected"); + } + } + // Get the list of users who accepted the resolution + const acceptedUsers = yield this.getAcceptedUsers(resolution.resolution_id); + // Move cluster members who ACCEPTED to a NEW CLUSTER + yield this.clusterService.moveAcceptedMembersToNewCluster(resolution.issue_id, acceptedUsers); + }); + } + getAcceptedUsers(resolutionId) { + return __awaiter(this, void 0, void 0, function* () { + return this.ResolutionResponseRepository.getAcceptedUsers(resolutionId); + }); + } + getUserResolutions(userId) { + return __awaiter(this, void 0, void 0, function* () { + return this.resolutionRepository.getUserResolutions(userId); + }); + } + deleteResolution(resolutionId, userId) { + return __awaiter(this, void 0, void 0, function* () { + yield this.resolutionRepository.deleteResolution(resolutionId, userId); + }); + } +} +exports.ResolutionService = ResolutionService; diff --git a/backend/public/modules/shared/models/cluster.js b/backend/public/modules/shared/models/cluster.js new file mode 100644 index 00000000..c8ad2e54 --- /dev/null +++ b/backend/public/modules/shared/models/cluster.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/backend/public/modules/shared/models/comment.js b/backend/public/modules/shared/models/comment.js new file mode 100644 index 00000000..c8ad2e54 --- /dev/null +++ b/backend/public/modules/shared/models/comment.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/backend/public/modules/shared/models/issue.js b/backend/public/modules/shared/models/issue.js new file mode 100644 index 00000000..c8ad2e54 --- /dev/null +++ b/backend/public/modules/shared/models/issue.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/backend/public/modules/shared/models/organization.js b/backend/public/modules/shared/models/organization.js new file mode 100644 index 00000000..c8ad2e54 --- /dev/null +++ b/backend/public/modules/shared/models/organization.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/backend/public/modules/shared/models/reaction.js b/backend/public/modules/shared/models/reaction.js new file mode 100644 index 00000000..c8ad2e54 --- /dev/null +++ b/backend/public/modules/shared/models/reaction.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/backend/public/modules/shared/models/reports.js b/backend/public/modules/shared/models/reports.js new file mode 100644 index 00000000..c8ad2e54 --- /dev/null +++ b/backend/public/modules/shared/models/reports.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/backend/public/modules/shared/models/resolution.js b/backend/public/modules/shared/models/resolution.js new file mode 100644 index 00000000..c8ad2e54 --- /dev/null +++ b/backend/public/modules/shared/models/resolution.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/backend/public/modules/shared/services/openAIService.js b/backend/public/modules/shared/services/openAIService.js new file mode 100644 index 00000000..a74d0177 --- /dev/null +++ b/backend/public/modules/shared/services/openAIService.js @@ -0,0 +1,47 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.OpenAIService = void 0; +const openai_1 = require("openai"); +const response_1 = require("@/types/response"); +class OpenAIService { + constructor() { + this.openai = new openai_1.OpenAI({ + apiKey: process.env.OPENAI_API_KEY || "" + }); + } + getEmbedding(text) { + return __awaiter(this, void 0, void 0, function* () { + try { + const response = yield this.openai.embeddings.create({ + model: "text-embedding-ada-002", + input: text, + }); + return response.data[0].embedding; + } + catch (error) { + console.error('Error getting embedding:', error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An error occurred while generating the embedding.", + }); + } + }); + } + cosineSimilarity(a, b) { + const dotProduct = a.reduce((sum, _, i) => sum + a[i] * b[i], 0); + const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0)); + const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0)); + return dotProduct / (magnitudeA * magnitudeB); + } +} +exports.OpenAIService = OpenAIService; diff --git a/backend/public/modules/shared/services/redisClient.js b/backend/public/modules/shared/services/redisClient.js new file mode 100644 index 00000000..6b552f43 --- /dev/null +++ b/backend/public/modules/shared/services/redisClient.js @@ -0,0 +1,20 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const ioredis_1 = __importDefault(require("ioredis")); +require("dotenv/config"); +let redisClient = null; +try { + redisClient = new ioredis_1.default(process.env.REDIS_URL); + redisClient.on('error', (err) => { + console.error('Redis Client Error', err); + redisClient = null; + }); + redisClient.on('connect', () => console.log('Connected to Redis')); +} +catch (error) { + console.error('Failed to initialize Redis client:', error); +} +exports.default = redisClient; diff --git a/backend/public/modules/shared/services/supabaseClient.js b/backend/public/modules/shared/services/supabaseClient.js new file mode 100644 index 00000000..dbb718b6 --- /dev/null +++ b/backend/public/modules/shared/services/supabaseClient.js @@ -0,0 +1,8 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const supabase_js_1 = require("@supabase/supabase-js"); +require("dotenv/config"); +const supabaseUrl = process.env.SUPABASE_URL || ""; +const supabaseKey = process.env.SUPABASE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY || ""; +const supabase = (0, supabase_js_1.createClient)(supabaseUrl, supabaseKey); +exports.default = supabase; diff --git a/backend/public/modules/subscriptions/controllers/subscriptionsController.js b/backend/public/modules/subscriptions/controllers/subscriptionsController.js new file mode 100644 index 00000000..dcf86676 --- /dev/null +++ b/backend/public/modules/subscriptions/controllers/subscriptionsController.js @@ -0,0 +1,68 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getNotifications = exports.getSubscriptions = exports.locationSubscriptions = exports.categorySubscriptions = exports.issueSubscriptions = void 0; +const response_1 = require("@/utilities/response"); +const subscriptionsService_1 = __importDefault(require("@/modules/subscriptions/services/subscriptionsService")); +const subscriptionsService = new subscriptionsService_1.default(); +const issueSubscriptions = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield subscriptionsService.issueSubscriptions(req.body); + (0, response_1.sendResponse)(res, response); + } + catch (error) { + (0, response_1.sendResponse)(res, error); + } +}); +exports.issueSubscriptions = issueSubscriptions; +const categorySubscriptions = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield subscriptionsService.categorySubscriptions(req.body); + (0, response_1.sendResponse)(res, response); + } + catch (error) { + (0, response_1.sendResponse)(res, error); + } +}); +exports.categorySubscriptions = categorySubscriptions; +const locationSubscriptions = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield subscriptionsService.locationSubscriptions(req.body); + (0, response_1.sendResponse)(res, response); + } + catch (error) { + (0, response_1.sendResponse)(res, error); + } +}); +exports.locationSubscriptions = locationSubscriptions; +const getSubscriptions = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield subscriptionsService.getSubscriptions(req.body); + (0, response_1.sendResponse)(res, response); + } + catch (error) { + (0, response_1.sendResponse)(res, error); + } +}); +exports.getSubscriptions = getSubscriptions; +const getNotifications = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield subscriptionsService.getNotifications(req.body); + (0, response_1.sendResponse)(res, response); + } + catch (error) { + (0, response_1.sendResponse)(res, error); + } +}); +exports.getNotifications = getNotifications; diff --git a/backend/public/modules/subscriptions/repositories/subscriptionsRepository.js b/backend/public/modules/subscriptions/repositories/subscriptionsRepository.js new file mode 100644 index 00000000..58950271 --- /dev/null +++ b/backend/public/modules/subscriptions/repositories/subscriptionsRepository.js @@ -0,0 +1,380 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const supabaseClient_1 = __importDefault(require("@/modules/shared/services/supabaseClient")); +const response_1 = require("@/types/response"); +class SubscriptionsRepository { + issueSubscriptions(_a) { + return __awaiter(this, arguments, void 0, function* ({ user_id, issue_id, }) { + if (!user_id || !issue_id) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Missing required parameters: user_id or issue_id", + }); + } + const { data: selectData, error: selectError } = yield supabaseClient_1.default + .from('subscriptions') + .select('*') + .eq('user_id', user_id) + .single(); + if (selectData && !selectError) { + if (selectData.issues.includes(issue_id === null || issue_id === void 0 ? void 0 : issue_id.toString())) { + const updatedIssues = selectData.issues.filter((issue) => issue !== issue_id); + const { data: updateData, error: updateError } = yield supabaseClient_1.default + .from('subscriptions') + .update({ issues: updatedIssues }) + .eq('user_id', user_id) + .select(); + if (updateError || !updateData) { + console.error(updateError); + console.error(updateData); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while updating the subscription. Please try again later.", + }); + } + return "Subscription successfully removed!"; + } + else { + const updatedIssues = [...selectData.issues, issue_id]; + const { data: updateData, error: updateError } = yield supabaseClient_1.default + .from('subscriptions') + .update({ issues: updatedIssues }) + .eq('user_id', user_id) + .select(); + if (updateError || !updateData) { + console.error(updateError); + console.error(updateData); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while updating the subscription. Please try again later.", + }); + } + return "Subscription successfully created!"; + } + } + else { + const { data: insertData, error: insertError } = yield supabaseClient_1.default + .from('subscriptions') + .insert({ + user_id: user_id, + issues: [issue_id], + categories: [], + locations: [], + }) + .select() + .single(); + if (insertError || !insertData) { + console.error(insertError); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while creating the subscription. Please try again later.", + }); + } + return "Subscription successfully created!"; + } + }); + } + categorySubscriptions(_a) { + return __awaiter(this, arguments, void 0, function* ({ user_id, category_id, }) { + if (!user_id || !category_id) { + throw (0, response_1.APIError)({ + code: 400, + success: false, + error: "Missing required parameters: user_id or category_id", + }); + } + const { data: selectData, error: selectError } = yield supabaseClient_1.default + .from('subscriptions') + .select('*') + .eq('user_id', user_id) + .single(); + if (selectData && !selectError) { + if (selectData.categories.includes(category_id === null || category_id === void 0 ? void 0 : category_id.toString())) { + const updatedCategories = selectData.categories.filter((category) => category !== category_id); + const { data: updateData, error: updateError } = yield supabaseClient_1.default + .from('subscriptions') + .update({ categories: updatedCategories }) + .eq('user_id', user_id) + .select(); + if (updateError || !updateData) { + console.error(updateError); + console.error(updateData); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while updating the subscription. Please try again later.", + }); + } + return "Subscription successfully removed!"; + } + else { + const updatedCategories = [...selectData.categories, category_id]; + const { data: updateData, error: updateError } = yield supabaseClient_1.default + .from('subscriptions') + .update({ categories: updatedCategories }) + .eq('user_id', user_id) + .select(); + if (updateError || !updateData) { + console.error(updateError); + console.error(updateData); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while updating the subscription. Please try again later.", + }); + } + return "Subscription successfully created!"; + } + } + else { + const { data: insertData, error: insertError } = yield supabaseClient_1.default + .from('subscriptions') + .insert({ + user_id: user_id, + categories: [category_id], + issues: [], + locations: [], + }) + .select() + .single(); + if (insertError || !insertData) { + console.error(insertError); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while creating the subscription. Please try again later.", + }); + } + return "Subscription successfully created!"; + } + }); + } + locationSubscriptions(_a) { + return __awaiter(this, arguments, void 0, function* ({ user_id, location_id, }) { + const { data: selectData, error: selectError } = yield supabaseClient_1.default + .from('subscriptions') + .select('locations') + .eq('user_id', user_id) + .single(); + if (selectData && !selectError) { + if (selectData.locations.includes(location_id === null || location_id === void 0 ? void 0 : location_id.toString())) { + const updatedlocations = selectData.locations.filter((location) => location !== location_id); + const { data: updateData, error: updateError } = yield supabaseClient_1.default + .from('subscriptions') + .update({ locations: updatedlocations }) + .eq('user_id', user_id) + .select(); + if (updateError || !updateData) { + console.error(updateError); + console.error(updateData); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while updating the subscription. Please try again later.", + }); + } + return "Subscription successfully removed!"; + } + else { + const updatedlocations = [...selectData.locations, location_id]; + const { data: updateData, error: updateError } = yield supabaseClient_1.default + .from('subscriptions') + .update({ locations: updatedlocations }) + .eq('user_id', user_id) + .select(); + if (updateError || !updateData) { + console.error(updateError); + console.error(updateData); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while updating the subscription. Please try again later.", + }); + } + return "Subscription successfully created!"; + } + } + else { + const { data: insertData, error: insertError } = yield supabaseClient_1.default + .from('subscriptions') + .insert({ + user_id: user_id, + locations: [location_id], + categories: [], + issues: [], + }) + .select() + .single(); + if (insertError || !insertData) { + console.error(insertError); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while creating the subscription. Please try again later.", + }); + } + return "Subscription successfully created!"; + } + }); + } + getSubscriptions(_a) { + return __awaiter(this, arguments, void 0, function* ({ user_id, }) { + const { data: selectData } = yield supabaseClient_1.default + .from('subscriptions') + .select('issues, categories, locations') + .eq('user_id', user_id) + .single(); + const result = { + issues: (selectData === null || selectData === void 0 ? void 0 : selectData.issues) || [], + categories: (selectData === null || selectData === void 0 ? void 0 : selectData.categories) || [], + locations: (selectData === null || selectData === void 0 ? void 0 : selectData.locations) || [], + }; + return result; + }); + } + getNotifications(_a) { + return __awaiter(this, arguments, void 0, function* ({ user_id }) { + const subscriptions = yield this.getSubscriptions({ user_id }); + if (!subscriptions) { + console.error('Failed to retrieve subscriptions'); + return []; + } + const { issues: subIssues, categories: subCategories, locations: subLocations } = subscriptions; + const { data: issueData, error: issueError } = yield supabaseClient_1.default + .from('issue') + .select(` + issue_id, + user_id, + location_id, + category_id, + content, + is_anonymous, + created_at, + sentiment, + comment ( + user_id, + content, + is_anonymous, + created_at + ), + reaction ( + user_id, + emoji, + created_at + ), + resolution ( + resolver_id, + resolution_text, + status, + created_at + ) + `); + if (issueError) { + console.error('Error fetching issue data:', issueError); + return []; + } + const { data: pointsData, error: pointsError } = yield supabaseClient_1.default + .from('points_history') + .select(` + user_id, + action, + points, + created_at + `); + if (pointsError) { + console.error('Error fetching points history:', pointsError); + return []; + } + const filteredNotifications = []; + const addedIssues = new Set(); + issueData.forEach(issue => { + var _a, _b, _c; + const issueIdStr = (_a = issue.issue_id) === null || _a === void 0 ? void 0 : _a.toString(); + const categoryIdStr = (_b = issue.category_id) === null || _b === void 0 ? void 0 : _b.toString(); + const locationIdStr = (_c = issue.location_id) === null || _c === void 0 ? void 0 : _c.toString(); + if (subIssues.includes(issueIdStr) || subCategories.includes(categoryIdStr) || subLocations.includes(locationIdStr) || issue.user_id === user_id) { + if (!addedIssues.has(issueIdStr)) { + filteredNotifications.push({ + type: 'issue', + content: issue.content, + issue_id: issue.issue_id, + category: issue.category_id, + location: issue.location_id, + created_at: issue.created_at + }); + addedIssues.add(issueIdStr); + } + } + if (issue.comment) { + issue.comment.forEach(comment => { + if (subIssues.includes(issueIdStr) || comment.user_id === user_id) { + filteredNotifications.push({ + type: 'comment', + content: comment.content, + issue_id: issue.issue_id, + category: issue.category_id, + location: issue.location_id, + created_at: comment.created_at + }); + } + }); + } + if (issue.reaction) { + issue.reaction.forEach(reaction => { + if (subIssues.includes(issueIdStr) || reaction.user_id === user_id) { + filteredNotifications.push({ + type: 'reaction', + content: `reacted with ${reaction.emoji}`, + issue_id: issue.issue_id, + category: issue.category_id, + location: issue.location_id, + created_at: reaction.created_at + }); + } + }); + } + if (issue.resolution) { + issue.resolution.forEach(resolution => { + if (subIssues.includes(issueIdStr) || resolution.resolver_id === user_id) { + filteredNotifications.push({ + type: 'resolution', + content: `Your ${resolution.resolution_text}`, + issue_id: issue.issue_id, + category: issue.category_id, + location: issue.location_id, + created_at: resolution.created_at + }); + } + }); + } + }); + pointsData.forEach(points => { + if (points.user_id === user_id) { + filteredNotifications.push({ + type: 'points', + content: `You earned ${points.points} points, because you ${points.action}.`, + created_at: points.created_at + }); + } + }); + return filteredNotifications.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); + }); + } +} +exports.default = SubscriptionsRepository; diff --git a/backend/public/modules/subscriptions/routes/subscriptionsRoutes.js b/backend/public/modules/subscriptions/routes/subscriptionsRoutes.js new file mode 100644 index 00000000..9ab12834 --- /dev/null +++ b/backend/public/modules/subscriptions/routes/subscriptionsRoutes.js @@ -0,0 +1,36 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const subscriptionsController = __importStar(require("@/modules/subscriptions/controllers/subscriptionsController")); +const middleware_1 = require("@/middleware/middleware"); +const router = (0, express_1.Router)(); +router.use(middleware_1.verifyAndGetUser); +router.post("/issue", subscriptionsController.issueSubscriptions); +router.post("/category", subscriptionsController.categorySubscriptions); +router.post("/location", subscriptionsController.locationSubscriptions); +router.post("/subscriptions", subscriptionsController.getSubscriptions); +router.post("/notifications", subscriptionsController.getNotifications); +exports.default = router; diff --git a/backend/public/modules/subscriptions/services/subscriptionsService.js b/backend/public/modules/subscriptions/services/subscriptionsService.js new file mode 100644 index 00000000..fb3eab81 --- /dev/null +++ b/backend/public/modules/subscriptions/services/subscriptionsService.js @@ -0,0 +1,105 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const subscriptionsRepository_1 = __importDefault(require("@/modules/subscriptions/repositories/subscriptionsRepository")); +const response_1 = require("@/types/response"); +class SubscriptionsService { + constructor() { + this.SubscriptionsRepository = new subscriptionsRepository_1.default(); + } + setSubscriptionsRepository(SubscriptionsRepository) { + this.SubscriptionsRepository = SubscriptionsRepository; + } + issueSubscriptions(params) { + return __awaiter(this, void 0, void 0, function* () { + try { + const data = yield this.SubscriptionsRepository.issueSubscriptions(params); + return { code: 200, success: true, data }; + } + catch (error) { + console.error("Error: ", error); + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "Issue: Something Went wrong", + }); + } + }); + } + categorySubscriptions(params) { + return __awaiter(this, void 0, void 0, function* () { + try { + const data = yield this.SubscriptionsRepository.categorySubscriptions(params); + return { code: 200, success: true, data }; + } + catch (error) { + console.error("Error: ", error); + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "Category: Something Went wrong", + }); + } + }); + } + locationSubscriptions(params) { + return __awaiter(this, void 0, void 0, function* () { + try { + const data = yield this.SubscriptionsRepository.locationSubscriptions(params); + return { code: 200, success: true, data }; + } + catch (error) { + console.error("Error: ", error); + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "Location: Something Went wrong", + }); + } + }); + } + getSubscriptions(params) { + return __awaiter(this, void 0, void 0, function* () { + try { + const data = yield this.SubscriptionsRepository.getSubscriptions(params); + return { code: 200, success: true, data }; + } + catch (error) { + console.error("Error: ", error); + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "Notifications: Something Went wrong", + }); + } + }); + } + getNotifications(params) { + return __awaiter(this, void 0, void 0, function* () { + try { + const data = yield this.SubscriptionsRepository.getNotifications(params); + return { code: 200, success: true, data }; + } + catch (error) { + console.error("Error: ", error); + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "Notifications: Something Went wrong", + }); + } + }); + } +} +exports.default = SubscriptionsService; diff --git a/backend/public/modules/users/controllers/userController.js b/backend/public/modules/users/controllers/userController.js new file mode 100644 index 00000000..dd53d8e5 --- /dev/null +++ b/backend/public/modules/users/controllers/userController.js @@ -0,0 +1,63 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.changePassword = exports.updateUsername = exports.updateUserProfile = exports.getUserById = void 0; +const response_1 = require("@/utilities/response"); +const userService_1 = require("@/modules/users/services/userService"); +const cacheMiddleware_1 = require("@/middleware/cacheMiddleware"); +const cacheUtils_1 = require("@/utilities/cacheUtils"); +const userService = new userService_1.UserService(); +exports.getUserById = [ + (0, cacheMiddleware_1.cacheMiddleware)(300), + (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield userService.getUserById(req.params.id, req.body.user_id); + (0, response_1.sendResponse)(res, response); + } + catch (error) { + (0, response_1.sendResponse)(res, error); + } + }) +]; +const updateUserProfile = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield userService.updateUserProfile(req.params.id, req.body, req.file); + (0, cacheUtils_1.clearCache)(`/api/users/${req.params.id}`); + (0, cacheUtils_1.clearCachePattern)('__express__/api/users*'); + (0, response_1.sendResponse)(res, response); + } + catch (error) { + (0, response_1.sendResponse)(res, error); + } +}); +exports.updateUserProfile = updateUserProfile; +const updateUsername = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield userService.updateUsername(req.params.id, req.body.username); + (0, cacheUtils_1.clearCache)(`/api/users/${req.params.id}`); + (0, cacheUtils_1.clearCachePattern)('__express__/api/users*'); + (0, response_1.sendResponse)(res, response); + } + catch (error) { + (0, response_1.sendResponse)(res, error); + } +}); +exports.updateUsername = updateUsername; +const changePassword = (req, res) => __awaiter(void 0, void 0, void 0, function* () { + try { + const response = yield userService.changePassword(req.params.id, req.body.currentPassword, req.body.newPassword); + (0, response_1.sendResponse)(res, response); + } + catch (error) { + (0, response_1.sendResponse)(res, error); + } +}); +exports.changePassword = changePassword; diff --git a/backend/public/modules/users/repositories/userRepository.js b/backend/public/modules/users/repositories/userRepository.js new file mode 100644 index 00000000..03e4a807 --- /dev/null +++ b/backend/public/modules/users/repositories/userRepository.js @@ -0,0 +1,173 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const supabaseClient_1 = __importDefault(require("../../shared/services/supabaseClient")); +const response_1 = require("../../../types/response"); +class UserRepository { + getUserById(userId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("user") + .select("*") + .eq("user_id", userId) + .maybeSingle(); + if (error) { + console.error("Supabase error:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + if (!data) { + console.error("User not found in database - userId:", userId); + return null; + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "User does not exist", + }); + } + const [{ count: totalIssues }, { count: resolvedIssues }] = yield Promise.all([ + supabaseClient_1.default + .from("issue") + .select("*", { count: "exact" }) + .eq("user_id", userId), + supabaseClient_1.default + .from("issue") + .select("*", { count: "exact" }) + .eq("user_id", userId) + .not("resolved_at", "is", null), + ]); + return Object.assign(Object.assign({}, data), { total_issues: totalIssues, resolved_issues: resolvedIssues }); + }); + } + updateUserProfile(userId, updateData) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("user") + .update(updateData) + .eq("user_id", userId) + .select() + .maybeSingle(); + if (error) { + console.error("Supabase error:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred. Please try again later.", + }); + } + if (!data) { + console.error("User not found in database after update - userId:", userId); + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "User does not exist", + }); + } + return data; + }); + } + updateUserLocation(userId, locationId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("user") + .update({ location_id: locationId }) + .eq("user_id", userId) + .single(); + if (error) { + console.error("Supabase error:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while updating user location.", + }); + } + return data; + }); + } + getUserWithLocation(userId) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("user") + .select(` + *, + location:location_id ( + location_id, + province, + city, + suburb, + district + ) + `) + .eq("user_id", userId) + .single(); + if (error) { + console.error("Supabase error:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while fetching user data.", + }); + } + return data; + }); + } + updateUsername(userId, newUsername) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("user") + .update({ username: newUsername }) + .eq("user_id", userId) + .select() + .single(); + if (error) { + console.error("Supabase error:", error); + if (error.message.includes("duplicate key value violates unique constraint")) { + throw (0, response_1.APIError)({ + code: 409, + success: false, + error: "Username already taken.", + }); + } + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while updating username.", + }); + } + return data; + }); + } + isUsernameTaken(username) { + return __awaiter(this, void 0, void 0, function* () { + const { data, error } = yield supabaseClient_1.default + .from("user") + .select("username") + .eq("username", username) + .maybeSingle(); + if (error) { + console.error("Supabase error:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while checking username availability.", + }); + } + return !!data; + }); + } +} +exports.default = UserRepository; diff --git a/backend/public/modules/users/routes/userRoutes.js b/backend/public/modules/users/routes/userRoutes.js new file mode 100644 index 00000000..0a20e8d9 --- /dev/null +++ b/backend/public/modules/users/routes/userRoutes.js @@ -0,0 +1,16 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = __importDefault(require("express")); +const multer_1 = __importDefault(require("multer")); +const userController_1 = require("@/modules/users/controllers/userController"); +const middleware_1 = require("@/middleware/middleware"); +const router = express_1.default.Router(); +const upload = (0, multer_1.default)(); +router.post("/:id", middleware_1.verifyAndGetUser, userController_1.getUserById); +router.put("/:id", middleware_1.verifyAndGetUser, upload.single("profile_picture"), userController_1.updateUserProfile); +router.put("/:id/username", middleware_1.verifyAndGetUser, userController_1.updateUsername); +router.put("/:id/password", middleware_1.verifyAndGetUser, userController_1.changePassword); +exports.default = router; diff --git a/backend/public/modules/users/services/userService.js b/backend/public/modules/users/services/userService.js new file mode 100644 index 00000000..358ae6e1 --- /dev/null +++ b/backend/public/modules/users/services/userService.js @@ -0,0 +1,280 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.UserService = void 0; +const userRepository_1 = __importDefault(require("../repositories/userRepository")); +const response_1 = require("../../../types/response"); +const supabaseClient_1 = __importDefault(require("../../shared/services/supabaseClient")); +class UserService { + constructor() { + this.userRepository = new userRepository_1.default(); + } + setUserRepository(userRepository) { + this.userRepository = userRepository; + } + getUserById(userId, authenticatedUserId) { + return __awaiter(this, void 0, void 0, function* () { + const user = yield this.userRepository.getUserById(userId); + if (!user) { + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "User not found", + }); + } + const isOwner = userId === authenticatedUserId; + return { + code: 200, + success: true, + data: Object.assign(Object.assign({}, user), { is_owner: isOwner }), + }; + }); + } + updateUserProfile(userId, updateData, file) { + return __awaiter(this, void 0, void 0, function* () { + const user = yield this.userRepository.getUserById(userId); + if (!user) { + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "User not found", + }); + } + if (file) { + // Delete the old profile picture if it exists and is not the default one + if (user.image_url && !user.image_url.includes("default.png")) { + try { + const fileName = user.image_url.split("/").pop(); + if (!fileName) { + throw new Error("Invalid image URL format"); + } + const { error: deleteError } = yield supabaseClient_1.default.storage + .from("user") + .remove([fileName]); + if (deleteError) { + console.error("Failed to delete old profile picture:", deleteError); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "Failed to delete old profile picture", + }); + } + } + catch (error) { + console.error("Error deleting old profile picture:", error); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "Failed to delete old profile picture", + }); + } + } + // Upload the new profile picture + const fileName = `profile_pictures/${userId}-${Date.now()}-${file.originalname}`; + const { error: uploadError } = yield supabaseClient_1.default.storage + .from("user") + .upload(fileName, file.buffer); + if (uploadError) { + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "Failed to upload new profile picture", + }); + } + // Retrieve the public URL for the new profile picture + const { data: urlData } = supabaseClient_1.default.storage + .from("user") + .getPublicUrl(fileName); + updateData.image_url = urlData.publicUrl; + } + if (updateData.location) { + const locationData = JSON.parse(updateData.location); + const { data: locationRecord, error: locationError } = yield supabaseClient_1.default + .from('location') + .upsert({ + province: locationData.value.province, + city: locationData.value.city, + suburb: locationData.value.suburb, + district: locationData.value.district, + place_id: locationData.value.place_id, + latitude: locationData.value.lat, + longitude: locationData.value.lng, + }) + .select() + .single(); + if (locationError) { + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "Failed to update location", + }); + } + updateData.location_id = locationRecord.location_id; + delete updateData.location; // Remove the location object from updateData + } + const updatedUser = yield this.userRepository.updateUserProfile(userId, updateData); + if (!updatedUser) { + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "User does not exist", + }); + } + return { + code: 200, + success: true, + data: updatedUser, + }; + }); + } + updateUserLocation(userId, locationId) { + return __awaiter(this, void 0, void 0, function* () { + const updatedUser = yield this.userRepository.updateUserLocation(userId, locationId); + if (!updatedUser) { + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "User not found", + }); + } + return { + code: 200, + success: true, + data: updatedUser, + }; + }); + } + getUserWithLocation(userId) { + return __awaiter(this, void 0, void 0, function* () { + const user = yield this.userRepository.getUserWithLocation(userId); + if (!user) { + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "User not found", + }); + } + return { + code: 200, + success: true, + data: user, + }; + }); + } + updateUsername(userId, newUsername) { + return __awaiter(this, void 0, void 0, function* () { + const updatedUser = yield this.userRepository.updateUsername(userId, newUsername); + if (!updatedUser) { + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "User not found", + }); + } + return { + code: 200, + success: true, + data: updatedUser, + }; + }); + } + checkUsernameAvailability(username) { + return __awaiter(this, void 0, void 0, function* () { + try { + const isUsernameTaken = yield this.userRepository.isUsernameTaken(username); + return { + code: 200, + success: true, + data: !isUsernameTaken + }; + } + catch (error) { + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred while checking username availability.", + }); + } + }); + } + changePassword(userId, currentPassword, newPassword) { + return __awaiter(this, void 0, void 0, function* () { + try { + const { data: userData, error: userError } = yield supabaseClient_1.default + .from('user') + .select('email_address') + .eq('user_id', userId) + .single(); + if (userError) { + console.error("Error fetching user data:", userError); + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "User not found", + }); + } + if (!userData) { + console.error("User data is null for userId:", userId); + throw (0, response_1.APIError)({ + code: 404, + success: false, + error: "User not found", + }); + } + // Verify the current password + const { error: signInError } = yield supabaseClient_1.default.auth.signInWithPassword({ + email: userData.email_address, + password: currentPassword, + }); + if (signInError) { + console.error("Sign-in error:", signInError); + throw (0, response_1.APIError)({ + code: 401, + success: false, + error: "Current password is incorrect", + }); + } + // If sign-in was successful, update the password + const { error: updateError } = yield supabaseClient_1.default.auth.updateUser({ + password: newPassword, + }); + if (updateError) { + console.error("Password update error:", updateError); + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "Failed to update password", + }); + } + return { + code: 200, + success: true, + data: undefined, + }; + } + catch (error) { + console.error("Caught error in changePassword:", error); + if (error instanceof response_1.APIError) { + throw error; + } + throw (0, response_1.APIError)({ + code: 500, + success: false, + error: "An unexpected error occurred", + }); + } + }); + } +} +exports.UserService = UserService; diff --git a/backend/public/modules/visualizations/controllers/visualizationController.js b/backend/public/modules/visualizations/controllers/visualizationController.js new file mode 100644 index 00000000..9a4683fe --- /dev/null +++ b/backend/public/modules/visualizations/controllers/visualizationController.js @@ -0,0 +1,27 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getVizData = void 0; +const visualizationService_1 = require("@/modules/visualizations/services/visualizationService"); +const response_1 = require("@/utilities/response"); +const visualizationService = new visualizationService_1.VisualizationService(); +function getVizData(req, res) { + return __awaiter(this, void 0, void 0, function* () { + try { + const response = yield visualizationService.getVizData(); + (0, response_1.sendResponse)(res, response); + } + catch (error) { + (0, response_1.sendResponse)(res, error); + } + }); +} +exports.getVizData = getVizData; diff --git a/backend/public/modules/visualizations/repositories/visualizationRepository.js b/backend/public/modules/visualizations/repositories/visualizationRepository.js new file mode 100644 index 00000000..0294391c --- /dev/null +++ b/backend/public/modules/visualizations/repositories/visualizationRepository.js @@ -0,0 +1,59 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.VisualizationRepository = void 0; +const issueRepository_1 = __importDefault(require("@/modules/issues/repositories/issueRepository")); +const issueRepository = new issueRepository_1.default(); +class VisualizationRepository { + getVizData() { + return __awaiter(this, void 0, void 0, function* () { + const issues = yield issueRepository.getIssues({ from: 0, amount: 99 }); + const data = {}; + for (const issue of issues) { + const location = issue.location; + this.addIssue(data, location + ? [ + location.province, + location.city, + location.suburb, + location.district, + ] + : [], issue.category.name); + } + return data; + }); + } + addIssue(data, place, category) { + var _a; + (_a = data["$count"]) !== null && _a !== void 0 ? _a : (data["$count"] = 0); + data["$count"]++; + this._addIssue(data, place, category); + } + _addIssue(data, place, category) { + var _a, _b; + while (place.length && !place[0]) { + place.shift(); + } + if (place.length === 0) { + (_a = data[category]) !== null && _a !== void 0 ? _a : (data[category] = { $count: 0 }); + data[category]["$count"]++; + return; + } + const placeName = place.shift(); + (_b = data[placeName]) !== null && _b !== void 0 ? _b : (data[placeName] = { $count: 0 }); + data[placeName]["$count"]++; + this._addIssue(data[placeName], place, category); + } +} +exports.VisualizationRepository = VisualizationRepository; diff --git a/backend/public/modules/visualizations/routes/visualizationRoutes.js b/backend/public/modules/visualizations/routes/visualizationRoutes.js new file mode 100644 index 00000000..e4fe3d33 --- /dev/null +++ b/backend/public/modules/visualizations/routes/visualizationRoutes.js @@ -0,0 +1,30 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const visualizationController = __importStar(require("@/modules/visualizations/controllers/visualizationController")); +const router = (0, express_1.Router)(); +router.post("/", visualizationController.getVizData); +exports.default = router; diff --git a/backend/public/modules/visualizations/services/visualizationService.js b/backend/public/modules/visualizations/services/visualizationService.js new file mode 100644 index 00000000..fc350479 --- /dev/null +++ b/backend/public/modules/visualizations/services/visualizationService.js @@ -0,0 +1,30 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.VisualizationService = void 0; +const visualizationRepository_1 = require("@/modules/visualizations/repositories/visualizationRepository"); +const response_1 = require("@/types/response"); +class VisualizationService { + constructor() { + this.visualizationRepository = new visualizationRepository_1.VisualizationRepository(); + } + getVizData() { + return __awaiter(this, void 0, void 0, function* () { + const vizData = yield this.visualizationRepository.getVizData(); + return (0, response_1.APIData)({ + code: 200, + success: true, + data: vizData, + }); + }); + } +} +exports.VisualizationService = VisualizationService; diff --git a/backend/public/types/comment.js b/backend/public/types/comment.js new file mode 100644 index 00000000..c8ad2e54 --- /dev/null +++ b/backend/public/types/comment.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/backend/public/types/issue.js b/backend/public/types/issue.js new file mode 100644 index 00000000..c8ad2e54 --- /dev/null +++ b/backend/public/types/issue.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/backend/public/types/pagination.js b/backend/public/types/pagination.js new file mode 100644 index 00000000..c8ad2e54 --- /dev/null +++ b/backend/public/types/pagination.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/backend/public/types/response.js b/backend/public/types/response.js new file mode 100644 index 00000000..ce957aff --- /dev/null +++ b/backend/public/types/response.js @@ -0,0 +1,11 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.APIData = exports.APIError = void 0; +function APIError(error) { + return error; +} +exports.APIError = APIError; +function APIData(data) { + return data; +} +exports.APIData = APIData; diff --git a/backend/public/types/shared.js b/backend/public/types/shared.js new file mode 100644 index 00000000..c8ad2e54 --- /dev/null +++ b/backend/public/types/shared.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/backend/public/types/subscriptions.js b/backend/public/types/subscriptions.js new file mode 100644 index 00000000..c8ad2e54 --- /dev/null +++ b/backend/public/types/subscriptions.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/backend/public/types/tempType.js b/backend/public/types/tempType.js new file mode 100644 index 00000000..c8ad2e54 --- /dev/null +++ b/backend/public/types/tempType.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/backend/public/types/users.js b/backend/public/types/users.js new file mode 100644 index 00000000..c8ad2e54 --- /dev/null +++ b/backend/public/types/users.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/backend/public/types/visualization.js b/backend/public/types/visualization.js new file mode 100644 index 00000000..c8ad2e54 --- /dev/null +++ b/backend/public/types/visualization.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/backend/public/utilities/cacheUtils.js b/backend/public/utilities/cacheUtils.js new file mode 100644 index 00000000..c8ea8e68 --- /dev/null +++ b/backend/public/utilities/cacheUtils.js @@ -0,0 +1,26 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.clearCachePattern = exports.clearCache = void 0; +const redisClient_1 = __importDefault(require("@/modules/shared/services/redisClient")); +const clearCache = (path, body) => { + if (!redisClient_1.default) + return; + const key = `__express__${path}${body ? `__${JSON.stringify(body)}` : ''}`; + redisClient_1.default.del(key).catch(err => console.error('Failed to clear cache:', err)); +}; +exports.clearCache = clearCache; +const clearCachePattern = (pattern) => { + if (!redisClient_1.default) + return; + redisClient_1.default.keys(pattern) + .then((keys) => { + if (keys.length > 0) { + redisClient_1.default === null || redisClient_1.default === void 0 ? void 0 : redisClient_1.default.del(keys); + } + }) + .catch(err => console.error('Failed to clear cache pattern:', err)); +}; +exports.clearCachePattern = clearCachePattern; diff --git a/backend/public/utilities/response.js b/backend/public/utilities/response.js new file mode 100644 index 00000000..45c9c1b8 --- /dev/null +++ b/backend/public/utilities/response.js @@ -0,0 +1,7 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.sendResponse = void 0; +function sendResponse(res, response) { + res.status(response.code).json(response); +} +exports.sendResponse = sendResponse; diff --git a/backend/public/utilities/validators.js b/backend/public/utilities/validators.js new file mode 100644 index 00000000..4cfd7015 --- /dev/null +++ b/backend/public/utilities/validators.js @@ -0,0 +1,11 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.validateOrganizationUsername = exports.validateOrganizationName = void 0; +function validateOrganizationName(name) { + return name.length >= 3 && name.length <= 50; +} +exports.validateOrganizationName = validateOrganizationName; +function validateOrganizationUsername(username) { + return /^[a-zA-Z0-9_-]{3,20}$/.test(username); +} +exports.validateOrganizationUsername = validateOrganizationUsername; From 0210ee5addf2da5881213541dcf8ee22045eb7b3 Mon Sep 17 00:00:00 2001 From: ShamaKamina Date: Thu, 26 Sep 2024 10:25:22 +0200 Subject: [PATCH 02/12] made the about page adhere to the theme preference of the user --- frontend/app/about/page.tsx | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/frontend/app/about/page.tsx b/frontend/app/about/page.tsx index 35feead5..5fd8d218 100644 --- a/frontend/app/about/page.tsx +++ b/frontend/app/about/page.tsx @@ -1,14 +1,15 @@ -"use client"; +"use client" import React from "react"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { motion } from "framer-motion"; import { ArrowLeft } from "lucide-react"; +import { useTheme } from "next-themes"; const AboutPage = () => { - const router = useRouter(); + const { theme } = useTheme(); const features = [ { title: "Incident Reporting", description: "Report issues with government services", icon: "🚨" }, { title: "Data Analysis", description: "Gain insights from reported data", icon: "📊" }, @@ -22,15 +23,18 @@ const AboutPage = () => { router.push("/"); }; + const isDarkMode = theme === 'dark'; + return ( -
+
{ priority width={250} height={150} - src="/images/b-logo-full-black.png" + src={isDarkMode ? "/images/b-logo-full.png" : "/images/b-logo-full-black.png"} alt="The Republic logo" /> @@ -58,11 +62,10 @@ const AboutPage = () => { {/* Hero Section */}
Revolutionize Citizen Engagement @@ -77,14 +80,13 @@ const AboutPage = () => {
{/* Project Overview */} -
+
About The Republic @@ -103,11 +105,10 @@ const AboutPage = () => {
Core Features @@ -120,12 +121,16 @@ const AboutPage = () => { {features.map((feature, index) => (
{feature.icon}
-

{feature.title}

+

{feature.title}

{feature.description}

))} @@ -134,7 +139,7 @@ const AboutPage = () => {
-