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 { EventEmitter } from "stream";
|
||||||
import { Claim } from "./utils/claims";
|
|
||||||
|
|
||||||
export type ChangeEvent = {
|
export type ChangeEvent = {
|
||||||
type: "insert" | "update" | "delete";
|
type: "insert" | "update" | "delete";
|
||||||
@@ -7,4 +6,18 @@ export type ChangeEvent = {
|
|||||||
document?: Object;
|
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();
|
export const dbEvents = new EventEmitter();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||||
import { ChangeEvent, dbEvents } from "../realtime";
|
import { AlertEvent, ChangeEvent, dbEvents } from "../realtime";
|
||||||
import { hasValidClaims } from "../auth";
|
import { hasValidClaims } from "../auth";
|
||||||
import { Claim } from "../utils/claims";
|
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", () => {
|
req.raw.on("close", () => {
|
||||||
res.raw.end();
|
res.raw.end();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { viewRoutes } from "./view/view.route";
|
|||||||
import { processedRoutes } from "./processed/processed.route";
|
import { processedRoutes } from "./processed/processed.route";
|
||||||
import { ctaskRoutes } from "./ctask/ctask.route";
|
import { ctaskRoutes } from "./ctask/ctask.route";
|
||||||
import { paymentRoutes } from "./payments/payment.route";
|
import { paymentRoutes } from "./payments/payment.route";
|
||||||
|
import { alertRoutes } from "./alert/alert.route";
|
||||||
|
|
||||||
export default async function routes(fastify: FastifyInstance) {
|
export default async function routes(fastify: FastifyInstance) {
|
||||||
fastify.addHook("preHandler", authHandler);
|
fastify.addHook("preHandler", authHandler);
|
||||||
@@ -32,5 +33,6 @@ export default async function routes(fastify: FastifyInstance) {
|
|||||||
fastify.register(viewRoutes, { prefix: "/views" });
|
fastify.register(viewRoutes, { prefix: "/views" });
|
||||||
fastify.register(processedRoutes, { prefix: "/processed" });
|
fastify.register(processedRoutes, { prefix: "/processed" });
|
||||||
fastify.register(paymentRoutes, { prefix: "/payments" });
|
fastify.register(paymentRoutes, { prefix: "/payments" });
|
||||||
|
fastify.register(alertRoutes, { prefix: "/alerts" });
|
||||||
fastify.register(realTimeRoutes);
|
fastify.register(realTimeRoutes);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export async function updateRtsHandler(req: FastifyRequest, res: FastifyReply) {
|
|||||||
const { rtsId } = req.params as { rtsId: string };
|
const { rtsId } = req.params as { rtsId: string };
|
||||||
|
|
||||||
try {
|
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" });
|
if (!updatedRts) return res.code(404).send({ error: "resource not found" });
|
||||||
|
|
||||||
return res.code(200).send(updatedRts);
|
return res.code(200).send(updatedRts);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { getFilterObject, getSortObject, PageQueryParams } from "../pagination";
|
|||||||
import { getUserWithoutPopulate } from "../user/user.service";
|
import { getUserWithoutPopulate } from "../user/user.service";
|
||||||
import mongoose from "mongoose";
|
import mongoose from "mongoose";
|
||||||
import { rtsPipeline } from "../utils/pipeline";
|
import { rtsPipeline } from "../utils/pipeline";
|
||||||
|
import { createAlert } from "../alert/alert.service";
|
||||||
|
|
||||||
export async function createRts(
|
export async function createRts(
|
||||||
input: CreateRtsInput,
|
input: CreateRtsInput,
|
||||||
@@ -38,6 +39,15 @@ export async function createRts(
|
|||||||
createdBy: user.userId ?? null,
|
createdBy: user.userId ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await createAlert(
|
||||||
|
user.tenantId,
|
||||||
|
`New Ready to Submit added`,
|
||||||
|
"team",
|
||||||
|
newRts.client?.toString(),
|
||||||
|
newRts.pid,
|
||||||
|
"rts"
|
||||||
|
);
|
||||||
|
|
||||||
return rtsModel
|
return rtsModel
|
||||||
.findById(newRts.id)
|
.findById(newRts.id)
|
||||||
.populate({ path: "county", select: "pid name avatar" })
|
.populate({ path: "county", select: "pid name avatar" })
|
||||||
@@ -192,15 +202,28 @@ export async function listRts(
|
|||||||
export async function updateRts(
|
export async function updateRts(
|
||||||
id: string,
|
id: string,
|
||||||
input: UpdateRtsInput,
|
input: UpdateRtsInput,
|
||||||
tenantId: string
|
user: AuthenticatedUser
|
||||||
) {
|
) {
|
||||||
const updatedRts = await rtsModel
|
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: "createdBy", select: "pid name avatar" })
|
||||||
.populate({ path: "county", select: "pid name avatar" })
|
.populate({ path: "county", select: "pid name avatar" })
|
||||||
.populate({ path: "client", select: "pid name avatar" })
|
.populate({ path: "client", select: "pid name avatar" })
|
||||||
.populate({ path: "assignedTo", 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;
|
return updatedRts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { viewSchemas } from "./view/view.schema";
|
|||||||
import { processedSchemas } from "./processed/processed.schema";
|
import { processedSchemas } from "./processed/processed.schema";
|
||||||
import { cTaskSchemas } from "./ctask/ctask.schema";
|
import { cTaskSchemas } from "./ctask/ctask.schema";
|
||||||
import { paymentSchemas } from "./payments/payment.schema";
|
import { paymentSchemas } from "./payments/payment.schema";
|
||||||
|
import { alertSchemas } from "./alert/alert.schema";
|
||||||
|
|
||||||
const app = fastify({ logger: true, trustProxy: true });
|
const app = fastify({ logger: true, trustProxy: true });
|
||||||
|
|
||||||
@@ -59,6 +60,7 @@ for (const schema of [
|
|||||||
...viewSchemas,
|
...viewSchemas,
|
||||||
...processedSchemas,
|
...processedSchemas,
|
||||||
...paymentSchemas,
|
...paymentSchemas,
|
||||||
|
...alertSchemas,
|
||||||
]) {
|
]) {
|
||||||
app.addSchema(schema);
|
app.addSchema(schema);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,4 +35,6 @@ export type Claim =
|
|||||||
| "note:read"
|
| "note:read"
|
||||||
| "note:write"
|
| "note:write"
|
||||||
| "note:delete"
|
| "note:delete"
|
||||||
| "payment:read";
|
| "payment:read"
|
||||||
|
| "alert:read"
|
||||||
|
| "alert:write";
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export const rules: Record<
|
|||||||
"note:write",
|
"note:write",
|
||||||
"note:delete",
|
"note:delete",
|
||||||
"payment:read",
|
"payment:read",
|
||||||
|
"alert:read",
|
||||||
|
"alert:write",
|
||||||
],
|
],
|
||||||
hiddenFields: {
|
hiddenFields: {
|
||||||
orgs: ["__v"],
|
orgs: ["__v"],
|
||||||
@@ -75,6 +77,8 @@ export const rules: Record<
|
|||||||
"note:write",
|
"note:write",
|
||||||
"note:delete",
|
"note:delete",
|
||||||
"payment:read",
|
"payment:read",
|
||||||
|
"alert:read",
|
||||||
|
"alert:write",
|
||||||
],
|
],
|
||||||
hiddenFields: {
|
hiddenFields: {
|
||||||
orgs: ["__v"],
|
orgs: ["__v"],
|
||||||
@@ -105,6 +109,8 @@ export const rules: Record<
|
|||||||
"note:read",
|
"note:read",
|
||||||
"note:write",
|
"note:write",
|
||||||
"note:delete",
|
"note:delete",
|
||||||
|
"alert:read",
|
||||||
|
"alert:write",
|
||||||
],
|
],
|
||||||
hiddenFields: {
|
hiddenFields: {
|
||||||
orgs: ["__v"],
|
orgs: ["__v"],
|
||||||
@@ -134,6 +140,8 @@ export const rules: Record<
|
|||||||
"ctask:delete",
|
"ctask:delete",
|
||||||
"note:read",
|
"note:read",
|
||||||
"note:write",
|
"note:write",
|
||||||
|
"alert:read",
|
||||||
|
"alert:write",
|
||||||
],
|
],
|
||||||
hiddenFields: {
|
hiddenFields: {
|
||||||
orgs: ["__v"],
|
orgs: ["__v"],
|
||||||
|
|||||||
Reference in New Issue
Block a user