add alert routes

This commit is contained in:
2025-06-21 12:29:53 +05:30
parent c6bb01d8e4
commit e821c8d11d
12 changed files with 336 additions and 6 deletions

View File

@@ -0,0 +1,42 @@
import { FastifyReply, FastifyRequest } from "fastify";
import { PageQueryParams } from "../pagination";
import { getUnreadCount, getUserAlerts, markAsRead } from "./alert.service";
export async function listAlertsHandler(
req: FastifyRequest,
res: FastifyReply
) {
const params = req.query as PageQueryParams;
try {
const alerts = await getUserAlerts(req.user, params);
return res.code(200).send(alerts);
} catch (err) {
return err;
}
}
export async function markReadHandler(req: FastifyRequest, res: FastifyReply) {
const { alertId } = req.params as { alertId: string };
try {
const updatedAlert = await markAsRead(alertId, req.user);
if (updatedAlert == null)
return res.code(404).send({ error: "resource not found" });
return res.code(200).send(updatedAlert);
} catch (err) {
return err;
}
}
export async function getUnreadCountHandler(
req: FastifyRequest,
res: FastifyReply
) {
try {
const unreadCount = await getUnreadCount(req.user);
return res.code(200).send({ unreadCount });
} catch (err) {
return err;
}
}

46
src/alert/alert.route.ts Normal file
View File

@@ -0,0 +1,46 @@
import { FastifyInstance } from "fastify";
import {
getUnreadCountHandler,
listAlertsHandler,
markReadHandler,
} from "./alert.controller";
import { $alert } from "./alert.schema";
export async function alertRoutes(fastify: FastifyInstance) {
fastify.get(
"",
{
schema: {
response: { 200: $alert("listAlertResponse") },
},
config: { requiredClaims: ["alert:read"] },
preHandler: [fastify.authorize],
},
listAlertsHandler
);
fastify.post(
"/:alertId/read",
{
schema: {
params: {
type: "object",
properties: { alertId: { type: "string" } },
},
response: { 200: $alert("alertResponse") },
},
config: { requiredClaims: ["alert:write"] },
preHandler: [fastify.authorize],
},
markReadHandler
);
fastify.get(
"/count",
{
config: { requiredClaims: ["alert:read"] },
preHandler: [fastify.authorize],
},
getUnreadCountHandler
);
}

54
src/alert/alert.schema.ts Normal file
View File

@@ -0,0 +1,54 @@
import { z } from "zod";
import mongoose from "mongoose";
import { buildJsonSchemas } from "fastify-zod";
import { pageQueryParams } from "../pagination";
export const alertsModel = mongoose.model(
"alert",
new mongoose.Schema({
tenantId: String,
pid: { type: String, unique: true },
title: { type: String, required: true },
referenceId: String,
referenceCollection: String,
recipientType: {
type: String,
required: true,
enum: ["user", "team"],
},
recipientId: { type: mongoose.Types.ObjectId, required: true },
createdAt: { type: Date, default: Date.now },
readBy: {
type: [String],
default: [],
},
})
);
const alertResponse = z.object({
pid: z.string(),
title: z.string(),
read: z.boolean(),
referenceId: z.string().optional(),
referenceCollection: z.string().optional(),
recipientType: z.enum(["user", "team"]),
createdAt: z.date(),
});
const listAlertResponse = z.object({
alerts: z.array(alertResponse),
metadata: z.object({
count: z.number(),
page: z.number(),
pageSize: z.number(),
}),
});
export const { schemas: alertSchemas, $ref: $alert } = buildJsonSchemas(
{
alertResponse,
listAlertResponse,
pageQueryParams,
},
{ $id: "alert" }
);

110
src/alert/alert.service.ts Normal file
View File

