ts-patch-mongoose 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +9 -6
  2. package/dist/cjs/helpers.js +12 -0
  3. package/dist/cjs/helpers.js.map +1 -0
  4. package/dist/cjs/hooks/delete-hooks.js +55 -0
  5. package/dist/cjs/hooks/delete-hooks.js.map +1 -0
  6. package/dist/cjs/hooks/save-hooks.js +28 -0
  7. package/dist/cjs/hooks/save-hooks.js.map +1 -0
  8. package/dist/cjs/hooks/update-hooks.js +89 -0
  9. package/dist/cjs/hooks/update-hooks.js.map +1 -0
  10. package/dist/cjs/plugin.js +26 -163
  11. package/dist/cjs/plugin.js.map +1 -1
  12. package/dist/cjs/types/helpers.d.ts +4 -0
  13. package/dist/cjs/types/helpers.d.ts.map +1 -0
  14. package/dist/cjs/types/hooks/delete-hooks.d.ts +28 -0
  15. package/dist/cjs/types/hooks/delete-hooks.d.ts.map +1 -0
  16. package/dist/cjs/types/hooks/save-hooks.d.ts +28 -0
  17. package/dist/cjs/types/hooks/save-hooks.d.ts.map +1 -0
  18. package/dist/cjs/types/hooks/update-hooks.d.ts +33 -0
  19. package/dist/cjs/types/hooks/update-hooks.d.ts.map +1 -0
  20. package/dist/cjs/types/models/History.d.ts +7 -1
  21. package/dist/cjs/types/models/History.d.ts.map +1 -1
  22. package/dist/cjs/types/plugin.d.ts +3 -1
  23. package/dist/cjs/types/plugin.d.ts.map +1 -1
  24. package/dist/cjs/types/version.d.ts +1 -0
  25. package/dist/cjs/types/version.d.ts.map +1 -1
  26. package/dist/cjs/version.js +2 -1
  27. package/dist/cjs/version.js.map +1 -1
  28. package/dist/esm/helpers.js +12 -0
  29. package/dist/esm/helpers.js.map +1 -0
  30. package/dist/esm/hooks/delete-hooks.js +55 -0
  31. package/dist/esm/hooks/delete-hooks.js.map +1 -0
  32. package/dist/esm/hooks/save-hooks.js +28 -0
  33. package/dist/esm/hooks/save-hooks.js.map +1 -0
  34. package/dist/esm/hooks/update-hooks.js +89 -0
  35. package/dist/esm/hooks/update-hooks.js.map +1 -0
  36. package/dist/esm/plugin.js.map +1 -1
  37. package/dist/esm/plugin.mjs +26 -163
  38. package/dist/esm/types/helpers.d.ts +4 -0
  39. package/dist/esm/types/helpers.d.ts.map +1 -0
  40. package/dist/esm/types/hooks/delete-hooks.d.ts +28 -0
  41. package/dist/esm/types/hooks/delete-hooks.d.ts.map +1 -0
  42. package/dist/esm/types/hooks/save-hooks.d.ts +28 -0
  43. package/dist/esm/types/hooks/save-hooks.d.ts.map +1 -0
  44. package/dist/esm/types/hooks/update-hooks.d.ts +33 -0
  45. package/dist/esm/types/hooks/update-hooks.d.ts.map +1 -0
  46. package/dist/esm/types/models/History.d.ts +7 -1
  47. package/dist/esm/types/models/History.d.ts.map +1 -1
  48. package/dist/esm/types/plugin.d.ts +3 -1
  49. package/dist/esm/types/plugin.d.ts.map +1 -1
  50. package/dist/esm/types/version.d.ts +1 -0
  51. package/dist/esm/types/version.d.ts.map +1 -1
  52. package/dist/esm/version.js +2 -1
  53. package/dist/esm/version.js.map +1 -1
  54. package/package.json +12 -12
  55. package/src/helpers.ts +10 -0
  56. package/src/hooks/delete-hooks.ts +59 -0
  57. package/src/hooks/save-hooks.ts +29 -0
  58. package/src/hooks/update-hooks.ts +100 -0
  59. package/src/plugin.ts +42 -194
  60. package/src/version.ts +1 -0
  61. package/tests/plugin-event-deleted.test.ts +13 -10
@@ -24,6 +24,12 @@
24
24
  /// <reference types="mongoose/types/inferschematype" />
25
25
  import { Schema } from 'mongoose';
26
26
  import type IHistory from '../interfaces/IHistory';
27
- declare const History: import("mongoose").Model<IHistory, {}, {}, {}, Schema<IHistory, import("mongoose").Model<IHistory, any, any, any, any>, {}, {}, {}, {}, import("mongoose").DefaultSchemaOptions, IHistory>>;
27
+ declare const History: import("mongoose").Model<IHistory, {}, {}, {}, import("mongoose").Document<unknown, {}, IHistory> & IHistory & {
28
+ _id: import("mongoose").Types.ObjectId;
29
+ }, Schema<IHistory, import("mongoose").Model<IHistory, any, any, any, import("mongoose").Document<unknown, any, IHistory> & IHistory & {
30
+ _id: import("mongoose").Types.ObjectId;
31
+ }, any>, {}, {}, {}, {}, import("mongoose").DefaultSchemaOptions, IHistory, import("mongoose").Document<unknown, {}, import("mongoose").FlatRecord<IHistory>> & import("mongoose").FlatRecord<IHistory> & {
32
+ _id: import("mongoose").Types.ObjectId;
33
+ }>>;
28
34
  export default History;
29
35
  //# sourceMappingURL=History.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"History.d.ts","sourceRoot":"","sources":["../../../../src/models/History.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAAA,OAAO,EAAE,MAAM,EAAS,MAAM,UAAU,CAAA;AAExC,OAAO,KAAK,QAAQ,MAAM,wBAAwB,CAAA;AA4ClD,QAAA,MAAM,OAAO,6LAA6C,CAAA;AAE1D,eAAe,OAAO,CAAA"}
