489 lines
11 KiB
JavaScript

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 };
}
};