@@ -0,0 +1,110 @@
import { AuthenticatedUser } from "../auth";
import { PageQueryParams } from "../pagination";
import { dbEvents } from "../realtime";
import { generateId } from "../utils/id";
import { alertsModel } from "./alert.schema";
export async function createAlert(
tenantId: string,
title: string,
recipientType: "user" | "team",
recipientId: string,
referenceId?: string,
referenceCollection?: string
) {
const newAlert = await alertsModel.create({
tenantId,
pid: generateId(),
title,
recipientType,
recipientId,
referenceId,
referenceCollection,
});
dbEvents.emit(
"alert",
{
type: "insert",
collection: "alerts",
document: {
pid: newAlert.pid,
title,
recipientType,
recipientId,
referenceId,
referenceCollection,
createdAt: new Date(),
},
},
["alert:read"]
);
}
export async function getUserAlerts(
user: AuthenticatedUser,
params: PageQueryParams
) {
const page = params.page || 1;
const pageSize = params.pageSize || 10;
const filters: Array<object> = [
{ recipientType: "user", recipientId: user.userId },
];
if (user.role == "client")
filters.push({ recipientType: "team", recipientId: user.orgId });
else filters.push({ recipientType: "team" });
const alerts = await alertsModel
.find({
$or: filters,
})
.sort({ createdAt: -1 })
.limit(pageSize)
.skip((page - 1) * pageSize);
const modifiedAlerts = alerts.map((alert) => {
return {
...alert.toObject(),
read: alert.readBy.includes(user.userId) ? true : false,
};
});
const count = await alertsModel.countDocuments({ $or: filters });
return { alerts: modifiedAlerts, metadata: { count, page, pageSize } };
}
export async function markAsRead(alertId: string, user: AuthenticatedUser) {
const updatedAlert = await alertsModel.findOneAndUpdate(
{ tenantId: user.tenantId, pid: alertId },
{ $addToSet: { readBy: user.userId } },
{ new: true }
);
if (!updatedAlert) return null;
updatedAlert["read"] = updatedAlert.readBy.includes(user.userId)
? true
: false;
return updatedAlert;
}
export async function getUnreadCount(user: AuthenticatedUser) {
const filters: Array<object> = [
{ recipientType: "user", recipientId: user.userId },
];
if (user.role == "client")
filters.push({ recipientType: "team", recipientId: user.orgId });
else filters.push({ recipientType: "team" });
const alerts = await alertsModel.find({
$or: filters,
readBy: { $ne: user.userId },
});
return alerts.length;
}

View File

@@ -1,5 +1,4 @@
import { EventEmitter } from "stream";
import { Claim } from "./utils/claims";
export type ChangeEvent = {
type: "insert" | "update" | "delete";
@@ -7,4 +6,18 @@ export type ChangeEvent = {
document?: Object;
};
export type AlertEvent = {
type: "insert";
collection: "alerts";
document: {
pid: string;
title: string;
recipientType: "user" | "team";
recipientId: string;
referenceId?: string;
referenceCollection?: string;
createdAt: Date;
};
};
export const dbEvents = new EventEmitter();

View File

@@ -1,5 +1,5 @@
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { ChangeEvent, dbEvents } from "../realtime";
import { AlertEvent, ChangeEvent, dbEvents } from "../realtime";
import { hasValidClaims } from "../auth";
import { Claim } from "../utils/claims";
@@ -20,6 +20,34 @@ export async function realTimeRoutes(fastify: FastifyInstance) {
}
});
dbEvents.on("alert", (event: AlertEvent, requiredClaims: Claim[]) => {
console.log(event);
if (hasValidClaims(req.user, requiredClaims)) {
let alertFlag = false;
if (
event.document.recipientType == "user" &&
event.document.recipientId == req.user.userId
)
alertFlag = true;
if (event.document.recipientType == "team") {
if (req.user.role != "client") alertFlag = true;
if (
req.user.role == "client" &&
event.document.recipientId == req.user.orgId
)
alertFlag = true;
}
if (alertFlag) {
res.raw.write(JSON.stringify(event));
}
}
});
req.raw.on("close", () => {
res.raw.end();
});

View File

