Skip to content

Commit

Permalink
Merge branch 'feat/mongoose-audit' into development
Browse files Browse the repository at this point in the history
  • Loading branch information
Akalanka47000 authored Mar 10, 2024
2 parents c588b1b + 2dcebcb commit f067ce3
Show file tree
Hide file tree
Showing 10 changed files with 498 additions and 857 deletions.
Empty file modified packages/templates/src/index.js
100644 → 100755
Empty file.
6 changes: 4 additions & 2 deletions plugins/mongoose-audit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
"bump-version": "bash ../../scripts/bump-version.sh --name=@sliit-foss/express-http-context",
"lint": "bash ../../scripts/lint.sh",
"release": "bash ../../scripts/release.sh",
"test": "if [ \"$CI\" = \"true\" ]; then \n bash ../../scripts/test/test.sh; else \n echo \"Skipping as it is not a CI environemnt\"; fi"
"test": "if [ \"$CI\" = \"true\" ]; then \n bash ../../scripts/test/test.sh; else \n echo \"Skipping as it is not a CI environment\"; fi"
},
"dependencies": {
"deep-diff": "^1.0.2"
"deep-diff": "1.0.2",
"dot-object": "2.1.4",
"lodash": "4.17.10"
},
"peerDependencies": {
"mongoose": ">=5"
Expand Down
2 changes: 2 additions & 0 deletions plugins/mongoose-audit/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

#### A rework of the [mongoose-audit-log](https://www.npmjs.com/package/mongoose-audit-log) package to support newer versions of mongoose and more flexible options<br>

#### !IMPORTANT - The behaviour of this is different from the original `mongoose-audit-log` package and cannot be considered as a drop in replacement for it.

It is a mongoose plugin to manage an audit log of changes to a MongoDB database.

## Features
Expand Down
7 changes: 7 additions & 0 deletions plugins/mongoose-audit/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,10 @@ export const AuditType = {
Edit: "Edit",
Delete: "Delete"
};

export const ChangeAuditType = {
N: AuditType.Add,
D: AuditType.Delete,
E: AuditType.Edit,
A: AuditType.Edit,
}
1 change: 0 additions & 1 deletion plugins/mongoose-audit/src/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const auditSchema = new mongoose.Schema(
{
entity_id: {},
entity: String,
collection: String,
changes: {},
user: {
type: mongoose.Schema.Types.Mixed,
Expand Down
180 changes: 80 additions & 100 deletions plugins/mongoose-audit/src/plugin.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
import { default as deepDiff } from "deep-diff";
import { dot } from "dot-object";
import { isEmpty, set } from "lodash"
import { default as Audit } from "./model";
import { AuditType } from "./constants";
import { extractArray, filter, flattenObject, isEmpty } from "./utils";
import { AuditType, ChangeAuditType } from "./constants";
import { extractArray, filter, flattenObject } from "./utils";

const options = {
getUser: () => undefined,
types: [AuditType.Add, AuditType.Edit, AuditType.delete],
exclude: [],
onAudit: undefined
onAudit: undefined,
background: true
};

const addAuditLogObject = (currentObject, original) => {
const user = currentObject.__user || options.getUser() || "Unknown User";
const user = currentObject.__user || options.getUser?.() || "Unknown User";
delete currentObject.__user;
let changes = deepDiff(original._doc || original, currentObject._doc || currentObject, filter);
if (changes && changes.length) {
let changes = deepDiff(JSON.parse(JSON.stringify(original ?? {})), JSON.parse(JSON.stringify(currentObject ?? {})), filter);
if (changes?.length) {
changes = changes.reduce((obj, change) => {
const key = change.path.join(".");
if (options.exclude.includes(key)) {
return obj;
}
if (change.kind === "D") {
handleAudits(change.lhs, "from", AuditType.delete, obj, key);
} else if (change.kind === "N") {
handleAudits(change.rhs, "to", AuditType.Add, obj, key);
} else if (change.kind === "A") {
if (options.exclude?.includes(key)) return obj;
if (change.kind === "A") {
if (!obj[key] && change.path.length) {
const data = {
from: extractArray(original, change.path),
Expand All @@ -33,26 +30,39 @@ const addAuditLogObject = (currentObject, original) => {
if (data.from.length && data.to.length) {
data.type = AuditType.Edit;
} else if (data.from.length) {
data.type = AuditType.delete;
data.type = AuditType.Delete;
} else if (data.to.length) {
data.type = AuditType.Add;
}
obj[key] = data;
set(obj, key, data)
}
} else if (typeof change.lhs === "object") {
Object.entries(dot(change.lhs)).forEach(([subKeys, value]) => {
set(obj, `${key}.${subKeys}`, {
from: value,
type: ChangeAuditType[change.kind]
})
})
} else if (typeof change.rhs === "object") {
Object.entries(dot(change.rhs)).forEach(([subKeys, value]) => {
set(obj, `${key}.${subKeys}`, {
to: value,
type: ChangeAuditType[change.kind]
})
})
} else {
obj[key] = {
set(obj, key, {
from: change.lhs,
to: change.rhs,
type: AuditType.Edit
};
type: ChangeAuditType[change.kind]
})
}
return obj;
return obj
}, {});
if (isEmpty(changes)) return Promise.resolve();
if (isEmpty(changes)) return;
const audit = {
entity_id: currentObject._id,
entity: currentObject.constructor.modelName,
collection: currentObject.constructor.collection.collectionName,
changes,
user
};
Expand All @@ -61,116 +71,86 @@ const addAuditLogObject = (currentObject, original) => {
}
return new Audit(audit).save();
}
return Promise.resolve();
return;
};

const handleAudits = (changes, target, type, obj, key) => {
if (typeof changes === "object") {
if (Object.keys(changes).filter((key) => key === "_id" || key === "id").length) {
// entity found
obj[key] = { [target]: changes, type };
} else {
// sibling/sub-object
Object.entries(changes).forEach(([sub, value]) => {
if (!isEmpty(value)) {
obj[`${key}.${sub}`] = { [target]: value, type };
}
});
}
} else {
// primitive value
obj[key] = { [target]: changes, type };
}
const addAuditLog = async (currentObject) => {
const original = await currentObject.constructor.findOne({ _id: currentObject._id }).lean()
const result = addAuditLogObject(currentObject, original)
/* istanbul ignore else */
if (!options.background) await result;
};

const addAuditLog = (currentObject, next) => {
currentObject.constructor
.findOne({ _id: currentObject._id })
.then((original) => addAuditLogObject(currentObject, original))
.then(next)
.catch(next);
};

const addUpdate = (query, next, multi) => {
const addUpdate = async (query, multi) => {
const updated = flattenObject(query._update);
let counter = 0;
return query
.find(query._conditions)
.lean(true)
.cursor()
.eachAsync((fromDb) => {
if (!multi && counter++) {
// handle 'multi: false'
return next();
}
const orig = Object.assign({ __user: query.options.__user }, fromDb, updated);
orig.constructor.modelName = query._collection.collectionName;
return addAuditLogObject(orig, fromDb);
})
.then(next)
.catch(next);
const originalDocs = await query.find(query._conditions).lean(true)
const promises = originalDocs.map((original) => {
if (!multi && counter++) {
return
}
const currentObject = Object.assign({ __user: query.options.__user }, original, updated);
currentObject.constructor.modelName = query.model.modelName;
return addAuditLogObject(currentObject, original);
})
/* istanbul ignore else */
if (!options.background) await Promise.allSettled(promises);
};

const addDelete = (currentObject, options, next) => {
const orig = Object.assign({}, currentObject._doc || currentObject);
orig.constructor.modelName = currentObject.constructor.modelName;
return addAuditLogObject(
const addDelete = async (currentObject, options) => {
const result = addAuditLogObject(
{
_id: currentObject._id,
__user: options.__user
},
orig
JSON.parse(JSON.stringify(currentObject))
)
.then(next)
.catch(next);
/* istanbul ignore else */
if (!options.background) await result;
};

const addFindAndDelete = (query, next) => {
query
.find()
.lean(true)
.cursor()
.eachAsync((fromDb) => addDelete(fromDb, query.options, next))
.then(next)
.catch(next);
const addFindAndDelete = async (query) => {
const originalDocs = await query.find().lean(true)
const promises = originalDocs.map((original) => {
return addDelete(original, query.options)
})
/* istanbul ignore else */
if (!options.background) await Promise.allSettled(promises);
};

const plugin = (schema, opts = {}) => {
Object.assign(options, opts);
if (options.types.includes(AuditType.Add)) {
schema.pre("save", function (next) {
if (this.isNew) {
return next();
}
addAuditLog(this, next);
schema.pre("save", function () {
return addAuditLog(this);
});
}
if (options.types.includes(AuditType.Edit)) {
schema.pre("update", function (next) {
addUpdate(this, next, !!this.options.multi);
schema.pre("update", function () {
return addUpdate(this, !!this.options.multi);
});
schema.pre("updateOne", function (next) {
addUpdate(this, next, false);
schema.pre("updateOne", function () {
return addUpdate(this, false);
});
schema.pre("findOneAndUpdate", function (next) {
addUpdate(this, next, false);
schema.pre("findOneAndUpdate", function () {
return addUpdate(this, false);
});
schema.pre("updateMany", function (next) {
addUpdate(this, next, true);
schema.pre("updateMany", function () {
return addUpdate(this, true);
});
schema.pre("replaceOne", function (next) {
addUpdate(this, next, false);
schema.pre("replaceOne", function () {
return addUpdate(this, false);
});
}
if (options.types.includes(AuditType.delete)) {
schema.pre("remove", function (next, options) {
addDelete(this, options, next);
schema.pre("remove", function (_, options) {
return addDelete(this, options);
});
schema.pre("findOneAndDelete", function (next) {
addFindAndDelete(this, next);
schema.pre("findOneAndDelete", function () {
return addFindAndDelete(this);
});
schema.pre("findOneAndRemove", function (next) {
addFindAndDelete(this, next);
schema.pre("findOneAndRemove", function () {
return addFindAndDelete(this);
});
}
};
Expand Down
6 changes: 0 additions & 6 deletions plugins/mongoose-audit/src/utils.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
export const filter = (path, key) => path.length === 0 && ~["_id", "__v", "createdAt", "updatedAt"].indexOf(key);

export const isEmpty = (value) =>
value === undefined ||
value === null ||
(typeof value === "object" && Object.keys(value).length === 0) ||
(typeof value === "string" && value.trim().length === 0);

export const extractArray = (data, path) => {
if (path.length === 1) {
return data[path[0]];
Expand Down
Loading

0 comments on commit f067ce3

Please sign in to comment.