diff --git a/src/database/__tests__/database.test.js b/src/database/__tests__/database.test.js index 185efbd..6849582 100644 --- a/src/database/__tests__/database.test.js +++ b/src/database/__tests__/database.test.js @@ -75,7 +75,8 @@ jest.unstable_mockModule('../schemas/management/file.schema.js', () => ({ // Now import the database utilities const { listObjects, getObject, newObject, editObject, deleteObject } = await import('../database.js'); -const { editAuditLog, editNotification } = await import('../../utils.js'); +const { deleteAuditLog, deleteNotification, editAuditLog, editNotification, newAuditLog } = + await import('../../utils.js'); describe('Database Utilities (CRUD)', () => { let mockModel; @@ -151,6 +152,26 @@ describe('Database Utilities (CRUD)', () => { expect(mockModel.create).toHaveBeenCalledWith(newData); expect(result).toEqual(createdData); }); + + it('omits schema hidden fields from the create audit log', async () => { + mockModel.hiddenFields = ['alerts', 'pendingSlicerUploads']; + const newData = { + name: 'Printer', + alerts: [{ type: 'error', message: 'hotend' }], + pendingSlicerUploads: [{ file: 'file-1' }], + }; + mockModel.create.mockResolvedValue({ + toObject: () => ({ _id: '456', ...newData }), + _id: '456', + }); + + const result = await newObject({ model: mockModel, newData }); + + expect(mockModel.create).toHaveBeenCalledWith(newData); + expect(newAuditLog).toHaveBeenCalledWith({ name: 'Printer' }, '456', 'TestModel', null); + expect(result.alerts).toEqual(newData.alerts); + expect(result.pendingSlicerUploads).toEqual(newData.pendingSlicerUploads); + }); }); describe('editObject', () => { @@ -199,6 +220,46 @@ describe('Database Utilities (CRUD)', () => { ); expect(editNotification).not.toHaveBeenCalled(); }); + + it('omits schema hidden fields from edit audit logs and notifications', async () => { + mockModel.hiddenFields = ['alerts', 'pendingSlicerUploads']; + mockModel.lean.mockResolvedValue({ + _id: '123', + name: 'Old', + alerts: [{ type: 'info', message: 'before' }], + pendingSlicerUploads: [], + }); + + const result = await editObject({ + model: mockModel, + id: '123', + updateData: { + name: 'Updated', + alerts: [{ type: 'error', message: 'after' }], + pendingSlicerUploads: [{ file: 'file-1' }], + }, + user: { _id: 'user-1' }, + }); + + const visiblePrevious = { _id: '123', name: 'Old' }; + const visibleUpdated = { _id: '123', name: 'Updated' }; + expect(editAuditLog).toHaveBeenCalledWith( + visiblePrevious, + visibleUpdated, + '123', + 'TestModel', + { _id: 'user-1' } + ); + expect(editNotification).toHaveBeenCalledWith( + visiblePrevious, + visibleUpdated, + '123', + 'TestModel', + { _id: 'user-1' } + ); + expect(result.alerts).toEqual([{ type: 'error', message: 'after' }]); + expect(result.pendingSlicerUploads).toEqual([{ file: 'file-1' }]); + }); }); describe('deleteObject', () => { @@ -214,5 +275,31 @@ describe('Database Utilities (CRUD)', () => { expect(mockModel.findByIdAndDelete).toHaveBeenCalledWith(id); expect(result).toEqual({ deleted: true, object: mockData }); }); + + it('omits schema hidden fields from delete audit logs and notifications', async () => { + mockModel.hiddenFields = ['alerts', 'pendingSlicerUploads']; + const mockData = { + _id: '123', + name: 'To be deleted', + alerts: [{ type: 'error', message: 'hotend' }], + pendingSlicerUploads: [{ file: 'file-1' }], + }; + mockModel.findByIdAndDelete.mockResolvedValue({ + toObject: () => mockData, + }); + + const result = await deleteObject({ + model: mockModel, + id: '123', + user: { _id: 'user-1' }, + }); + + const visible = { _id: '123', name: 'To be deleted' }; + expect(deleteAuditLog).toHaveBeenCalledWith(visible, '123', 'TestModel', { _id: 'user-1' }); + expect(deleteNotification).toHaveBeenCalledWith(visible, '123', 'TestModel', { + _id: 'user-1', + }); + expect(result.object).toEqual(mockData); + }); }); }); diff --git a/src/database/database.js b/src/database/database.js index 9432135..7ac97e3 100644 --- a/src/database/database.js +++ b/src/database/database.js @@ -34,6 +34,24 @@ import { convertObjectIdStringsInFilter } from './utils.js'; const BASIC_OBJECT_FIELDS = '_id _reference name color state'; +// Schema statics such as printerSchema.statics.hiddenFields are copied onto the model. +// Drop them from snapshots that become audit logs or notifications; the stored +// document and live updates still include them. +const omitHiddenFields = (model, value) => { + const hiddenFields = model?.hiddenFields; + if ( + !Array.isArray(hiddenFields) || + hiddenFields.length === 0 || + value == null || + typeof value !== 'object' || + Array.isArray(value) + ) { + return value; + } + + return _.omit(value, hiddenFields); +}; + const logger = log4js.getLogger('Database'); logger.level = config.server.logLevel; @@ -1221,8 +1239,11 @@ export const editObject = async ({ model, id, updateData, user, populate, recalc } } + const previousForAudit = omitHiddenFields(model, previousExpandedObject); + const updatedForAudit = omitHiddenFields(model, updatedObject); + // Audit log before update - await editAuditLog(previousExpandedObject, updatedObject, id, parentType, user); + await editAuditLog(previousForAudit, updatedForAudit, id, parentType, user); if ( user !== 'system' && @@ -1231,7 +1252,7 @@ export const editObject = async ({ model, id, updateData, user, populate, recalc parentType !== 'userNotifier' && parentType !== 'objectView' ) { - await editNotification(previousExpandedObject, updatedObject, id, parentType, user); + await editNotification(previousForAudit, updatedForAudit, id, parentType, user); } // Distribute update await distributeUpdate(updateData, id, parentType); @@ -1310,7 +1331,7 @@ export const newObject = async ( typeof result.toObject === 'function' ? result.toObject() : result.$toObject(); const created = expandObjectIds(serialized); - await newAuditLog(newData, created._id, parentType, user); + await newAuditLog(omitHiddenFields(model, newData), created._id, parentType, user); if (distributeChanges == true) { await distributeNew(created, parentType); @@ -1587,8 +1608,10 @@ export const deleteObject = async ( typeof result.toObject === 'function' ? result.toObject() : result.$toObject(); const deleted = expandObjectIds(serialized); + const deletedForAudit = omitHiddenFields(model, deleted); + // Audit log the deletion - await deleteAuditLog(deleted, id.toString(), parentType, user); + await deleteAuditLog(deletedForAudit, id.toString(), parentType, user); if ( user !== 'system' && @@ -1597,7 +1620,7 @@ export const deleteObject = async ( parentType !== 'userNotifier' && parentType !== 'objectView' ) { - await deleteNotification(deleted, id.toString(), parentType, user); + await deleteNotification(deletedForAudit, id.toString(), parentType, user); } if (distributeChanges == true) { diff --git a/src/database/schemas/production/printer.schema.js b/src/database/schemas/production/printer.schema.js index 750d003..fe5bd4b 100644 --- a/src/database/schemas/production/printer.schema.js +++ b/src/database/schemas/production/printer.schema.js @@ -101,6 +101,8 @@ const rollupConfigs = [ }, ]; +printerSchema.statics.hiddenFields = ['alerts', 'pendingSlicerUploads']; + printerSchema.statics.stats = async function () { const results = await aggregateRollups({ model: this,