ts-patch-mongoose 3.0.0 → 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/dist/index.mjs CHANGED
@@ -138,6 +138,12 @@ const cloneImmutable = (value) => {
138
138
  cloned.lastIndex = re.lastIndex;
139
139
  return cloned;
140
140
  }
141
+ case "[object Error]": {
142
+ const err = value;
143
+ const cloned = new err.constructor(err.message);
144
+ if (err.stack) cloned.stack = err.stack;
145
+ return cloned;
146
+ }
141
147
  case "[object ArrayBuffer]":
142
148
  return cloneArrayBuffer(value);
143
149
  case "[object DataView]": {
@@ -186,7 +192,11 @@ const cloneDeep = (value, seen = /* @__PURE__ */ new WeakMap()) => {
186
192
  if (seen.has(value)) return seen.get(value);
187
193
  const immutable = cloneImmutable(value);
188
194
  if (immutable !== void 0) return immutable;
189
- if ("toJSON" in value && typeof value.toJSON === "function") {
195
+ const record = value;
196
+ if (typeof record._bsontype === "string" && typeof record.toHexString === "function") {
197
+ return new value.constructor(record.toHexString());
198
+ }
199
+ if (typeof record.toJSON === "function") {
190
200
  return JSON.parse(JSON.stringify(value));
191
201
  }
192
202
  return cloneCollection(value, seen);
@@ -205,7 +215,7 @@ const toObjectOptions = {
205
215
  depopulate: true,
206
216
  virtuals: false
207
217
  };
208
- const setPatchHistoryTTL = async (ttl) => {
218
+ const setPatchHistoryTTL = async (ttl, onError) => {
209
219
  const name = "createdAt_1_TTL";
210
220
  try {
211
221
  const indexes = await HistoryModel.collection.indexes();
@@ -228,7 +238,8 @@ const setPatchHistoryTTL = async (ttl) => {
228
238
  }
229
239
  await HistoryModel.collection.createIndex({ createdAt: 1 }, { expireAfterSeconds, name });
230
240
  } catch (err) {
231
- console.error("Couldn't create or update index for history collection", err);
241
+ const handler = onError ?? console.error;
242
+ handler(err);
232
243
  }
233
244
  };
234
245
 
@@ -244,113 +255,65 @@ const isPlainObject = (val) => {
244
255
  const isUnsafeKey = (key) => {
245
256
  return key === "__proto__" || key === "constructor" || key === "prototype";
246
257
  };
247
- const getValue$1 = (obj, path) => {
248
- const segs = path.split(".");
249
- let current = obj;
250
- for (const seg of segs) {
251
- if (current == null || typeof current !== "object") return void 0;
252
- current = current[seg];
253
- }
254
- return current;
255
- };
256
- const hasValue = (val) => {
257
- if (val == null) return false;
258
- if (typeof val === "boolean" || typeof val === "number" || typeof val === "function") return true;
259
- if (typeof val === "string") return val.length !== 0;
260
- if (Array.isArray(val)) return val.length !== 0;
261
- if (val instanceof RegExp) return val.source !== "(?:)" && val.source !== "";
262
- if (val instanceof Error) return val.message !== "";
263
- if (val instanceof Map || val instanceof Set) return val.size !== 0;
264
- if (typeof val === "object") {
265
- for (const key of Object.keys(val)) {
266
- if (hasValue(val[key])) return true;
267
- }
268
- return false;
269
- }
270
- return true;
271
- };
272
- const has = (obj, path) => {
273
- if (obj != null && typeof obj === "object" && typeof path === "string") {
274
- return hasValue(getValue$1(obj, path));
275
- }
276
- return false;
277
- };
278
- const unset = (obj, prop) => {
279
- if (typeof obj !== "object" || obj === null) return false;
280
- if (Object.hasOwn(obj, prop)) {
281
- delete obj[prop];
282
- return true;
283
- }
284
- if (has(obj, prop)) {
285
- const segs = prop.split(".");
286
- let last = segs.pop();
287
- while (segs.length && segs.at(-1)?.slice(-1) === "\\") {
288
- last = `${segs.pop().slice(0, -1)}.${last}`;
289
- }
290
- let target = obj;
291
- while (segs.length) {
292
- const seg = segs.shift();
293
- if (isUnsafeKey(seg)) return false;
294
- target = target[seg];
258
+ const classifyKeys = (omitKeys) => {
259
+ const topLevel = /* @__PURE__ */ new Set();
260
+ const nested = /* @__PURE__ */ new Map();
261
+ for (const key of omitKeys) {
262
+ const dotIdx = key.indexOf(".");
263
+ if (dotIdx === -1) {
264
+ topLevel.add(key);
265
+ } else {
266
+ const head = key.slice(0, dotIdx);
267
+ const tail = key.slice(dotIdx + 1);
268
+ if (!isUnsafeKey(head)) {
269
+ const existing = nested.get(head) ?? [];
270
+ existing.push(tail);
271
+ nested.set(head, existing);
272
+ }
295
273
  }
296
- return delete target[last ?? ""];
297
274
  }
298
- return true;
275
+ return { topLevel, nested };
299
276
  };
300
277
  const omitDeep = (value, keys) => {
301
278
  if (value === void 0) return {};
302
279
  if (Array.isArray(value)) {
303
- for (let i = 0; i < value.length; i++) {
304
- value[i] = omitDeep(value[i], keys);
305
- }
306
- return value;
280
+ return value.map((item) => omitDeep(item, keys));
307
281
  }
308
282
  if (!isPlainObject(value)) return value;
309
283
  const omitKeys = typeof keys === "string" ? [keys] : keys;
310
284
  if (!Array.isArray(omitKeys)) return value;
311
- for (const key of omitKeys) {
312
- unset(value, key);
313
- }
285
+ const { topLevel, nested } = classifyKeys(omitKeys);
286
+ const result = {};
314
287
  for (const key of Object.keys(value)) {
315
- value[key] = omitDeep(value[key], omitKeys);
288
+ if (topLevel.has(key)) continue;
289
+ const nestedKeys = nested.get(key);
290
+ result[key] = omitDeep(value[key], nestedKeys ?? omitKeys);
316
291
  }
317
- return value;
292
+ return result;
318
293
  };
319
294
 
320
295
  const isPatchHistoryEnabled = (opts, context) => {
321
296
  return !opts.patchHistoryDisabled && !context.ignorePatchHistory;
322
297
  };
298
+ const applyOmit = (object, opts) => {
299
+ return opts.omit ? omitDeep(object, opts.omit) : object;
300
+ };
301
+ const replacer = (_key, value) => typeof value === "bigint" ? value.toString() : value;
323
302
  const getJsonOmit = (opts, doc) => {
324
- const object = JSON.parse(JSON.stringify(doc));
325
- if (opts.omit) {
326
- return omitDeep(object, opts.omit);
327
- }
328
- return object;
303
+ return applyOmit(JSON.parse(JSON.stringify(doc, replacer)), opts);
329
304
  };
330
305
  const getObjectOmit = (opts, doc) => {
331
- if (opts.omit) {
332
- return omitDeep(isFunction(doc?.toObject) ? doc.toObject() : doc, opts.omit);
333
- }
334
- return doc;
335
- };
336
- const getUser = async (opts, doc) => {
337
- if (isFunction(opts.getUser)) {
338
- return await opts.getUser(doc);
339
- }
340
- return void 0;
341
- };
342
- const getReason = async (opts, doc) => {
343
- if (isFunction(opts.getReason)) {
344
- return await opts.getReason(doc);
345
- }
346
- return void 0;
306
+ return applyOmit(isFunction(doc?.toObject) ? doc.toObject() : doc, opts);
347
307
  };
348
- const getMetadata = async (opts, doc) => {
349
- if (isFunction(opts.getMetadata)) {
350
- return await opts.getMetadata(doc);
308
+ const getOptionalField = async (fn, doc) => {
309
+ if (isFunction(fn)) {
310
+ return await fn(doc);
351
311
  }
352
312
  return void 0;
353
313
  };
314
+ const getUser = async (opts, doc) => getOptionalField(opts.getUser, doc);
315
+ const getReason = async (opts, doc) => getOptionalField(opts.getReason, doc);
316
+ const getMetadata = async (opts, doc) => getOptionalField(opts.getMetadata, doc);
354
317
  const getValue = (item) => {
355
318
  return item.status === "fulfilled" ? item.value : void 0;
356
319
  };
@@ -361,7 +324,10 @@ const getData = async (opts, doc) => {
361
324
  };
362
325
  const emitEvent = (context, event, data) => {
363
326
  if (event && !context.ignoreEvent) {
364
- em.emit(event, data);
327
+ try {
328
+ em.emit(event, data);
329
+ } catch {
330
+ }
365
331
  }
366
332
  };
367
333
  const bulkPatch = async (opts, context, eventKey, docsKey) => {
@@ -374,7 +340,8 @@ const bulkPatch = async (opts, context, eventKey, docsKey) => {
374
340
  for (const batch of chunks) {
375
341
  const bulk = [];
376
342
  for (const doc of batch) {
377
- emitEvent(context, event, { [key]: doc });
343
+ const omitted = getObjectOmit(opts, doc);
344
+ emitEvent(context, event, { [key]: omitted });
378
345
  if (history) {
379
346
  const [user, reason, metadata] = await getData(opts, doc);
380
347
  bulk.push({
@@ -384,7 +351,7 @@ const bulkPatch = async (opts, context, eventKey, docsKey) => {
384
351
  modelName: context.modelName,
385
352
  collectionName: context.collectionName,
386
353
  collectionId: doc._id,
387
- doc: getObjectOmit(opts, doc),
354
+ doc: omitted,
388
355
  version: 0,
389
356
  ...user !== void 0 && { user },
390
357
  ...reason !== void 0 && { reason },
@@ -395,8 +362,9 @@ const bulkPatch = async (opts, context, eventKey, docsKey) => {
395
362
  }
396
363
  }
397
364
  if (history && !isEmpty(bulk)) {
365
+ const onError = opts.onError ?? console.error;
398
366
  await HistoryModel.bulkWrite(bulk, { ordered: false }).catch((error) => {
399
- console.error(error.message);
367
+ onError(error);
400
368
  });
401
369
  }
402
370
  }
@@ -419,6 +387,7 @@ const updatePatch = async (opts, context, current, original) => {
419
387
  version = lastHistory.version + 1;
420
388
  }
421
389
  const [user, reason, metadata] = await getData(opts, current);
390
+ const onError = opts.onError ?? console.error;
422
391
  await HistoryModel.create({
423
392
  op: context.op,
424
393
  modelName: context.modelName,
@@ -429,6 +398,8 @@ const updatePatch = async (opts, context, current, original) => {
429
398
  ...user !== void 0 && { user },
430
399
  ...reason !== void 0 && { reason },
431
400
  ...metadata !== void 0 && { metadata }
401
+ }).catch((error) => {
402
+ onError(error);
432
403
  });
433
404
  }
434
405
  };
@@ -445,8 +416,8 @@ const deleteHooksInitialize = (schema, opts) => {
445
416
  const filter = this.getFilter();
446
417
  this._context = {
447
418
  op: this.op,
448
- modelName: opts.modelName ?? this.model.modelName,
449
- collectionName: opts.collectionName ?? this.model.collection.collectionName,
419
+ modelName: opts.modelName ?? model.modelName,
420
+ collectionName: opts.collectionName ?? model.collection.collectionName,
450
421
  ignoreEvent: options.ignoreEvent,
451
422
  ignorePatchHistory: options.ignorePatchHistory
452
423
  };
@@ -462,12 +433,13 @@ const deleteHooksInitialize = (schema, opts) => {
462
433
  }
463
434
  }
464
435
  if (opts.preDelete && isArray(this._context.deletedDocs) && !isEmpty(this._context.deletedDocs)) {
465
- await opts.preDelete(this._context.deletedDocs);
436
+ await opts.preDelete(cloneDeep(this._context.deletedDocs));
466
437
  }
467
438
  });
468
439
  schema.post(deleteMethods, { document: false, query: true }, async function() {
469
440
  const options = this.getOptions();
470
441
  if (isHookIgnored(options)) return;
442
+ if (!this._context) return;
471
443
  await deletePatch(opts, this._context);
472
444
  });
473
445
  };
@@ -495,15 +467,42 @@ const saveHooksInitialize = (schema, opts) => {
495
467
  };
496
468
 
497
469
  const updateMethods = ["update", "updateOne", "replaceOne", "updateMany", "findOneAndUpdate", "findOneAndReplace", "findByIdAndUpdate"];
470
+ const trackChangedFields = (fields, updated, changed) => {
471
+ if (!fields) return;
472
+ for (const key of Object.keys(fields)) {
473
+ const root = key.split(".")[0];
474
+ changed.set(root, updated[root]);
475
+ }
476
+ };
477
+ const applyPullAll = (updated, fields, changed) => {
478
+ for (const [field, values] of Object.entries(fields)) {
479
+ const arr = updated[field];
480
+ if (Array.isArray(arr)) {
481
+ const filtered = arr.filter((item) => !values.some((v) => JSON.stringify(v) === JSON.stringify(item)));
482
+ updated[field] = filtered;
483
+ changed.set(field, filtered);
484
+ }
485
+ }
486
+ };
498
487
  const assignUpdate = (document, update, commands) => {
499
488
  let updated = assign(document.toObject(toObjectOptions), update);
489
+ const changedByCommand = /* @__PURE__ */ new Map();
500
490
  for (const command of commands) {
491
+ const op = Object.keys(command)[0];
492
+ const fields = command[op];
501
493
  try {
502
494
  updated = assign(updated, command);
495
+ trackChangedFields(fields, updated, changedByCommand);
503
496
  } catch {
497
+ if (op === "$pullAll" && fields) {
498
+ applyPullAll(updated, fields, changedByCommand);
499
+ }
504
500
  }
505
501
  }
506
502
  const doc = document.set(updated).toObject(toObjectOptions);
503
+ for (const [field, value] of changedByCommand) {
504
+ doc[field] = value;
505
+ }
507
506
  if (update.createdAt) doc.createdAt = update.createdAt;
508
507
  return doc;
509
508
  };
@@ -523,17 +522,16 @@ const splitUpdateAndCommands = (updateQuery) => {
523
522
  return { update, commands };
524
523
  };
525
524
  const updateHooksInitialize = (schema, opts) => {
526
- schema.pre(updateMethods, async function() {
525
+ schema.pre(updateMethods, { document: false, query: true }, async function() {
527
526
  const options = this.getOptions();
528
527
  if (isHookIgnored(options)) return;
529
528
  const model = this.model;
530
529
  const filter = this.getFilter();
531
- const count = await this.model.countDocuments(filter).exec();
532
530
  this._context = {
533
531
  op: this.op,
534
- modelName: opts.modelName ?? this.model.modelName,
535
- collectionName: opts.collectionName ?? this.model.collection.collectionName,
536
- isNew: Boolean(options.upsert) && count === 0,
532
+ modelName: opts.modelName ?? model.modelName,
533
+ collectionName: opts.collectionName ?? model.collection.collectionName,
534
+ isNew: Boolean(options.upsert) && await model.countDocuments(filter).exec() === 0,
537
535
  ignoreEvent: options.ignoreEvent,
538
536
  ignorePatchHistory: options.ignorePatchHistory
539
537
  };
@@ -545,24 +543,20 @@ const updateHooksInitialize = (schema, opts) => {
545
543
  await updatePatch(opts, this._context, assignUpdate(doc, update, commands), origDoc);
546
544
  });
547
545
  });
548
- schema.post(updateMethods, async function() {
546
+ schema.post(updateMethods, { document: false, query: true }, async function() {
549
547
  const options = this.getOptions();
550
548
  if (isHookIgnored(options)) return;
549
+ if (!this._context) return;
551
550
  if (!this._context.isNew) return;
552
551
  const model = this.model;
553
552
  const updateQuery = this.getUpdate();
554
553
  const { update, commands } = splitUpdateAndCommands(updateQuery);
555
- let current = null;
556
554
  const filter = this.getFilter();
557
- const combined = assignUpdate(model.hydrate({}), update, commands);
558
- if (!isEmpty(update) && !current) {
559
- current = await model.findOne(update).sort("desc").lean().exec();
560
- }
561
- if (!isEmpty(combined) && !current) {
562
- current = await model.findOne(combined).sort("desc").lean().exec();
563
- }
564
- if (!isEmpty(filter) && !current) {
565
- current = await model.findOne(filter).sort("desc").lean().exec();
555
+ const candidates = [update, assignUpdate(model.hydrate({}), update, commands), filter];
556
+ let current = null;
557
+ for (const query of candidates) {
558
+ if (current || isEmpty(query)) continue;
559
+ current = await model.findOne(query).sort({ _id: -1 }).lean().exec();
566
560
  }
567
561
  if (current) {
568
562
  this._context.createdDocs = [current];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-patch-mongoose",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Patch history & events for mongoose models",
5
5
  "author": "ilovepixelart",
6
6
  "license": "MIT",
@@ -75,17 +75,17 @@
75
75
  "power-assign": "0.2.10"
76
76
  },
77
77
  "devDependencies": {
78
- "@biomejs/biome": "2.4.7",
78
+ "@biomejs/biome": "2.4.9",
79
79
  "@types/node": "25.5.0",
80
- "@vitest/coverage-v8": "4.1.0",
80
+ "@vitest/coverage-v8": "4.1.2",
81
81
  "mongodb-memory-server": "11.0.1",
82
- "mongoose": "9.3.0",
82
+ "mongoose": "9.3.3",
83
83
  "np": "11.0.2",
84
- "open-cli": "8.0.0",
84
+ "open-cli": "9.0.0",
85
85
  "pkgroll": "2.27.0",
86
86
  "simple-git-hooks": "2.13.1",
87
87
  "typescript": "5.9.3",
88
- "vitest": "4.1.0"
88
+ "vitest": "4.1.2"
89
89
  },
90
90
  "peerDependencies": {
91
91
  "mongoose": ">=6.6.0 < 10"
package/src/helpers.ts CHANGED
@@ -44,6 +44,12 @@ const cloneImmutable = <T>(value: T): T | undefined => {
44
44
  cloned.lastIndex = re.lastIndex
45
45
  return cloned as T
46
46
  }
47
+ case '[object Error]': {
48
+ const err = value as unknown as Error
49
+ const cloned = new (err.constructor as ErrorConstructor)(err.message)
50
+ if (err.stack) cloned.stack = err.stack
51
+ return cloned as T
52
+ }
47
53
  case '[object ArrayBuffer]':
48
54
  return cloneArrayBuffer(value as unknown as ArrayBuffer) as T
49
55
  case '[object DataView]': {
@@ -101,7 +107,13 @@ export const cloneDeep = <T>(value: T, seen = new WeakMap<object, unknown>()): T
101
107
  const immutable = cloneImmutable(value)
102
108
  if (immutable !== undefined) return immutable
103
109
 
104
- if ('toJSON' in value && typeof (value as Record<string, unknown>).toJSON === 'function') {
110
+ const record = value as Record<string, unknown>
111
+
112
+ if (typeof record._bsontype === 'string' && typeof record.toHexString === 'function') {
113
+ return new (value.constructor as new (hex: string) => T)((record.toHexString as () => string)())
114
+ }
115
+
116
+ if (typeof record.toJSON === 'function') {
105
117
  // NOSONAR — structuredClone cannot handle objects with non-cloneable methods (e.g. mongoose documents)
106
118
  return JSON.parse(JSON.stringify(value)) as T
107
119
  }
@@ -126,7 +138,7 @@ export const toObjectOptions: ToObjectOptions = {
126
138
  virtuals: false,
127
139
  }
128
140
 
129
- export const setPatchHistoryTTL = async (ttl: Duration): Promise<void> => {
141
+ export const setPatchHistoryTTL = async (ttl: Duration, onError?: (error: Error) => void): Promise<void> => {
130
142
  const name = 'createdAt_1_TTL'
131
143
  try {
132
144
  const indexes = await HistoryModel.collection.indexes()
@@ -156,6 +168,7 @@ export const setPatchHistoryTTL = async (ttl: Duration): Promise<void> => {
156
168
 
157
169
  await HistoryModel.collection.createIndex({ createdAt: 1 }, { expireAfterSeconds, name })
158
170
  } catch (err) {
159
- console.error("Couldn't create or update index for history collection", err)
171
+ const handler = onError ?? console.error
172
+ handler(err as Error)
160
173
  }
161
174
  }
@@ -1,4 +1,4 @@
1
- import { isArray, isEmpty, isHookIgnored } from '../helpers'
1
+ import { cloneDeep, isArray, isEmpty, isHookIgnored } from '../helpers'
2
2
  import { deletePatch } from '../patch'
3
3
 
4
4
  import type { HydratedDocument, Model, MongooseQueryMiddleware, Schema } from 'mongoose'
@@ -16,8 +16,8 @@ export const deleteHooksInitialize = <T>(schema: Schema<T>, opts: PluginOptions<
16
16
 
17
17
  this._context = {
18
18
  op: this.op,
19
- modelName: opts.modelName ?? this.model.modelName,
20
- collectionName: opts.collectionName ?? this.model.collection.collectionName,
19
+ modelName: opts.modelName ?? model.modelName,
20
+ collectionName: opts.collectionName ?? model.collection.collectionName,
21
21
  ignoreEvent: options.ignoreEvent as boolean,
22
22
  ignorePatchHistory: options.ignorePatchHistory as boolean,
23
23
  }
@@ -35,13 +35,14 @@ export const deleteHooksInitialize = <T>(schema: Schema<T>, opts: PluginOptions<
35
35
  }
36
36
 
37
37
  if (opts.preDelete && isArray(this._context.deletedDocs) && !isEmpty(this._context.deletedDocs)) {
38
- await opts.preDelete(this._context.deletedDocs)
38
+ await opts.preDelete(cloneDeep(this._context.deletedDocs))
39
39
  }
40
40
  })
41
41
 
42
42
  schema.post(deleteMethods as MongooseQueryMiddleware[], { document: false, query: true }, async function (this: HookContext<T>) {
43
43
  const options = this.getOptions()
44
44
  if (isHookIgnored(options)) return
45
+ if (!this._context) return
45
46
 
46
47
  await deletePatch(opts, this._context)
47
48
  })
@@ -7,17 +7,46 @@ import type { HookContext, PluginOptions } from '../types'
7
7
 
8
8
  const updateMethods = ['update', 'updateOne', 'replaceOne', 'updateMany', 'findOneAndUpdate', 'findOneAndReplace', 'findByIdAndUpdate']
9
9
 
10
+ const trackChangedFields = (fields: Record<string, unknown> | undefined, updated: Record<string, unknown>, changed: Map<string, unknown>): void => {
11
+ if (!fields) return
12
+ for (const key of Object.keys(fields)) {
13
+ const root = key.split('.')[0] as string
14
+ changed.set(root, updated[root])
15
+ }
16
+ }
17
+
18
+ const applyPullAll = (updated: Record<string, unknown>, fields: Record<string, unknown[]>, changed: Map<string, unknown>): void => {
19
+ for (const [field, values] of Object.entries(fields)) {
20
+ const arr = updated[field]
21
+ if (Array.isArray(arr)) {
22
+ const filtered = arr.filter((item: unknown) => !values.some((v) => JSON.stringify(v) === JSON.stringify(item)))
23
+ updated[field] = filtered
24
+ changed.set(field, filtered)
25
+ }
26
+ }
27
+ }
28
+
10
29
  export const assignUpdate = <T>(document: HydratedDocument<T>, update: UpdateQuery<T>, commands: Record<string, unknown>[]): HydratedDocument<T> => {
11
- let updated = assign(document.toObject(toObjectOptions), update)
30
+ let updated = assign(document.toObject(toObjectOptions), update) as Record<string, unknown>
31
+ const changedByCommand = new Map<string, unknown>()
32
+
12
33
  for (const command of commands) {
34
+ const op = Object.keys(command)[0] as string
35
+ const fields = command[op] as Record<string, unknown> | undefined
13
36
  try {
14
37
  updated = assign(updated, command)
38
+ trackChangedFields(fields, updated, changedByCommand)
15
39
  } catch {
16
- // we catch assign keys that are not implemented
40
+ if (op === '$pullAll' && fields) {
41
+ applyPullAll(updated, fields as Record<string, unknown[]>, changedByCommand)
42
+ }
17
43
  }
18
44
  }
19
45
 
20
46
  const doc = document.set(updated).toObject(toObjectOptions) as HydratedDocument<T> & { createdAt?: Date }
47
+ for (const [field, value] of changedByCommand) {
48
+ ;(doc as unknown as Record<string, unknown>)[field] = value
49
+ }
21
50
  if (update.createdAt) doc.createdAt = update.createdAt
22
51
  return doc
23
52
  }
@@ -41,19 +70,18 @@ export const splitUpdateAndCommands = <T>(updateQuery: UpdateWithAggregationPipe
41
70
  }
42
71
 
43
72
  export const updateHooksInitialize = <T>(schema: Schema<T>, opts: PluginOptions<T>): void => {
44
- schema.pre(updateMethods as MongooseQueryMiddleware[], async function (this: HookContext<T>) {
73
+ schema.pre(updateMethods as MongooseQueryMiddleware[], { document: false, query: true }, async function (this: HookContext<T>) {
45
74
  const options = this.getOptions()
46
75
  if (isHookIgnored(options)) return
47
76
 
48
77
  const model = this.model as Model<T>
49
78
  const filter = this.getFilter()
50
- const count = await this.model.countDocuments(filter).exec()
51
79
 
52
80
  this._context = {
53
81
  op: this.op,
54
- modelName: opts.modelName ?? this.model.modelName,
55
- collectionName: opts.collectionName ?? this.model.collection.collectionName,
56
- isNew: Boolean(options.upsert) && count === 0,
82
+ modelName: opts.modelName ?? model.modelName,
83
+ collectionName: opts.collectionName ?? model.collection.collectionName,
84
+ isNew: Boolean(options.upsert) && (await model.countDocuments(filter).exec()) === 0,
57
85
  ignoreEvent: options.ignoreEvent as boolean,
58
86
  ignorePatchHistory: options.ignorePatchHistory as boolean,
59
87
  }
@@ -68,9 +96,10 @@ export const updateHooksInitialize = <T>(schema: Schema<T>, opts: PluginOptions<
68
96
  })
69
97
  })
70
98
 
71
- schema.post(updateMethods as MongooseQueryMiddleware[], async function (this: HookContext<T>) {
99
+ schema.post(updateMethods as MongooseQueryMiddleware[], { document: false, query: true }, async function (this: HookContext<T>) {
72
100
  const options = this.getOptions()
73
101
  if (isHookIgnored(options)) return
102
+ if (!this._context) return
74
103
 
75
104
  if (!this._context.isNew) return
76
105
 
@@ -78,19 +107,13 @@ export const updateHooksInitialize = <T>(schema: Schema<T>, opts: PluginOptions<
78
107
  const updateQuery = this.getUpdate()
79
108
  const { update, commands } = splitUpdateAndCommands(updateQuery)
80
109
 
81
- let current: HydratedDocument<T> | null = null
82
110
  const filter = this.getFilter()
83
- const combined = assignUpdate(model.hydrate({}), update, commands)
84
- if (!isEmpty(update) && !current) {
85
- current = (await model.findOne(update).sort('desc').lean().exec()) as HydratedDocument<T>
86
- }
111
+ const candidates = [update, assignUpdate(model.hydrate({}), update, commands), filter]
87
112
 
88
- if (!isEmpty(combined) && !current) {
89
- current = (await model.findOne(combined).sort('desc').lean().exec()) as HydratedDocument<T>
90
- }
91
-
92
- if (!isEmpty(filter) && !current) {
93
- current = (await model.findOne(filter).sort('desc').lean().exec()) as HydratedDocument<T>
113
+ let current: HydratedDocument<T> | null = null
114
+ for (const query of candidates) {
115
+ if (current || isEmpty(query)) continue
116
+ current = (await model.findOne(query).sort({ _id: -1 }).lean().exec()) as HydratedDocument<T>
94
117
  }
95
118
 
96
119
  if (current) {