Enhance database functionality by adding user parameter to edit, new, and delete object functions; implement email account, template, and message schemas; update audit log schema to include system flag and improve owner resolution logic.
All checks were successful
farmcontrol/farmcontrol-scheduler/pipeline/head This commit looks good

This commit is contained in:
Tom Butcher 2026-09-12 21:39:53 +01:00
parent 10b5074f94
commit 39060f0cea
7 changed files with 257 additions and 24 deletions

View File

@ -512,6 +512,7 @@ export const editObject = async ({
model,
id,
updateData,
user = undefined,
owner = undefined,
ownerType = undefined,
populate = [],
@ -546,7 +547,10 @@ export const editObject = async ({
const previousExpandedObject = expandObjectIds(previousObject);
const newExpandedObject = expandObjectIds(newObject);
if (auditLog == true && owner != undefined && ownerType != undefined) {
if (
auditLog == true &&
(user != undefined || (owner != undefined && ownerType != undefined))
) {
// Audit log before update
await editAuditLog(
previousExpandedObject,
@ -555,6 +559,7 @@ export const editObject = async ({
parentType,
owner,
ownerType,
user,
);
}
@ -589,7 +594,7 @@ export const editObject = async ({
if (model.recalculate && recalculate == true) {
logger.debug(`Recalculating ${model.modelName}`);
await model.recalculate(newExpandedObject, owner, ownerType);
await model.recalculate(newExpandedObject, user ?? owner, ownerType);
}
if (model.stats) {
@ -609,6 +614,7 @@ export const editObject = async ({
export const newObject = async ({
model,
newData,
user = undefined,
owner = null,
ownerType = undefined,
}) => {
@ -621,8 +627,15 @@ export const newObject = async ({
}
const created = result;
if (owner != undefined && ownerType != undefined) {
await newAuditLog(newData, created._id, parentType, owner, ownerType);
if (user != undefined || (owner != undefined && ownerType != undefined)) {
await newAuditLog(
newData,
created._id,
parentType,
owner,
ownerType,
user,
);
}
await distributeNew(created, parentType);
@ -645,6 +658,7 @@ export const newObject = async ({
export const deleteObject = async ({
model,
id,
user = undefined,
owner = null,
ownerType = undefined,
}) => {
@ -657,9 +671,9 @@ export const deleteObject = async ({
return { error: `${parentType} not found.`, code: 404 };
}
if (owner != undefined && ownerType != undefined) {
if (user != undefined || (owner != undefined && ownerType != undefined)) {
// Audit log the deletion
await deleteAuditLog(result, id, parentType, owner, ownerType);
await deleteAuditLog(result, id, parentType, owner, ownerType, user);
}
deleteObjectCache({ model: model, id: id.toString() });

View File

@ -7,36 +7,45 @@ const auditLogSchema = new Schema(
_reference: { type: String, default: () => generateId()() },
changes: {
old: { type: Object, required: false },
new: { type: Object, required: false },
new: { type: Object, required: false }
},
operation: {
type: String,
required: true,
required: true
},
parent: {
type: Schema.Types.ObjectId,
refPath: 'parentType',
required: true,
required: true
},
parentType: {
type: String,
required: true,
required: true
},
owner: {
type: Schema.Types.ObjectId,
refPath: 'ownerType',
required: true,
required: false
},
ownerType: {
type: String,
required: true,
enum: ['user', 'printer', 'host', 'marketplace'],
required: false,
enum: ['user', 'printer', 'host', 'marketplace']
},
system: {
type: Boolean,
required: true,
default: false
}
},
{ timestamps: true }
);
auditLogSchema.index({ operation: 'text', parentType: 'text', ownerType: 'text' });
auditLogSchema.index({
operation: 'text',
parentType: 'text',
ownerType: 'text'
});
// Add virtual id getter
auditLogSchema.virtual('id').get(function () {

View File

@ -0,0 +1,33 @@
import mongoose from 'mongoose';
import { generateId } from '../../utils.js';
const { Schema } = mongoose;
const emailAccountSchema = new Schema(
{
_reference: { type: String, default: () => generateId()() },
name: { type: String, required: true, unique: true },
active: { type: Boolean, required: true, default: true },
host: { type: String, required: true },
port: { type: Number, required: true, default: 587, min: 1, max: 65535 },
secure: { type: Boolean, required: true, default: false },
username: { type: String, required: false },
password: { type: String, required: false, select: false },
fromName: { type: String, required: true },
fromEmail: { type: String, required: true },
},
{ timestamps: true }
);
emailAccountSchema.index({ name: 'text', host: 'text', fromEmail: 'text' });
emailAccountSchema.virtual('id').get(function () {
return this._id;
});
function omitPassword(_document, value) {
delete value.password;
return value;
}
emailAccountSchema.set('toJSON', { virtuals: true, transform: omitPassword });
emailAccountSchema.set('toObject', { virtuals: true, transform: omitPassword });
export const emailAccountModel = mongoose.model('emailAccount', emailAccountSchema);

View File

@ -0,0 +1,68 @@
import mongoose from 'mongoose';
import { generateId } from '../../utils.js';
const { Schema } = mongoose;
const emailMessageSchema = new Schema(
{
_reference: { type: String, default: () => generateId()() },
name: { type: String, required: true },
emailTemplate: {
type: Schema.Types.ObjectId,
ref: 'emailTemplate',
required: true
},
objectType: { type: String, required: true },
object: {
type: Schema.Types.ObjectId,
refPath: 'objectType',
required: true
},
emailAccount: {
type: Schema.Types.ObjectId,
ref: 'emailAccount',
required: true
},
recipientEmail: { type: String, required: true },
recipientType: {
type: String,
enum: ['client', 'vendor'],
required: false
},
recipient: {
type: Schema.Types.ObjectId,
refPath: 'recipientType',
required: false
},
fromEmail: { type: String, required: true, immutable: true },
messageId: { type: String, required: false },
subject: { type: String, required: false, default: '' },
content: { type: String, required: false, default: '' },
attachments: [{ type: Schema.Types.ObjectId, ref: 'file', required: false }],
state: {
type: { type: String, required: true, default: 'queued' },
progress: { type: Number, required: false, default: 0 },
message: { type: String, required: false }
},
read: { type: Boolean, required: true, default: false },
sentAt: { type: Date, required: false },
readAt: { type: Date, required: false }
},
{ timestamps: true, suppressReservedKeysWarning: true }
);
emailMessageSchema.index({
name: 'text',
recipientEmail: 'text',
fromEmail: 'text',
objectType: 'text'
});
emailMessageSchema.virtual('id').get(function () {
return this._id;
});
emailMessageSchema.set('toJSON', { virtuals: true });
export const emailMessageModel = mongoose.model(
'emailMessage',
emailMessageSchema
);

View File

@ -0,0 +1,78 @@
import mongoose from 'mongoose';
import { generateId } from '../../utils.js';
const { Schema } = mongoose;
const RENDER_EMAIL_TEMPLATE_CALL = /fc\.renderEmailTemplate\s*\(\s*(['"])([^'"]+)\1/g;
function extractReferences(content) {
const references = [];
const seen = new Set();
for (const match of String(content || '').matchAll(RENDER_EMAIL_TEMPLATE_CALL)) {
const reference = match[2]?.trim();
if (reference && !seen.has(reference)) {
seen.add(reference);
references.push(reference);
}
}
return references;
}
const emailTemplateAttachmentSchema = new Schema(
{
type: { type: String, enum: ['file', 'documentTemplate'], required: true },
file: { type: Schema.Types.ObjectId, refPath: 'attachments.type', required: true },
fileName: { type: String, required: true },
fileType: { type: String, enum: ['pdf', 'jpeg', 'png', 'svg'], required: false },
},
{ _id: true }
);
const emailTemplateSchema = new Schema(
{
_reference: { type: String, default: () => generateId()() },
name: { type: String, required: true, unique: true },
objectType: { type: String, required: false },
tags: [{ type: String }],
active: { type: Boolean, required: true, default: true },
global: { type: Boolean, required: true, default: false },
parent: { type: Schema.Types.ObjectId, ref: 'emailTemplate', required: false },
referencedTemplates: [
{ type: Schema.Types.ObjectId, ref: 'emailTemplate', required: false },
],
subject: { type: String, required: false, default: '' },
content: { type: String, required: false, default: '<Container></Container>' },
testObject: { type: Schema.Types.ObjectId, refPath: 'objectType', required: false },
attachments: { type: [emailTemplateAttachmentSchema], default: [] },
},
{ timestamps: true }
);
emailTemplateSchema.index({ name: 'text', tags: 'text', objectType: 'text' });
emailTemplateSchema.virtual('id').get(function () {
return this._id;
});
emailTemplateSchema.set('toJSON', { virtuals: true });
emailTemplateSchema.statics.recalculate = async function (emailTemplate, user) {
const id = emailTemplate?._id || emailTemplate;
if (!id || !(await this.exists({ _id: id }))) return;
const referencedTemplates = [];
for (const reference of extractReferences(emailTemplate?.content)) {
const normalizedReference = reference.replace(/^ETP:/i, '').trim();
const found = await this.findOne({ _reference: normalizedReference }).select('_id').lean();
if (found?._id) referencedTemplates.push(found._id);
}
const { editObject } = await import('../../database.js');
await editObject({
model: this,
id,
updateData: { referencedTemplates },
user,
populate: [{ path: 'referencedTemplates', strictPopulate: false }],
recalculate: false,
});
};
export const emailTemplateModel = mongoose.model('emailTemplate', emailTemplateSchema);

View File

@ -35,6 +35,9 @@ import { userNotifierModel } from './misc/usernotifier.schema.js';
import { objectViewModel } from './misc/objectview.schema.js';
import { documentSizeModel } from './management/documentsize.schema.js';
import { documentTemplateModel } from './management/documenttemplate.schema.js';
import { emailAccountModel } from './management/emailaccount.schema.js';
import { emailTemplateModel } from './management/emailtemplate.schema.js';
import { emailMessageModel } from './management/emailmessage.schema.js';
import { hostModel } from './management/host.schema.js';
import { documentPrinterModel } from './management/documentprinter.schema.js';
import { documentJobModel } from './management/documentjob.schema.js';
@ -107,6 +110,9 @@ export const models = {
OVW: modelEntry(() => objectViewModel, 'objectView', 'Object View'),
DSZ: modelEntry(() => documentSizeModel, 'documentSize', 'Document Size'),
DTP: modelEntry(() => documentTemplateModel, 'documentTemplate', 'Document Template'),
EAC: modelEntry(() => emailAccountModel, 'emailAccount', 'Email Account'),
ETP: modelEntry(() => emailTemplateModel, 'emailTemplate', 'Email Template'),
EMS: modelEntry(() => emailMessageModel, 'emailMessage', 'Email Message'),
DPR: modelEntry(() => documentPrinterModel, 'documentPrinter', 'Document Printer'),
DJB: modelEntry(() => documentJobModel, 'documentJob', 'Document Job'),
HST: modelEntry(() => hostModel, 'host', 'Host'),

View File

@ -19,7 +19,7 @@ const AUDIT_EXCLUDED_MODELS = [
'marketplaceEvent'
];
const AUDIT_EXCLUDED_CHANGES = ['state.message'];
const SENSITIVE_KEYS = ['secret'];
const SENSITIVE_KEYS = ['secret', 'password'];
function omitSensitive(obj) {
if (obj == null || typeof obj !== 'object') return obj;
@ -32,6 +32,25 @@ function omitSensitive(obj) {
return result;
}
function resolveAuditOwner(owner, ownerType, user) {
if (user === 'system') return { system: true };
const actor = user ?? owner;
const resolvedOwner = actor?._id ?? actor;
const resolvedOwnerType =
ownerType ??
(['user', 'printer', 'host', 'marketplace'].includes(actor?._objectType)
? actor._objectType
: 'user');
return {
system: false,
...(resolvedOwner != null
? { owner: resolvedOwner, ownerType: resolvedOwnerType }
: {})
};
}
function omitPath(obj, path) {
if (obj == null || typeof obj !== 'object') return obj;
const keys = path.split('.');
@ -450,7 +469,14 @@ function getChangedValues(oldObj, newObj, old = false) {
return changes;
}
async function newAuditLog(newValue, parentId, parentType, owner, ownerType) {
async function newAuditLog(
newValue,
parentId,
parentType,
owner,
ownerType,
user
) {
if (AUDIT_EXCLUDED_MODELS.includes(parentType)) return;
// Filter out createdAt, updatedAt, and sensitive fields from newValue
@ -463,8 +489,7 @@ async function newAuditLog(newValue, parentId, parentType, owner, ownerType) {
},
parent: parentId,
parentType,
owner: owner._id,
ownerType: ownerType,
...resolveAuditOwner(owner, ownerType, user),
operation: 'new'
});
@ -479,7 +504,8 @@ async function editAuditLog(
parentId,
parentType,
owner,
ownerType
ownerType,
user
) {
if (parentType === 'stockEvent') {
return;
@ -513,8 +539,7 @@ async function editAuditLog(
},
parent: parentId,
parentType,
owner: owner._id,
ownerType: ownerType,
...resolveAuditOwner(owner, ownerType, user),
operation: 'edit'
});
@ -528,7 +553,8 @@ async function deleteAuditLog(
parentId,
parentType,
owner,
ownerType
ownerType,
user
) {
const auditLog = new auditLogModel({
changes: {
@ -536,8 +562,7 @@ async function deleteAuditLog(
},
parent: parentId,
parentType,
owner: owner._id,
ownerType: ownerType,
...resolveAuditOwner(owner, ownerType, user),
operation: 'delete'
});