diff --git a/packages/functions/src/async.js b/packages/functions/src/async.js
index 9e29e6a..41f8085 100644
--- a/packages/functions/src/async.js
+++ b/packages/functions/src/async.js
@@ -6,25 +6,25 @@ const logger = moduleLogger("tracer");
const _asyncHandler =
(fn, trace = false) =>
- async (req, res, next) => {
- let fnName;
- try {
- if (trace) {
- fnName = _fnName(fn);
- await _traced(fn.bind(this, req, res, next), {}, fnName);
- } else {
- await fn(req, res, next);
- }
- if (!res.headersSent) next();
- } catch (err) {
- if (!trace) {
- fnName = fnName ?? _fnName(fn);
- logger.error(`${fnName} execution failed - error: ${err.message} - stack: ${err.stack}`);
- }
- res.errorLogged = true;
- if (!res.headersSent) next(err);
+ async (req, res, next) => {
+ let fnName;
+ try {
+ if (trace) {
+ fnName = _fnName(fn);
+ await _traced(fn.bind(this, req, res, next), {}, fnName);
+ } else {
+ await fn(req, res, next);
}
- };
+ if (!res.headersSent) next();
+ } catch (err) {
+ if (!trace) {
+ fnName = fnName ?? _fnName(fn);
+ logger.error(`${fnName} execution failed - error: ${err.message} - stack: ${err.stack}`);
+ }
+ res.errorLogged = true;
+ if (!res.headersSent) next(err);
+ }
+ };
export const asyncHandler = (fn) => _asyncHandler(fn);
@@ -42,16 +42,16 @@ export const fallibleAsyncHandler = (fn) => async (req, res, next) => {
export const plainAsyncHandler = (fn) => async (req, res, next) => {
try {
- const result = fn(req, res, next)
+ const result = fn(req, res, next);
if (result instanceof Promise) await result;
} catch (e) {
- next(e)
+ next(e);
}
-}
+};
export default {
asyncHandler,
tracedAsyncHandler,
fallibleAsyncHandler,
- plainAsyncHandler,
+ plainAsyncHandler
};
diff --git a/packages/functions/src/traced.js b/packages/functions/src/traced.js
index b6f8e31..ba95b85 100644
--- a/packages/functions/src/traced.js
+++ b/packages/functions/src/traced.js
@@ -7,7 +7,8 @@ const logger = moduleLogger("tracer");
export const _traced = (fn, loggable = {}, fnName, layer, fallible) => {
let startTime;
- const disableTracing = process.env.DISABLE_FUNCTION_TRACING === "true" || process.env.DISABLE_FUNCTION_TRACING === "1";
+ const disableTracing =
+ process.env.DISABLE_FUNCTION_TRACING === "true" || process.env.DISABLE_FUNCTION_TRACING === "1";
if (!disableTracing) {
fnName = fnName ?? _fnName(fn, layer);
logger.info(`${fnName} execution initiated`, loggable);
diff --git a/packages/functions/test/async.test.js b/packages/functions/test/async.test.js
index 1838092..6ec8f16 100644
--- a/packages/functions/test/async.test.js
+++ b/packages/functions/test/async.test.js
@@ -44,13 +44,13 @@ describe("asyncHandler", () => {
});
test("test plain async handler with async function", async () => {
await plainAsyncHandler(async () => {
- throw new Error("test")
+ throw new Error("test");
})(mockReq, mockRes, mockNext);
expect(mockNext).toHaveBeenCalled();
});
test("test plain async handler with normal function", async () => {
await plainAsyncHandler(() => {
- throw new Error("test")
+ throw new Error("test");
})(mockReq, mockRes, mockNext);
expect(mockNext).toHaveBeenCalled();
});
diff --git a/plugins/mongoose-audit/package.json b/plugins/mongoose-audit/package.json
new file mode 100644
index 0000000..5e8f602
--- /dev/null
+++ b/plugins/mongoose-audit/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "@sliit-foss/mongoose-audit",
+ "version": "0.0.0",
+ "description": "A rework of the mongoose-audit-log package to support newer versions of mongoose and more flexible options",
+ "main": "dist/index.js",
+ "types": "types/index.d.ts",
+ "scripts": {
+ "build": "node ../../scripts/esbuild.config.js",
+ "build:watch": "bash ../../scripts/esbuild.watch.sh",
+ "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"
+ },
+ "dependencies": {
+ "deep-diff": "^1.0.2"
+ },
+ "peerDependencies": {
+ "mongoose": ">=5"
+ },
+ "author": "SLIIT FOSS",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/sliit-foss/npm-catalogue.git"
+ },
+ "homepage": "https://github.com/sliit-foss/npm-catalogue/blob/main/plugins/mongoose-audit/readme.md",
+ "keywords": [
+ "mongoose",
+ "mongoose-plugin",
+ "audit",
+ "log",
+ "trail",
+ "history",
+ "version"
+ ],
+ "bugs": {
+ "url": "https://github.com/sliit-foss/npm-catalogue/issues"
+ }
+}
\ No newline at end of file
diff --git a/plugins/mongoose-audit/readme.md b/plugins/mongoose-audit/readme.md
new file mode 100644
index 0000000..90e09b9
--- /dev/null
+++ b/plugins/mongoose-audit/readme.md
@@ -0,0 +1,66 @@
+# @sliit-foss/mongoose-audit
+
+#### 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
+
+It is a mongoose plugin to manage an audit log of changes to a MongoDB database.
+
+## Features
+
+- Store changes to entities on persist (save, update, delete)
+- Remember the user, that executed the change
+- Log when the change has been done
+
+## Storing the current user
+
+In order to collect the information about who actually did a change to an entity, the user information is required.
+This can be set on a per usage (1) or global (2) level:
+
+1. Set the current user on an entity right before persisting:
+
+```javascript
+Order.findById(123)
+ .then((order) => {
+ order.__user = "me@test.de";
+ order.amount = 1000;
+ })
+ .save();
+```
+
+2. Set it as an option when registering the plugin:
+
+```javascript
+const { plugin } = require("@sliit-foss/mongoose-audit");
+
+SomeSchema.plugin(plugin, {
+ getUser: () => "user details from wherever you wish to get it"
+});
+```
+
+## Query history
+
+Please find below an example express route, to request the history of a given type and id:
+
+```javascript
+const { plugin, Audit } = require("@sliit-foss/mongoose-audit");
+
+router.get("/api/users/:id/history", (req, res, next) => {
+ Audit.find({ entity_id: req.params.id, entity: "User" })
+ .then((history) => res.json(history))
+ .catch(next);
+});
+```
+
+## All supported plugin options
+
+```javascript
+const { plugin, AuditType } = require("@sliit-foss/mongoose-audit");
+
+SomeSchema.plugin(plugin, {
+ getUser: () => "user details from wherever you wish to get it",
+ types: [AuditType.Edit], // default: ['add', 'edit', 'delete']
+ exclude: ["field1", "field2"],
+ onAudit: (audit) => {
+ // Called before persisting the audit is saved. Use this to use your own audit model instead of the default one.
+ }
+});
+```
diff --git a/plugins/mongoose-audit/src/constants.js b/plugins/mongoose-audit/src/constants.js
new file mode 100644
index 0000000..53eb443
--- /dev/null
+++ b/plugins/mongoose-audit/src/constants.js
@@ -0,0 +1,5 @@
+export const AuditType = {
+ Add: "Add",
+ Edit: "Edit",
+ Delete: "Delete"
+};
diff --git a/plugins/mongoose-audit/src/index.js b/plugins/mongoose-audit/src/index.js
new file mode 100644
index 0000000..ebc8223
--- /dev/null
+++ b/plugins/mongoose-audit/src/index.js
@@ -0,0 +1,11 @@
+import { default as plugin } from "./plugin";
+import { default as Audit } from "./model";
+import { AuditType } from "./constants";
+
+export { plugin, Audit, AuditType as auditType };
+
+export default {
+ plugin,
+ Audit,
+ auditType: AuditType
+};
diff --git a/plugins/mongoose-audit/src/model.js b/plugins/mongoose-audit/src/model.js
new file mode 100644
index 0000000..baf6cec
--- /dev/null
+++ b/plugins/mongoose-audit/src/model.js
@@ -0,0 +1,25 @@
+const mongoose = require("mongoose");
+
+const auditSchema = new mongoose.Schema(
+ {
+ entity_id: {},
+ entity: String,
+ collection: String,
+ changes: {},
+ user: {
+ type: mongoose.Schema.Types.Mixed,
+ ref: "User",
+ collection: "users"
+ }
+ },
+ {
+ timestamps: {
+ createdAt: "created_at",
+ updatedAt: false
+ }
+ }
+);
+
+const model = mongoose.model("Audit", auditSchema);
+
+export default model;
diff --git a/plugins/mongoose-audit/src/plugin.js b/plugins/mongoose-audit/src/plugin.js
new file mode 100644
index 0000000..2b0bbd1
--- /dev/null
+++ b/plugins/mongoose-audit/src/plugin.js
@@ -0,0 +1,178 @@
+import { default as deepDiff } from "deep-diff";
+import { default as Audit } from "./model";
+import { AuditType } from "./constants";
+import { extractArray, filter, flattenObject, isEmpty } from "./utils";
+
+const options = {
+ getUser: () => undefined,
+ types: [AuditType.Add, AuditType.Edit, AuditType.delete],
+ exclude: [],
+ onAudit: undefined
+};
+
+const addAuditLogObject = (currentObject, original) => {
+ 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) {
+ 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 (!obj[key] && change.path.length) {
+ const data = {
+ from: extractArray(original, change.path),
+ to: extractArray(currentObject, change.path)
+ };
+ if (data.from.length && data.to.length) {
+ data.type = AuditType.Edit;
+ } else if (data.from.length) {
+ data.type = AuditType.delete;
+ } else if (data.to.length) {
+ data.type = AuditType.Add;
+ }
+ obj[key] = data;
+ }
+ } else {
+ obj[key] = {
+ from: change.lhs,
+ to: change.rhs,
+ type: AuditType.Edit
+ };
+ }
+ return obj;
+ }, {});
+ if (isEmpty(changes)) return Promise.resolve();
+ const audit = {
+ entity_id: currentObject._id,
+ entity: currentObject.constructor.modelName,
+ collection: currentObject.constructor.collection.collectionName,
+ changes,
+ user
+ };
+ if (options.onAudit) {
+ return options.onAudit(audit);
+ }
+ return new Audit(audit).save();
+ }
+ return Promise.resolve();
+};
+
+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 = (currentObject, next) => {
+ currentObject.constructor
+ .findOne({ _id: currentObject._id })
+ .then((original) => addAuditLogObject(currentObject, original))
+ .then(next)
+ .catch(next);
+};
+
+const addUpdate = (query, next, 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 addDelete = (currentObject, options, next) => {
+ const orig = Object.assign({}, currentObject._doc || currentObject);
+ orig.constructor.modelName = currentObject.constructor.modelName;
+ return addAuditLogObject(
+ {
+ _id: currentObject._id,
+ __user: options.__user
+ },
+ orig
+ )
+ .then(next)
+ .catch(next);
+};
+
+const addFindAndDelete = (query, next) => {
+ query
+ .find()
+ .lean(true)
+ .cursor()
+ .eachAsync((fromDb) => addDelete(fromDb, query.options, next))
+ .then(next)
+ .catch(next);
+};
+
+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);
+ });
+ }
+ if (options.types.includes(AuditType.Edit)) {
+ schema.pre("update", function (next) {
+ addUpdate(this, next, !!this.options.multi);
+ });
+ schema.pre("updateOne", function (next) {
+ addUpdate(this, next, false);
+ });
+ schema.pre("findOneAndUpdate", function (next) {
+ addUpdate(this, next, false);
+ });
+ schema.pre("updateMany", function (next) {
+ addUpdate(this, next, true);
+ });
+ schema.pre("replaceOne", function (next) {
+ addUpdate(this, next, false);
+ });
+ }
+ if (options.types.includes(AuditType.delete)) {
+ schema.pre("remove", function (next, options) {
+ addDelete(this, options, next);
+ });
+ schema.pre("findOneAndDelete", function (next) {
+ addFindAndDelete(this, next);
+ });
+ schema.pre("findOneAndRemove", function (next) {
+ addFindAndDelete(this, next);
+ });
+ }
+};
+
+export default plugin;
diff --git a/plugins/mongoose-audit/src/utils.js b/plugins/mongoose-audit/src/utils.js
new file mode 100644
index 0000000..4311802
--- /dev/null
+++ b/plugins/mongoose-audit/src/utils.js
@@ -0,0 +1,29 @@
+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]];
+ }
+ const parts = [].concat(path);
+ const last = parts.pop();
+ const value = parts.reduce((current, part) => {
+ return current ? current[part] : undefined;
+ }, data);
+ return value ? value[last] : undefined;
+};
+
+export const flattenObject = (obj) =>
+ Object.keys(obj).reduce((data, key) => {
+ if (key.indexOf("$") === 0) {
+ Object.assign(data, obj[key]);
+ } else {
+ data[key] = obj[key];
+ }
+ return data;
+ }, {});
diff --git a/plugins/mongoose-audit/test/index.test.js b/plugins/mongoose-audit/test/index.test.js
new file mode 100644
index 0000000..7cb1b1d
--- /dev/null
+++ b/plugins/mongoose-audit/test/index.test.js
@@ -0,0 +1,821 @@
+import { default as mongoose } from "mongoose";
+import { exec } from "child_process";
+import { promisify } from "util";
+import { plugin, Audit } from "../src";
+import { AuditType } from "../src/constants";
+
+const execute = promisify(exec);
+
+const setupMongoServer = async (done) => {
+ await execute("docker run -d -p 27017:27017 --name audit-test mongo:4.2.8");
+ mongoose.connect("mongodb://localhost:27018/audit-test", { useNewUrlParser: true }).then(done).catch(done);
+};
+
+const createTestModel = (getUser) => {
+ const testSchema = new mongoose.Schema(
+ {
+ name: String,
+ number: Number,
+ date: {
+ type: Date,
+ default: Date.now()
+ },
+ empty: String,
+ child: {
+ name: String,
+ number: Number
+ },
+ entity: {
+ _id: String,
+ id: String,
+ name: String,
+ array: []
+ }
+ },
+ { timestamps: true }
+ );
+ testSchema.plugin(plugin, getUser);
+ return mongoose.model("tests", testSchema);
+};
+
+describe("audit", function () {
+ before(setupMongoServer);
+ afterEach(function (done) {
+ Promise.all([
+ mongoose.connection.collections[TestObject.collection.collectionName].drop(),
+ mongoose.connection.collections[Audit.collection.collectionName].drop()
+ ])
+ .then(() => done())
+ .catch((err) => done(err.code !== 26 && err));
+ });
+
+ const TestObject = createTestModel();
+
+ it("should return undefined on getUser", function () {
+ expect(plugin.getUser()).to.be.undefined();
+ });
+
+ describe("plugin: pre *", function () {
+ const auditUser = "Jack";
+
+ let test;
+ beforeEach((done) => {
+ test = new TestObject({ name: "Lucky", number: 7 });
+ test
+ .save()
+ .then(() => done())
+ .catch(done);
+ });
+
+ it("should create single values for changes on siblings (non-entities)", function () {
+ const expectedName = "test";
+ const expectedNumber = 123;
+ test.child = { name: expectedName, number: expectedNumber };
+ test.__user = auditUser;
+ return test.save().then(() =>
+ Audit.find({ entity_id: test._id }, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(1);
+ const entry = audit[0];
+ expect(entry.changes["child.name"].to).equal(expectedName);
+ expect(entry.changes["child.name"].type).equal(AuditType.Add);
+ expect(entry.changes["child.number"].to).equal(expectedNumber);
+ expect(entry.changes["child.number"].type).equal(AuditType.Add);
+ })
+ );
+ });
+
+ it("should create single values for changes of siblings (non-entities) on remove", function () {
+ const expectedName = "test";
+ const expectedNumber = 123;
+ test.child = { name: expectedName, number: expectedNumber };
+ test.__user = auditUser;
+ return test.save().then((result) => {
+ result.child = undefined;
+ result.__user = auditUser;
+ return result.save().then(() =>
+ Audit.find({ entity_id: test._id }, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(2);
+ const entry = audit[1];
+ expect(entry.changes["child.name"].from).equal(expectedName);
+ expect(entry.changes["child.name"].to).to.be.undefined();
+ expect(entry.changes["child.name"].type).equal(AuditType.Delete);
+ expect(entry.changes["child.number"].from).equal(expectedNumber);
+ expect(entry.changes["child.number"].to).to.be.undefined();
+ expect(entry.changes["child.number"].type).equal(AuditType.Delete);
+ })
+ );
+ });
+ });
+
+ it('should create combined value for changes on entities (with "_id"-field)', function () {
+ const expectedId = "123";
+ const expectedName = "test";
+ test.entity = { name: expectedName, _id: expectedId };
+ test.__user = auditUser;
+ return test.save().then(() =>
+ Audit.find({ entity_id: test._id }, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(1);
+ const entry = audit[0];
+ expect(entry.changes["entity._id"].to).equal(expectedId);
+ expect(entry.changes["entity.name"].to).equal(expectedName);
+ expect(entry.changes["entity._id"].type).equal(AuditType.Add);
+ expect(entry.changes["entity.name"].type).equal(AuditType.Add);
+ })
+ );
+ });
+
+ it('should create combined value for changes on entities (with "id"-field)', function () {
+ const expectedId = "123";
+ const expectedName = "test";
+ test.entity = { name: expectedName, id: expectedId };
+ test.__user = auditUser;
+ return test.save().then(() =>
+ Audit.find({ entity_id: test._id }, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(1);
+ const entry = audit[0];
+ expect(entry.changes["entity.id"].to).equal(expectedId);
+ expect(entry.changes["entity.name"].to).equal(expectedName);
+ expect(entry.changes["entity.id"].type).equal(AuditType.Add);
+ expect(entry.changes["entity.name"].type).equal(AuditType.Add);
+ })
+ );
+ });
+
+ it('should create combined value for changes of entities (with "_id"-field) on remove', function () {
+ const expectedId = "123";
+ const expectedName = "test";
+ test.entity = { name: expectedName, _id: expectedId };
+ test.__user = auditUser;
+ return test.save().then((result) => {
+ result.entity = undefined;
+ result.__user = auditUser;
+ return result.save().then(() =>
+ Audit.find({ entity_id: test._id }, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(2);
+ const entry = audit[1];
+ expect(entry.changes.entity.from._id).equal(expectedId);
+ expect(entry.changes.entity.from.name).equal(expectedName);
+ expect(entry.changes.entity.to).to.be.undefined();
+ expect(entry.changes.entity.type).equal(AuditType.Delete);
+ })
+ );
+ });
+ });
+
+ it('should create combined value for changes of entities (with "id"-field) on remove', function () {
+ const expectedId = "123";
+ const expectedName = "test";
+ test.entity = { name: expectedName, id: expectedId };
+ test.__user = auditUser;
+ return test.save().then((result) => {
+ result.entity = undefined;
+ result.__user = auditUser;
+ return result.save().then(() =>
+ Audit.find({ entity_id: test._id }, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(2);
+ const entry = audit[1];
+ expect(entry.changes.entity.from.id).equal(expectedId);
+ expect(entry.changes.entity.from.name).equal(expectedName);
+ expect(entry.changes.entity.to).to.be.undefined();
+ expect(entry.changes.entity.type).equal("Delete");
+ })
+ );
+ });
+ });
+
+ it('should create type "Add" for adding values to arrays', function () {
+ const expectedValues = ["1", "2", "X"];
+ test.entity = { array: [].concat(expectedValues) };
+ test.__user = auditUser;
+ return test.save().then(() =>
+ Audit.find({ entity_id: test._id }, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(1);
+ const entry = audit[0];
+ expect(entry.changes["entity.array"].from.length).equal(0);
+ expect(entry.changes["entity.array"].to).to.have.members(expectedValues);
+ expect(entry.changes["entity.array"].type).equal("Add");
+ })
+ );
+ });
+
+ it('should create combined type "Edit" for adding values on arrays', function () {
+ const previousValues = ["1", "2", "X"];
+ const expectedValues = previousValues.concat(["Y"]);
+ test.entity = { array: [].concat(previousValues) };
+ test.__user = auditUser;
+ return test.save().then((filled) => {
+ filled.entity.array = [].concat(expectedValues);
+ filled.__user = auditUser;
+ return filled.save().then(() =>
+ Audit.find({ entity_id: test._id }, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(2);
+ const entry = audit[1];
+ expect(entry.changes["entity.array"].from).to.have.members(previousValues);
+ expect(entry.changes["entity.array"].to).to.have.members(expectedValues);
+ expect(entry.changes["entity.array"].type).equal("Edit");
+ })
+ );
+ });
+ });
+
+ it('should create combined type "Edit" for removing values on arrays', function () {
+ const previousValues = ["1", "2", "X"];
+ const expectedValues = ["1", "X"];
+ test.entity = { array: [].concat(previousValues) };
+ test.__user = auditUser;
+ return test.save().then((filled) => {
+ filled.entity.array = [].concat(expectedValues);
+ filled.__user = auditUser;
+ return filled.save().then(() =>
+ Audit.find({ entity_id: test._id }, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(2);
+ const entry = audit[1];
+ expect(entry.changes["entity.array"].from).to.have.members(previousValues);
+ expect(entry.changes["entity.array"].to).to.have.members(expectedValues);
+ expect(entry.changes["entity.array"].type).equal("Edit");
+ })
+ );
+ });
+ });
+
+ it('should create type "Delete" for removing all values from arrays', function () {
+ const previousValues = ["1", "2", "X"];
+ const expectedValues = [];
+ test.entity = { array: [].concat(previousValues) };
+ test.__user = auditUser;
+ return test.save().then((filled) => {
+ filled.entity.array = [].concat(expectedValues);
+ filled.__user = auditUser;
+ return filled.save().then(() =>
+ Audit.find({ entity_id: test._id }, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(2);
+ const entry = audit[1];
+ expect(entry.changes["entity.array"].from).to.have.members(previousValues);
+ expect(entry.changes["entity.array"].to).to.have.members(expectedValues);
+ expect(entry.changes["entity.array"].type).equal("Delete");
+ })
+ );
+ });
+ });
+
+ it('should create type "Add" for new values', function () {
+ test.empty = "test";
+ test.__user = auditUser;
+ return test.save().then(() =>
+ Audit.find({ entity_id: test._id }, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(1);
+ expect(audit[0].changes.empty.type).equal("Add");
+ })
+ );
+ });
+
+ it('should create type "Delete" when value is being removed', function () {
+ test.name = undefined;
+ test.__user = auditUser;
+ return test.save().then(() =>
+ Audit.find({ entity_id: test._id }, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(1);
+ expect(audit[0].changes.name.type).equal("Delete");
+ })
+ );
+ });
+
+ it("should create audit trail on save", function (done) {
+ const expectedName = "Unlucky";
+ const expectedNumber = 13;
+ test.name = expectedName;
+ test.number = expectedNumber;
+ test.__user = auditUser;
+ test.save().then(() => {
+ Audit.find({ entity_id: test._id }, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(1);
+ const entry = audit[0];
+ expect(entry.entity_id.toString()).equal(test._id.toString());
+ expect(Object.values(entry.changes).length).equal(2);
+ expect(entry.changes.name.from).equal("Lucky");
+ expect(entry.changes.name.to).equal(expectedName);
+ expect(entry.changes.number.from).equal(7);
+ expect(entry.changes.number.to).equal(expectedNumber);
+ expect(entry.user).equal(auditUser);
+ expect(entry.createdAt).not.null();
+ expect(entry.createdAt).not.undefined();
+ expect(entry.updatedAt).not.null();
+ expect(entry.updatedAt).not.undefined();
+ expect(entry.entity).equal(TestObject.modelName);
+ TestObject.find({}, function (err, items) {
+ expect(err).to.null();
+ expect(items.length).equal(1);
+ expect(items[0].number).equal(expectedNumber);
+ expect(items[0].name).equal(expectedName);
+ done();
+ });
+ });
+ });
+ });
+
+ it("should not create audit trail if nothing changed", function (done) {
+ test.__user = auditUser;
+ test.save().then(() => {
+ Audit.find({ entity_id: test._id }, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(0);
+ TestObject.find({}, function (err, items) {
+ expect(err).to.null();
+ expect(items.length).equal(1);
+ expect(items[0].number).equal(test.number);
+ expect(items[0].name).equal(test.name);
+ done();
+ });
+ });
+ });
+ });
+
+ it("should not create audit trail if only change is updatedAt", function (done) {
+ test.__user = auditUser;
+ test.updatedAt = Date.now();
+ test.save().then(() => {
+ Audit.find({ entity_id: test._id }, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(0);
+ TestObject.find({}, function (err, items) {
+ expect(err).to.null();
+ expect(items.length).equal(1);
+ expect(items[0].number).equal(test.number);
+ expect(items[0].name).equal(test.name);
+ done();
+ });
+ });
+ });
+ });
+
+ it("should create audit trail for update on class", function (done) {
+ const test2 = new TestObject({ name: "Unlucky", number: 13 });
+ const expected = 123;
+ test2
+ .save()
+ .then(() => TestObject.update({}, { number: expected }, { __user: auditUser, multi: true }))
+ .then(() => {
+ Audit.find({}, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(2);
+ expect(Object.values(audit[0].changes).length).equal(1);
+ expect(audit[0].entity_id.toString()).equal(test._id.toString());
+ expect(audit[0].changes.number.from).equal(test.number);
+ expect(audit[0].changes.number.to).equal(expected);
+ expect(audit[0].user).equal(auditUser);
+ expect(audit[0].entity).equal(TestObject.modelName);
+ expect(Object.values(audit[1].changes).length).equal(1);
+ expect(audit[1].entity_id.toString()).equal(test2._id.toString());
+ expect(audit[1].changes.number.from).equal(test2.number);
+ expect(audit[1].changes.number.to).equal(expected);
+ expect(audit[1].user).equal(auditUser);
+ expect(audit[1].entity).equal(TestObject.modelName);
+ TestObject.find({}, function (err, items) {
+ expect(err).to.null();
+ expect(items.length).equal(2);
+ expect(items[0].number).equal(expected);
+ expect(items[1].number).equal(expected);
+ done();
+ });
+ });
+ })
+ .catch(done);
+ });
+
+ it("should create audit trail for updateMany", function (done) {
+ const test2 = new TestObject({ name: "Unlucky", number: 13 });
+ const expected = 123;
+ test2
+ .save()
+ .then(() => TestObject.updateMany({}, { number: expected }, { __user: auditUser }))
+ .then(() => {
+ Audit.find({}, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(2);
+ expect(Object.values(audit[0].changes).length).equal(1);
+ expect(audit[0].entity_id.toString()).equal(test._id.toString());
+ expect(audit[0].changes.number.from).equal(test.number);
+ expect(audit[0].changes.number.to).equal(expected);
+ expect(audit[0].user).equal(auditUser);
+ expect(audit[0].entity).equal(TestObject.modelName);
+ expect(Object.values(audit[1].changes).length).equal(1);
+ expect(audit[1].entity_id.toString()).equal(test2._id.toString());
+ expect(audit[1].changes.number.from).equal(test2.number);
+ expect(audit[1].changes.number.to).equal(expected);
+ expect(audit[1].user).equal(auditUser);
+ expect(audit[1].entity).equal(TestObject.modelName);
+ TestObject.find({}, function (err, items) {
+ expect(err).to.null();
+ expect(items.length).equal(2);
+ expect(items[0].number).equal(expected);
+ expect(items[1].number).equal(expected);
+ done();
+ });
+ });
+ })
+ .catch(done);
+ });
+
+ it("should create audit trail for updateMany ignoring multi value", function (done) {
+ const test2 = new TestObject({ name: "Unlucky", number: 13 });
+ const expected = 123;
+ test2
+ .save()
+ .then(() => TestObject.updateMany({}, { number: expected }, { __user: auditUser, multi: false }))
+ .then(() => {
+ Audit.find({}, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(2);
+ expect(audit[0].entity_id.toString()).equal(test._id.toString());
+ expect(audit[1].entity_id.toString()).equal(test2._id.toString());
+ TestObject.find({}, function (err, items) {
+ expect(err).to.null();
+ expect(items.length).equal(2);
+ expect(items[0].number).equal(expected);
+ expect(items[1].number).equal(expected);
+ done();
+ });
+ });
+ })
+ .catch(done);
+ });
+
+ it("should create audit trail for update only for first elem if not multi", function (done) {
+ const test2 = new TestObject({ name: "Unlucky", number: 13 });
+ const expected = 123;
+ test2
+ .save()
+ .then(() => TestObject.update({}, { number: expected }, { __user: auditUser, multi: false }))
+ .then(() => {
+ Audit.find({}, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(1);
+ expect(Object.values(audit[0].changes).length).equal(1);
+ expect(audit[0].entity_id.toString()).equal(test._id.toString());
+ expect(audit[0].changes.number.from).equal(test.number);
+ expect(audit[0].changes.number.to).equal(expected);
+ expect(audit[0].user).equal(auditUser);
+ expect(audit[0].entity).equal(TestObject.modelName);
+ TestObject.find({}, function (err, items) {
+ expect(err).to.null();
+ expect(items.length).equal(2);
+ expect(items[0].number).equal(expected);
+ expect(items[1].number).equal(test2.number);
+ done();
+ });
+ });
+ })
+ .catch(done);
+ });
+
+ it("should create audit trail for update on instance", function (done) {
+ const test2 = new TestObject({ name: "Unlucky", number: 13 });
+ const expected = 123;
+ test2
+ .save()
+ .then(() => test.update({ number: expected }, { __user: auditUser }))
+ .then(() => {
+ Audit.find({}, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(1);
+ expect(Object.values(audit[0].changes).length).equal(1);
+ expect(audit[0].entity_id.toString()).equal(test._id.toString());
+ expect(audit[0].changes.number.from).equal(test.number);
+ expect(audit[0].changes.number.to).equal(expected);
+ expect(audit[0].user).equal(auditUser);
+ expect(audit[0].entity).equal(TestObject.modelName);
+ TestObject.find({}, function (err, items) {
+ expect(err).to.null();
+ expect(items.length).equal(2);
+ expect(items[0].number).equal(expected);
+ expect(items[1].number).equal(test2.number);
+ done();
+ });
+ });
+ })
+ .catch(done);
+ });
+
+ it("should create audit trail on update with $set", function (done) {
+ const test2 = new TestObject({ name: "Unlucky", number: 13 });
+ const expected = 123;
+ test2
+ .save()
+ .then(() => TestObject.update({}, { $set: { number: expected } }, { __user: auditUser }))
+ .then(() => {
+ Audit.find({}, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(1);
+ expect(audit[0].entity_id.toString()).equal(test._id.toString());
+ TestObject.find({}, function (err, items) {
+ expect(err).to.null();
+ expect(items.length).equal(2);
+ expect(items[0].number).equal(expected);
+ expect(items[1].number).equal(test2.number);
+ done();
+ });
+ });
+ })
+ .catch(done);
+ });
+
+ it("should create audit trail on update with $set if multi", function (done) {
+ const test2 = new TestObject({ name: "Unlucky", number: 13 });
+ const expected = 123;
+ test2
+ .save()
+ .then(() => TestObject.update({}, { $set: { number: expected } }, { multi: true, __user: auditUser }))
+ .then(() => {
+ Audit.find({}, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(2);
+ expect(audit[0].entity_id.toString()).equal(test._id.toString());
+ expect(audit[1].entity_id.toString()).equal(test2._id.toString());
+ TestObject.find({}, function (err, items) {
+ expect(err).to.null();
+ expect(items.length).equal(2);
+ expect(items[0].number).equal(expected);
+ expect(items[1].number).equal(expected);
+ done();
+ });
+ });
+ })
+ .catch(done);
+ });
+
+ it("should create audit trail on updateOne", function (done) {
+ const test2 = new TestObject({ name: "Unlucky", number: 13 });
+ const expected = 123;
+ test2
+ .save()
+ .then(() => TestObject.updateOne({ _id: test._id }, { number: expected }, { __user: auditUser }))
+ .then(() => {
+ Audit.find({}, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(1);
+ expect(Object.values(audit[0].changes).length).equal(1);
+ expect(audit[0].entity_id.toString()).equal(test._id.toString());
+ expect(audit[0].changes.number.from).equal(test.number);
+ expect(audit[0].changes.number.to).equal(expected);
+ expect(audit[0].user).equal(auditUser);
+ expect(audit[0].entity).equal(TestObject.modelName);
+ TestObject.find({}, function (err, items) {
+ expect(err).to.null();
+ expect(items.length).equal(2);
+ expect(items[0].number).equal(expected);
+ expect(items[1].number).equal(test2.number);
+ done();
+ });
+ });
+ })
+ .catch(done);
+ });
+
+ it("should create audit trail on findOneAndUpdate", function (done) {
+ const test2 = new TestObject({ name: "Unlucky", number: 13 });
+ const expected = 123;
+ test2
+ .save()
+ .then(() => TestObject.findOneAndUpdate({ _id: test._id }, { number: expected }, { __user: auditUser }))
+ .then(() => {
+ Audit.find({}, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(1);
+ expect(Object.values(audit[0].changes).length).equal(1);
+ expect(audit[0].entity_id.toString()).equal(test._id.toString());
+ expect(audit[0].changes.number.from).equal(test.number);
+ expect(audit[0].changes.number.to).equal(expected);
+ expect(audit[0].user).equal(auditUser);
+ expect(audit[0].entity).equal(TestObject.modelName);
+ TestObject.find({}, function (err, items) {
+ expect(err).to.null();
+ expect(items.length).equal(2);
+ expect(items[0].number).equal(expected);
+ expect(items[1].number).equal(test2.number);
+ done();
+ });
+ });
+ })
+ .catch(done);
+ });
+
+ it("should create audit trail on replaceOne", function (done) {
+ const test2 = new TestObject({ name: "Unlucky", number: 13 });
+ const expected = 123;
+ const replace = Object.assign({}, test._doc);
+ replace.number = expected;
+ replace.__v += 1;
+ test2
+ .save()
+ .then(() => TestObject.replaceOne({ _id: test._id }, replace, { __user: auditUser }))
+ .then(() => {
+ Audit.find({}, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(1);
+ expect(Object.values(audit[0].changes).length).equal(1);
+ expect(audit[0].entity_id.toString()).equal(test._id.toString());
+ expect(audit[0].changes.number.from).equal(test.number);
+ expect(audit[0].changes.number.to).equal(expected);
+ expect(audit[0].user).equal(auditUser);
+ expect(audit[0].entity).equal(TestObject.modelName);
+ TestObject.find({}, function (err, items) {
+ expect(err).to.null();
+ expect(items.length).equal(2);
+ expect(items[0].number).equal(expected);
+ expect(items[0].__v).equal(1);
+ expect(items[1].number).equal(test2.number);
+ done();
+ });
+ });
+ })
+ .catch(done);
+ });
+
+ const expectDeleteValues = (entry) => {
+ expect(Object.values(entry.changes).length).equal(3);
+ expect(entry.entity_id.toString()).equal(test._id.toString());
+ expect(entry.changes.date.type).equal("Delete");
+ expect(entry.changes.name.type).equal("Delete");
+ expect(entry.changes.number.type).equal("Delete");
+ expect(entry.changes.date.from).equal(test.date.toISOString());
+ expect(entry.changes.name.from).equal(test.name);
+ expect(entry.changes.number.from).equal(test.number);
+ expect(entry.user).equal(auditUser);
+ expect(entry.entity).equal(TestObject.modelName);
+ };
+
+ it("should create audit trail on remove", function (done) {
+ test
+ .remove({ __user: auditUser })
+ .then(() =>
+ Audit.find({}, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(1);
+ expectDeleteValues(audit[0]);
+ TestObject.find({}, function (err, items) {
+ expect(err).to.null();
+ expect(items.length).equal(0);
+ done();
+ });
+ })
+ )
+ .catch(done);
+ });
+
+ it("should create audit trail on findOneAndDelete", function (done) {
+ TestObject.findOneAndDelete({ _id: test._id }, { __user: auditUser })
+ .then(() =>
+ Audit.find({}, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(1);
+ expectDeleteValues(audit[0]);
+ TestObject.find({}, function (err, items) {
+ expect(err).to.null();
+ expect(items.length).equal(0);
+ done();
+ });
+ })
+ )
+ .catch(done);
+ });
+
+ it("should create audit trail on findOneAndDelete only for one item", function (done) {
+ const test2 = new TestObject({ name: "Unlucky", number: 13 });
+ test2
+ .save()
+ .then(() => TestObject.findOneAndDelete({ _id: test._id }, { __user: auditUser }))
+ .then(() =>
+ Audit.find({}, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(1);
+ expectDeleteValues(audit[0]);
+ TestObject.find({}, function (err, items) {
+ expect(err).to.null();
+ expect(items.length).equal(1);
+ expect(items[0]._id.toString()).equal(test2._id.toString());
+ done();
+ });
+ })
+ )
+ .catch(done);
+ });
+
+ it("should create audit trail on findOneAndRemove", function (done) {
+ TestObject.findOneAndRemove({ _id: test._id }, { __user: auditUser })
+ .then(() =>
+ Audit.find({}, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(1);
+ expectDeleteValues(audit[0]);
+ TestObject.find({}, function (err, items) {
+ expect(err).to.null();
+ expect(items.length).equal(0);
+ done();
+ });
+ })
+ )
+ .catch(done);
+ });
+
+ it("should create audit trail on findOneAndRemove only for one item", function (done) {
+ const test2 = new TestObject({ name: "Unlucky", number: 13 });
+ test2
+ .save()
+ .then(() => TestObject.findOneAndRemove({ _id: test._id }, { __user: auditUser }))
+ .then(() =>
+ Audit.find({}, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(1);
+ expectDeleteValues(audit[0]);
+ TestObject.find({}, function (err, items) {
+ expect(err).to.null();
+ expect(items.length).equal(1);
+ expect(items[0]._id.toString()).equal(test2._id.toString());
+ done();
+ });
+ })
+ )
+ .catch(done);
+ });
+ });
+
+ describe("plugin: user callback", function () {
+ it("should use the user callback if provided", function (done) {
+ const expectedUser = "User from function";
+ plugin.getUser = () => expectedUser;
+
+ const test = new TestObject({ name: "Lucky", number: 7 });
+ test
+ .save()
+ .then((test) => {
+ test.name = "Unlucky";
+ test.number = 13;
+ test.save().then(() => {
+ Audit.find({ entity_id: test._id }, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(1);
+ expect(audit[0].user).equal(expectedUser);
+ done();
+ });
+ });
+ })
+ .catch(done);
+ });
+
+ it("should use the user if provided", function (done) {
+ const expectedUser = "User from parameter";
+ plugin.getUser = () => expectedUser;
+
+ const test = new TestObject({ name: "Lucky", number: 7 });
+ test
+ .save()
+ .then((test) => {
+ test.name = "Unlucky";
+ test.number = 13;
+ test.__user = "";
+ test.save().then(() => {
+ Audit.find({ entity_id: test._id }, function (err, audit) {
+ expect(err).to.null();
+ expect(audit.length).equal(1);
+ expect(audit[0].user).equal(expectedUser);
+ done();
+ });
+ });
+ })
+ .catch(done);
+ });
+
+ it("should throw error if no user is provided", function (done) {
+ plugin.getUser = () => undefined;
+
+ const test = new TestObject({ name: "Lucky", number: 7 });
+ test
+ .save()
+ .then((test) => {
+ test.name = "Unlucky";
+ test.number = 13;
+ test
+ .save()
+ .then((_) => done(new Error("should not have succeeded!")))
+ .catch((err) => {
+ expect(err.message).to.be.equal("User missing in audit log!");
+ done();
+ });
+ })
+ .catch(done);
+ });
+ });
+});
diff --git a/plugins/mongoose-audit/types/index.d.ts b/plugins/mongoose-audit/types/index.d.ts
new file mode 100644
index 0000000..fffbed2
--- /dev/null
+++ b/plugins/mongoose-audit/types/index.d.ts
@@ -0,0 +1,36 @@
+import mongoose from "mongoose";
+
+type auditType = "add" | "edit" | "delete";
+
+interface Audit {
+ entity_id: any;
+ entity: string;
+ collection: string;
+ changes: any;
+ user: any;
+ created_at: string;
+}
+
+interface PluginOptions {
+ /** The user extractor function to use. This probably will be fetching the current user from a context or something similar. */
+ getUser?: () => any;
+ /** The types of audit to record. */
+ types?: auditType[];
+ /** The fields to exclude from the audit. */
+ exclude?: string[];
+ /** Called before persisting the audit is saved. Use this to use your own audit model instead of the default one. */
+ onAudit?: (audit: Audit) => Promise;
+}
+
+/** Registers the plugin with the provided options. */
+export declare function plugin(schema: mongoose.Schema, options: PluginOptions): void;
+
+/**
+ * The Audit model.
+ */
+export const Audit: mongoose.Model;
+
+/**
+ * The audit type enum.
+ */
+export const AuditType: Record;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 047d0f4..c3adf56 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1,4 +1,4 @@
-lockfileVersion: '6.0'
+lockfileVersion: '6.1'
settings:
autoInstallPeers: true
@@ -218,6 +218,15 @@ importers:
specifier: ^2.27.5
version: 2.27.5(eslint@8.38.0)
+ plugins/mongoose-audit:
+ dependencies:
+ deep-diff:
+ specifier: ^1.0.2
+ version: 1.0.2
+ mongoose:
+ specifier: '>=5'
+ version: 5.0.0
+
packages:
/@actions/exec@1.1.1:
@@ -3327,6 +3336,12 @@ packages:
stack-chain: 1.3.7
dev: false
+ /async@2.1.4:
+ resolution: {integrity: sha512-ZAxi5cea9DNM37Ld7lIj7c8SmOVaK/ns1pTiNI8vnQbyGsS5WuL+ImnU5UVECiIw43wlx9Wnr9iXn7MJymXacA==}
+ dependencies:
+ lodash: 4.17.21
+ dev: false
+
/async@3.2.4:
resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==}
dev: false
@@ -3485,6 +3500,10 @@ packages:
readable-stream: 3.6.2
dev: false
+ /bluebird@3.5.0:
+ resolution: {integrity: sha512-3LE8m8bqjGdoxfvf71yhFNrUcwy3NLy00SAo+b6MfJ8l+Bc2DzQ7mUHwX6pjK2AxfgV+YfsjCeVW3T5HLQTBsQ==}
+ dev: false
+
/body-parser@1.18.2:
resolution: {integrity: sha512-XIXhPptoLGNcvFyyOzjNXCjDYIbYj4iuXO0VU9lM0f3kYdG0ar5yg7C+pIc3OyoTlZXDu5ObpLTmS2Cgp89oDg==}
engines: {node: '>= 0.8'}
@@ -3537,6 +3556,12 @@ packages:
node-int64: 0.4.0
dev: false
+ /bson@1.0.9:
+ resolution: {integrity: sha512-IQX9/h7WdMBIW/q/++tGd+emQr0XMdeZ6icnT/74Xk9fnabWn+gZgpE+9V+gujL3hhJOoNrnDVY7tWdzc7NUTg==}
+ engines: {node: '>=0.6.19'}
+ deprecated: Fixed a critical issue with BSON serialization documented in CVE-2019-2391, see https://bit.ly/2KcpXdo for more details
+ dev: false
+
/buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
dev: false
@@ -3946,7 +3971,6 @@ packages:
optional: true
dependencies:
ms: 2.0.0
- dev: true
/debug@3.2.7(supports-color@5.5.0):
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
@@ -3988,6 +4012,10 @@ packages:
resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==}
dev: false
+ /deep-diff@1.0.2:
+ resolution: {integrity: sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==}
+ dev: false
+
/deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
dev: true
@@ -6107,6 +6135,10 @@ packages:
setimmediate: 1.0.5
dev: false
+ /kareem@2.0.1:
+ resolution: {integrity: sha512-SsR+TZe595qXYzbWS5KWHBt4mM5h1MA7HFXp3oZnPkunxjaymx0fKhB8cxl6/R7Qm8aFXnI6J7DnyxV/QUSKLA==}
+ dev: false
+
/kind-of@6.0.3:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
@@ -6422,9 +6454,64 @@ packages:
resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==}
dev: false
+ /mongodb-core@3.0.1:
+ resolution: {integrity: sha512-aEy7iaynWVkydrkE9vtRffQ0RZiETsbhmqo6p5dIyB3sin3SrKXJruj16bxi87chF++S2QLnUyQ1i+dZn4GdJw==}
+ dependencies:
+ bson: 1.0.9
+ require_optional: 1.0.1
+ dev: false
+
+ /mongodb@3.0.1:
+ resolution: {integrity: sha512-JaPqe+qN1f0m6bLzpNsp4tsNNasPa6+G3CW/rUtJUtFIxaGf9cD+FD8TTe8cqk1uWWhTWl7kVZoaUdvkoOKE5w==}
+ engines: {node: '>=4'}
+ dependencies:
+ mongodb-core: 3.0.1
+ dev: false
+
+ /mongoose-legacy-pluralize@1.0.1(mongoose@5.0.0):
+ resolution: {integrity: sha512-X5/N3sNj1p+y7Bg1vouQdST1vkInEzNAwqVjfDpNrhnugih2p2rV7jLrrb71sbQUPMJPm0Hhe6rH5fQV1Ve4XQ==}
+ peerDependencies:
+ mongoose: '*'
+ dependencies:
+ mongoose: 5.0.0
+ dev: false
+
+ /mongoose@5.0.0:
+ resolution: {integrity: sha512-ciHZSJsy37SpUXotPmhPR4uVXG6YEUDVAjPmYO3g5n7JCGnPeczH9ipwyCfDCORyu6vic2AKY0TMYW0WIuRdFA==}
+ engines: {node: '>=4.0.0'}
+ dependencies:
+ async: 2.1.4
+ bson: 1.0.9
+ kareem: 2.0.1
+ lodash.get: 4.4.2
+ mongodb: 3.0.1
+ mongoose-legacy-pluralize: 1.0.1(mongoose@5.0.0)
+ mpath: 0.3.0
+ mquery: 3.0.0-rc0
+ ms: 2.0.0
+ regexp-clone: 0.0.1
+ sliced: 1.0.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /mpath@0.3.0:
+ resolution: {integrity: sha512-43/pbEQ9nmJ+lklKWsK5gtCf9QC5b9lYTJvGtTHXzyqWHHJ1BSkyvfYd7ICoPJB5CF+7yTnGBEIdCB2AqHpS3g==}
+ dev: false
+
+ /mquery@3.0.0-rc0:
+ resolution: {integrity: sha512-tEAVSvlmd22irKJ8Q/tyI0LKRv8cV3aEkQ/EHW391ktGRWDDlfcpZyq6GYqu8yXGoz2JkC4aMJdNGca19wU1NQ==}
+ dependencies:
+ bluebird: 3.5.0
+ debug: 2.6.9
+ regexp-clone: 0.0.1
+ sliced: 0.0.5
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
/ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
- dev: true
/ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
@@ -7081,6 +7168,10 @@ packages:
'@babel/runtime': 7.21.0
dev: false
+ /regexp-clone@0.0.1:
+ resolution: {integrity: sha512-tfYXF0HXEYh3AtgdjqNLQ8+tmZSAKIS7KtOjmB1laJgfbsi+Lf2RVNwLZVOE3U27yBXikzQuIXglLlakvb8Thw==}
+ dev: false
+
/regexp.prototype.flags@1.4.3:
resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==}
engines: {node: '>= 0.4'}
@@ -7127,6 +7218,13 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /require_optional@1.0.1:
+ resolution: {integrity: sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==}
+ dependencies:
+ resolve-from: 2.0.0
+ semver: 5.7.1
+ dev: false
+
/resolve-cwd@3.0.0:
resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==}
engines: {node: '>=8'}
@@ -7142,6 +7240,11 @@ packages:
global-modules: 1.0.0
dev: false
+ /resolve-from@2.0.0:
+ resolution: {integrity: sha512-qpFcKaXsq8+oRoLilkwyc7zHGF5i9Q2/25NIgLQQ/+VVv9rU4qvr6nXVAw1DsnXJyQkZsR4Ytfbtg5ehfcUssQ==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
/resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -7449,6 +7552,14 @@ packages:
engines: {node: '>=12'}
dev: false
+ /sliced@0.0.5:
+ resolution: {integrity: sha512-9bYT917D6H3+q8GlQBJmLVz3bc4OeVGfZ2BB12wvLnluTGfG6/8UdOUbKJDW1EEx9SZMDbjnatkau5/XcUeyOw==}
+ dev: false
+
+ /sliced@1.0.1:
+ resolution: {integrity: sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==}
+ dev: false
+
/snake-case@3.0.4:
resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
dependencies: