489 lines
11 KiB
JavaScript
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 };
|
|
}
|
|
};
|