diff --git a/loopback/common/models/loggable.js b/loopback/common/models/loggable.js index f87f1aaee..9f1cb29d1 100644 --- a/loopback/common/models/loggable.js +++ b/loopback/common/models/loggable.js @@ -6,11 +6,6 @@ module.exports = function(Self) { 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 loopBackContext = LoopBackContext.getCurrentContext(); const models = ctx.Model.app.models; @@ -21,10 +16,11 @@ module.exports = function(Self) { if (!opts) opts = ctx.options = {}; // Check for transactions - let tx; - if (!opts.transaction) { - opts.transaction = tx = await Model.beginTransaction({}); - opts.tx = true; + if (opts.txLevel) + opts.txLevel++; + else if (!opts.transaction) { + opts.txLevel = 1; + opts.transaction = await Model.beginTransaction({}); } try { @@ -59,11 +55,143 @@ module.exports = function(Self) { ctx.hookState.oldInstance = oldInstance; ctx.hookState.newInstance = newInstance; } catch (e) { - if (tx) await tx.rollback(); - throw e; + await txEnd(ctx, true); } }); + async function txEnd(ctx, undoChanges) { + const opts = ctx.options || {}; + if (opts.txLevel) { + opts.txLevel--; + if (opts.txLevel === 0) { + const tx = opts.transaction; + delete opts.txLevel; + delete opts.transaction; + + if (undoChanges) + await tx.rollback(); + else + await tx.commit(); + } + } + } + + Self.observe('after save', async function(ctx) { + const loopBackContext = LoopBackContext.getCurrentContext(); + await logInModel(ctx, loopBackContext); + }); + + async function logInModel(ctx, loopBackContext) { + const models = ctx.Model.app.models; + const definition = ctx.Model.definition; + const Model = models[definition.name]; + const settings = ctx.Model.definition.settings; + const relations = ctx.Model.relations; + + let opts = ctx.options; + if (!opts) opts = ctx.options = {}; + + try { + 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 && !settings.log.relation) { + originId = ctx.instance.id; + changedModelId = ctx.instance.id; + } else if (settings.log.relation) { + primaryKey = relations[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 = settings.log.showField; + let where; + if (showField && (!ctx.instance || !ctx.instance[showField]) && ctx.where) { + changedModelId = []; + where = []; + let changedInstances = await models[definition.name].find({ + where: ctx.where, + fields: ['id', showField, primaryKey] + }, opts); + + 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; + + const action = setActionType(ctx); + + removeUnloggable(definition, oldInstance); + removeUnloggable(definition, newInstance); + + oldInstance = await fkToValue(oldInstance, ctx); + newInstance = await fkToValue(newInstance, ctx); + + // 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 + }; + + const logsToSave = setLogsToSave(where, changedModelId, logRecord, ctx); + const logModel = settings.log.model; + await models[logModel].create(logsToSave, opts); + + // Check if grabUser is active + if (settings.log && settings.log.grabUser) await Model.rawSql(`CALL account.myUser_logout()`, null, opts); + } catch (e) { + await txEnd(ctx, true); + throw e; + } + + await txEnd(ctx); + } + Self.observe('before delete', async function(ctx) { const models = ctx.Model.app.models; const definition = ctx.Model.definition; @@ -193,124 +321,6 @@ module.exports = function(Self) { return result; } - async function logInModel(ctx, loopBackContext) { - const models = ctx.Model.app.models; - const definition = ctx.Model.definition; - const Model = models[definition.name]; - const settings = ctx.Model.definition.settings; - const relations = ctx.Model.relations; - - let opts = ctx.options; - if (!opts) opts = ctx.options = {}; - - // Check for transactions - let tx; - if (opts.tx === true) tx = opts.transaction; - - try { - 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 && !settings.log.relation) { - originId = ctx.instance.id; - changedModelId = ctx.instance.id; - } else if (settings.log.relation) { - primaryKey = relations[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 = settings.log.showField; - let where; - if (showField && (!ctx.instance || !ctx.instance[showField]) && ctx.where) { - changedModelId = []; - where = []; - let changedInstances = await models[definition.name].find({ - where: ctx.where, - fields: ['id', showField, primaryKey] - }, opts); - - 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; - - const action = setActionType(ctx); - - removeUnloggable(definition, oldInstance); - removeUnloggable(definition, newInstance); - - oldInstance = await fkToValue(oldInstance, ctx); - newInstance = await fkToValue(newInstance, ctx); - - // 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 - }; - - const logsToSave = setLogsToSave(where, changedModelId, logRecord, ctx); - const logModel = settings.log.model; - await models[logModel].create(logsToSave, opts); - - // Check if grabUser is active - if (settings.log && settings.log.grabUser) await Model.rawSql(`CALL account.myUser_logout()`, null, opts); - if (tx) { - await tx.commit(); - delete opts.transaction; - delete opts.tx; - } - } catch (e) { - if (tx) await tx.rollback(); - throw e; - } - } - /** * Removes unwanted properties * @param {*} definition Model definition