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) { let options = {}; if (ctx.options && ctx.options.transaction) options.transaction = ctx.options.transaction; let oldInstance; let oldInstanceFk; let newInstance; if (ctx.data) { oldInstanceFk = pick(ctx.currentInstance, Object.keys(ctx.data)); newInstance = await fkToValue(ctx.data, ctx); oldInstance = await fkToValue(oldInstanceFk, ctx); if (ctx.where && !ctx.currentInstance) { let fields = Object.keys(ctx.data); ctx.oldInstances = await ctx.Model.app.models[ctx.Model.definition.name].find({where: ctx.where, fields: fields}, options); } } 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) { let options = {}; if (ctx.options && ctx.options.transaction) options.transaction = ctx.options.transaction; if (ctx.where) { let affectedModel = ctx.Model.definition.name; let definition = ctx.Model.definition; let deletedInstances = await ctx.Model.app.models[affectedModel].find({where: ctx.where}, options); let relation = definition.settings.log.relation; if (relation) { let primaryKey = ctx.Model.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) { 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 definition = ctx.Model.definition; let changedModelValue = definition.settings.log.changedModelValue; let logRecord = { originFk: instance.originFk, userFk: userFk, action: 'delete', changedModel: ctx.Model.definition.name, changedModelId: instance.id, changedModelValue: instance[changedModelValue], oldInstance: instance, newInstance: {} }; delete instance.originFk; let logModel = definition.settings.log.model; await ctx.Model.app.models[logModel].create(logRecord, options); }); } async function fkToValue(instance, ctx) { let options = {}; if (ctx.options && ctx.options.transaction) options.transaction = ctx.options.transaction; let cleanInstance = JSON.parse(JSON.stringify(instance)); let result = {}; for (let key in cleanInstance) { let val = cleanInstance[key]; if (val === undefined || val === null) continue; for (let key1 in ctx.Model.relations) { let val1 = ctx.Model.relations[key1]; if (val1.keyFrom == key && key != 'id') { let recordSet = await ctx.Model.app.models[val1.modelTo.modelName].findById(val, null, options); let showField = val1.modelTo && val1.modelTo.definition.settings.log && val1.modelTo.definition.settings.log.showField && recordSet && recordSet[val1.modelTo.definition.settings.log.showField]; if (!showField) { const showFieldNames = [ 'name', 'description', 'code' ]; for (field of showFieldNames) { if (val1.modelTo.definition.properties && val1.modelTo.definition.properties[field] && recordSet && recordSet[field]) { showField = field; break; } } } if (showField && recordSet && recordSet[showField]) { val = recordSet[showField]; break; } val = recordSet && recordSet.id || val; break; } } result[key] = val; } return result; } async function logInModel(ctx, loopBackContext) { let options = {}; if (ctx.options && ctx.options.transaction) options.transaction = ctx.options.transaction; let definition = ctx.Model.definition; 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 && !definition.settings.log.relation) { originId = ctx.instance.id; changedModelId = ctx.instance.id; } else if (definition.settings.log.relation) { primaryKey = ctx.Model.relations[definition.settings.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 = definition.settings.log.showField; let where; if (showField && (!ctx.instance || !ctx.instance[showField]) && ctx.where) { changedModelId = []; where = []; let changedInstances = await ctx.Model.app.models[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); removeUnloggableProperties(definition, oldInstance); removeUnloggableProperties(definition, newInstance); let logRecord = { originFk: originId, userFk: userFk, action: action, changedModel: ctx.Model.definition.name, changedModelId: changedModelId, changedModelValue: where, oldInstance: oldInstance, newInstance: newInstance }; let logsToSave = setLogsToSave(where, changedModelId, logRecord, ctx); let logModel = definition.settings.log.model; await ctx.Model.app.models[logModel].create(logsToSave, options); } /** * Removes unwanted properties * @param {*} definition Model definition * @param {*} properties Modified object properties */ function removeUnloggableProperties(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'; } };