// Application hooks that run for every service const fp= require('lodash/fp'); const os = require('os'); const { Conflict } = require('@feathersjs/errors'); const {iff} = require('feathers-hooks-common'); const jsonifyError = require("jsonify-error"); function debugError(context){ //https://docs.feathersjs.com/api/errors.html#error-handling if(context.app.get('verbose_errors')===false) return;//defaults to being on console.error(context.error.stack); console.error(fp.omit(['hook.service','hook.app'],context.error)); //jsonifyError.log(context.error); // console.error(context.error.code,context.error.message); } const { v4 } = require('uuid'); async function setLoggingInformation(context){ context.messages = []; //defensive: set here for internal service calls, which don't run middleware. Prefer global middleware (where we NEED to do it for the IP) so we trigger the time start sooner. if(!context.params.trace) context.params.trace=v4(); if(!context.params.timeStart) context.params.timeStart=new Date().toISOString(); return context; } const loggerService='logs';//extract so we never hit a loop async function saveRequest(context){ if(context.path==loggerService) return context;//IMPORTANT: avoid recursion from this service call! Logs all the way down! if(context.path=='authentication' && context.method=='create') return context;//login noise if(context.path=='users' && context.method=='get') return context;//probably login noise //trace, IP, timeStart I added with middleware //params.messages is an array we can push message into var data = fp.pick([ 'path','method','id','error' ,'messages'//things I added ,'data','result'// --BIG STUFF! ],context); if(data.method=='find') delete data.result;//don't log the actual results when there's so much data coming back if(data.error) data.error = jsonifyError(data.error);//errors don't coerce nicely to objects on their own. if(data.error && fp.has('error.enumerableFields.hook',data)){ delete data.error.enumerableFields.hook;//we saved the parts we wanted already. } if(context.path=='hotmobile-line' && fp.has('error.enumerableFields.data',data)){ data.error.data = data.error.enumerableFields.data; delete data.error.enumerableFields; } if(context.path=='webhook-hotmobile' || context.path=='hotmobile-historical'){ //very bulky, no need to save delete data.data; delete data.result; } if(data.result && typeof data.result=="string") data.result = JSON.stringify(data.result);//turn into JSON data.messages = data.messages.join('\n'); data = fp.assign(data,fp.pick(['trace','ip','timeStart','provider','query'],context.params)); if(context.params.user) {//shouldn't need `authenticated` - just check if there's a user ID data.user = fp.get('id',context.params.user); data.userEmail = fp.get('email',context.params.user); data.userPermissions = fp.get('permissions',context.params.user); } data['user-agent'] = fp.get('params.user-agent',context) || fp.get('params.headers.user-agent',context);//allow an internal service to set the user-agent, e.g. the script name data.duration = Date.now()-new Date(data.timeStart).getTime(); data.machine=os.hostname(); const dates = ['createdAt','orderPlaced','orderFulfilled']; //dates must be converted before comparison if(['update','patch'].includes(data.method) && context.params.before){//we have a stashBefore var updated = {}; for (const key in context.data) { if(key=='updatedAt') ; else if(dates.includes(key)){//need to compare something other than the raw Date object. if(context.params.before[key]==context.data[key]) ; //both the same, e.g. null else if ( (!context.params.before[key] && context.data[key]) //wasn't set but now it is || (new Date(context.params.before[key]).getTime()!=new Date(context.data[key]).getTime())//actual update ) { updated[key]={submitted: context.data[key], saved: context.params.before[key]}; } } else if(context.params.before[key]!=context.data[key]) { updated[key]={submitted: context.data[key], saved: context.params.before[key]}; } } data.data = updated; //console.log(updated); } //console.log(data); await context.app.service(loggerService).create(data,{query: {$noSelect:true}}).catch(console.error);//have to wait for scripts that automatically exit. don't try to query the actual input } const excludedPathsForStash=['hotmobile-line','new_password'] function safeForStash(context){ return !excludedPathsForStash.includes(context.path); } function checkIfUpdateIsSafe(context){ if(context.params.before.updatedAt!=context.data.updatedAt) { var updated = {}; for (const key in context.data) { if(key=='updatedAt') ; else if(context.params.before[key]!=context.data[key]) { updated[key]={submitted: context.data[key], saved: context.params.before[key]}; } } throw new Conflict(updated) } return context; } //standard stashBefore doesn't pass along full params. async function stashBeforeCustom(context){ return context.service.get(context.id, {trace: context.params.trace})//pass along user data and IP too? or just the trace so we know it's internal. .then(data => { context.params.before = data; return context; }) .catch(() => context); } module.exports = { before: { all: [setLoggingInformation], find: [], get: [], create: [], //checkIfUpdateIsSafe -- we're not properly returning the modifiedAt time, so we can't do subsequent updates update: [iff(safeForStash,stashBeforeCustom)], //stash the before so we can have better logs of what changed patch: [iff(safeForStash,stashBeforeCustom)], remove: [] }, after: { all: [saveRequest], find: [], get: [], create: [], update: [], patch: [], remove: [] }, error: { all: [saveRequest,debugError], find: [], get: [], create: [], update: [], patch: [], remove: [] } };