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: