Add notification handling in editObject and utility functions
Some checks failed
farmcontrol/farmcontrol-ws/pipeline/head There was a failure building this commit

- 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.
This commit is contained in:
Tom Butcher 2026-08-19 18:58:37 +01:00
parent 6e74e0dfec
commit c36765cc47
3 changed files with 148 additions and 2 deletions

View File

@ -3,6 +3,7 @@ import {
deleteAuditLog, deleteAuditLog,
expandObjectIds, expandObjectIds,
editAuditLog, editAuditLog,
editNotification,
distributeUpdate, distributeUpdate,
newAuditLog, newAuditLog,
distributeNew, distributeNew,
@ -423,7 +424,8 @@ export const editObject = async ({
owner = undefined, owner = undefined,
ownerType = undefined, ownerType = undefined,
populate = [], populate = [],
auditLog = true auditLog = true,
notify = true
}) => { }) => {
try { try {
// Determine parentType from model name // 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 // Distribute update
await distributeUpdate(updateData, id, parentType); await distributeUpdate(updateData, id, parentType);

View File

@ -1,9 +1,51 @@
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import { auditLogModel } from './schemas/management/auditlog.schema.js'; 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 { natsServer } from './nats.js';
import { customAlphabet } from 'nanoid'; 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'; const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
export const generateId = () => { export const generateId = () => {
// 10 characters // 10 characters
@ -444,6 +486,86 @@ async function distributeNew(id, type) {
await natsServer.publish(`${type}s.new`, id); 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) { function flatternObjectIds(object) {
if (!object || typeof object !== 'object') { if (!object || typeof object !== 'object') {
return object; return object;
@ -546,6 +668,9 @@ export {
distributeUpdate, distributeUpdate,
distributeNew, distributeNew,
distributeStats, distributeStats,
editNotification,
notfiyObjectUserNotifiers,
createNotification,
getFilter, // <-- add here getFilter, // <-- add here
convertPropertiesString convertPropertiesString
}; };

View File

@ -172,7 +172,8 @@ export class SocketHost {
populate: data.populate, populate: data.populate,
owner: this.host, owner: this.host,
ownerType: 'host', ownerType: 'host',
auditLog: data?.auditLog auditLog: data?.auditLog,
notify: data?.notify
}); });
callback(object); callback(object);
} }