From c36765cc470881496cf168fddb3c585cc58239ed Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Wed, 19 Aug 2026 18:58:37 +0100 Subject: [PATCH] Add notification handling in editObject and utility functions - Introduced notification functionality in the editObject method to notify users of changes when editing objects, enhancing user awareness of updates. - Added utility functions for creating and managing notifications, including omitSensitive for filtering sensitive data and notificationUserFromOwner for user retrieval based on owner type. - Updated SocketHost to support the new notify parameter, ensuring notifications can be triggered during object edits. --- src/database/database.js | 22 ++++++- src/database/utils.js | 125 +++++++++++++++++++++++++++++++++++++++ src/socket/sockethost.js | 3 +- 3 files changed, 148 insertions(+), 2 deletions(-) diff --git a/src/database/database.js b/src/database/database.js index d8a6102..4e12704 100644 --- a/src/database/database.js +++ b/src/database/database.js @@ -3,6 +3,7 @@ import { deleteAuditLog, expandObjectIds, editAuditLog, + editNotification, distributeUpdate, newAuditLog, distributeNew, @@ -423,7 +424,8 @@ export const editObject = async ({ owner = undefined, ownerType = undefined, populate = [], - auditLog = true + auditLog = true, + notify = true }) => { try { // Determine parentType from model name @@ -464,6 +466,24 @@ export const editObject = async ({ ); } + if ( + notify == true && + owner != undefined && + ownerType != undefined && + parentType !== 'notification' && + parentType !== 'auditLog' && + parentType !== 'userNotifier' + ) { + await editNotification( + previousExpandedObject, + newExpandedObject, + id, + parentType, + owner, + ownerType + ); + } + // Distribute update await distributeUpdate(updateData, id, parentType); diff --git a/src/database/utils.js b/src/database/utils.js index d641e49..e4d1057 100644 --- a/src/database/utils.js +++ b/src/database/utils.js @@ -1,9 +1,51 @@ import { ObjectId } from 'mongodb'; import { auditLogModel } from './schemas/management/auditlog.schema.js'; +import { notificationModel } from './schemas/misc/notification.schema.js'; +import { userNotifierModel } from './schemas/misc/usernotifier.schema.js'; +import { models } from './schemas/models.js'; import { natsServer } from './nats.js'; import { customAlphabet } from 'nanoid'; +const NOTIFICATION_EXCLUDED_MODELS = ['notification', 'userNotifier', 'auditLog']; +const SENSITIVE_KEYS = ['secret']; + +function omitSensitive(obj) { + if (obj == null || typeof obj !== 'object') return obj; + if (Array.isArray(obj)) return obj.map(omitSensitive); + const result = {}; + for (const [key, value] of Object.entries(obj)) { + if (SENSITIVE_KEYS.includes(key)) continue; + result[key] = omitSensitive(value); + } + return result; +} + +function getModelEntryByType(parentType) { + return Object.values(models).find( + entry => entry.type === parentType || entry.model?.modelName === parentType + ); +} + +function notificationUserFromOwner(owner, ownerType) { + if (!owner) return null; + if (ownerType === 'user') { + return owner; + } + if (ownerType === 'host') { + return { + _id: owner._id, + firstName: owner.name ?? 'unknown', + lastName: '' + }; + } + return { + _id: owner._id, + firstName: owner.name ?? owner.firstName ?? 'unknown', + lastName: owner.lastName ?? '' + }; +} + const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; export const generateId = () => { // 10 characters @@ -444,6 +486,86 @@ async function distributeNew(id, type) { await natsServer.publish(`${type}s.new`, id); } +async function editNotification( + oldValue, + newValue, + parentId, + parentType, + owner, + ownerType +) { + if (NOTIFICATION_EXCLUDED_MODELS.includes(parentType)) return; + + const modelEntry = getModelEntryByType(parentType); + const user = notificationUserFromOwner(owner, ownerType); + const objectName = + oldValue?.name ?? newValue?.name ?? modelEntry?.label ?? parentType; + const changedOldValues = omitSensitive(getChangedValues(oldValue, newValue, true)); + const changedNewValues = omitSensitive(getChangedValues(oldValue, newValue, false)); + + if ( + Object.keys(changedOldValues).length === 0 || + Object.keys(changedNewValues).length === 0 + ) { + return; + } + + await notfiyObjectUserNotifiers( + parentId, + parentType, + `${objectName} edited by ${user?.firstName ?? 'unknown'} ${user?.lastName ?? ''}`, + `The ${parentType} ${parentId} has been updated.`, + 'editObject', + { + old: changedOldValues, + new: changedNewValues, + objectType: parentType, + object: { _id: String(parentId ?? '') }, + user: { + _id: String(user?._id ?? ''), + firstName: user?.firstName, + lastName: user?.lastName + } + } + ); +} + +async function notfiyObjectUserNotifiers( + id, + objectType, + title, + message, + type = 'info', + metadata +) { + const userNotifiers = await userNotifierModel + .find({ object: id, objectType }) + .populate('user'); + for (const userNotifier of userNotifiers) { + await createNotification( + userNotifier.user._id, + title, + message, + type, + metadata + ); + } +} + +async function createNotification(user, title, message, type = 'info', metadata) { + const notification = new notificationModel({ + user, + title, + message, + type, + metadata: omitSensitive(metadata ?? {}) + }); + await notification.save(); + const value = notification.toJSON ? notification.toJSON() : notification; + await natsServer.publish(`notifications.${user._id ?? user}`, value); + return notification; +} + function flatternObjectIds(object) { if (!object || typeof object !== 'object') { return object; @@ -546,6 +668,9 @@ export { distributeUpdate, distributeNew, distributeStats, + editNotification, + notfiyObjectUserNotifiers, + createNotification, getFilter, // <-- add here convertPropertiesString }; diff --git a/src/socket/sockethost.js b/src/socket/sockethost.js index 4138983..82bd034 100644 --- a/src/socket/sockethost.js +++ b/src/socket/sockethost.js @@ -172,7 +172,8 @@ export class SocketHost { populate: data.populate, owner: this.host, ownerType: 'host', - auditLog: data?.auditLog + auditLog: data?.auditLog, + notify: data?.notify }); callback(object); }