├── .gitignore ├── src ├── utils │ └── logger.ts ├── types │ └── express.d.ts ├── config │ └── cassandra.ts ├── routes │ └── notification.route.ts ├── index.ts ├── socket │ └── socket.ts ├── services │ └── notification.service.ts └── controllers │ └── notification.controller.ts ├── Dockerfile ├── Dockerfile.build ├── package.json ├── tsconfig.json └── Jenkinsfile-sun /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | dist -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import pino from 'pino'; 2 | export const logger = pino({ 3 | level: 'info' 4 | }); -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | WORKDIR /usr/src/app 3 | COPY . . 4 | RUN npm install 5 | 6 | RUN npm run build 7 | 8 | CMD ["npm", "start"] 9 | -------------------------------------------------------------------------------- /src/types/express.d.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'socket.io'; 2 | declare global { 3 | namespace Express { 4 | interface Request { 5 | io: Server; 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /Dockerfile.build: -------------------------------------------------------------------------------- 1 | FROM node:18 2 | 3 | WORKDIR /usr/src/app 4 | 5 | # Copy all files 6 | COPY . . 7 | 8 | # Install dependencies and build the application 9 | RUN npm install 10 | RUN npm run build 11 | 12 | # Start the application 13 | CMD ["npm", "start"] 14 | -------------------------------------------------------------------------------- /src/config/cassandra.ts: -------------------------------------------------------------------------------- 1 | import { Client } from 'cassandra-driver'; 2 | 3 | const cassandraIP=process.env.CASSANDRA_IP||"localhost:9042"; 4 | const client = new Client({ 5 | contactPoints: [cassandraIP], 6 | localDataCenter: 'datacenter1', 7 | keyspace: process.env.CASSANDRA_KEYSPACE||'sunbird', 8 | }); 9 | 10 | // Connect to the database 11 | client.connect() 12 | .then(() => console.log('Connected to Cassandra')) 13 | .catch((err) => console.error('Error connecting to Cassandra:', err)); 14 | 15 | export { client }; 16 | -------------------------------------------------------------------------------- /src/routes/notification.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as notificationController from '../controllers/notification.controller'; 3 | 4 | const router = express.Router(); 5 | 6 | router.post('/notifications', notificationController.createNotification); 7 | router.get('/notifications/:userid', notificationController.getUserNotifications); 8 | router.post('/notifications/read', notificationController.markNotificationAsRead); 9 | router.post('/notifications/readAll', notificationController.markAllNotificationAsRead); 10 | 11 | 12 | export default router; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notification-engine", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "node dist/index.js", 7 | "build": "tsc", 8 | "dev": "ts-node src/index.ts", 9 | "lint": "eslint . --ext .ts", 10 | "format": "prettier --write \"src/**/*.ts\"", 11 | "clean": "rm -rf dist" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "description": "", 16 | "devDependencies": { 17 | "@types/express": "^5.0.0", 18 | "@types/node": "^22.10.5", 19 | "@types/socket.io": "^3.0.1", 20 | "ts-node": "^10.9.2", 21 | "typescript": "^5.7.2" 22 | }, 23 | "dependencies": { 24 | "cassandra-driver": "^4.7.2", 25 | "cors": "^2.8.5", 26 | "dotenv": "^16.4.7", 27 | "express": "^4.21.2", 28 | "pino": "^9.6.0", 29 | "socket.io": "^4.8.1", 30 | "uuid": "^11.0.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | import express from 'express'; 3 | import cors from 'cors'; 4 | import notificationRoutes from './routes/notification.route'; 5 | import { createServer } from 'http'; 6 | import { Server } from 'socket.io'; 7 | import { setupSocket } from './socket/socket'; 8 | import { logger } from "./utils/logger"; 9 | 10 | const applicationPort= process.env.APPLICATION_PORT || "3013" 11 | const app = express(); 12 | const httpServer = createServer(app); 13 | 14 | const io = new Server(httpServer, { 15 | cors: { 16 | origin: '*', 17 | methods: ['GET', 'POST'], 18 | }, 19 | }); 20 | 21 | export const attachIo = (io: Server) => { 22 | return (req: any, res: any, next: any) => { 23 | req.io = io; 24 | next(); 25 | }; 26 | }; 27 | 28 | app.use((req, _res, next) => { 29 | logger.info(`Requested Route: ${req.method} ${req.url}`); 30 | next(); 31 | }); 32 | app.use(attachIo(io)); 33 | app.use(cors()); 34 | app.use(express.json()); 35 | app.use('/v1', notificationRoutes); 36 | setupSocket(io); 37 | 38 | httpServer.listen(applicationPort, () => { 39 | console.log(`Server running at port ${applicationPort}`); 40 | }); 41 | export default app; 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", // Target JavaScript version 4 | "module": "CommonJS", // Use CommonJS modules 5 | "rootDir": "./src", // Root directory for TypeScript files 6 | "outDir": "./dist", // Output directory for compiled JavaScript files 7 | "strict": true, // Enable strict type checking 8 | "esModuleInterop": true, // Enable compatibility with CommonJS and ES modules 9 | "forceConsistentCasingInFileNames": true, // Enforce consistent casing 10 | "skipLibCheck": true, // Skip type checking of declaration files 11 | "moduleResolution": "node", // Use Node module resolution 12 | "resolveJsonModule": true, // Support importing JSON files 13 | "types": ["node"], // Include Node.js type definitions 14 | "lib": ["ES2020", "DOM"] // Include modern JavaScript and DOM APIs 15 | }, 16 | "include": ["src/**/*"], // Include all TypeScript files in the src folder 17 | "exclude": ["node_modules", "dist"] // Exclude node_modules and the dist folder 18 | } 19 | -------------------------------------------------------------------------------- /src/socket/socket.ts: -------------------------------------------------------------------------------- 1 | import { Server, Socket } from 'socket.io'; 2 | import { markAsRead, getNotifications, markAllAsRead } from '../services/notification.service'; 3 | import { logger } from '../utils/logger'; 4 | 5 | const setupSocket = (io: Server) => { 6 | io.on('connection', (socket: Socket) => { 7 | // Mark notification as read 8 | socket.on('markAsRead', async (data: { notificationId: string, userId: string }) => { 9 | try { 10 | logger.info('Marking notification as read event triggered'); 11 | await markAsRead(data.notificationId, data.userId); 12 | socket.emit('readConfirmation', { notificationIds: data.notificationId }); 13 | } catch (error) { 14 | console.error('Error marking notification as read:', error); 15 | } 16 | }); 17 | //Mark all notifications as read 18 | socket.on('markAllAsRead', async (data: { userId: string }) => { 19 | try { 20 | logger.info('Marking all notification as read triggered'); 21 | await markAllAsRead(data.userId); 22 | socket.emit('readConfirmation', { userId: data.userId }); 23 | } catch (error) { 24 | console.error('Error marking all notifications as read:', error); 25 | } 26 | }); 27 | socket.on('getNotifications', async (data: { userId: string }) => { 28 | try { 29 | logger.info('Get notifications event triggered'); 30 | const unreadNotificationsData = await getNotifications(data.userId); 31 | socket.emit('notificationsData', { notificationData: unreadNotificationsData }); 32 | } catch (error) { 33 | console.error('Error getting all notifications', error); 34 | } 35 | }); 36 | socket.on('disconnect', () => { 37 | console.log(`User disconnected: ${socket.id}`); 38 | }); 39 | }); 40 | }; 41 | 42 | export { setupSocket }; 43 | -------------------------------------------------------------------------------- /src/services/notification.service.ts: -------------------------------------------------------------------------------- 1 | import { client } from '../config/cassandra'; 2 | import { Server } from 'socket.io'; 3 | 4 | // Create a notification 5 | export const createNotification = async (newNotification: any, io: Server) => { 6 | const query = ` 7 | INSERT INTO user_notifications ( 8 | id, userid, category, createdby, createdon, data, expireon, priority, status, updatedby, updatedon 9 | ) 10 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 11 | `; 12 | 13 | const { 14 | id = uuidv4(), 15 | userid, 16 | category, 17 | createdby, 18 | createdon, 19 | data, 20 | expireon, 21 | priority, 22 | status = 'unread', 23 | updatedby, 24 | updatedon, 25 | } = newNotification; 26 | 27 | await client.execute(query, [ 28 | id, 29 | userid, 30 | category, 31 | createdby, 32 | createdon, 33 | data, 34 | expireon ? new Date(expireon) : new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // Default expiry: 7 days 35 | priority, 36 | status, 37 | updatedby, 38 | updatedon, 39 | ], { prepare: true }); 40 | io.emit('newNotification', { userid, notificationData: await getNotifications(userid)}); 41 | 42 | }; 43 | 44 | // Get notifications for a user 45 | export const getNotifications = async (userId: string) => { 46 | const query = ` 47 | SELECT * 48 | FROM user_notifications 49 | WHERE userid = ? AND status = 'unread' LIMIT 20 50 | `; 51 | const result = await client.execute(query, [userId], { prepare: true }); 52 | return result.rows; 53 | }; 54 | 55 | // Mark a notification as read 56 | export const markAsRead = async (notificationId: string,userId: string) => { 57 | const query = ` 58 | UPDATE user_notifications 59 | SET status = 'read' 60 | WHERE id = ? AND userid = ?; 61 | `; 62 | 63 | await client.execute(query, [notificationId,userId], { prepare: true }); 64 | } 65 | export const markAllAsRead = async (userId: string) => { 66 | const getAllUnreadNotifications = await getNotifications(userId); 67 | const batchQueries = getAllUnreadNotifications.map((notification) => ({ 68 | query: 'UPDATE user_notifications SET status = ? WHERE id = ? AND userid = ?;', 69 | params: ['read', notification.id,userId], 70 | })); 71 | await client.batch(batchQueries, { prepare: true }) 72 | } 73 | function uuidv4() { 74 | throw new Error('Function not implemented.'); 75 | } 76 | 77 | -------------------------------------------------------------------------------- /Jenkinsfile-sun: -------------------------------------------------------------------------------- 1 | node('build-slave') { 2 | try { 3 | String ANSI_GREEN = "\u001B[32m" 4 | String ANSI_NORMAL = "\u001B[0m" 5 | String ANSI_BOLD = "\u001B[1m" 6 | String ANSI_RED = "\u001B[31m" 7 | String ANSI_YELLOW = "\u001B[33m" 8 | 9 | ansiColor('xterm') { 10 | stage('Checkout') { 11 | if (!env.hub_org) { 12 | println(ANSI_BOLD + ANSI_RED + "Uh Oh! Please set a Jenkins environment variable named hub_org with value as registery/sunbidrded" + ANSI_NORMAL) 13 | error 'Please resolve the errors and rerun..' 14 | } else 15 | println(ANSI_BOLD + ANSI_GREEN + "Found environment variable named hub_org with value as: " + hub_org + ANSI_NORMAL) 16 | } 17 | 18 | cleanWs() 19 | checkout scm 20 | commit_hash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() 21 | build_tag = sh(script: "echo " + params.github_release_tag.split('/')[-1] + "_" + commit_hash + "_" + env.BUILD_NUMBER, returnStdout: true).trim() 22 | echo "build_tag: " + build_tag 23 | 24 | /* stage('SonarQube analysis') { 25 | 26 | sh 'cd $docker_file_path && npm install typescript' 27 | // requires SonarQube Scanner 2.8+ 28 | def scannerHome = tool 'sonar_scanner'; 29 | withSonarQubeEnv('sonarqube') { 30 | sh ''' 31 | cd $docker_file_path && pwd && /var/lib/jenkins/tools/hudson.plugins.sonar.SonarRunnerInstallation/sonar_scanner/bin/sonar-scanner 32 | ''' 33 | } 34 | }*/ 35 | 36 | 37 | 38 | 39 | stage('Build') { 40 | env.NODE_ENV = "build" 41 | print "Environment will be : ${env.NODE_ENV}" 42 | sh('chmod 777 build.sh') 43 | sh("bash -x build.sh ${build_tag} ${env.NODE_NAME} ${hub_org}") 44 | } 45 | 46 | 47 | stage('ArchiveArtifacts') { 48 | sh ("echo ${build_tag} > build_tag.txt") 49 | archiveArtifacts "metadata.json" 50 | archiveArtifacts "build_tag.txt" 51 | currentBuild.description = "${build_tag}" 52 | } 53 | 54 | } 55 | } 56 | catch (err) { 57 | currentBuild.result = "FAILURE" 58 | throw err 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/controllers/notification.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import * as notificationService from '../services/notification.service'; 3 | import { logger } from '../utils/logger'; 4 | // Create a new notification 5 | export const createNotification = async (req: Request, res: Response): Promise => { 6 | try { 7 | const io = req.io; 8 | const userList = req.body 9 | const notificationsIdList=[] 10 | for (const user of userList) { 11 | const { 12 | userid, 13 | category, 14 | createdby, 15 | data, 16 | expireon, 17 | priority, 18 | } = user 19 | const newNotification = { 20 | id: require('uuid').v4(), 21 | userid, 22 | category, 23 | createdby, 24 | createdon: new Date(), 25 | data: JSON.stringify(data), 26 | expireon: expireon ? new Date(expireon) : new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // Default expiry: 7 days 27 | priority, 28 | status: 'unread', 29 | updatedby: createdby, 30 | updatedon: new Date(), 31 | }; 32 | notificationsIdList.push(newNotification.id) 33 | logger.info(newNotification); 34 | await notificationService.createNotification(newNotification, io); 35 | } 36 | res.status(201).json({ message: 'Notification created successfully', ids: notificationsIdList }); 37 | } catch (error) { 38 | console.log(error) 39 | logger.info('Error creating notification:', error); 40 | res.status(500).json({ error: 'Failed to create notification' }); 41 | } 42 | }; 43 | 44 | // Fetch all notifications for a user 45 | export const getUserNotifications = async (req: Request, res: Response): Promise => { 46 | try { 47 | const { userid } = req.params; 48 | if (!userid) { 49 | res.status(400).json({ error: 'User ID is required' }); 50 | return; 51 | } 52 | const notifications = await notificationService.getNotifications(userid); 53 | res.status(200).json(notifications); 54 | } catch (error) { 55 | console.error('Error fetching notifications:', error); 56 | res.status(500).json({ error: 'Failed to fetch notifications' }); 57 | } 58 | }; 59 | 60 | // Mark a notification as read 61 | export const markNotificationAsRead = async (req: Request, res: Response): Promise => { 62 | try { 63 | const { id, userId } = req.body; 64 | if (!id || !userId) { 65 | res.status(400).json({ error: 'Notification ID or User ID is required' }); 66 | return; 67 | } 68 | await notificationService.markAsRead(id, userId); 69 | res.status(200).json({ message: 'Notification marked as read' }); 70 | } catch (error) { 71 | console.error('Error marking notification as read:', error); 72 | res.status(500).json({ error: 'Failed to mark notification as read' }); 73 | } 74 | }; 75 | export const markAllNotificationAsRead = async (req: Request, res: Response): Promise => { 76 | try { 77 | const { userId } = req.body; 78 | if (!userId) { 79 | res.status(400).json({ error: 'User ID is required' }); 80 | return; 81 | } 82 | await notificationService.markAllAsRead(userId); 83 | res.status(200).json({ message: 'Notification marked as read' }); 84 | } catch (error) { 85 | console.error('Error marking notification as read:', error); 86 | res.status(500).json({ error: 'Failed to mark notification as read' }); 87 | } 88 | }; 89 | 90 | 91 | --------------------------------------------------------------------------------