1
+ {"version":3,"file":"History.d.ts","sourceRoot":"","sources":["../../../../src/models/History.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAAA,OAAO,EAAE,MAAM,EAAS,MAAM,UAAU,CAAA;AAExC,OAAO,KAAK,QAAQ,MAAM,wBAAwB,CAAA;AA4ClD,QAAA,MAAM,OAAO;;;;;;GAA6C,CAAA;AAE1D,eAAe,OAAO,CAAA"}
@@ -1,3 +1,4 @@
1
+ /// <reference types="node" />
1
2
  /// <reference types="mongoose/types/aggregate" />
2
3
  /// <reference types="mongoose/types/callback" />
3
4
  /// <reference types="mongoose/types/collection" />
@@ -25,6 +26,7 @@
25
26
  import type { Model, Schema } from 'mongoose';
26
27
  import type IPluginOptions from './interfaces/IPluginOptions';
27
28
  export declare const patchEventEmitter: {
29
+ [EventEmitter.captureRejectionSymbol]?(error: Error, event: string, ...args: any[]): void;
28
30
  addListener(eventName: string | symbol, listener: (...args: any[]) => void): any;
29
31
  on(eventName: string | symbol, listener: (...args: any[]) => void): any;
30
32
  once(eventName: string | symbol, listener: (...args: any[]) => void): any;
@@ -41,5 +43,5 @@ export declare const patchEventEmitter: {
41
43
  prependOnceListener(eventName: string | symbol, listener: (...args: any[]) => void): any;
42
44
  eventNames(): (string | symbol)[];
43
45
  };
44
- export declare const patchHistoryPlugin: <T>(schema: Schema<T, Model<T, any, any, any, any>, {}, {}, {}, {}, import("mongoose").DefaultSchemaOptions, import("mongoose").ObtainDocumentType<any, T, import("mongoose").DefaultSchemaOptions>>, opts: IPluginOptions<T>) => void;
46
+ export declare const patchHistoryPlugin: <T>(schema: Schema<T, Model<T, any, any, any, import("mongoose").IfAny<T, any, import("mongoose").Document<unknown, any, T> & import("mongoose").Require_id<T>>, any>, {}, {}, {}, {}, import("mongoose").DefaultSchemaOptions, import("mongoose").ObtainDocumentType<any, T, import("mongoose").ResolveSchemaOptions<import("mongoose").DefaultSchemaOptions>>, import("mongoose").IfAny<import("mongoose").FlatRecord<import("mongoose").ObtainDocumentType<any, T, import("mongoose").ResolveSchemaOptions<import("mongoose").DefaultSchemaOptions>>>, any, import("mongoose").Document<unknown, {}, import("mongoose").FlatRecord<import("mongoose").ObtainDocumentType<any, T, import("mongoose").ResolveSchemaOptions<import("mongoose").DefaultSchemaOptions>>>> & import("mongoose").Require_id<import("mongoose").FlatRecord<import("mongoose").ObtainDocumentType<any, T, import("mongoose").ResolveSchemaOptions<import("mongoose").DefaultSchemaOptions>>>>>>, opts: IPluginOptions<T>) => void;
45
47
  //# sourceMappingURL=plugin.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/plugin.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAGA,OAAO,KAAK,EAAoB,KAAK,EAAyC,MAAM,EAA+D,MAAM,UAAU,CAAA;AAEnK,OAAO,KAAK,cAAc,MAAM,6BAA6B,CAAA;AA0E7D,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;CAAK,CAAA;AAQnC,eAAO,MAAM,kBAAkB,oOAAoE,IA4IlG,CAAA"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/plugin.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAWA,OAAO,KAAK,EAAoB,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAC/D,OAAO,KAAK,cAAc,MAAM,6BAA6B,CAAA;AAQ7D,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;CAAK,CAAA;AAQnC,eAAO,MAAM,kBAAkB,y8BAAoE,IA+ClG,CAAA"}
@@ -1,3 +1,4 @@
1
+ export declare const isMongooseLessThan8: boolean;
1
2
  export declare const isMongooseLessThan7: boolean;
2
3
  export declare const isMongoose6: boolean;
3
4
  //# sourceMappingURL=version.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../../src/version.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,SAAoC,CAAA;AACpE,eAAO,MAAM,WAAW,SAAmC,CAAA"}
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../../src/version.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,SAAoC,CAAA;AACpE,eAAO,MAAM,mBAAmB,SAAoC,CAAA;AACpE,eAAO,MAAM,WAAW,SAAmC,CAAA"}
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isMongoose6 = exports.isMongooseLessThan7 = void 0;
3
+ exports.isMongoose6 = exports.isMongooseLessThan7 = exports.isMongooseLessThan8 = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const semver_1 = require("semver");
6
6
  const mongoose_1 = tslib_1.__importDefault(require("mongoose"));
7
+ exports.isMongooseLessThan8 = (0, semver_1.satisfies)(mongoose_1.default.version, '<8');
7
8
  exports.isMongooseLessThan7 = (0, semver_1.satisfies)(mongoose_1.default.version, '<7');
8
9
  exports.isMongoose6 = (0, semver_1.satisfies)(mongoose_1.default.version, '6');
9
10
  if (exports.isMongoose6) {
@@ -1 +1 @@
1
- {"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/version.ts"],"names":[],"mappings":";;;;AAAA,mCAAkC;AAClC,gEAA+B;AAElB,QAAA,mBAAmB,GAAG,IAAA,kBAAS,EAAC,kBAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;AACvD,QAAA,WAAW,GAAG,IAAA,kBAAS,EAAC,kBAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;AAE3D,IAAI,mBAAW,EAAE;IACf,kBAAQ,CAAC,GAAG,CAAC,aAAa,EAAE,KAAK,CAAC,CAAA;CACnC"}
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/version.ts"],"names":[],"mappings":";;;;AAAA,mCAAkC;AAClC,gEAA+B;AAElB,QAAA,mBAAmB,GAAG,IAAA,kBAAS,EAAC,kBAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;AACvD,QAAA,mBAAmB,GAAG,IAAA,kBAAS,EAAC,kBAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;AACvD,QAAA,WAAW,GAAG,IAAA,kBAAS,EAAC,kBAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;AAE3D,IAAI,mBAAW,EAAE;IACf,kBAAQ,CAAC,GAAG,CAAC,aAAa,EAAE,KAAK,CAAC,CAAA;CACnC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-patch-mongoose",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Patch history & events for mongoose models",
5
5
  "author": "Alex Eagle",
6
6
  "license": "MIT",
@@ -85,22 +85,22 @@
85
85
  "devDependencies": {
86
86
  "@shelf/jest-mongodb": "4.1.7",
87
87
  "@swc/cli": "0.1.62",
88
- "@swc/core": "1.3.91",
89
- "@swc/helpers": "0.5.2",
88
+ "@swc/core": "1.3.96",
89
+ "@swc/helpers": "0.5.3",
90
90
  "@swc/jest": "0.2.29",
91
91
  "@swc/register": "0.1.10",
92
- "@types/jest": "29.5.5",
93
- "@types/lodash": "4.14.199",
94
- "@types/node": "18",
95
- "@typescript-eslint/eslint-plugin": "6.7.3",
96
- "@typescript-eslint/parser": "6.7.3",
97
- "eslint": "8.50.0",
98
- "eslint-plugin-jest": "27.4.2",
92
+ "@types/jest": "29.5.7",
93
+ "@types/lodash": "4.14.200",
94
+ "@types/node": "20",
95
+ "@typescript-eslint/eslint-plugin": "6.9.1",
96
+ "@typescript-eslint/parser": "6.9.1",
97
+ "eslint": "8.53.0",
98
+ "eslint-plugin-jest": "27.6.0",
99
99
  "eslint-plugin-jest-formatting": "3.1.0",
100
- "eslint-plugin-sonarjs": "0.21.0",
100
+ "eslint-plugin-sonarjs": "0.23.0",
101
101
  "jest": "29.7.0",
102
102
  "merge": "2.1.1",
103
- "mongoose": "6.12.0",
103
+ "mongoose": "latest",
104
104
  "open-cli": "7.2.0",
105
105
  "ts-node": "10.9.1",
106
106
  "typescript": "5.2.2"
package/src/helpers.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { QueryOptions, ToObjectOptions } from 'mongoose'
2
+
3
+ export const isHookIgnored = <T>(options: QueryOptions<T>): boolean => {
4
+ return options.ignoreHook === true || (options.ignoreEvent === true && options.ignorePatchHistory === true)
5
+ }
6
+
7
+ export const toObjectOptions: ToObjectOptions = {
8
+ depopulate: true,
9
+ virtuals: false
10
+ }
@@ -0,0 +1,59 @@
1
+ import _ from 'lodash'
2
+
3
+ import { deletePatch } from '../patch'
4
+ import { isHookIgnored } from '../helpers'
5
+
6
+ import type { HydratedDocument, Model, MongooseQueryMiddleware, Schema } from 'mongoose'
7
+ import type IPluginOptions from '../interfaces/IPluginOptions'
8
+ import type IHookContext from '../interfaces/IHookContext'
9
+
10
+ const deleteMethods = [
11
+ 'remove',
12
+ 'findOneAndDelete',
13
+ 'findOneAndRemove',
14
+ 'findByIdAndDelete',
15
+ 'findByIdAndRemove',
16
+ 'deleteOne',
17
+ 'deleteMany'
18
+ ]
19
+
20
+ export const deleteHooksInitialize = <T>(schema: Schema<T>, opts: IPluginOptions<T>): void => {
21
+ schema.pre(deleteMethods as MongooseQueryMiddleware[], { document: false, query: true }, async function (this: IHookContext<T>) {
22
+ const options = this.getOptions()
23
+ if (isHookIgnored(options)) return
24
+
25
+ const model = this.model as Model<T>
26
+ const filter = this.getFilter()
27
+
28
+ this._context = {
29
+ op: this.op,
30
+ modelName: opts.modelName ?? this.model.modelName,
31
+ collectionName: opts.collectionName ?? this.model.collection.collectionName,
32
+ ignoreEvent: options.ignoreEvent as boolean,
33
+ ignorePatchHistory: options.ignorePatchHistory as boolean
34
+ }
35
+
36
+ if (['remove', 'deleteMany'].includes(this._context.op) && !options.single) {
37
+ const docs = await model.find(filter).lean().exec()
38
+ if (!_.isEmpty(docs)) {
39
+ this._context.deletedDocs = docs as HydratedDocument<T>[]
40
+ }
41
+ } else {
42
+ const doc = await model.findOne(filter).lean().exec()
43
+ if (!_.isEmpty(doc)) {
44
+ this._context.deletedDocs = [doc] as HydratedDocument<T>[]
45
+ }
46
+ }
47
+
48
+ if (opts.preDelete && _.isArray(this._context.deletedDocs) && !_.isEmpty(this._context.deletedDocs)) {
49
+ await opts.preDelete(this._context.deletedDocs)
50
+ }
51
+ })
52
+
53
+ schema.post(deleteMethods as MongooseQueryMiddleware[], { document: false, query: true }, async function (this: IHookContext<T>) {
54
+ const options = this.getOptions()
55
+ if (isHookIgnored(options)) return
56
+
57
+ await deletePatch(opts, this._context)
58
+ })
59
+ }
@@ -0,0 +1,29 @@
1
+ import { createPatch, updatePatch } from '../patch'
2
+ import { toObjectOptions } from '../helpers'
3
+
4
+ import type { HydratedDocument, Model, Schema } from 'mongoose'
5
+ import type IPluginOptions from '../interfaces/IPluginOptions'
6
+ import type IContext from '../interfaces/IContext'
7
+
8
+ export const saveHooksInitialize = <T>(schema: Schema<T>, opts: IPluginOptions<T>): void => {
9
+ schema.pre('save', async function () {
10
+ const current = this.toObject(toObjectOptions) as HydratedDocument<T>
11
+ const model = this.constructor as Model<T>
12
+
13
+ const context: IContext<T> = {
14
+ op: this.isNew ? 'create' : 'update',
15
+ modelName: opts.modelName ?? model.modelName,
16
+ collectionName: opts.collectionName ?? model.collection.collectionName,
17
+ createdDocs: [current]
18
+ }
19
+
20
+ if (this.isNew) {
21
+ await createPatch(opts, context)
22
+ } else {
23
+ const original = await model.findById(current._id).lean().exec()
24
+ if (original) {
25
+ await updatePatch(opts, context, current, original as HydratedDocument<T>)
26
+ }
27
+ }
28
+ })
29
+ }
@@ -0,0 +1,100 @@
1
+ import _ from 'lodash'
2
+ import { assign } from 'power-assign'
3
+
4
+ import { createPatch, updatePatch } from '../patch'
5
+ import { isHookIgnored } from '../helpers'
6
+
7
+ import type { HydratedDocument, Model, MongooseQueryMiddleware, Schema, UpdateQuery, UpdateWithAggregationPipeline } from 'mongoose'
8
+ import type IPluginOptions from '../interfaces/IPluginOptions'
9
+ import type IHookContext from '../interfaces/IHookContext'
10
+
11
+ const updateMethods = [
12
+ 'update',
13
+ 'updateOne',
14
+ 'replaceOne',
15
+ 'updateMany',
16
+ 'findOneAndUpdate',
17
+ 'findOneAndReplace',
18
+ 'findByIdAndUpdate'
19
+ ]
20
+
21
+ export const assignUpdate = <T>(document: HydratedDocument<T>, update: UpdateQuery<T>, commands: Record<string, unknown>[]): HydratedDocument<T> => {
22
+ let updated = assign(document, update)
23
+ _.forEach(commands, (command) => {
24
+ try {
25
+ updated = assign(updated, command)
26
+ } catch {
27
+ // we catch assign keys that are not implemented
28
+ }
29
+ })
30
+
31
+ return updated
32
+ }
33
+
34
+ export const splitUpdateAndCommands = <T>(updateQuery: UpdateWithAggregationPipeline | UpdateQuery<T> | null): { update: UpdateQuery<T>, commands: Record<string, unknown>[] } => {
35
+ let update: UpdateQuery<T> = {}
36
+ const commands: Record<string, unknown>[] = []
37
+
38
+ if (!_.isEmpty(updateQuery) && !_.isArray(updateQuery) && _.isObjectLike(updateQuery)) {
39
+ update = _.cloneDeep(updateQuery)
40
+ const keys = _.keys(update).filter((key) => key.startsWith('$'))
41
+ if (!_.isEmpty(keys)) {
42
+ _.forEach(keys, (key) => {
43
+ commands.push({ [key]: update[key] as unknown })
44
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
45
+ delete update[key]
46
+ })
47
+ }
48
+ }
49
+
50
+ return { update, commands }
51
+ }
52
+
53
+ export const updateHooksInitialize = <T>(schema: Schema<T>, opts: IPluginOptions<T>): void => {
54
+ schema.pre(updateMethods as MongooseQueryMiddleware[], async function (this: IHookContext<T>) {
55
+ const options = this.getOptions()
56
+ if (isHookIgnored(options)) return
57
+
58
+ const model = this.model as Model<T>
59
+ const filter = this.getFilter()
60
+ const count = await this.model.countDocuments(filter).exec()
61
+
62
+ this._context = {
63
+ op: this.op,
64
+ modelName: opts.modelName ?? this.model.modelName,
65
+ collectionName: opts.collectionName ?? this.model.collection.collectionName,
66
+ isNew: options.upsert && count === 0,
67
+ ignoreEvent: options.ignoreEvent as boolean,
68
+ ignorePatchHistory: options.ignorePatchHistory as boolean
69
+ }
70
+
71
+ const updateQuery = this.getUpdate()
72
+ const { update, commands } = splitUpdateAndCommands(updateQuery)
73
+
74
+ const cursor = model.find(filter).lean().cursor()
75
+ await cursor.eachAsync(async (doc: HydratedDocument<T>) => {
76
+ await updatePatch(opts, this._context, assignUpdate(doc, update, commands), doc)
77
+ })
78
+ })
79
+
80
+ schema.post(updateMethods as MongooseQueryMiddleware[], async function (this: IHookContext<T>) {
81
+ const options = this.getOptions()
82
+ if (isHookIgnored(options)) return
83
+
84
+ if (!this._context.isNew) return
85
+
86
+ const model = this.model as Model<T>
87
+ const updateQuery = this.getUpdate()
88
+ const { update, commands } = splitUpdateAndCommands(updateQuery)
89
+
90
+ const filter = assignUpdate({} as HydratedDocument<T>, update, commands)
91
+ if (!_.isEmpty(filter)) {
92
+ const current = await model.findOne(update).lean().exec()
93
+ if (current) {
94
+ this._context.createdDocs = [current] as HydratedDocument<T>[]
95
+
96
+ await createPatch(opts, this._context)
97
+ }
98
+ }
99
+ })
100
+ }
package/src/plugin.ts CHANGED
@@ -1,79 +1,20 @@
1
1
  import _ from 'lodash'
2
- import { assign } from 'power-assign'
2
+ import em from './em'
3
+
4
+ import { createPatch, deletePatch } from './patch'
5
+ import { isMongooseLessThan7, isMongooseLessThan8 } from './version'
6
+ import { toObjectOptions } from './helpers'
3
7
 
4
- import type { HydratedDocument, Model, MongooseQueryMiddleware, QueryOptions, Schema, ToObjectOptions, UpdateQuery, UpdateWithAggregationPipeline } from 'mongoose'
8
+ import { saveHooksInitialize } from './hooks/save-hooks'
9
+ import { updateHooksInitialize } from './hooks/update-hooks'
10
+ import { deleteHooksInitialize } from './hooks/delete-hooks'
5
11
 
12
+ import type { HydratedDocument, Model, Schema } from 'mongoose'
6
13
  import type IPluginOptions from './interfaces/IPluginOptions'
7
14
  import type IContext from './interfaces/IContext'
8
- import type IHookContext from './interfaces/IHookContext'
9
-
10
- import { createPatch, updatePatch, deletePatch } from './patch'
11
- import { isMongooseLessThan7 } from './version'
12
- import em from './em'
13
15
 
14
16
  const remove = isMongooseLessThan7 ? 'remove' : 'deleteOne'
15
17
 
16
- const toObjectOptions: ToObjectOptions = {
17
- depopulate: true,
18
- virtuals: false
19
- }
20
-
21
- const updateMethods = [
22
- 'update',
23
- 'updateOne',
24
- 'replaceOne',
25
- 'updateMany',
26
- 'findOneAndUpdate',
27
- 'findOneAndReplace',
28
- 'findByIdAndUpdate'
29
- ]
30
-
31
- const deleteMethods = [
32
- 'remove',
33
- 'findOneAndDelete',
34
- 'findOneAndRemove',
35
- 'findByIdAndDelete',
36
- 'findByIdAndRemove',
37
- 'deleteOne',
38
- 'deleteMany'
39
- ]
40
-
41
- function isHookIgnored<T> (options: QueryOptions<T>): boolean {
42
- return options.ignoreHook === true || (options.ignoreEvent === true && options.ignorePatchHistory === true)
43
- }
44
-
45
- function splitUpdateAndCommands<T> (updateQuery: UpdateWithAggregationPipeline | UpdateQuery<T> | null): { update: UpdateQuery<T>, commands: Record<string, unknown>[] } {
46
- let update: UpdateQuery<T> = {}
47
- const commands: Record<string, unknown>[] = []
48
-
49
- if (!_.isEmpty(updateQuery) && !_.isArray(updateQuery) && _.isObjectLike(updateQuery)) {
50
- update = _.cloneDeep(updateQuery)
51
- const keys = _.keys(update).filter((key) => key.startsWith('$'))
52
- if (!_.isEmpty(keys)) {
53
- _.forEach(keys, (key) => {
54
- commands.push({ [key]: update[key] as unknown })
55
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
56
- delete update[key]
57
- })
58
- }
59
- }
60
-
61
- return { update, commands }
62
- }
63
-
64
- function assignUpdate<T> (document: HydratedDocument<T>, update: UpdateQuery<T>, commands: Record<string, unknown>[]): HydratedDocument<T> {
65
- let updated = assign(document, update)
66
- _.forEach(commands, (command) => {
67
- try {
68
- updated = assign(updated, command)
69
- } catch {
70
- // we catch assign keys that are not implemented
71
- }
72
- })
73
-
74
- return updated
75
- }
76
-
77
18
  /**
78
19
  * @description Patch patch event emitter
79
20
  */
@@ -86,27 +27,12 @@ export const patchEventEmitter = em
86
27
  * @returns {void}
87
28
  */
88
29
  export const patchHistoryPlugin = function plugin<T> (schema: Schema<T>, opts: IPluginOptions<T>): void {
89
- schema.pre('save', async function () {
90
- const current = this.toObject(toObjectOptions) as HydratedDocument<T>
91
- const model = this.constructor as Model<T>
92
-
93
- const context: IContext<T> = {
94
- op: this.isNew ? 'create' : 'update',
95
- modelName: opts.modelName ?? model.modelName,
96
- collectionName: opts.collectionName ?? model.collection.collectionName,
97
- createdDocs: [current]
98
- }
99
-
100
- if (this.isNew) {
101
- await createPatch(opts, context)
102
- } else {
103
- const original = await model.findById(current._id).lean().exec()
104
- if (original) {
105
- await updatePatch(opts, context, current, original as HydratedDocument<T>)
106
- }
107
- }
108
- })
109
-
30
+ // Initialize hooks
31
+ saveHooksInitialize(schema, opts)
32
+ updateHooksInitialize(schema, opts)
33
+ deleteHooksInitialize(schema, opts)
34
+
35
+ // Corner case for insertMany()
110
36
  schema.post('insertMany', async function (docs) {
111
37
  const context = {
112
38
  op: 'create',
@@ -118,111 +44,33 @@ export const patchHistoryPlugin = function plugin<T> (schema: Schema<T>, opts: I
118
44
  await createPatch(opts, context)
119
45
  })
120
46
 
121
- schema.pre(updateMethods as MongooseQueryMiddleware[], async function (this: IHookContext<T>) {
122
- const options = this.getOptions()
123
- if (isHookIgnored(options)) return
124
-
125
- const model = this.model as Model<T>
126
- const filter = this.getFilter()
127
- const count = await this.model.count(filter).exec()
128
-
129
- this._context = {
130
- op: this.op,
131
- modelName: opts.modelName ?? this.model.modelName,
132
- collectionName: opts.collectionName ?? this.model.collection.collectionName,
133
- isNew: options.upsert && count === 0,
134
- ignoreEvent: options.ignoreEvent as boolean,
135
- ignorePatchHistory: options.ignorePatchHistory as boolean
136
- }
137
-
138
- const updateQuery = this.getUpdate()
139
- const { update, commands } = splitUpdateAndCommands(updateQuery)
140
-
141
- const cursor = model.find(filter).lean().cursor()
142
- await cursor.eachAsync(async (doc: HydratedDocument<T>) => {
143
- await updatePatch(opts, this._context, assignUpdate(doc, update, commands), doc)
144
- })
145
- })
146
-
147
- schema.post(updateMethods as MongooseQueryMiddleware[], async function (this: IHookContext<T>) {
148
- const options = this.getOptions()
149
- if (isHookIgnored(options)) return
150
-
151
- if (!this._context.isNew) return
152
-
153
- const model = this.model as Model<T>
154
- const updateQuery = this.getUpdate()
155
- const { update, commands } = splitUpdateAndCommands(updateQuery)
156
-
157
- const filter = assignUpdate({} as HydratedDocument<T>, update, commands)
158
- if (!_.isEmpty(filter)) {
159
- const current = await model.findOne(update).lean().exec()
160
- if (current) {
161
- this._context.createdDocs = [current] as HydratedDocument<T>[]
162
-
163
- await createPatch(opts, this._context)
47
+ // In Mongoose 7, doc.deleteOne() returned a promise that resolved to doc.
48
+ // In Mongoose 8, doc.deleteOne() returns a query for easier chaining, as well as consistency with doc.updateOne().
49
+ if (isMongooseLessThan8) {
50
+ // @ts-expect-error - Mongoose 7 and below
51
+ schema.pre(remove, { document: true, query: false }, async function () {
52
+ // @ts-expect-error - Mongoose 7 and below
53
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
54
+ const original = this.toObject(toObjectOptions) as HydratedDocument<T>
55
+
56
+ if (opts.preDelete && !_.isEmpty(original)) {
57
+ await opts.preDelete([original])
164
58
  }
165
- }
166
- })
167
-
168
- schema.pre(remove, { document: true, query: false }, async function () {
169
- const original = this.toObject(toObjectOptions) as HydratedDocument<T>
170
-
171
- if (opts.preDelete && !_.isEmpty(original)) {
172
- await opts.preDelete([original])
173
- }
174
- })
175
-
176
- schema.post(remove, { document: true, query: false }, async function (this: HydratedDocument<T>) {
177
- const original = this.toObject(toObjectOptions) as HydratedDocument<T>
178
- const model = this.constructor as Model<T>
179
-
180
- const context: IContext<T> = {
181
- op: 'delete',
182
- modelName: opts.modelName ?? model.modelName,
183
- collectionName: opts.collectionName ?? model.collection.collectionName,
184
- deletedDocs: [original]
185
- }
186
-
187
- await deletePatch(opts, context)
188
- })
189
-
190
- schema.pre(deleteMethods as MongooseQueryMiddleware[], { document: false, query: true }, async function (this: IHookContext<T>) {
191
- const options = this.getOptions()
192
- if (isHookIgnored(options)) return
193
-
194
- const model = this.model as Model<T>
195
- const filter = this.getFilter()
196
-
197
- this._context = {
198
- op: this.op,
199
- modelName: opts.modelName ?? this.model.modelName,
200
- collectionName: opts.collectionName ?? this.model.collection.collectionName,
201
- ignoreEvent: options.ignoreEvent as boolean,
202
- ignorePatchHistory: options.ignorePatchHistory as boolean
203
- }
204
-
205
- if (['remove', 'deleteMany'].includes(this._context.op) && !options.single) {
206
- const docs = await model.find(filter).lean().exec()
207
- if (!_.isEmpty(docs)) {
208
- this._context.deletedDocs = docs as HydratedDocument<T>[]
209
- }
210
- } else {
211
- const doc = await model.findOne(filter).lean().exec()
212
- if (!_.isEmpty(doc)) {
213
- this._context.deletedDocs = [doc] as HydratedDocument<T>[]
59
+ })
60
+
61
+ // @ts-expect-error - Mongoose 7 and below
62
+ schema.post(remove, { document: true, query: false }, async function (this: HydratedDocument<T>) {
63
+ const original = this.toObject(toObjectOptions) as HydratedDocument<T>
64
+ const model = this.constructor as Model<T>
65
+
66
+ const context: IContext<T> = {
67
+ op: 'delete',
68
+ modelName: opts.modelName ?? model.modelName,
69
+ collectionName: opts.collectionName ?? model.collection.collectionName,
70
+ deletedDocs: [original]
214
71
  }
215
- }
216
-
217
- if (opts.preDelete && _.isArray(this._context.deletedDocs) && !_.isEmpty(this._context.deletedDocs)) {
218
- await opts.preDelete(this._context.deletedDocs)
219
- }
220
- })
221
-
222
- schema.post(deleteMethods as MongooseQueryMiddleware[], { document: false, query: true }, async function (this: IHookContext<T>) {
223
- const options = this.getOptions()
224
- if (isHookIgnored(options)) return
225
-
226
- await deletePatch(opts, this._context)
227
- })
72
+
73
+ await deletePatch(opts, context)
74
+ })
75
+ }
228
76
  }
package/src/version.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { satisfies } from 'semver'
2
2
  import mongoose from 'mongoose'
3
3
 
4
+ export const isMongooseLessThan8 = satisfies(mongoose.version, '<8')
4
5
  export const isMongooseLessThan7 = satisfies(mongoose.version, '<7')
5
6
  export const isMongoose6 = satisfies(mongoose.version, '6')
6
7
 
@@ -1,14 +1,13 @@
1
1
  import { isMongooseLessThan7 } from '../src/version'
2
2
  import mongoose, { model } from 'mongoose'
3
3
 
4
- import type { ToObjectOptions } from 'mongoose'
5
-
6
4
  import UserSchema from './schemas/UserSchema'
7
5
  import { patchHistoryPlugin } from '../src/plugin'
8
6
  import History from '../src/models/History'
9
7
 
10
8
  import em from '../src/em'
11
9
  import { USER_DELETED } from './constants/events'
10
+ import { toObjectOptions } from '../src/helpers'
12
11
 
13
12
  jest.mock('../src/em', () => {
14
13
  return {
@@ -16,10 +15,6 @@ jest.mock('../src/em', () => {
16
15
  }
17
16
  })
18
17
 
19
- const toObjectOptions: ToObjectOptions = {
20
- depopulate: true,
21
- virtuals: false
22
- }
23
18
 
24
19
  describe('plugin - event delete & patch history disabled', () => {
25
20
  const uri = `${globalThis.__MONGO_URI__}${globalThis.__MONGO_DB_NAME__}`
@@ -164,13 +159,17 @@ describe('plugin - event delete & patch history disabled', () => {
164
159
  it('should findOneAndRemove() and emit one delete event', async () => {
165
160
  const users = await User.create([
166
161
  { name: 'John', role: 'user' },
167
- { name: 'Alice', role: 'user' },
162
+ { name: 'Alice', role: 'admin' },
168
163
  { name: 'Bob', role: 'admin' }
169
164
  ])
170
165
 
171
166
  const [john] = users
172
167
 
173
- await User.findOneAndRemove({ role: 'user' }).exec()
168
+ if (isMongooseLessThan7) {
169
+ await User.findOneAndRemove({ role: 'user' }).exec()
170
+ } else {
171
+ await User.findOneAndDelete({ role: 'user' }).exec()
172
+ }
174
173
 
175
174
  const history = await History.find({})
176
175
  expect(history).toHaveLength(0)
@@ -224,7 +223,11 @@ describe('plugin - event delete & patch history disabled', () => {
224
223
 
225
224
  const [john] = users
226
225
 
227
- await User.findByIdAndRemove(john._id).exec()
226
+ if (isMongooseLessThan7) {
227
+ await User.findByIdAndRemove(john._id).exec()
228
+ } else {
229
+ await User.findByIdAndDelete(john._id).exec()
230
+ }
228
231
 
229
232
  const history = await History.find({})
230
233
  expect(history).toHaveLength(0)
@@ -245,7 +248,7 @@ describe('plugin - event delete & patch history disabled', () => {
245
248
  it('should deleteOne() and emit one delete event', async () => {
246
249
  const users = await User.create([
247
250
  { name: 'John', role: 'user' },
248
- { name: 'Alice', role: 'user' },
251
+ { name: 'Alice', role: 'admin' },
249
252
  { name: 'Bob', role: 'admin' }
250
253
  ])
251
254