@@ -15,6 +15,7 @@ import { viewRoutes } from "./view/view.route";
import { processedRoutes } from "./processed/processed.route";
import { ctaskRoutes } from "./ctask/ctask.route";
import { paymentRoutes } from "./payments/payment.route";
import { alertRoutes } from "./alert/alert.route";
export default async function routes(fastify: FastifyInstance) {
fastify.addHook("preHandler", authHandler);
@@ -32,5 +33,6 @@ export default async function routes(fastify: FastifyInstance) {
fastify.register(viewRoutes, { prefix: "/views" });
fastify.register(processedRoutes, { prefix: "/processed" });
fastify.register(paymentRoutes, { prefix: "/payments" });
fastify.register(alertRoutes, { prefix: "/alerts" });
fastify.register(realTimeRoutes);
}

View File

@@ -50,7 +50,7 @@ export async function updateRtsHandler(req: FastifyRequest, res: FastifyReply) {
const { rtsId } = req.params as { rtsId: string };
try {
const updatedRts = await updateRts(rtsId, input, req.user.tenantId);
const updatedRts = await updateRts(rtsId, input, req.user);
if (!updatedRts) return res.code(404).send({ error: "resource not found" });
return res.code(200).send(updatedRts);

View File

@@ -11,6 +11,7 @@ import { getFilterObject, getSortObject, PageQueryParams } from "../pagination";
import { getUserWithoutPopulate } from "../user/user.service";
import mongoose from "mongoose";
import { rtsPipeline } from "../utils/pipeline";
import { createAlert } from "../alert/alert.service";
export async function createRts(
input: CreateRtsInput,
@@ -38,6 +39,15 @@ export async function createRts(
createdBy: user.userId ?? null,
});
await createAlert(
user.tenantId,
`New Ready to Submit added`,
"team",
newRts.client?.toString(),
newRts.pid,
"rts"
);
return rtsModel
.findById(newRts.id)
.populate({ path: "county", select: "pid name avatar" })
@@ -192,15 +202,28 @@ export async function listRts(
export async function updateRts(
id: string,
input: UpdateRtsInput,
tenantId: string
user: AuthenticatedUser
) {
const updatedRts = await rtsModel
.findOneAndUpdate({ pid: id, tenantId: tenantId }, input, { new: true })
.findOneAndUpdate({ pid: id, tenantId: user.tenantId }, input, {
new: true,
})
.populate({ path: "createdBy", select: "pid name avatar" })
.populate({ path: "county", select: "pid name avatar" })
.populate({ path: "client", select: "pid name avatar" })
.populate({ path: "assignedTo", select: "pid name avatar" });
if (input.assignedTo) {
await createAlert(
user.tenantId,
`You are assigned to RTS`,
"user",
user.userId,
updatedRts.pid,
"rts"
);
}
return updatedRts;
}

View File

@@ -23,6 +23,7 @@ import { viewSchemas } from "./view/view.schema";
import { processedSchemas } from "./processed/processed.schema";
import { cTaskSchemas } from "./ctask/ctask.schema";
import { paymentSchemas } from "./payments/payment.schema";
import { alertSchemas } from "./alert/alert.schema";
const app = fastify({ logger: true, trustProxy: true });
@@ -59,6 +60,7 @@ for (const schema of [
...viewSchemas,
...processedSchemas,
...paymentSchemas,
...alertSchemas,
]) {
app.addSchema(schema);
}

View File

@@ -35,4 +35,6 @@ export type Claim =
| "note:read"
| "note:write"
| "note:delete"
| "payment:read";
| "payment:read"
| "alert:read"
| "alert:write";

View File

@@ -37,6 +37,8 @@ export const rules: Record<
"note:write",
"note:delete",
"payment:read",
"alert:read",
"alert:write",
],
hiddenFields: {
orgs: ["__v"],
@@ -75,6 +77,8 @@ export const rules: Record<
"note:write",
"note:delete",
"payment:read",
"alert:read",
"alert:write",
],
hiddenFields: {
orgs: ["__v"],
@@ -105,6 +109,8 @@ export const rules: Record<
"note:read",
"note:write",
"note:delete",
"alert:read",
"alert:write",
],
hiddenFields: {
orgs: ["__v"],
@@ -134,6 +140,8 @@ export const rules: Record<
"ctask:delete",
"note:read",
"note:write",
"alert:read",
"alert:write",
],
hiddenFields: {
orgs: ["__v"],