const pick = require('object.pick'); const LoopBackContext = require('loopback-context'); module.exports = function(Self) { Self.setup = function() { Self.super_.setup.call(this); }; Self.observe('after save', async function(ctx) { const loopBackContext = LoopBackContext.getCurrentContext(); await logInModel(ctx, loopBackContext); }); Self.observe('before save', async function(ctx) { const appModels = ctx.Model.app.models; const definition = ctx.Model.definition; const options = {}; // Check for transactions if (ctx.options && ctx.options.transaction) options.transaction = ctx.options.transaction; let oldInstance; let newInstance; if (ctx.data) { const changes = pick(ctx.currentInstance, Object.keys(ctx.data)); newInstance = await fkToValue(ctx.data, ctx); oldInstance = await fkToValue(changes, ctx); if (ctx.where && !ctx.currentInstance) { const fields = Object.keys(ctx.data); const modelName = definition.name; ctx.oldInstances = await appModels[modelName].find({ where: ctx.where, fields: fields }, options); } } // Get changes from created instance if (ctx.isNewInstance) newInstance = await fkToValue(ctx.instance.__data, ctx); ctx.hookState.oldInstance = oldInstance; ctx.hookState.newInstance = newInstance; }); Self.observe('before delete', async function(ctx) { const appModels = ctx.Model.app.models; const definition = ctx.Model.definition; const relations = ctx.Model.relations; let options = {}; if (ctx.options && ctx.options.transaction) options.transaction = ctx.options.transaction; if (ctx.where) { let affectedModel = definition.name; let deletedInstances = await appModels[affectedModel].find({ where: ctx.where }, options); let relation = definition.settings.log.relation; if (relation) { let primaryKey = relations[relation].keyFrom; let arrangedDeletedInstances = []; for (let i = 0; i < deletedInstances.length; i++) { if (primaryKey) deletedInstances[i].originFk = deletedInstances[i][primaryKey]; let arrangedInstance = await fkToValue(deletedInstances[i], ctx); arrangedDeletedInstances[i] = arrangedInstance; } ctx.hookState.oldInstance = arrangedDeletedInstances; } } }); Self.observe('after delete', async function(ctx) { const loopBackContext = LoopBackContext.getCurrentContext(); if (ctx.hookState.oldInstance) logDeletedInstances(ctx, loopBackContext); }); async function logDeletedInstances(ctx, loopBackContext) { const appModels = ctx.Model.app.models; const definition = ctx.Model.definition; let options = {}; if (ctx.options && ctx.options.transaction) options.transaction = ctx.options.transaction; ctx.hookState.oldInstance.forEach(async instance => { let userFk; if (loopBackContext) userFk = loopBackContext.active.accessToken.userId; let changedModelValue = definition.settings.log.changedModelValue; let logRecord = { originFk: instance.originFk, userFk: userFk, action: 'delete', changedModel: definition.name, changedModelId: instance.id, changedModelValue: instance[changedModelValue], oldInstance: instance, newInstance: {} }; delete instance.originFk; let logModel = definition.settings.log.model; await appModels[logModel].create(logRecord, options); }); } // Get log values from a foreign key async function fkToValue(instance, ctx) { const appModels = ctx.Model.app.models; const relations = ctx.Model.relations; let options = {}; // Check for transactions if (ctx.options && ctx.options.transaction) options.transaction = ctx.options.transaction; const instanceCopy = JSON.parse(JSON.stringify(instance)); const result = {}; for (const key in instanceCopy) { let value = instanceCopy[key]; if (value instanceof Object) continue; if (value === undefined || value === null) continue; for (let relationName in relations) { const relation = relations[relationName]; if (relation.keyFrom == key && key != 'id') { const model = relation.modelTo; const modelName = relation.modelTo.modelName; const properties = model && model.definition.properties; const settings = model && model.definition.settings; const recordSet = await appModels[modelName].findById(value, null, options); const hasShowField = settings.log && settings.log.showField; let showField = hasShowField && recordSet && recordSet[settings.log.showField]; if (!showField) { const showFieldNames = [ 'name', 'description', 'code', 'nickname' ]; for (field of showFieldNames) { const propField = properties && properties[field]; const recordField = recordSet && recordSet[field]; if (propField && recordField) { showField = field; break; } } } if (showField && recordSet && recordSet[showField]) { value = recordSet[showField]; break; } value = recordSet && recordSet.id || value; break; } } result[key] = value; } return result; } async function logInModel(ctx, loopBackContext) { const appModels = ctx.Model.app.models; const definition = ctx.Model.definition; const defSettings = ctx.Model.definition.settings; const relations = ctx.Model.relations; const options = {}; if (ctx.options && ctx.options.transaction) options.transaction = ctx.options.transaction; let primaryKey; for (let property in definition.properties) { if (definition.properties[property].id) { primaryKey = property; break; } } if (!primaryKey) throw new Error('Primary key not found'); let originId; // RELATIONS LOG let changedModelId; if (ctx.instance && !defSettings.log.relation) { originId = ctx.instance.id; changedModelId = ctx.instance.id; } else if (defSettings.log.relation) { primaryKey = relations[defSettings.log.relation].keyFrom; if (ctx.where && ctx.where[primaryKey]) originId = ctx.where[primaryKey]; else if (ctx.instance) { originId = ctx.instance[primaryKey]; changedModelId = ctx.instance.id; } } else { originId = ctx.currentInstance.id; changedModelId = ctx.currentInstance.id; } // Sets the changedModelValue to save and the instances changed in case its an updateAll let showField = defSettings.log.showField; let where; if (showField && (!ctx.instance || !ctx.instance[showField]) && ctx.where) { changedModelId = []; where = []; let changedInstances = await appModels[definition.name].find({ where: ctx.where, fields: ['id', showField, primaryKey] }, options); changedInstances.forEach(element => { where.push(element[showField]); changedModelId.push(element.id); originId = element[primaryKey]; }); } else if (ctx.hookState.oldInstance) where = ctx.instance[showField]; // Set oldInstance, newInstance, userFk and action let oldInstance = {}; if (ctx.hookState.oldInstance) Object.assign(oldInstance, ctx.hookState.oldInstance); let newInstance = {}; if (ctx.hookState.newInstance) Object.assign(newInstance, ctx.hookState.newInstance); let userFk; if (loopBackContext) userFk = loopBackContext.active.accessToken.userId; let action = setActionType(ctx); removeUnloggable(definition, oldInstance); removeUnloggable(definition, newInstance); // Prevent log with no new changes const hasNewChanges = Object.keys(newInstance).length; if (!hasNewChanges) return; let logRecord = { originFk: originId, userFk: userFk, action: action, changedModel: definition.name, changedModelId: changedModelId, // Model property with an different data type will throw a NaN error changedModelValue: where, oldInstance: oldInstance, newInstance: newInstance }; let logsToSave = setLogsToSave(where, changedModelId, logRecord, ctx); let logModel = defSettings.log.model; await appModels[logModel].create(logsToSave, options); } /** * Removes unwanted properties * @param {*} definition Model definition * @param {*} properties Modified object properties */ function removeUnloggable(definition, properties) { const propList = Object.keys(properties); const propDefs = new Map(); for (let property in definition.properties) { const propertyDef = definition.properties[property]; propDefs.set(property, propertyDef); } for (let property of propList) { const propertyDef = propDefs.get(property); if (!propertyDef) return; if (propertyDef.log === false) delete properties[property]; else if (propertyDef.logValue === false) properties[property] = null; } } // this function retuns all the instances changed in case this is an updateAll function setLogsToSave(changedInstances, changedInstancesIds, logRecord, ctx) { let promises = []; if (changedInstances && typeof changedInstances == 'object') { for (let i = 0; i < changedInstances.length; i++) { logRecord.changedModelId = changedInstancesIds[i]; logRecord.changedModelValue = changedInstances[i]; if (ctx.oldInstances) logRecord.oldInstance = ctx.oldInstances[i]; promises.push(JSON.parse(JSON.stringify(logRecord))); } } else return logRecord; return promises; } function setActionType(ctx) { let oldInstance = ctx.hookState.oldInstance; let newInstance = ctx.hookState.newInstance; if (oldInstance && newInstance) return 'update'; else if (!oldInstance && newInstance) return 'insert'; return 'delete'; } };