ts-patch-mongoose 2.9.6 → 3.1.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.
package/README.md CHANGED
@@ -23,8 +23,8 @@ I need to track changes of mongoose models and save them as patch history (audit
23
23
 
24
24
  ```json
25
25
  {
26
- "node": "18.x || 20.x || 22.x",
27
- "mongoose": ">=6.6.x || 7.x || 8.x",
26
+ "node": "20.x || 22.x || 24.x",
27
+ "mongoose": ">=6.6.x || 7.x || 8.x || 9.x",
28
28
  }
29
29
  ```
30
30
 
@@ -40,37 +40,19 @@ I need to track changes of mongoose models and save them as patch history (audit
40
40
 
41
41
  ## Installation
42
42
 
43
- - Locally inside your project
43
+ `mongoose` is a peer dependency — install it alongside `ts-patch-mongoose`.
44
44
 
45
45
  ```bash
46
- npm install ts-patch-mongoose
47
- pnpm add ts-patch-mongoose
48
- yarn add ts-patch-mongoose
49
- bun add ts-patch-mongoose
50
- ```
51
-
52
- - This plugin requires mongoose `>=6.6.x || 7.x || 8.x` to be installed as a peer dependency
53
-
54
- ```bash
55
- # For latest mongoose 6
56
- npm install mongoose@6
57
- pnpm add mongoose@6
58
- yarn add mongoose@6
59
- bun add mongoose@6
60
- # For latest mongoose 7
61
- npm install mongoose@7
62
- pnpm add mongoose@7
63
- yarn add mongoose@7
64
- bun add mongoose@7
65
- # For latest mongoose 8
66
- npm install mongoose@8
67
- pnpm add mongoose@8
68
- yarn add mongoose@8
69
- bun add mongoose@8
46
+ npm install ts-patch-mongoose mongoose
47
+ pnpm add ts-patch-mongoose mongoose
48
+ yarn add ts-patch-mongoose mongoose
49
+ bun add ts-patch-mongoose mongoose
70
50
  ```
71
51
 
72
52
  ## Example
73
53
 
54
+ Works with any Node.js framework — Express, Fastify, Koa, Hono, Nest, etc.
55
+ \
74
56
  How to use it with express [ts-express-tsx](https://github.com/ilovepixelart/ts-express-tsx)
75
57
 
76
58
  Create your event constants `events.ts`
@@ -213,6 +195,39 @@ patchEventEmitter.on(BOOK_DELETED, ({ oldDoc }) => {
213
195
  })
214
196
  ```
215
197
 
198
+ ## NestJS
199
+
200
+ ```typescript
201
+ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
202
+ import { patchHistoryPlugin } from 'ts-patch-mongoose'
203
+
204
+ @Schema({ timestamps: true })
205
+ export class Book {
206
+ @Prop({ type: String, required: true })
207
+ title!: string
208
+
209
+ @Prop({ type: String })
210
+ description?: string
211
+ }
212
+
213
+ export const BookSchema = SchemaFactory.createForClass(Book)
214
+
215
+ BookSchema.plugin(patchHistoryPlugin, {
216
+ eventCreated: 'book-created',
217
+ eventUpdated: 'book-updated',
218
+ eventDeleted: 'book-deleted',
219
+ omit: ['__v', 'createdAt', 'updatedAt'],
220
+ })
221
+ ```
222
+
223
+ ## Contributing
224
+
225
+ Check [CONTRIBUTING.md](CONTRIBUTING.md)
226
+
227
+ ## License
228
+
229
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details
230
+
216
231
  ## Check my other projects
217
232
 
218
233
  - [ts-migrate-mongoose](https://github.com/ilovepixelart/ts-migrate-mongoose) - Migration framework for mongoose
package/biome.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://biomejs.dev/schemas/2.4.4/schema.json",
2
+ "$schema": "https://biomejs.dev/schemas/2.4.9/schema.json",
3
3
  "vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false },
4
4
  "files": {
5
5
  "ignoreUnknown": false,
@@ -27,10 +27,7 @@
27
27
  "linter": {
28
28
  "enabled": true,
29
29
  "rules": {
30
- "recommended": true,
31
- "correctness": {
32
- "noUnusedVariables": "off"
33
- }
30
+ "recommended": true
34
31
  }
35
32
  },
36
33
  "javascript": {
package/dist/index.cjs CHANGED
@@ -1,20 +1,9 @@
1
1
  'use strict';
2
2
 
3
- var isEmpty = require('lodash/isEmpty.js');
4
- var ms = require('ms');
5
3
  var mongoose = require('mongoose');
6
- var isArray = require('lodash/isArray.js');
7
4
  var jsonpatch = require('fast-json-patch');
8
- var chunk = require('lodash/chunk.js');
9
- var isFunction = require('lodash/isFunction.js');
10
- var omit = require('omit-deep');
11
5
  var EventEmitter = require('node:events');
12
- var cloneDeep = require('lodash/cloneDeep.js');
13
- var forEach = require('lodash/forEach.js');
14
- var isObjectLike = require('lodash/isObjectLike.js');
15
- var keys = require('lodash/keys.js');
16
6
  var powerAssign = require('power-assign');
17
- var semver = require('semver');
18
7
 
19
8
  const HistorySchema = new mongoose.Schema(
20
9
  {
@@ -61,6 +50,166 @@ HistorySchema.index({ collectionId: 1, version: -1 });
61
50
  HistorySchema.index({ op: 1, modelName: 1, collectionName: 1, collectionId: 1, reason: 1, version: 1 });
62
51
  const HistoryModel = mongoose.model("History", HistorySchema, "history");
63
52
 
53
+ const s = 1e3;
54
+ const m = s * 60;
55
+ const h = m * 60;
56
+ const d = h * 24;
57
+ const w = d * 7;
58
+ const y = d * 365.25;
59
+ const mo = y / 12;
60
+ const UNITS = {
61
+ milliseconds: 1,
62
+ millisecond: 1,
63
+ msecs: 1,
64
+ msec: 1,
65
+ ms: 1,
66
+ seconds: s,
67
+ second: s,
68
+ secs: s,
69
+ sec: s,
70
+ s,
71
+ minutes: m,
72
+ minute: m,
73
+ mins: m,
74
+ min: m,
75
+ m,
76
+ hours: h,
77
+ hour: h,
78
+ hrs: h,
79
+ hr: h,
80
+ h,
81
+ days: d,
82
+ day: d,
83
+ d,
84
+ weeks: w,
85
+ week: w,
86
+ w,
87
+ months: mo,
88
+ month: mo,
89
+ mo,
90
+ years: y,
91
+ year: y,
92
+ yrs: y,
93
+ yr: y,
94
+ y
95
+ };
96
+ const unitPattern = Object.keys(UNITS).sort((a, b) => b.length - a.length).join("|");
97
+ const RE = new RegExp(String.raw`^(-?(?:\d+)?\.?\d+)\s*(${unitPattern})?$`, "i");
98
+ const ms = (val) => {
99
+ const str = String(val);
100
+ if (str.length > 100) return Number.NaN;
101
+ const match = RE.exec(str);
102
+ if (!match) return Number.NaN;
103
+ const n = Number.parseFloat(match[1] ?? "");
104
+ const type = (match[2] ?? "ms").toLowerCase();
105
+ return n * (UNITS[type] ?? 0);
106
+ };
107
+
108
+ const isArray = Array.isArray;
109
+ const isEmpty = (value) => {
110
+ if (value == null) return true;
111
+ if (Array.isArray(value) || typeof value === "string") return value.length === 0;
112
+ if (value instanceof Map || value instanceof Set) return value.size === 0;
113
+ if (typeof value === "object") {
114
+ for (const key in value) {
115
+ if (Object.hasOwn(value, key)) return false;
116
+ }
117
+ return true;
118
+ }
119
+ return true;
120
+ };
121
+ const isFunction = (value) => {
122
+ return typeof value === "function";
123
+ };
124
+ const isObjectLike = (value) => {
125
+ return typeof value === "object" && value !== null;
126
+ };
127
+ const cloneArrayBuffer = (arrayBuffer) => {
128
+ const result = new ArrayBuffer(arrayBuffer.byteLength);
129
+ new Uint8Array(result).set(new Uint8Array(arrayBuffer));
130
+ return result;
131
+ };
132
+ const cloneImmutable = (value) => {
133
+ const tag = Object.prototype.toString.call(value);
134
+ switch (tag) {
135
+ case "[object Date]":
136
+ return /* @__PURE__ */ new Date(+value);
137
+ case "[object RegExp]": {
138
+ const re = value;
139
+ const cloned = new RegExp(re.source, re.flags);
140
+ cloned.lastIndex = re.lastIndex;
141
+ return cloned;
142
+ }
143
+ case "[object Error]": {
144
+ const err = value;
145
+ const cloned = new err.constructor(err.message);
146
+ if (err.stack) cloned.stack = err.stack;
147
+ return cloned;
148
+ }
149
+ case "[object ArrayBuffer]":
150
+ return cloneArrayBuffer(value);
151
+ case "[object DataView]": {
152
+ const dv = value;
153
+ const buffer = cloneArrayBuffer(dv.buffer);
154
+ return new DataView(buffer, dv.byteOffset, dv.byteLength);
155
+ }
156
+ }
157
+ if (ArrayBuffer.isView(value)) {
158
+ const ta = value;
159
+ const buffer = cloneArrayBuffer(ta.buffer);
160
+ return new value.constructor(buffer, ta.byteOffset, ta.length);
161
+ }
162
+ return void 0;
163
+ };
164
+ const cloneCollection = (value, seen) => {
165
+ if (value instanceof Map) {
166
+ const map = /* @__PURE__ */ new Map();
167
+ seen.set(value, map);
168
+ for (const [k, v] of value) map.set(k, cloneDeep(v, seen));
169
+ return map;
170
+ }
171
+ if (value instanceof Set) {
172
+ const set = /* @__PURE__ */ new Set();
173
+ seen.set(value, set);
174
+ for (const v of value) set.add(cloneDeep(v, seen));
175
+ return set;
176
+ }
177
+ if (Array.isArray(value)) {
178
+ const arr = new Array(value.length);
179
+ seen.set(value, arr);
180
+ for (let i = 0; i < value.length; i++) {
181
+ arr[i] = cloneDeep(value[i], seen);
182
+ }
183
+ return arr;
184
+ }
185
+ const result = typeof value.constructor === "function" ? Object.create(Object.getPrototypeOf(value)) : {};
186
+ seen.set(value, result);
187
+ for (const key of Object.keys(value)) {
188
+ result[key] = cloneDeep(value[key], seen);
189
+ }
190
+ return result;
191
+ };
192
+ const cloneDeep = (value, seen = /* @__PURE__ */ new WeakMap()) => {
193
+ if (value === null || typeof value !== "object") return value;
194
+ if (seen.has(value)) return seen.get(value);
195
+ const immutable = cloneImmutable(value);
196
+ if (immutable !== void 0) return immutable;
197
+ const record = value;
198
+ if (typeof record._bsontype === "string" && typeof record.toHexString === "function") {
199
+ return new value.constructor(record.toHexString());
200
+ }
201
+ if (typeof record.toJSON === "function") {
202
+ return JSON.parse(JSON.stringify(value));
203
+ }
204
+ return cloneCollection(value, seen);
205
+ };
206
+ const chunk = (array, size) => {
207
+ const result = [];
208
+ for (let i = 0; i < array.length; i += size) {
209
+ result.push(array.slice(i, i + size));
210
+ }
211
+ return result;
212
+ };
64
213
  const isHookIgnored = (options) => {
65
214
  return options.ignoreHook === true || options.ignoreEvent === true && options.ignorePatchHistory === true;
66
215
  };
@@ -68,7 +217,7 @@ const toObjectOptions = {
68
217
  depopulate: true,
69
218
  virtuals: false
70
219
  };
71
- const setPatchHistoryTTL = async (ttl) => {
220
+ const setPatchHistoryTTL = async (ttl, onError) => {
72
221
  const name = "createdAt_1_TTL";
73
222
  try {
74
223
  const indexes = await HistoryModel.collection.indexes();
@@ -77,7 +226,7 @@ const setPatchHistoryTTL = async (ttl) => {
77
226
  await HistoryModel.collection.dropIndex(name);
78
227
  return;
79
228
  }
80
- const milliseconds = typeof ttl === "string" ? ms(ttl) : ttl;
229
+ const milliseconds = ms(ttl);
81
230
  if (milliseconds < 1e3 && existingIndex) {
82
231
  await HistoryModel.collection.dropIndex(name);
83
232
  return;
@@ -91,7 +240,8 @@ const setPatchHistoryTTL = async (ttl) => {
91
240
  }
92
241
  await HistoryModel.collection.createIndex({ createdAt: 1 }, { expireAfterSeconds, name });
93
242
  } catch (err) {
94
- console.error("Couldn't create or update index for history collection", err);
243
+ const handler = onError ?? console.error;
244
+ handler(err);
95
245
  }
96
246
  };
97
247
 
@@ -99,64 +249,101 @@ class PatchEventEmitter extends EventEmitter {
99
249
  }
100
250
  const em = new PatchEventEmitter();
101
251
 
102
- function isPatchHistoryEnabled(opts, context) {
103
- return !opts.patchHistoryDisabled && !context.ignorePatchHistory;
104
- }
105
- function getJsonOmit(opts, doc) {
106
- const object = JSON.parse(JSON.stringify(doc));
107
- if (opts.omit) {
108
- return omit(object, opts.omit);
109
- }
110
- return object;
111
- }
112
- function getObjectOmit(opts, doc) {
113
- if (opts.omit) {
114
- return omit(isFunction(doc?.toObject) ? doc.toObject() : doc, opts.omit);
252
+ const isPlainObject = (val) => {
253
+ if (Object.prototype.toString.call(val) !== "[object Object]") return false;
254
+ const prot = Object.getPrototypeOf(val);
255
+ return prot === null || prot === Object.prototype;
256
+ };
257
+ const isUnsafeKey = (key) => {
258
+ return key === "__proto__" || key === "constructor" || key === "prototype";
259
+ };
260
+ const classifyKeys = (omitKeys) => {
261
+ const topLevel = /* @__PURE__ */ new Set();
262
+ const nested = /* @__PURE__ */ new Map();
263
+ for (const key of omitKeys) {
264
+ const dotIdx = key.indexOf(".");
265
+ if (dotIdx === -1) {
266
+ topLevel.add(key);
267
+ } else {
268
+ const head = key.slice(0, dotIdx);
269
+ const tail = key.slice(dotIdx + 1);
270
+ if (!isUnsafeKey(head)) {
271
+ const existing = nested.get(head) ?? [];
272
+ existing.push(tail);
273
+ nested.set(head, existing);
274
+ }
275
+ }
115
276
  }
116
- return doc;
117
- }
118
- async function getUser(opts, doc) {
119
- if (isFunction(opts.getUser)) {
120
- return await opts.getUser(doc);
277
+ return { topLevel, nested };
278
+ };
279
+ const omitDeep = (value, keys) => {
280
+ if (value === void 0) return {};
281
+ if (Array.isArray(value)) {
282
+ return value.map((item) => omitDeep(item, keys));
121
283
  }
122
- return void 0;
123
- }
124
- async function getReason(opts, doc) {
125
- if (isFunction(opts.getReason)) {
126
- return await opts.getReason(doc);
284
+ if (!isPlainObject(value)) return value;
285
+ const omitKeys = typeof keys === "string" ? [keys] : keys;
286
+ if (!Array.isArray(omitKeys)) return value;
287
+ const { topLevel, nested } = classifyKeys(omitKeys);
288
+ const result = {};
289
+ for (const key of Object.keys(value)) {
290
+ if (topLevel.has(key)) continue;
291
+ const nestedKeys = nested.get(key);
292
+ result[key] = omitDeep(value[key], nestedKeys ?? omitKeys);
127
293
  }
128
- return void 0;
129
- }
130
- async function getMetadata(opts, doc) {
131
- if (isFunction(opts.getMetadata)) {
132
- return await opts.getMetadata(doc);
294
+ return result;
295
+ };
296
+
297
+ const isPatchHistoryEnabled = (opts, context) => {
298
+ return !opts.patchHistoryDisabled && !context.ignorePatchHistory;
299
+ };
300
+ const applyOmit = (object, opts) => {
301
+ return opts.omit ? omitDeep(object, opts.omit) : object;
302
+ };
303
+ const replacer = (_key, value) => typeof value === "bigint" ? value.toString() : value;
304
+ const getJsonOmit = (opts, doc) => {
305
+ return applyOmit(JSON.parse(JSON.stringify(doc, replacer)), opts);
306
+ };
307
+ const getObjectOmit = (opts, doc) => {
308
+ return applyOmit(isFunction(doc?.toObject) ? doc.toObject() : doc, opts);
309
+ };
310
+ const getOptionalField = async (fn, doc) => {
311
+ if (isFunction(fn)) {
312
+ return await fn(doc);
133
313
  }
134
314
  return void 0;
135
- }
136
- function getValue(item) {
315
+ };
316
+ const getUser = async (opts, doc) => getOptionalField(opts.getUser, doc);
317
+ const getReason = async (opts, doc) => getOptionalField(opts.getReason, doc);
318
+ const getMetadata = async (opts, doc) => getOptionalField(opts.getMetadata, doc);
319
+ const getValue = (item) => {
137
320
  return item.status === "fulfilled" ? item.value : void 0;
138
- }
139
- async function getData(opts, doc) {
321
+ };
322
+ const getData = async (opts, doc) => {
140
323
  return Promise.allSettled([getUser(opts, doc), getReason(opts, doc), getMetadata(opts, doc)]).then(([user, reason, metadata]) => {
141
324
  return [getValue(user), getValue(reason), getValue(metadata)];
142
325
  });
143
- }
144
- function emitEvent(context, event, data) {
326
+ };
327
+ const emitEvent = (context, event, data) => {
145
328
  if (event && !context.ignoreEvent) {
146
- em.emit(event, data);
329
+ try {
330
+ em.emit(event, data);
331
+ } catch {
332
+ }
147
333
  }
148
- }
149
- async function bulkPatch(opts, context, eventKey, docsKey) {
334
+ };
335
+ const bulkPatch = async (opts, context, eventKey, docsKey) => {
150
336
  const history = isPatchHistoryEnabled(opts, context);
151
337
  const event = opts[eventKey];
152
338
  const docs = context[docsKey];
153
339
  const key = eventKey === "eventCreated" ? "doc" : "oldDoc";
154
- if (isEmpty(docs) || !event && !history) return;
340
+ if (isEmpty(docs) || !docs || !event && !history) return;
155
341
  const chunks = chunk(docs, 1e3);
156
- for (const chunk2 of chunks) {
342
+ for (const batch of chunks) {
157
343
  const bulk = [];
158
- for (const doc of chunk2) {
159
- emitEvent(context, event, { [key]: doc });
344
+ for (const doc of batch) {
345
+ const omitted = getObjectOmit(opts, doc);
346
+ emitEvent(context, event, { [key]: omitted });
160
347
  if (history) {
161
348
  const [user, reason, metadata] = await getData(opts, doc);
162
349
  bulk.push({
@@ -166,7 +353,7 @@ async function bulkPatch(opts, context, eventKey, docsKey) {
166
353
  modelName: context.modelName,
167
354
  collectionName: context.collectionName,
168
355
  collectionId: doc._id,
169
- doc: getObjectOmit(opts, doc),
356
+ doc: omitted,
170
357
  version: 0,
171
358
  ...user !== void 0 && { user },
172
359
  ...reason !== void 0 && { reason },
@@ -177,16 +364,17 @@ async function bulkPatch(opts, context, eventKey, docsKey) {
177
364
  }
178
365
  }
179
366
  if (history && !isEmpty(bulk)) {
367
+ const onError = opts.onError ?? console.error;
180
368
  await HistoryModel.bulkWrite(bulk, { ordered: false }).catch((error) => {
181
- console.error(error.message);
369
+ onError(error);
182
370
  });
183
371
  }
184
372
  }
185
- }
186
- async function createPatch(opts, context) {
373
+ };
374
+ const createPatch = async (opts, context) => {
187
375
  await bulkPatch(opts, context, "eventCreated", "createdDocs");
188
- }
189
- async function updatePatch(opts, context, current, original) {
376
+ };
377
+ const updatePatch = async (opts, context, current, original) => {
190
378
  const history = isPatchHistoryEnabled(opts, context);
191
379
  const currentObject = getJsonOmit(opts, current);
192
380
  const originalObject = getJsonOmit(opts, original);
@@ -201,6 +389,7 @@ async function updatePatch(opts, context, current, original) {
201
389
  version = lastHistory.version + 1;
202
390
  }
203
391
  const [user, reason, metadata] = await getData(opts, current);
392
+ const onError = opts.onError ?? console.error;
204
393
  await HistoryModel.create({
205
394
  op: context.op,
206
395
  modelName: context.modelName,
@@ -211,12 +400,14 @@ async function updatePatch(opts, context, current, original) {
211
400
  ...user !== void 0 && { user },
212
401
  ...reason !== void 0 && { reason },
213
402
  ...metadata !== void 0 && { metadata }
403
+ }).catch((error) => {
404
+ onError(error);
214
405
  });
215
406
  }
216
- }
217
- async function deletePatch(opts, context) {
407
+ };
408
+ const deletePatch = async (opts, context) => {
218
409
  await bulkPatch(opts, context, "eventDeleted", "deletedDocs");
219
- }
410
+ };
220
411
 
221
412
  const deleteMethods = ["remove", "findOneAndDelete", "findOneAndRemove", "findByIdAndDelete", "findByIdAndRemove", "deleteOne", "deleteMany"];
222
413
  const deleteHooksInitialize = (schema, opts) => {
@@ -227,8 +418,8 @@ const deleteHooksInitialize = (schema, opts) => {
227
418
  const filter = this.getFilter();
228
419
  this._context = {
229
420
  op: this.op,
230
- modelName: opts.modelName ?? this.model.modelName,
231
- collectionName: opts.collectionName ?? this.model.collection.collectionName,
421
+ modelName: opts.modelName ?? model.modelName,
422
+ collectionName: opts.collectionName ?? model.collection.collectionName,
232
423
  ignoreEvent: options.ignoreEvent,
233
424
  ignorePatchHistory: options.ignorePatchHistory
234
425
  };
@@ -244,12 +435,13 @@ const deleteHooksInitialize = (schema, opts) => {
244
435
  }
245
436
  }
246
437
  if (opts.preDelete && isArray(this._context.deletedDocs) && !isEmpty(this._context.deletedDocs)) {
247
- await opts.preDelete(this._context.deletedDocs);
438
+ await opts.preDelete(cloneDeep(this._context.deletedDocs));
248
439
  }
249
440
  });
250
441
  schema.post(deleteMethods, { document: false, query: true }, async function() {
251
442
  const options = this.getOptions();
252
443
  if (isHookIgnored(options)) return;
444
+ if (!this._context) return;
253
445
  await deletePatch(opts, this._context);
254
446
  });
255
447
  };
@@ -277,15 +469,42 @@ const saveHooksInitialize = (schema, opts) => {
277
469
  };
278
470
 
279
471
  const updateMethods = ["update", "updateOne", "replaceOne", "updateMany", "findOneAndUpdate", "findOneAndReplace", "findByIdAndUpdate"];
472
+ const trackChangedFields = (fields, updated, changed) => {
473
+ if (!fields) return;
474
+ for (const key of Object.keys(fields)) {
475
+ const root = key.split(".")[0];
476
+ changed.set(root, updated[root]);
477
+ }
478
+ };
479
+ const applyPullAll = (updated, fields, changed) => {
480
+ for (const [field, values] of Object.entries(fields)) {
481
+ const arr = updated[field];
482
+ if (Array.isArray(arr)) {
483
+ const filtered = arr.filter((item) => !values.some((v) => JSON.stringify(v) === JSON.stringify(item)));
484
+ updated[field] = filtered;
485
+ changed.set(field, filtered);
486
+ }
487
+ }
488
+ };
280
489
  const assignUpdate = (document, update, commands) => {
281
490
  let updated = powerAssign.assign(document.toObject(toObjectOptions), update);
282
- forEach(commands, (command) => {
491
+ const changedByCommand = /* @__PURE__ */ new Map();
492
+ for (const command of commands) {
493
+ const op = Object.keys(command)[0];
494
+ const fields = command[op];
283
495
  try {
284
496
  updated = powerAssign.assign(updated, command);
497
+ trackChangedFields(fields, updated, changedByCommand);
285
498
  } catch {
499
+ if (op === "$pullAll" && fields) {
500
+ applyPullAll(updated, fields, changedByCommand);
501
+ }
286
502
  }
287
- });
503
+ }
288
504
  const doc = document.set(updated).toObject(toObjectOptions);
505
+ for (const [field, value] of changedByCommand) {
506
+ doc[field] = value;
507
+ }
289
508
  if (update.createdAt) doc.createdAt = update.createdAt;
290
509
  return doc;
291
510
  };
@@ -294,28 +513,27 @@ const splitUpdateAndCommands = (updateQuery) => {
294
513
  const commands = [];
295
514
  if (!isEmpty(updateQuery) && !isArray(updateQuery) && isObjectLike(updateQuery)) {
296
515
  update = cloneDeep(updateQuery);
297
- const keysWithDollarSign = keys(update).filter((key) => key.startsWith("$"));
516
+ const keysWithDollarSign = Object.keys(update).filter((key) => key.startsWith("$"));
298
517
  if (!isEmpty(keysWithDollarSign)) {
299
- forEach(keysWithDollarSign, (key) => {
518
+ for (const key of keysWithDollarSign) {
300
519
  commands.push({ [key]: update[key] });
301
520
  delete update[key];
302
- });
521
+ }
303
522
  }
304
523
  }
305
524
  return { update, commands };
306
525
  };
307
526
  const updateHooksInitialize = (schema, opts) => {
308
- schema.pre(updateMethods, async function() {
527
+ schema.pre(updateMethods, { document: false, query: true }, async function() {
309
528
  const options = this.getOptions();
310
529
  if (isHookIgnored(options)) return;
311
530
  const model = this.model;
312
531
  const filter = this.getFilter();
313
- const count = await this.model.countDocuments(filter).exec();
314
532
  this._context = {
315
533
  op: this.op,
316
- modelName: opts.modelName ?? this.model.modelName,
317
- collectionName: opts.collectionName ?? this.model.collection.collectionName,
318
- isNew: Boolean(options.upsert) && count === 0,
534
+ modelName: opts.modelName ?? model.modelName,
535
+ collectionName: opts.collectionName ?? model.collection.collectionName,
536
+ isNew: Boolean(options.upsert) && await model.countDocuments(filter).exec() === 0,
319
537
  ignoreEvent: options.ignoreEvent,
320
538
  ignorePatchHistory: options.ignorePatchHistory
321
539
  };
@@ -327,25 +545,20 @@ const updateHooksInitialize = (schema, opts) => {
327
545
  await updatePatch(opts, this._context, assignUpdate(doc, update, commands), origDoc);
328
546
  });
329
547
  });
330
- schema.post(updateMethods, async function() {
548
+ schema.post(updateMethods, { document: false, query: true }, async function() {
331
549
  const options = this.getOptions();
332
550
  if (isHookIgnored(options)) return;
551
+ if (!this._context) return;
333
552
  if (!this._context.isNew) return;
334
553
  const model = this.model;
335
554
  const updateQuery = this.getUpdate();
336
555
  const { update, commands } = splitUpdateAndCommands(updateQuery);
337
- let current = null;
338
556
  const filter = this.getFilter();
339
- const combined = assignUpdate(model.hydrate({}), update, commands);
340
- if (!isEmpty(update) && !current) {
341
- current = await model.findOne(update).sort("desc").lean().exec();
342
- }
343
- if (!isEmpty(combined) && !current) {
344
- current = await model.findOne(combined).sort("desc").lean().exec();
345
- }
346
- if (!isEmpty(filter) && !current) {
347
- console.log("filter", filter);
348
- current = await model.findOne(filter).sort("desc").lean().exec();
557
+ const candidates = [update, assignUpdate(model.hydrate({}), update, commands), filter];
558
+ let current = null;
559
+ for (const query of candidates) {
560
+ if (current || isEmpty(query)) continue;
561
+ current = await model.findOne(query).sort({ _id: -1 }).lean().exec();
349
562
  }
350
563
  if (current) {
351
564
  this._context.createdDocs = [current];
@@ -354,15 +567,16 @@ const updateHooksInitialize = (schema, opts) => {
354
567
  });
355
568
  };
356
569
 
357
- const isMongooseLessThan8 = semver.satisfies(mongoose.version, "<8");
358
- const isMongooseLessThan7 = semver.satisfies(mongoose.version, "<7");
359
- const isMongoose6 = semver.satisfies(mongoose.version, "6");
570
+ const major = Number.parseInt(mongoose.version, 10);
571
+ const isMongooseLessThan8 = major < 8;
572
+ const isMongooseLessThan7 = major < 7;
573
+ const isMongoose6 = major === 6;
360
574
  if (isMongoose6) {
361
575
  mongoose.set("strictQuery", false);
362
576
  }
363
577
 
364
578
  const remove = isMongooseLessThan7 ? "remove" : "deleteOne";
365
- const patchHistoryPlugin = function plugin(schema, opts) {
579
+ const patchHistoryPlugin = (schema, opts) => {
366
580
  saveHooksInitialize(schema, opts);
367
581
  updateHooksInitialize(schema, opts);
368
582
  deleteHooksInitialize(schema, opts);