Implemented notifications.
Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
This commit is contained in:
parent
838e48ade6
commit
3e47cb131b
@ -1,6 +1,7 @@
|
|||||||
import config from '../config.js';
|
import config from '../config.js';
|
||||||
import { fileModel } from './schemas/management/file.schema.js';
|
import { fileModel } from './schemas/management/file.schema.js';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import log4js from 'log4js';
|
||||||
import {
|
import {
|
||||||
deleteAuditLog,
|
deleteAuditLog,
|
||||||
distributeDelete,
|
distributeDelete,
|
||||||
@ -8,9 +9,6 @@ import {
|
|||||||
modelHasRef,
|
modelHasRef,
|
||||||
getFieldsByRef,
|
getFieldsByRef,
|
||||||
getQueryToCacheKey,
|
getQueryToCacheKey,
|
||||||
} from '../utils.js';
|
|
||||||
import log4js from 'log4js';
|
|
||||||
import {
|
|
||||||
editAuditLog,
|
editAuditLog,
|
||||||
distributeUpdate,
|
distributeUpdate,
|
||||||
newAuditLog,
|
newAuditLog,
|
||||||
@ -19,6 +17,8 @@ import {
|
|||||||
distributeChildDelete,
|
distributeChildDelete,
|
||||||
distributeChildNew,
|
distributeChildNew,
|
||||||
distributeStats,
|
distributeStats,
|
||||||
|
editNotification,
|
||||||
|
deleteNotification,
|
||||||
} from '../utils.js';
|
} from '../utils.js';
|
||||||
import { getAllModels } from '../services/misc/model.js';
|
import { getAllModels } from '../services/misc/model.js';
|
||||||
import { redisServer } from './redis.js';
|
import { redisServer } from './redis.js';
|
||||||
@ -802,6 +802,20 @@ export const editObject = async ({ model, id, updateData, user, populate, recalc
|
|||||||
parentType,
|
parentType,
|
||||||
user
|
user
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
parentType !== 'notification' &&
|
||||||
|
parentType !== 'auditLog' &&
|
||||||
|
parentType !== 'userNotifier'
|
||||||
|
) {
|
||||||
|
await editNotification(
|
||||||
|
previousExpandedObject,
|
||||||
|
{ ...previousExpandedObject, ...updateData },
|
||||||
|
id,
|
||||||
|
parentType,
|
||||||
|
user
|
||||||
|
);
|
||||||
|
}
|
||||||
// Distribute update
|
// Distribute update
|
||||||
await distributeUpdate(updateData, id, parentType);
|
await distributeUpdate(updateData, id, parentType);
|
||||||
// Call childUpdate event for any child objects
|
// Call childUpdate event for any child objects
|
||||||
@ -878,6 +892,7 @@ export const newObject = async ({ model, newData, user = null }, distributeChang
|
|||||||
const created = expandObjectIds(result.toObject());
|
const created = expandObjectIds(result.toObject());
|
||||||
|
|
||||||
await newAuditLog(newData, created._id, parentType, user);
|
await newAuditLog(newData, created._id, parentType, user);
|
||||||
|
|
||||||
if (distributeChanges == true) {
|
if (distributeChanges == true) {
|
||||||
await distributeNew(created, parentType);
|
await distributeNew(created, parentType);
|
||||||
}
|
}
|
||||||
@ -922,7 +937,15 @@ export const deleteObject = async ({ model, id, user = null }, distributeChanges
|
|||||||
|
|
||||||
const deleted = expandObjectIds(result.toObject());
|
const deleted = expandObjectIds(result.toObject());
|
||||||
// Audit log the deletion
|
// Audit log the deletion
|
||||||
await deleteAuditLog(deleted, id, parentType, user, 'delete');
|
await deleteAuditLog(deleted, id, parentType, user);
|
||||||
|
|
||||||
|
if (
|
||||||
|
parentType !== 'notification' &&
|
||||||
|
parentType !== 'auditLog' &&
|
||||||
|
parentType !== 'userNotifier'
|
||||||
|
) {
|
||||||
|
await deleteNotification(deleted, id, parentType, user);
|
||||||
|
}
|
||||||
|
|
||||||
if (distributeChanges == true) {
|
if (distributeChanges == true) {
|
||||||
await distributeDelete(deleted, parentType);
|
await distributeDelete(deleted, parentType);
|
||||||
|
|||||||
51
src/database/schemas/misc/notification.schema.js
Normal file
51
src/database/schemas/misc/notification.schema.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import { generateId } from '../../utils.js';
|
||||||
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
|
const notificationSchema = new mongoose.Schema({
|
||||||
|
_reference: { type: String, default: () => generateId()() },
|
||||||
|
user: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'user',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
default: 'info',
|
||||||
|
},
|
||||||
|
read: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: Date,
|
||||||
|
required: true,
|
||||||
|
default: Date.now,
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: Date,
|
||||||
|
required: true,
|
||||||
|
default: Date.now,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
notificationSchema.virtual('id').get(function () {
|
||||||
|
return this._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
notificationSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
|
export const notificationModel = mongoose.model('notification', notificationSchema);
|
||||||
44
src/database/schemas/misc/usernotifier.schema.js
Normal file
44
src/database/schemas/misc/usernotifier.schema.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import { generateId } from '../../utils.js';
|
||||||
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
|
const userNotifierSchema = new mongoose.Schema({
|
||||||
|
_reference: { type: String, default: () => generateId()() },
|
||||||
|
user: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'user',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
object: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
refPath: 'objectType',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
objectType: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: Date,
|
||||||
|
required: true,
|
||||||
|
default: Date.now,
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: Date,
|
||||||
|
required: true,
|
||||||
|
default: Date.now,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
userNotifierSchema.virtual('id').get(function () {
|
||||||
|
return this._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
userNotifierSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
|
export const userNotifierModel = mongoose.model('userNotifier', userNotifierSchema);
|
||||||
@ -16,6 +16,8 @@ import { auditLogModel } from './management/auditlog.schema.js';
|
|||||||
import { userModel } from './management/user.schema.js';
|
import { userModel } from './management/user.schema.js';
|
||||||
import { noteTypeModel } from './management/notetype.schema.js';
|
import { noteTypeModel } from './management/notetype.schema.js';
|
||||||
import { noteModel } from './misc/note.schema.js';
|
import { noteModel } from './misc/note.schema.js';
|
||||||
|
import { notificationModel } from './misc/notification.schema.js';
|
||||||
|
import { userNotifierModel } from './misc/usernotifier.schema.js';
|
||||||
import { documentSizeModel } from './management/documentsize.schema.js';
|
import { documentSizeModel } from './management/documentsize.schema.js';
|
||||||
import { documentTemplateModel } from './management/documenttemplate.schema.js';
|
import { documentTemplateModel } from './management/documenttemplate.schema.js';
|
||||||
import { hostModel } from './management/host.schema.js';
|
import { hostModel } from './management/host.schema.js';
|
||||||
@ -33,77 +35,243 @@ import { salesOrderModel } from './sales/salesorder.schema.js';
|
|||||||
|
|
||||||
// Map prefixes to models and id fields
|
// Map prefixes to models and id fields
|
||||||
export const models = {
|
export const models = {
|
||||||
PRN: { model: printerModel, idField: '_id', type: 'printer', referenceField: '_reference' },
|
PRN: {
|
||||||
FIL: { model: filamentModel, idField: '_id', type: 'filament', referenceField: '_reference' },
|
model: printerModel,
|
||||||
GCF: { model: gcodeFileModel, idField: '_id', type: 'gcodeFile', referenceField: '_reference' },
|
idField: '_id',
|
||||||
JOB: { model: jobModel, idField: '_id', type: 'job', referenceField: '_reference' },
|
type: 'printer',
|
||||||
PRT: { model: partModel, idField: '_id', type: 'part', referenceField: '_reference' },
|
referenceField: '_reference',
|
||||||
PRD: { model: productModel, idField: '_id', type: 'product', referenceField: '_reference' },
|
label: 'Printer',
|
||||||
VEN: { model: vendorModel, idField: '_id', type: 'vendor', referenceField: '_reference' },
|
},
|
||||||
SJB: { model: subJobModel, idField: '_id', type: 'subJob', referenceField: '_reference' },
|
FIL: {
|
||||||
|
model: filamentModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'filament',
|
||||||
|
referenceField: '_reference',
|
||||||
|
label: 'Filament',
|
||||||
|
},
|
||||||
|
GCF: {
|
||||||
|
model: gcodeFileModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'gcodeFile',
|
||||||
|
referenceField: '_reference',
|
||||||
|
label: 'G-Code File',
|
||||||
|
},
|
||||||
|
JOB: { model: jobModel, idField: '_id', type: 'job', referenceField: '_reference', label: 'Job' },
|
||||||
|
PRT: {
|
||||||
|
model: partModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'part',
|
||||||
|
referenceField: '_reference',
|
||||||
|
label: 'Part',
|
||||||
|
},
|
||||||
|
PRD: {
|
||||||
|
model: productModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'product',
|
||||||
|
referenceField: '_reference',
|
||||||
|
label: 'Product',
|
||||||
|
},
|
||||||
|
VEN: {
|
||||||
|
model: vendorModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'vendor',
|
||||||
|
referenceField: '_reference',
|
||||||
|
label: 'Vendor',
|
||||||
|
},
|
||||||
|
SJB: {
|
||||||
|
model: subJobModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'subJob',
|
||||||
|
referenceField: '_reference',
|
||||||
|
label: 'Sub Job',
|
||||||
|
},
|
||||||
FLS: {
|
FLS: {
|
||||||
model: filamentStockModel,
|
model: filamentStockModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'filamentStock',
|
type: 'filamentStock',
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
|
label: 'Filament Stock',
|
||||||
|
},
|
||||||
|
SEV: {
|
||||||
|
model: stockEventModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'stockEvent',
|
||||||
|
referenceField: '_reference',
|
||||||
|
label: 'Stock Event',
|
||||||
|
},
|
||||||
|
SAU: {
|
||||||
|
model: stockAuditModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'stockAudit',
|
||||||
|
referenceField: '_reference',
|
||||||
|
label: 'Stock Audit',
|
||||||
|
},
|
||||||
|
PTS: {
|
||||||
|
model: partStockModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'partStock',
|
||||||
|
referenceField: '_reference',
|
||||||
|
label: 'Part Stock',
|
||||||
|
},
|
||||||
|
PDS: {
|
||||||
|
model: null,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'productStock',
|
||||||
|
referenceField: '_reference',
|
||||||
|
label: 'Product Stock',
|
||||||
|
}, // No productStockModel found
|
||||||
|
ADL: {
|
||||||
|
model: auditLogModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'auditLog',
|
||||||
|
referenceField: '_reference',
|
||||||
|
label: 'Audit Log',
|
||||||
|
},
|
||||||
|
USR: {
|
||||||
|
model: userModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'user',
|
||||||
|
referenceField: '_reference',
|
||||||
|
label: 'User',
|
||||||
|
},
|
||||||
|
NTY: {
|
||||||
|
model: noteTypeModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'noteType',
|
||||||
|
referenceField: '_reference',
|
||||||
|
label: 'Note Type',
|
||||||
|
},
|
||||||
|
NTE: {
|
||||||
|
model: noteModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'note',
|
||||||
|
referenceField: '_reference',
|
||||||
|
label: 'Note',
|
||||||
|
},
|
||||||
|
NTF: {
|
||||||
|
model: notificationModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'notification',
|
||||||
|
label: 'Notification',
|
||||||
|
referenceField: '_reference',
|
||||||
|
},
|
||||||
|
ONF: {
|
||||||
|
model: userNotifierModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'userNotifier',
|
||||||
|
label: 'User Notifier',
|
||||||
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
SEV: { model: stockEventModel, idField: '_id', type: 'stockEvent', referenceField: '_reference' },
|
|
||||||
SAU: { model: stockAuditModel, idField: '_id', type: 'stockAudit', referenceField: '_reference' },
|
|
||||||
PTS: { model: partStockModel, idField: '_id', type: 'partStock', referenceField: '_reference' },
|
|
||||||
PDS: { model: null, idField: '_id', type: 'productStock', referenceField: '_reference' }, // No productStockModel found
|
|
||||||
ADL: { model: auditLogModel, idField: '_id', type: 'auditLog', referenceField: '_reference' },
|
|
||||||
USR: { model: userModel, idField: '_id', type: 'user', referenceField: '_reference' },
|
|
||||||
NTY: { model: noteTypeModel, idField: '_id', type: 'noteType', referenceField: '_reference' },
|
|
||||||
NTE: { model: noteModel, idField: '_id', type: 'note', referenceField: '_reference' },
|
|
||||||
DSZ: {
|
DSZ: {
|
||||||
model: documentSizeModel,
|
model: documentSizeModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'documentSize',
|
type: 'documentSize',
|
||||||
|
label: 'Document Size',
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
DTP: {
|
DTP: {
|
||||||
model: documentTemplateModel,
|
model: documentTemplateModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'documentTemplate',
|
type: 'documentTemplate',
|
||||||
|
label: 'Document Template',
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
DPR: {
|
DPR: {
|
||||||
model: documentPrinterModel,
|
model: documentPrinterModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'documentPrinter',
|
type: 'documentPrinter',
|
||||||
|
label: 'Document Printer',
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
DJB: {
|
DJB: {
|
||||||
model: documentJobModel,
|
model: documentJobModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'documentJob',
|
type: 'documentJob',
|
||||||
|
label: 'Document Job',
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
HST: { model: hostModel, idField: '_id', type: 'host', referenceField: '_reference' },
|
HST: {
|
||||||
FLE: { model: fileModel, idField: '_id', type: 'file', referenceField: '_reference' },
|
model: hostModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'host',
|
||||||
|
referenceField: '_reference',
|
||||||
|
label: 'Host',
|
||||||
|
},
|
||||||
|
FLE: {
|
||||||
|
model: fileModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'file',
|
||||||
|
referenceField: '_reference',
|
||||||
|
label: 'File',
|
||||||
|
},
|
||||||
POR: {
|
POR: {
|
||||||
model: purchaseOrderModel,
|
model: purchaseOrderModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'purchaseOrder',
|
type: 'purchaseOrder',
|
||||||
|
label: 'Purchase Order',
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
ODI: {
|
ODI: {
|
||||||
model: orderItemModel,
|
model: orderItemModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'orderItem',
|
type: 'orderItem',
|
||||||
|
label: 'Order Item',
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
COS: {
|
COS: {
|
||||||
model: courierServiceModel,
|
model: courierServiceModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'courierService',
|
type: 'courierService',
|
||||||
|
label: 'Courier Service',
|
||||||
|
referenceField: '_reference',
|
||||||
|
},
|
||||||
|
COR: {
|
||||||
|
model: courierModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'courier',
|
||||||
|
label: 'Courier',
|
||||||
|
referenceField: '_reference',
|
||||||
|
},
|
||||||
|
TXR: {
|
||||||
|
model: taxRateModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'taxRate',
|
||||||
|
label: 'Tax Rate',
|
||||||
|
referenceField: '_reference',
|
||||||
|
},
|
||||||
|
TXD: {
|
||||||
|
model: taxRecordModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'taxRecord',
|
||||||
|
label: 'Tax Record',
|
||||||
|
referenceField: '_reference',
|
||||||
|
},
|
||||||
|
SHP: {
|
||||||
|
model: shipmentModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'shipment',
|
||||||
|
label: 'Shipment',
|
||||||
|
referenceField: '_reference',
|
||||||
|
},
|
||||||
|
INV: {
|
||||||
|
model: invoiceModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'invoice',
|
||||||
|
label: 'Invoice',
|
||||||
|
referenceField: '_reference',
|
||||||
|
},
|
||||||
|
CLI: {
|
||||||
|
model: clientModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'client',
|
||||||
|
label: 'Client',
|
||||||
|
referenceField: '_reference',
|
||||||
|
},
|
||||||
|
SOR: {
|
||||||
|
model: salesOrderModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'salesOrder',
|
||||||
|
label: 'Sales Order',
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
COR: { model: courierModel, idField: '_id', type: 'courier', referenceField: '_reference' },
|
|
||||||
TXR: { model: taxRateModel, idField: '_id', type: 'taxRate', referenceField: '_reference' },
|
|
||||||
TXD: { model: taxRecordModel, idField: '_id', type: 'taxRecord', referenceField: '_reference' },
|
|
||||||
SHP: { model: shipmentModel, idField: '_id', type: 'shipment', referenceField: '_reference' },
|
|
||||||
INV: { model: invoiceModel, idField: '_id', type: 'invoice', referenceField: '_reference' },
|
|
||||||
CLI: { model: clientModel, idField: '_id', type: 'client', referenceField: '_reference' },
|
|
||||||
SOR: { model: salesOrderModel, idField: '_id', type: 'salesOrder', referenceField: '_reference' },
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -41,6 +41,8 @@ import {
|
|||||||
paymentRoutes,
|
paymentRoutes,
|
||||||
clientRoutes,
|
clientRoutes,
|
||||||
salesOrderRoutes,
|
salesOrderRoutes,
|
||||||
|
userNotifierRoutes,
|
||||||
|
notificationRoutes,
|
||||||
} from './routes/index.js';
|
} from './routes/index.js';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
@ -148,6 +150,8 @@ app.use('/payments', paymentRoutes);
|
|||||||
app.use('/clients', clientRoutes);
|
app.use('/clients', clientRoutes);
|
||||||
app.use('/salesorders', salesOrderRoutes);
|
app.use('/salesorders', salesOrderRoutes);
|
||||||
app.use('/notes', noteRoutes);
|
app.use('/notes', noteRoutes);
|
||||||
|
app.use('/usernotifiers', userNotifierRoutes);
|
||||||
|
app.use('/notifications', notificationRoutes);
|
||||||
|
|
||||||
// Start the application
|
// Start the application
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
|
|||||||
@ -34,6 +34,8 @@ import paymentRoutes from './finance/payments.js';
|
|||||||
import clientRoutes from './sales/clients.js';
|
import clientRoutes from './sales/clients.js';
|
||||||
import salesOrderRoutes from './sales/salesorders.js';
|
import salesOrderRoutes from './sales/salesorders.js';
|
||||||
import noteRoutes from './misc/notes.js';
|
import noteRoutes from './misc/notes.js';
|
||||||
|
import userNotifierRoutes from './misc/usernotifiers.js';
|
||||||
|
import notificationRoutes from './misc/notifications.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
userRoutes,
|
userRoutes,
|
||||||
@ -72,4 +74,6 @@ export {
|
|||||||
paymentRoutes,
|
paymentRoutes,
|
||||||
clientRoutes,
|
clientRoutes,
|
||||||
salesOrderRoutes,
|
salesOrderRoutes,
|
||||||
|
userNotifierRoutes,
|
||||||
|
notificationRoutes,
|
||||||
};
|
};
|
||||||
|
|||||||
37
src/routes/misc/notifications.js
Normal file
37
src/routes/misc/notifications.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { isAuthenticated } from '../../keycloak.js';
|
||||||
|
import {
|
||||||
|
listNotificationsRouteHandler,
|
||||||
|
markNotificationAsReadRouteHandler,
|
||||||
|
markAllNotificationsAsReadRouteHandler,
|
||||||
|
deleteNotificationRouteHandler,
|
||||||
|
deleteAllNotificationsRouteHandler,
|
||||||
|
} from '../../services/misc/notifications.js';
|
||||||
|
import { getFilter } from '../../utils.js';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', isAuthenticated, (req, res) => {
|
||||||
|
const { page = 1, limit = 50, sort = 'createdAt', order = 'descend' } = req.query;
|
||||||
|
const allowedFilters = ['user'];
|
||||||
|
const filter = getFilter(req.query, allowedFilters);
|
||||||
|
listNotificationsRouteHandler(req, res, page, limit, filter, sort, order);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/read-all', isAuthenticated, (req, res) => {
|
||||||
|
markAllNotificationsAsReadRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/:id/read', isAuthenticated, (req, res) => {
|
||||||
|
markNotificationAsReadRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/', isAuthenticated, (req, res) => {
|
||||||
|
deleteAllNotificationsRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/:id', isAuthenticated, (req, res) => {
|
||||||
|
deleteNotificationRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
37
src/routes/misc/usernotifiers.js
Normal file
37
src/routes/misc/usernotifiers.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { isAuthenticated } from '../../keycloak.js';
|
||||||
|
import {
|
||||||
|
listUserNotifiersRouteHandler,
|
||||||
|
getUserNotifierRouteHandler,
|
||||||
|
newUserNotifierRouteHandler,
|
||||||
|
deleteUserNotifierRouteHandler,
|
||||||
|
editUserNotifierRouteHandler,
|
||||||
|
} from '../../services/misc/usernotifiers.js';
|
||||||
|
import { getFilter } from '../../utils.js';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', isAuthenticated, (req, res) => {
|
||||||
|
const { page, limit, property, search, sort, order } = req.query;
|
||||||
|
const allowedFilters = ['user', 'object', 'objectType'];
|
||||||
|
const filter = getFilter(req.query, allowedFilters);
|
||||||
|
listUserNotifiersRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/', isAuthenticated, (req, res) => {
|
||||||
|
newUserNotifierRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:id', isAuthenticated, (req, res) => {
|
||||||
|
getUserNotifierRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/:id', isAuthenticated, (req, res) => {
|
||||||
|
editUserNotifierRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/:id', isAuthenticated, (req, res) => {
|
||||||
|
deleteUserNotifierRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
105
src/services/misc/notifications.js
Normal file
105
src/services/misc/notifications.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import config from '../../config.js';
|
||||||
|
import { notificationModel } from '../../database/schemas/misc/notification.schema.js';
|
||||||
|
import log4js from 'log4js';
|
||||||
|
import { listObjects, editObject } from '../../database/database.js';
|
||||||
|
|
||||||
|
const logger = log4js.getLogger('Notifications');
|
||||||
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
export const listNotificationsRouteHandler = async (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
page = 1,
|
||||||
|
limit = 50,
|
||||||
|
filter = {},
|
||||||
|
sort = 'createdAt',
|
||||||
|
order = 'descend'
|
||||||
|
) => {
|
||||||
|
const userFilter = { ...filter, user: req.user._id };
|
||||||
|
const result = await listObjects({
|
||||||
|
model: notificationModel,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
filter: userFilter,
|
||||||
|
sort,
|
||||||
|
order,
|
||||||
|
populate: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error listing notifications.');
|
||||||
|
res.status(result.code || 500).send(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`List of notifications (Page ${page}, Limit ${limit}). Count: ${result.length}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const markNotificationAsReadRouteHandler = async (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
|
||||||
|
const existing = await notificationModel.findById(id).lean();
|
||||||
|
if (!existing) {
|
||||||
|
return res.status(404).send({ error: 'Notification not found' });
|
||||||
|
}
|
||||||
|
if (String(existing.user) !== String(req.user._id)) {
|
||||||
|
return res.status(403).send({ error: 'Forbidden: you can only update your own notifications' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await editObject({
|
||||||
|
model: notificationModel,
|
||||||
|
id,
|
||||||
|
updateData: { read: true, updatedAt: new Date() },
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error marking notification as read:', result.error);
|
||||||
|
return res.status(result.code || 500).send(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Marked notification as read: ${id}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const markAllNotificationsAsReadRouteHandler = async (req, res) => {
|
||||||
|
try {
|
||||||
|
await notificationModel.updateMany(
|
||||||
|
{ user: req.user._id, read: false },
|
||||||
|
{ $set: { read: true, updatedAt: new Date() } }
|
||||||
|
);
|
||||||
|
logger.debug('Marked all notifications as read');
|
||||||
|
res.send({ status: 'ok' });
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Error marking all notifications as read:', err);
|
||||||
|
res.status(500).send({ error: 'Failed to mark all as read' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteNotificationRouteHandler = async (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
|
||||||
|
const existing = await notificationModel.findById(id).lean();
|
||||||
|
if (!existing) {
|
||||||
|
return res.status(404).send({ error: 'Notification not found' });
|
||||||
|
}
|
||||||
|
if (String(existing.user._id) !== String(req.user._id)) {
|
||||||
|
return res.status(403).send({ error: 'Forbidden: you can only delete your own notifications' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await notificationModel.findByIdAndDelete(id);
|
||||||
|
logger.debug('Deleted notification:', id);
|
||||||
|
res.send({ status: 'ok' });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteAllNotificationsRouteHandler = async (req, res) => {
|
||||||
|
try {
|
||||||
|
await notificationModel.deleteMany({ user: req.user._id });
|
||||||
|
logger.debug('Deleted all notifications');
|
||||||
|
res.send({ status: 'ok' });
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Error deleting all notifications:', err);
|
||||||
|
res.status(500).send({ error: 'Failed to delete all notifications' });
|
||||||
|
}
|
||||||
|
};
|
||||||
146
src/services/misc/usernotifiers.js
Normal file
146
src/services/misc/usernotifiers.js
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import config from '../../config.js';
|
||||||
|
import { userNotifierModel } from '../../database/schemas/misc/usernotifier.schema.js';
|
||||||
|
import log4js from 'log4js';
|
||||||
|
import {
|
||||||
|
deleteObject,
|
||||||
|
editObject,
|
||||||
|
getObject,
|
||||||
|
listObjects,
|
||||||
|
newObject,
|
||||||
|
} from '../../database/database.js';
|
||||||
|
|
||||||
|
const logger = log4js.getLogger('UserNotifiers');
|
||||||
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
export const listUserNotifiersRouteHandler = async (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
page = 1,
|
||||||
|
limit = 25,
|
||||||
|
property = '',
|
||||||
|
filter = {},
|
||||||
|
search = '',
|
||||||
|
sort = '',
|
||||||
|
order = 'ascend'
|
||||||
|
) => {
|
||||||
|
const result = await listObjects({
|
||||||
|
model: userNotifierModel,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
property,
|
||||||
|
filter,
|
||||||
|
search,
|
||||||
|
sort,
|
||||||
|
order,
|
||||||
|
populate: ['user', 'object'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error listing user notifiers.');
|
||||||
|
res.status(result.code).send(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`List of user notifiers (Page ${page}, Limit ${limit}). Count: ${result.length}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUserNotifierRouteHandler = async (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
const result = await getObject({
|
||||||
|
model: userNotifierModel,
|
||||||
|
id,
|
||||||
|
populate: ['user', 'object'],
|
||||||
|
});
|
||||||
|
if (result?.error) {
|
||||||
|
logger.warn(`User notifier not found with supplied id.`);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
logger.debug(`Retrieved user notifier with ID: ${id}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const newUserNotifierRouteHandler = async (req, res) => {
|
||||||
|
const newData = {
|
||||||
|
user: req.user._id,
|
||||||
|
object: req.body.object,
|
||||||
|
objectType: req.body.objectType,
|
||||||
|
};
|
||||||
|
const result = await newObject({
|
||||||
|
model: userNotifierModel,
|
||||||
|
newData,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
if (result.error) {
|
||||||
|
logger.error('No user notifier created:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`New user notifier with ID: ${result._id}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const editUserNotifierRouteHandler = async (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
|
||||||
|
const existing = await getObject({
|
||||||
|
model: userNotifierModel,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
if (existing?.error) {
|
||||||
|
return res.status(existing.code).send(existing);
|
||||||
|
}
|
||||||
|
if (String(existing.user._id) !== String(req.user._id)) {
|
||||||
|
return res.status(403).send({ error: 'Forbidden: you can only edit your own notifiers' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
email: req.body.email,
|
||||||
|
};
|
||||||
|
const result = await editObject({
|
||||||
|
model: userNotifierModel,
|
||||||
|
id,
|
||||||
|
updateData,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
logger.error('Error editing user notifier:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Edited user notifier with ID: ${id}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteUserNotifierRouteHandler = async (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
|
||||||
|
const existing = await getObject({
|
||||||
|
model: userNotifierModel,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
if (existing?.error) {
|
||||||
|
return res.status(existing.code).send(existing);
|
||||||
|
}
|
||||||
|
if (String(existing.user._id) !== String(req.user._id)) {
|
||||||
|
return res.status(403).send({ error: 'Forbidden: you can only delete your own notifiers' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await deleteObject({
|
||||||
|
model: userNotifierModel,
|
||||||
|
id,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('No user notifier deleted:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Successfully deleted user notifier ${id}`);
|
||||||
|
res.send({
|
||||||
|
status: 'ok',
|
||||||
|
});
|
||||||
|
};
|
||||||
79
src/utils.js
79
src/utils.js
@ -1,11 +1,14 @@
|
|||||||
import { mongoose } from 'mongoose';
|
import { mongoose } from 'mongoose';
|
||||||
import { auditLogModel } from './database/schemas/management/auditlog.schema.js';
|
import { auditLogModel } from './database/schemas/management/auditlog.schema.js';
|
||||||
|
import { notificationModel } from './database/schemas/misc/notification.schema.js';
|
||||||
|
import { userNotifierModel } from './database/schemas/misc/usernotifier.schema.js';
|
||||||
import exifr from 'exifr';
|
import exifr from 'exifr';
|
||||||
import { natsServer } from './database/nats.js';
|
import { natsServer } from './database/nats.js';
|
||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
import config from './config.js';
|
import config from './config.js';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import canonicalize from 'canonical-json';
|
import canonicalize from 'canonical-json';
|
||||||
|
import { getModelByName } from './services/misc/model.js';
|
||||||
|
|
||||||
const logger = log4js.getLogger('Utils');
|
const logger = log4js.getLogger('Utils');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
@ -350,7 +353,11 @@ function getChangedValues(oldObj, newObj, old = false) {
|
|||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const AUDIT_EXCLUDED_MODELS = ['notification', 'userNotifier'];
|
||||||
|
|
||||||
async function newAuditLog(newValue, parentId, parentType, user) {
|
async function newAuditLog(newValue, parentId, parentType, user) {
|
||||||
|
if (AUDIT_EXCLUDED_MODELS.includes(parentType)) return;
|
||||||
|
|
||||||
// Filter out createdAt and updatedAt from newValue
|
// Filter out createdAt and updatedAt from newValue
|
||||||
const filteredNewValue = { ...newValue };
|
const filteredNewValue = { ...newValue };
|
||||||
delete filteredNewValue.createdAt;
|
delete filteredNewValue.createdAt;
|
||||||
@ -372,6 +379,8 @@ async function newAuditLog(newValue, parentId, parentType, user) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function editAuditLog(oldValue, newValue, parentId, parentType, user) {
|
async function editAuditLog(oldValue, newValue, parentId, parentType, user) {
|
||||||
|
if (AUDIT_EXCLUDED_MODELS.includes(parentType)) return;
|
||||||
|
|
||||||
// Get only the changed values
|
// Get only the changed values
|
||||||
const changedOldValues = getChangedValues(oldValue, newValue, true);
|
const changedOldValues = getChangedValues(oldValue, newValue, true);
|
||||||
const changedNewValues = getChangedValues(oldValue, newValue, false);
|
const changedNewValues = getChangedValues(oldValue, newValue, false);
|
||||||
@ -398,7 +407,35 @@ async function editAuditLog(oldValue, newValue, parentId, parentType, user) {
|
|||||||
await distributeNew(auditLog._id, 'auditLog');
|
await distributeNew(auditLog._id, 'auditLog');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function editNotification(oldValue, newValue, parentId, parentType, user) {
|
||||||
|
const model = getModelByName(parentType);
|
||||||
|
const objectName = oldValue?.name ?? newValue?.name ?? model?.label ?? parentType;
|
||||||
|
const changedOldValues = getChangedValues(oldValue, newValue, true);
|
||||||
|
const changedNewValues = 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: parentId },
|
||||||
|
user: { _id: user._id, firstName: user.firstName, lastName: user.lastName },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteAuditLog(deleteValue, parentId, parentType, user) {
|
async function deleteAuditLog(deleteValue, parentId, parentType, user) {
|
||||||
|
if (AUDIT_EXCLUDED_MODELS.includes(parentType)) return;
|
||||||
|
|
||||||
const auditLog = new auditLogModel({
|
const auditLog = new auditLogModel({
|
||||||
changes: {
|
changes: {
|
||||||
old: deleteValue,
|
old: deleteValue,
|
||||||
@ -415,6 +452,24 @@ async function deleteAuditLog(deleteValue, parentId, parentType, user) {
|
|||||||
await distributeNew(auditLog._id, 'auditLog');
|
await distributeNew(auditLog._id, 'auditLog');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteNotification(object, parentId, parentType, user) {
|
||||||
|
const model = getModelByName(parentType);
|
||||||
|
const objectName = oldValue?.name ?? newValue?.name ?? model?.label ?? parentType;
|
||||||
|
await notfiyObjectUserNotifiers(
|
||||||
|
parentId,
|
||||||
|
parentType,
|
||||||
|
`${objectName} deleted by ${user?.firstName ?? 'unknown'} ${user?.lastName ?? ''}`,
|
||||||
|
`The ${parentType} ${parentId} has been deleted.`,
|
||||||
|
'deleteObject',
|
||||||
|
{
|
||||||
|
object: object,
|
||||||
|
objectType: parentType,
|
||||||
|
object: { _id: parentId },
|
||||||
|
user: { _id: user._id, firstName: user.firstName, lastName: user.lastName },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function getAuditLogs(idOrIds) {
|
async function getAuditLogs(idOrIds) {
|
||||||
if (Array.isArray(idOrIds)) {
|
if (Array.isArray(idOrIds)) {
|
||||||
return auditLogModel.find({ parent: { $in: idOrIds } }).populate('owner');
|
return auditLogModel.find({ parent: { $in: idOrIds } }).populate('owner');
|
||||||
@ -493,6 +548,27 @@ async function distributeChildNew(value, id, model) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function notfiyObjectUserNotifiers(id, objectType, title, message, type = 'info', metadata) {
|
||||||
|
const userNotifiers = await userNotifierModel.find({ object: id, objectType: objectType });
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
await notification.save();
|
||||||
|
const value = notification.toJSON ? notification.toJSON() : notification;
|
||||||
|
await natsServer.publish(`notifications.${user._id}`, value);
|
||||||
|
return notification;
|
||||||
|
}
|
||||||
|
|
||||||
function flatternObjectIds(object) {
|
function flatternObjectIds(object) {
|
||||||
if (!object || typeof object !== 'object') {
|
if (!object || typeof object !== 'object') {
|
||||||
return object;
|
return object;
|
||||||
@ -707,6 +783,8 @@ export {
|
|||||||
parseFilter,
|
parseFilter,
|
||||||
convertToCamelCase,
|
convertToCamelCase,
|
||||||
newAuditLog,
|
newAuditLog,
|
||||||
|
editNotification,
|
||||||
|
deleteNotification,
|
||||||
editAuditLog,
|
editAuditLog,
|
||||||
deleteAuditLog,
|
deleteAuditLog,
|
||||||
getAuditLogs,
|
getAuditLogs,
|
||||||
@ -719,6 +797,7 @@ export {
|
|||||||
distributeChildUpdate,
|
distributeChildUpdate,
|
||||||
distributeChildDelete,
|
distributeChildDelete,
|
||||||
distributeChildNew,
|
distributeChildNew,
|
||||||
|
notfiyObjectUserNotifiers,
|
||||||
getFilter, // <-- add here
|
getFilter, // <-- add here
|
||||||
convertPropertiesString,
|
convertPropertiesString,
|
||||||
getFileMeta,
|
getFileMeta,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user