add alert routes
This commit is contained in:
42
src/alert/alert.controller.ts
Normal file
42
src/alert/alert.controller.ts
Normal 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
46
src/alert/alert.route.ts
Normal 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
54
src/alert/alert.schema.ts
Normal 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
110
src/alert/alert.service.ts
Normal 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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -35,4 +35,6 @@ export type Claim =
|
||||
| "note:read"
|
||||
| "note:write"
|
||||
| "note:delete"
|
||||
| "payment:read";
|
||||
| "payment:read"
|
||||
| "alert:read"
|
||||
| "alert:write";
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user