import _ from 'lodash'; import { deleteAuditLog, expandObjectIds, editAuditLog, distributeUpdate, newAuditLog, distributeNew } from './utils.js'; import log4js from 'log4js'; import { loadConfig } from '../config.js'; import { jsonToCacheKey } from '../utils.js'; import { redisServer } from './redis.js'; const config = loadConfig(); const logger = log4js.getLogger('Database'); const cacheLogger = log4js.getLogger('Local Cache'); logger.level = config.server.logLevel; cacheLogger.level = config.server.logLevel; // Default cache TTL in seconds (similar to previous in-memory cache) const CACHE_TTL_SECONDS = config.database?.redis?.ttlSeconds || 5; export const retrieveObjectCache = async ({ model, id, populate = [] }) => { const cacheKeyObject = { model: model.modelName, id: id?.toString() }; const cacheKey = jsonToCacheKey(cacheKeyObject); cacheLogger.trace('Retrieving:', cacheKeyObject); try { const cachedObject = await redisServer.getKey(cacheKey); if (cachedObject == null) { cacheLogger.trace('Miss:', cacheKeyObject); return undefined; } cacheLogger.trace('Hit:', { model: model.modelName, id: cacheKeyObject.id }); return cachedObject; } catch (err) { cacheLogger.error('Error retrieving object from Redis cache:', err); return undefined; } }; export const retrieveListCache = async ({ model, populate = [], filter = {}, sort = '', order = 'ascend', project = {} }) => { const cacheKeyObject = { model: model.modelName, populate, filter, sort, project, order }; const cacheKey = jsonToCacheKey(cacheKeyObject); cacheLogger.trace('Retrieving:', cacheKeyObject); try { const cachedList = await redisServer.getKey(cacheKey); if (cachedList != null) { cacheLogger.trace('Hit:', { ...cacheKeyObject, length: cachedList.length }); return cachedList; } cacheLogger.trace('Miss:', { model: model.modelName }); return undefined; } catch (err) { cacheLogger.error('Error retrieving list from Redis cache:', err); return undefined; } }; export const updateObjectCache = async ({ model, id, object }) => { const cacheKeyObject = { model: model.modelName, id: id?.toString() }; const cacheKey = jsonToCacheKey(cacheKeyObject); cacheLogger.trace('Updating:', cacheKeyObject); try { const cachedObject = (await redisServer.getKey(cacheKey)) || {}; const mergedObject = _.merge(cachedObject, object); await redisServer.setKey(cacheKey, mergedObject, CACHE_TTL_SECONDS); cacheLogger.trace('Updated:', { ...cacheKeyObject }); return mergedObject; } catch (err) { cacheLogger.error('Error updating object in Redis cache:', err); // Fallback to returning the provided object if cache fails return object; } }; export const deleteObjectCache = async ({ model, id }) => { const cacheKeyObject = { model: model.modelName, id: id?.toString() }; cacheLogger.trace('Deleting:', { ...cacheKeyObject }); try { // Note: we currently delete the non-populated key; populated variants will expire via TTL. const cacheKey = jsonToCacheKey({ ...cacheKeyObject, populate: [] }); await redisServer.deleteKey(cacheKey); cacheLogger.trace('Deleted:', { ...cacheKeyObject }); } catch (err) { cacheLogger.error('Error deleting object from Redis cache:', err); } }; export const updateListCache = ({ model, objects, populate = [], filter = {}, sort = '', order = 'ascend', project = {} }) => { const cacheKeyObject = { model: model.modelName, populate, filter, sort, project, order }; cacheLogger.trace('Updating:', { ...cacheKeyObject, length: objects.length }); const cacheKey = jsonToCacheKey(cacheKeyObject); return (async () => { try { await redisServer.setKey(cacheKey, objects, CACHE_TTL_SECONDS); cacheLogger.trace('Updated:', { ...cacheKeyObject, length: objects.length }); } catch (err) { cacheLogger.error('Error updating list in Redis cache:', err); } return objects; })(); }; // Reusable function to list objects with aggregation, filtering, search, sorting, and pagination export const listObjects = async ({ model, populate = [], filter = {}, sort = '', order = 'ascend', project = {}, // optional: override default projection cached = false }) => { try { logger.trace('Listing objects:', { model, populate, filter, sort, order, project, cached }); if (cached == true) { const objectsCache = await retrieveListCache({ model, populate, filter, sort, order, project }); if (objectsCache != undefined) { return objectsCache; } } // Fix: descend should be -1, ascend should be 1 const sortOrder = order === 'descend' ? -1 : 1; if (!sort || sort === '') { sort = 'createdAt'; } // Translate parent._id to parent for Mongoose if (filter['parent._id']) { filter.parent = filter['parent._id']; delete filter['parent._id']; } // Translate owner._id to owner for Mongoose if (filter['owner._id']) { filter.owner = filter['owner._id']; delete filter['owner._id']; } // Use find with population and filter let query = model.find(filter).sort({ [sort]: sortOrder }); // Handle populate (array or single value) if (populate.length > 0) { if (Array.isArray(populate)) { for (const pop of populate) { query = query.populate(pop); } } else if (typeof populate === 'string' || typeof populate === 'object') { query = query.populate(populate); } } // Handle select (projection) if (project && Object.keys(project).length > 0) { query = query.select(project); } query = query.lean(); const queryResult = await query; const finalResult = expandObjectIds(queryResult); updateListCache({ model, objects: finalResult, populate, filter, sort, order, project }); logger.trace('Retreived from database:', { model, populate, filter, sort, order, project, cached, length: finalResult.length }); return finalResult; } catch (error) { logger.error('Object list error:', error); return { error: error, code: 500 }; } }; // Reusable function to get a single object by ID export const getObject = async ({ model, id, populate = [], cached = false }) => { try { logger.trace('Getting object:', { model, id, populate }); if (cached == true) { const cachedObject = await retrieveObjectCache({ model, id, populate }); if (cachedObject != undefined) { return cachedObject; } } let query = model.findById(id).lean(); // Handle populate (array or single value) if (populate) { if (Array.isArray(populate)) { for (const pop of populate) { query = query.populate(pop); } } else if (typeof populate === 'string' || typeof populate === 'object') { query = query.populate(populate); } } const finalResult = await query; if (!finalResult) { logger.warn('Object not found in database:', { model, id, populate }); return undefined; } logger.trace('Retreived object from database:', { model, id, populate }); updateObjectCache({ model: model, id: finalResult._id.toString(), populate, object: finalResult }); return finalResult; } catch (error) { logger.error('An error retreiving object:', error.message); throw error; return undefined; } }; // Reusable function to edit an object by ID, with audit logging and distribution export const editObject = async ({ model, id, updateData, owner = undefined, ownerType = undefined, populate = [], auditLog = true }) => { try { // Determine parentType from model name const parentType = model.modelName ? model.modelName : 'unknown'; // Fetch the and update object var query = model.findByIdAndUpdate(id, updateData).lean(); var newQuery = model.findById(id).lean(); if (populate) { if (Array.isArray(populate)) { for (const pop of populate) { query = query.populate(pop); newQuery = newQuery.populate(pop); } } else if (typeof populate === 'string' || typeof populate === 'object') { query = query.populate(populate); newQuery = newQuery.populate(populate); } } const previousObject = await query; const newObject = await newQuery; if (!previousObject || !newObject) { return { error: `${parentType} not found.`, code: 404 }; } const previousExpandedObject = expandObjectIds(previousObject); const newExpandedObject = expandObjectIds(newObject); if (auditLog == true && owner != undefined && ownerType != undefined) { // Audit log before update await editAuditLog( previousExpandedObject, newExpandedObject, id, parentType, owner, ownerType ); } // Distribute update await distributeUpdate(updateData, id, parentType); updateObjectCache({ model: model, id: id.toString(), object: { ...previousExpandedObject, ...updateData }, populate }); return { ...previousExpandedObject, ...updateData }; } catch (error) { logger.error('editObject error:', error); return { error: error.message, code: 500 }; } }; // Reusable function to create a new object export const newObject = async ({ model, newData, owner = null, ownerType = undefined }) => { try { const parentType = model.modelName ? model.modelName : 'unknown'; const result = await model.create(newData); if (!result || result.length === 0) { return { error: 'No object created.', code: 500 }; } const created = result; if (owner != undefined && ownerType != undefined) { await newAuditLog(newData, created._id, parentType, owner, ownerType); } await distributeNew(created._id, parentType); updateObjectCache({ model: model, id: created._id.toString(), object: { _id: created._id, ...newData } }); return created; } catch (error) { logger.error('newObject error:', error); return { error: error.message, code: 500 }; } }; // Reusable function to delete an object by ID, with audit logging and distribution export const deleteObject = async ({ model, id, owner = null, ownerType = undefined }) => { try { const parentType = model.modelName ? model.modelName : 'unknown'; // Delete the object const result = await model.findByIdAndDelete(id); if (!result) { return { error: `${parentType} not found.`, code: 404 }; } if (owner != undefined && ownerType != undefined) { // Audit log the deletion await deleteAuditLog(result, id, parentType, owner, ownerType); } deleteObjectCache({ model: model, id: id.toString() }); // Distribute the deletion event await distributeUpdate({ deleted: true }, id, parentType); return { deleted: true, id: id.toString() }; } catch (error) { logger.error('deleteObject error:', error); return { error: error.message, code: 500 }; } };