ts-patch-mongoose 3.0.0 → 3.1.2

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 (41) hide show
  1. package/README.md +31 -25
  2. package/dist/index.cjs +112 -115
  3. package/dist/index.d.cts +4 -1
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.mts +4 -1
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +112 -115
  8. package/package.json +14 -15
  9. package/src/helpers.ts +16 -3
  10. package/src/hooks/delete-hooks.ts +5 -4
  11. package/src/hooks/update-hooks.ts +47 -19
  12. package/src/index.ts +8 -5
  13. package/src/ms.ts +4 -3
  14. package/src/omit-deep.ts +24 -63
  15. package/src/patch.ts +30 -33
  16. package/src/types.ts +3 -0
  17. package/biome.json +0 -50
  18. package/tests/constants/events.ts +0 -7
  19. package/tests/em.test.ts +0 -54
  20. package/tests/helpers.test.ts +0 -311
  21. package/tests/mongo/.gitignore +0 -3
  22. package/tests/mongo/server.ts +0 -31
  23. package/tests/ms.test.ts +0 -113
  24. package/tests/omit-deep.test.ts +0 -220
  25. package/tests/patch.test.ts +0 -199
  26. package/tests/plugin-all-features.test.ts +0 -741
  27. package/tests/plugin-complex-data.test.ts +0 -1332
  28. package/tests/plugin-event-created.test.ts +0 -371
  29. package/tests/plugin-event-deleted.test.ts +0 -400
  30. package/tests/plugin-event-updated.test.ts +0 -503
  31. package/tests/plugin-global.test.ts +0 -545
  32. package/tests/plugin-omit-all.test.ts +0 -349
  33. package/tests/plugin-patch-history-disabled.test.ts +0 -162
  34. package/tests/plugin-pre-delete.test.ts +0 -160
  35. package/tests/plugin-pre-save.test.ts +0 -54
  36. package/tests/plugin.test.ts +0 -576
  37. package/tests/schemas/Description.ts +0 -15
  38. package/tests/schemas/Product.ts +0 -38
  39. package/tests/schemas/User.ts +0 -22
  40. package/tsconfig.json +0 -32
  41. package/vite.config.mts +0 -23
package/README.md CHANGED
@@ -15,16 +15,16 @@ Patch history (audit log) & events plugin for mongoose
15
15
 
16
16
  ## Motivation
17
17
 
18
- ts-patch-mongoose is a plugin for mongoose
18
+ ts-patch-mongoose is a plugin for mongoose.
19
19
  \
20
- I need to track changes of mongoose models and save them as patch history (audit log) in separate collection. Changes must also emit events that I can subscribe to and react in other parts of my application. I also want to omit some fields from patch history.
20
+ I need to track changes of mongoose models and save them as patch history (audit log) in a separate collection. Changes must also emit events that I can subscribe to and react in other parts of my application. I also want to omit some fields from patch history.
21
21
 
22
22
  ## Supports and tested with
23
23
 
24
24
  ```json
25
25
  {
26
26
  "node": "20.x || 22.x || 24.x",
27
- "mongoose": ">=6.6.x || 7.x || 8.x || 9.x",
27
+ "mongoose": ">=6.6.0 || 7.x || 8.x || 9.x",
28
28
  }
29
29
  ```
30
30
 
@@ -53,9 +53,9 @@ bun add ts-patch-mongoose mongoose
53
53
 
54
54
  Works with any Node.js framework — Express, Fastify, Koa, Hono, Nest, etc.
55
55
  \
56
- How to use it with express [ts-express-tsx](https://github.com/ilovepixelart/ts-express-tsx)
56
+ How to use it with Express: [ts-express-tsx](https://github.com/ilovepixelart/ts-express-tsx)
57
57
 
58
- Create your event constants `events.ts`
58
+ Create your event constants in `events.ts`
59
59
 
60
60
  ```typescript
61
61
  export const BOOK_CREATED = 'book-created'
@@ -77,33 +77,34 @@ export type Book = {
77
77
  }
78
78
  ```
79
79
 
80
- Setup your mongoose model `Book.ts`
80
+ Set up your mongoose model in `Book.ts`
81
81
 
82
82
  ```typescript
83
83
  import { Schema, model } from 'mongoose'
84
84
 
85
- import type { HydratedDocument, Types } from 'mongoose'
85
+ import type { HydratedDocument } from 'mongoose'
86
86
  import type { Book } from '../types'
87
87
 
88
88
  import { patchHistoryPlugin, setPatchHistoryTTL } from 'ts-patch-mongoose'
89
89
  import { BOOK_CREATED, BOOK_UPDATED, BOOK_DELETED } from '../constants/events'
90
90
 
91
- // You can set patch history TTL in plain english or in milliseconds as you wish.
91
+ // You can set patch history TTL in plain English or in milliseconds as you wish.
92
92
  // This will determine how long you want to keep patch history.
93
93
  // You don't need to use this global config in case you want to keep patch history forever.
94
- // Execute this method after you connected to you database somewhere in your application.
95
- setPatchHistoryTTL('1 month')
94
+ // Execute this method after you connected to your database somewhere in your application.
95
+ // Optional second argument for custom error handling
96
+ setPatchHistoryTTL('1 month', (error) => console.error('TTL setup failed:', error))
96
97
 
97
98
  const BookSchema = new Schema<Book>({
98
- name: {
99
- title: String,
99
+ title: {
100
+ type: String,
100
101
  required: true
101
102
  },
102
103
  description: {
103
104
  type: String,
104
105
  },
105
106
  authorId: {
106
- type: Types.ObjectId,
107
+ type: Schema.Types.ObjectId,
107
108
  required: true
108
109
  }
109
110
  }, { timestamps: true })
@@ -117,25 +118,25 @@ BookSchema.plugin(patchHistoryPlugin, {
117
118
  // You can omit some properties in case you don't want to save them to patch history
118
119
  omit: ['__v', 'createdAt', 'updatedAt'],
119
120
 
120
- // Addition options for patchHistoryPlugin plugin
121
- // Everything bellow is optional and just shows you what you can do:
121
+ // Additional options for patchHistoryPlugin
122
+ // Everything below is optional and just shows you what you can do:
122
123
 
123
- // Code bellow is abstract example, you can use any other way to get user, reason, metadata
124
- // These three properties will be added to patch history document automatically and give you flexibility to track who, why and when made changes to your documents
124
+ // Code below is an abstract example, you can use any other way to get user, reason, metadata
125
+ // These three properties will be added to patch history document automatically and gives you flexibility to track who, why and when made changes to your documents
125
126
  getUser: async (doc: HydratedDocument<Book>) => {
126
127
  // For example: get user from http context
127
128
  // You should return an object, in case you want to save user to patch history
128
129
  return httpContext.get('user') as Record<string, unknown>
129
130
  },
130
131
 
131
- // Reason of document (create/update/delete) like: 'Excel upload', 'Manual update', 'API call', etc.
132
+ // Reason for the document change (create/update/delete) like: 'Excel upload', 'Manual update', 'API call', etc.
132
133
  getReason: async (doc: HydratedDocument<Book>) => {
133
134
  // For example: get reason from http context, or any other place of your application
134
- // You shout return a string, in case you want to save reason to patch history
135
+ // You should return a string, in case you want to save reason to patch history
135
136
  return httpContext.get('reason') as string
136
137
  },
137
138
 
138
- // You can provide any information you want to save in along with patch history
139
+ // You can provide any information you want to save along with patch history
139
140
  getMetadata: async (doc: HydratedDocument<Book>) => {
140
141
  // For example: get metadata from http context, or any other place of your application
141
142
  // You should return an object, in case you want to save metadata to patch history
@@ -143,15 +144,20 @@ BookSchema.plugin(patchHistoryPlugin, {
143
144
  },
144
145
 
145
146
  // Do something before deleting documents
146
- // This method will be executed before deleting document or documents and always returns a nonempty array of documents
147
+ // This method will be executed before deleting document or documents and always returns a non-empty array of documents
147
148
  preDelete: async (docs) => {
148
149
  const bookIds = docs.map((doc) => doc._id)
149
150
  await SomeOtherModel.deleteMany({ bookId: { $in: bookIds } })
150
151
  },
151
152
 
152
- // In case you just want to track changes in your models using events below.
153
- // And don't want to save changes to patch history collection
154
- patchHistoryDisabled: true,
153
+ // Custom error handler for history write failures (defaults to console.error)
154
+ onError: (error) => {
155
+ console.error('Patch history error:', error)
156
+ },
157
+
158
+ // In case you just want to track changes in your models using events
159
+ // and don't want to save changes to patch history collection
160
+ // patchHistoryDisabled: true,
155
161
  })
156
162
 
157
163
  const Book = model('Book', BookSchema)
@@ -188,7 +194,7 @@ patchEventEmitter.on(BOOK_UPDATED, ({ doc, oldDoc, patch }) => {
188
194
  patchEventEmitter.on(BOOK_DELETED, ({ oldDoc }) => {
189
195
  try {
190
196
  console.log('Event - book deleted', oldDoc)
191
- // Do something with doc here
197
+ // Do something with oldDoc here
192
198
  } catch (error) {
193
199
  console.error(error)
194
200
  }
package/dist/index.cjs CHANGED
@@ -100,9 +100,10 @@ const ms = (val) => {
100
100
  if (str.length > 100) return Number.NaN;
101
101
  const match = RE.exec(str);
102
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);
103
+ const [, numStr, unitStr] = match;
104
+ const n = Number.parseFloat(String(numStr));
105
+ const type = (unitStr ?? "ms").toLowerCase();
106
+ return n * UNITS[type];
106
107
  };
107
108
 
108
109
  const isArray = Array.isArray;
@@ -140,6 +141,12 @@ const cloneImmutable = (value) => {
140
141
  cloned.lastIndex = re.lastIndex;
141
142
  return cloned;
142
143
  }
144
+ case "[object Error]": {
145
+ const err = value;
146
+ const cloned = new err.constructor(err.message);
147
+ if (err.stack) cloned.stack = err.stack;
148
+ return cloned;
149
+ }
143
150
  case "[object ArrayBuffer]":
144
151
  return cloneArrayBuffer(value);
145
152
  case "[object DataView]": {
@@ -188,7 +195,11 @@ const cloneDeep = (value, seen = /* @__PURE__ */ new WeakMap()) => {
188
195
  if (seen.has(value)) return seen.get(value);
189
196
  const immutable = cloneImmutable(value);
190
197
  if (immutable !== void 0) return immutable;
191
- if ("toJSON" in value && typeof value.toJSON === "function") {
198
+ const record = value;
199
+ if (typeof record._bsontype === "string" && typeof record.toHexString === "function") {
200
+ return new value.constructor(record.toHexString());
201
+ }
202
+ if (typeof record.toJSON === "function") {
192
203
  return JSON.parse(JSON.stringify(value));
193
204
  }
194
205
  return cloneCollection(value, seen);
@@ -207,7 +218,7 @@ const toObjectOptions = {
207
218
  depopulate: true,
208
219
  virtuals: false
209
220
  };
210
- const setPatchHistoryTTL = async (ttl) => {
221
+ const setPatchHistoryTTL = async (ttl, onError) => {
211
222
  const name = "createdAt_1_TTL";
212
223
  try {
213
224
  const indexes = await HistoryModel.collection.indexes();
@@ -230,7 +241,8 @@ const setPatchHistoryTTL = async (ttl) => {
230
241
  }
231
242
  await HistoryModel.collection.createIndex({ createdAt: 1 }, { expireAfterSeconds, name });
232
243
  } catch (err) {
233
- console.error("Couldn't create or update index for history collection", err);
244
+ const handler = onError ?? console.error;
245
+ handler(err);
234
246
  }
235
247
  };
236
248
 
@@ -246,113 +258,65 @@ const isPlainObject = (val) => {
246
258
  const isUnsafeKey = (key) => {
247
259
  return key === "__proto__" || key === "constructor" || key === "prototype";
248
260
  };
249
- const getValue$1 = (obj, path) => {
250
- const segs = path.split(".");
251
- let current = obj;
252
- for (const seg of segs) {
253
- if (current == null || typeof current !== "object") return void 0;
254
- current = current[seg];
255
- }
256
- return current;
257
- };
258
- const hasValue = (val) => {
259
- if (val == null) return false;
260
- if (typeof val === "boolean" || typeof val === "number" || typeof val === "function") return true;
261
- if (typeof val === "string") return val.length !== 0;
262
- if (Array.isArray(val)) return val.length !== 0;
263
- if (val instanceof RegExp) return val.source !== "(?:)" && val.source !== "";
264
- if (val instanceof Error) return val.message !== "";
265
- if (val instanceof Map || val instanceof Set) return val.size !== 0;
266
- if (typeof val === "object") {
267
- for (const key of Object.keys(val)) {
268
- if (hasValue(val[key])) return true;
269
- }
270
- return false;
271
- }
272
- return true;
273
- };
274
- const has = (obj, path) => {
275
- if (obj != null && typeof obj === "object" && typeof path === "string") {
276
- return hasValue(getValue$1(obj, path));
277
- }
278
- return false;
279
- };
280
- const unset = (obj, prop) => {
281
- if (typeof obj !== "object" || obj === null) return false;
282
- if (Object.hasOwn(obj, prop)) {
283
- delete obj[prop];
284
- return true;
285
- }
286
- if (has(obj, prop)) {
287
- const segs = prop.split(".");
288
- let last = segs.pop();
289
- while (segs.length && segs.at(-1)?.slice(-1) === "\\") {
290
- last = `${segs.pop().slice(0, -1)}.${last}`;
291
- }
292
- let target = obj;
293
- while (segs.length) {
294
- const seg = segs.shift();
295
- if (isUnsafeKey(seg)) return false;
296
- target = target[seg];
261
+ const classifyKeys = (omitKeys) => {
262
+ const topLevel = /* @__PURE__ */ new Set();
263
+ const nested = /* @__PURE__ */ new Map();
264
+ for (const key of omitKeys) {
265
+ const dotIdx = key.indexOf(".");
266
+ if (dotIdx === -1) {
267
+ topLevel.add(key);
268
+ } else {
269
+ const head = key.slice(0, dotIdx);
270
+ const tail = key.slice(dotIdx + 1);
271
+ if (!isUnsafeKey(head)) {
272
+ const existing = nested.get(head) ?? [];
273
+ existing.push(tail);
274
+ nested.set(head, existing);
275
+ }
297
276
  }
298
- return delete target[last ?? ""];
299
277
  }
300
- return true;
278
+ return { topLevel, nested };
301
279
  };
302
280
  const omitDeep = (value, keys) => {
303
281
  if (value === void 0) return {};
304
282
  if (Array.isArray(value)) {
305
- for (let i = 0; i < value.length; i++) {
306
- value[i] = omitDeep(value[i], keys);
307
- }
308
- return value;
283
+ return value.map((item) => omitDeep(item, keys));
309
284
  }
310
285
  if (!isPlainObject(value)) return value;
311
286
  const omitKeys = typeof keys === "string" ? [keys] : keys;
312
287
  if (!Array.isArray(omitKeys)) return value;
313
- for (const key of omitKeys) {
314
- unset(value, key);
315
- }
288
+ const { topLevel, nested } = classifyKeys(omitKeys);
289
+ const result = {};
316
290
  for (const key of Object.keys(value)) {
317
- value[key] = omitDeep(value[key], omitKeys);
291
+ if (topLevel.has(key)) continue;
292
+ const nestedKeys = nested.get(key);
293
+ result[key] = omitDeep(value[key], nestedKeys ?? omitKeys);
318
294
  }
319
- return value;
295
+ return result;
320
296
  };
321
297
 
322
298
  const isPatchHistoryEnabled = (opts, context) => {
323
299
  return !opts.patchHistoryDisabled && !context.ignorePatchHistory;
324
300
  };
301
+ const applyOmit = (object, opts) => {
302
+ return opts.omit ? omitDeep(object, opts.omit) : object;
303
+ };
304
+ const replacer = (_key, value) => typeof value === "bigint" ? value.toString() : value;
325
305
  const getJsonOmit = (opts, doc) => {
326
- const object = JSON.parse(JSON.stringify(doc));
327
- if (opts.omit) {
328
- return omitDeep(object, opts.omit);
329
- }
330
- return object;
306
+ return applyOmit(JSON.parse(JSON.stringify(doc, replacer)), opts);
331
307
  };
332
308
  const getObjectOmit = (opts, doc) => {
333
- if (opts.omit) {
334
- return omitDeep(isFunction(doc?.toObject) ? doc.toObject() : doc, opts.omit);
335
- }
336
- return doc;
337
- };
338
- const getUser = async (opts, doc) => {
339
- if (isFunction(opts.getUser)) {
340
- return await opts.getUser(doc);
341
- }
342
- return void 0;
343
- };
344
- const getReason = async (opts, doc) => {
345
- if (isFunction(opts.getReason)) {
346
- return await opts.getReason(doc);
347
- }
348
- return void 0;
309
+ return applyOmit(isFunction(doc?.toObject) ? doc.toObject() : doc, opts);
349
310
  };
350
- const getMetadata = async (opts, doc) => {
351
- if (isFunction(opts.getMetadata)) {
352
- return await opts.getMetadata(doc);
311
+ const getOptionalField = async (fn, doc) => {
312
+ if (isFunction(fn)) {
313
+ return await fn(doc);
353
314
  }
354
315
  return void 0;
355
316
  };
317
+ const getUser = async (opts, doc) => getOptionalField(opts.getUser, doc);
318
+ const getReason = async (opts, doc) => getOptionalField(opts.getReason, doc);
319
+ const getMetadata = async (opts, doc) => getOptionalField(opts.getMetadata, doc);
356
320
  const getValue = (item) => {
357
321
  return item.status === "fulfilled" ? item.value : void 0;
358
322
  };
@@ -363,7 +327,10 @@ const getData = async (opts, doc) => {
363
327
  };
364
328
  const emitEvent = (context, event, data) => {
365
329
  if (event && !context.ignoreEvent) {
366
- em.emit(event, data);
330
+ try {
331
+ em.emit(event, data);
332
+ } catch {
333
+ }
367
334
  }
368
335
  };
369
336
  const bulkPatch = async (opts, context, eventKey, docsKey) => {
@@ -376,7 +343,8 @@ const bulkPatch = async (opts, context, eventKey, docsKey) => {
376
343
  for (const batch of chunks) {
377
344
  const bulk = [];
378
345
  for (const doc of batch) {
379
- emitEvent(context, event, { [key]: doc });
346
+ const omitted = getObjectOmit(opts, doc);
347
+ emitEvent(context, event, { [key]: omitted });
380
348
  if (history) {
381
349
  const [user, reason, metadata] = await getData(opts, doc);
382
350
  bulk.push({
@@ -386,7 +354,7 @@ const bulkPatch = async (opts, context, eventKey, docsKey) => {
386
354
  modelName: context.modelName,
387
355
  collectionName: context.collectionName,
388
356
  collectionId: doc._id,
389
- doc: getObjectOmit(opts, doc),
357
+ doc: omitted,
390
358
  version: 0,
391
359
  ...user !== void 0 && { user },
392
360
  ...reason !== void 0 && { reason },
@@ -397,8 +365,9 @@ const bulkPatch = async (opts, context, eventKey, docsKey) => {
397
365
  }
398
366
  }
399
367
  if (history && !isEmpty(bulk)) {
368
+ const onError = opts.onError ?? console.error;
400
369
  await HistoryModel.bulkWrite(bulk, { ordered: false }).catch((error) => {
401
- console.error(error.message);
370
+ onError(error);
402
371
  });
403
372
  }
404
373
  }
@@ -417,10 +386,11 @@ const updatePatch = async (opts, context, current, original) => {
417
386
  if (history) {
418
387
  let version = 0;
419
388
  const lastHistory = await HistoryModel.findOne({ collectionId: original._id }).sort("-version").exec();
420
- if (lastHistory && lastHistory.version >= 0) {
389
+ if (lastHistory) {
421
390
  version = lastHistory.version + 1;
422
391
  }
423
392
  const [user, reason, metadata] = await getData(opts, current);
393
+ const onError = opts.onError ?? console.error;
424
394
  await HistoryModel.create({
425
395
  op: context.op,
426
396
  modelName: context.modelName,
@@ -431,6 +401,8 @@ const updatePatch = async (opts, context, current, original) => {
431
401
  ...user !== void 0 && { user },
432
402
  ...reason !== void 0 && { reason },
433
403
  ...metadata !== void 0 && { metadata }
404
+ }).catch((error) => {
405
+ onError(error);
434
406
  });
435
407
  }
436
408
  };
@@ -447,8 +419,8 @@ const deleteHooksInitialize = (schema, opts) => {
447
419
  const filter = this.getFilter();
448
420
  this._context = {
449
421
  op: this.op,
450
- modelName: opts.modelName ?? this.model.modelName,
451
- collectionName: opts.collectionName ?? this.model.collection.collectionName,
422
+ modelName: opts.modelName ?? model.modelName,
423
+ collectionName: opts.collectionName ?? model.collection.collectionName,
452
424
  ignoreEvent: options.ignoreEvent,
453
425
  ignorePatchHistory: options.ignorePatchHistory
454
426
  };
@@ -464,12 +436,13 @@ const deleteHooksInitialize = (schema, opts) => {
464
436
  }
465
437
  }
466
438
  if (opts.preDelete && isArray(this._context.deletedDocs) && !isEmpty(this._context.deletedDocs)) {
467
- await opts.preDelete(this._context.deletedDocs);
439
+ await opts.preDelete(cloneDeep(this._context.deletedDocs));
468
440
  }
469
441
  });
470
442
  schema.post(deleteMethods, { document: false, query: true }, async function() {
471
443
  const options = this.getOptions();
472
444
  if (isHookIgnored(options)) return;
445
+ if (!this._context) return;
473
446
  await deletePatch(opts, this._context);
474
447
  });
475
448
  };
@@ -497,15 +470,42 @@ const saveHooksInitialize = (schema, opts) => {
497
470
  };
498
471
 
499
472
  const updateMethods = ["update", "updateOne", "replaceOne", "updateMany", "findOneAndUpdate", "findOneAndReplace", "findByIdAndUpdate"];
473
+ const trackChangedFields = (fields, updated, changed) => {
474
+ if (!fields) return;
475
+ for (const key of Object.keys(fields)) {
476
+ const [root = key] = key.split(".");
477
+ changed.set(root, updated[root]);
478
+ }
479
+ };
480
+ const applyPullAll = (updated, fields, changed) => {
481
+ for (const [field, values] of Object.entries(fields)) {
482
+ const arr = updated[field];
483
+ if (Array.isArray(arr)) {
484
+ const filtered = arr.filter((item) => !values.some((v) => JSON.stringify(v) === JSON.stringify(item)));
485
+ updated[field] = filtered;
486
+ changed.set(field, filtered);
487
+ }
488
+ }
489
+ };
500
490
  const assignUpdate = (document, update, commands) => {
501
491
  let updated = powerAssign.assign(document.toObject(toObjectOptions), update);
492
+ const changedByCommand = /* @__PURE__ */ new Map();
502
493
  for (const command of commands) {
494
+ const [op = ""] = Object.keys(command);
495
+ const fields = command[op];
503
496
  try {
504
497
  updated = powerAssign.assign(updated, command);
498
+ trackChangedFields(fields, updated, changedByCommand);
505
499
  } catch {
500
+ if (op === "$pullAll" && fields) {
501
+ applyPullAll(updated, fields, changedByCommand);
502
+ }
506
503
  }
507
504
  }
508
505
  const doc = document.set(updated).toObject(toObjectOptions);
506
+ for (const [field, value] of changedByCommand) {
507
+ doc[field] = value;
508
+ }
509
509
  if (update.createdAt) doc.createdAt = update.createdAt;
510
510
  return doc;
511
511
  };
@@ -525,17 +525,16 @@ const splitUpdateAndCommands = (updateQuery) => {
525
525
  return { update, commands };
526
526
  };
527
527
  const updateHooksInitialize = (schema, opts) => {
528
- schema.pre(updateMethods, async function() {
528
+ schema.pre(updateMethods, { document: false, query: true }, async function() {
529
529
  const options = this.getOptions();
530
530
  if (isHookIgnored(options)) return;
531
531
  const model = this.model;
532
532
  const filter = this.getFilter();
533
- const count = await this.model.countDocuments(filter).exec();
534
533
  this._context = {
535
534
  op: this.op,
536
- modelName: opts.modelName ?? this.model.modelName,
537
- collectionName: opts.collectionName ?? this.model.collection.collectionName,
538
- isNew: Boolean(options.upsert) && count === 0,
535
+ modelName: opts.modelName ?? model.modelName,
536
+ collectionName: opts.collectionName ?? model.collection.collectionName,
537
+ isNew: Boolean(options.upsert) && await model.countDocuments(filter).exec() === 0,
539
538
  ignoreEvent: options.ignoreEvent,
540
539
  ignorePatchHistory: options.ignorePatchHistory
541
540
  };
@@ -547,24 +546,21 @@ const updateHooksInitialize = (schema, opts) => {
547
546
  await updatePatch(opts, this._context, assignUpdate(doc, update, commands), origDoc);
548
547
  });
549
548
  });
550
- schema.post(updateMethods, async function() {
549
+ schema.post(updateMethods, { document: false, query: true }, async function() {
551
550
  const options = this.getOptions();
552
551
  if (isHookIgnored(options)) return;
552
+ if (!this._context) return;
553
553
  if (!this._context.isNew) return;
554
554
  const model = this.model;
555
555
  const updateQuery = this.getUpdate();
556
556
  const { update, commands } = splitUpdateAndCommands(updateQuery);
557
- let current = null;
558
557
  const filter = this.getFilter();
559
- const combined = assignUpdate(model.hydrate({}), update, commands);
560
- if (!isEmpty(update) && !current) {
561
- current = await model.findOne(update).sort("desc").lean().exec();
562
- }
563
- if (!isEmpty(combined) && !current) {
564
- current = await model.findOne(combined).sort("desc").lean().exec();
565
- }
566
- if (!isEmpty(filter) && !current) {
567
- current = await model.findOne(filter).sort("desc").lean().exec();
558
+ const candidates = [update, assignUpdate(model.hydrate({}), update, commands), filter];
559
+ let current = null;
560
+ for (const query of candidates) {
561
+ if (current || isEmpty(query)) continue;
562
+ const found = await model.findOne(query).sort({ _id: -1 }).lean().exec();
563
+ current = found;
568
564
  }
569
565
  if (current) {
570
566
  this._context.createdDocs = [current];
@@ -596,13 +592,14 @@ const patchHistoryPlugin = (schema, opts) => {
596
592
  await createPatch(opts, context);
597
593
  });
598
594
  if (isMongooseLessThan8) {
599
- schema.pre(remove, { document: true, query: false }, async function() {
595
+ const legacySchema = schema;
596
+ legacySchema.pre(remove, { document: true, query: false }, async function() {
600
597
  const original = this.toObject(toObjectOptions);
601
598
  if (opts.preDelete && !isEmpty(original)) {
602
599
  await opts.preDelete([original]);
603
600
  }
604
601
  });
605
- schema.post(remove, { document: true, query: false }, async function() {
602
+ legacySchema.post(remove, { document: true, query: false }, async function() {
606
603
  const original = this.toObject(toObjectOptions);
607
604
  const model = this.constructor;
608
605
  const context = {
package/dist/index.d.cts CHANGED
@@ -13,6 +13,8 @@ interface History {
13
13
  reason?: string;
14
14
  metadata?: object;
15
15
  patch?: Operation[];
16
+ createdAt?: Date;
17
+ updatedAt?: Date;
16
18
  }
17
19
  interface PatchEvent<T> {
18
20
  oldDoc?: HydratedDocument<T>;
@@ -47,6 +49,7 @@ interface PluginOptions<T> {
47
49
  omit?: string[];
48
50
  patchHistoryDisabled?: boolean;
49
51
  preDelete?: (docs: HydratedDocument<T>[]) => Promise<void>;
52
+ onError?: (error: Error) => void;
50
53
  }
51
54
 
52
55
  declare class PatchEventEmitter extends EventEmitter {
@@ -92,7 +95,7 @@ declare const UNITS: {
92
95
  type Unit = keyof typeof UNITS;
93
96
  type Duration = number | `${number}` | `${number}${Unit}` | `${number} ${Unit}`;
94
97
 
95
- declare const setPatchHistoryTTL: (ttl: Duration) => Promise<void>;
98
+ declare const setPatchHistoryTTL: (ttl: Duration, onError?: (error: Error) => void) => Promise<void>;
96
99
 
97
100
  declare const patchHistoryPlugin: <T>(schema: Schema<T>, opts: PluginOptions<T>) => void;
98
101
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","sources":["../src/types.ts","../src/em.ts","../src/ms.ts","../src/helpers.ts","../src/index.ts"],"mappings":";;;;AAGM,UAAW,OAAO;;;;kBAIR,KAAK,CAAC,QAAQ;;;;;;YAMpB,SAAS;;AAGb,UAAW,UAAU;aAChB,gBAAgB;UACnB,gBAAgB;YACd,SAAS;;AAGb,UAAW,YAAY;;;;;kBAKb,gBAAgB;kBAChB,gBAAgB;;;;AAK1B,KAAM,WAAW,MAAM,KAAK;;cAAiC,YAAY;;AAEzE,KAAM,IAAI,GAAG,MAAM;AAEnB,KAAM,QAAQ,GAAG,MAAM;AAEvB,UAAW,aAAa;;;;;;oBAMZ,gBAAgB,QAAQ,OAAO,CAAC,IAAI,IAAI,IAAI;sBAC1C,gBAAgB,QAAQ,OAAO;wBAC7B,gBAAgB,QAAQ,OAAO,CAAC,QAAQ,IAAI,QAAQ;;;uBAGrD,gBAAgB,UAAU,OAAO;;;AChDtD,cAAM,iBAAkB,SAAQ,YAAY;;AAC5C,cAAM,EAAE,mBAA0B;;ACKlC,cAAa,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCZ,KAAM,IAAI,gBAAgB,KAAK;AAE/B,KAAM,QAAQ,sCAAsC,IAAI,kBAAkB,IAAI;;ACiFpF,cAAa,kBAAkB,QAAe,QAAQ,KAAG,OAAO;;AC9GhE,cAAa,kBAAkB,cAAe,MAAM,WAAW,aAAa","names":[]}
1
+ {"version":3,"file":"index.d.cts","sources":["../src/types.ts","../src/em.ts","../src/ms.ts","../src/helpers.ts","../src/index.ts"],"mappings":";;;;AAGM,UAAW,OAAO;;;;kBAIR,KAAK,CAAC,QAAQ;;;;;;YAMpB,SAAS;gBACL,IAAI;gBACJ,IAAI;;AAGZ,UAAW,UAAU;aAChB,gBAAgB;UACnB,gBAAgB;YACd,SAAS;;AAGb,UAAW,YAAY;;;;;kBAKb,gBAAgB;kBAChB,gBAAgB;;;;AAK1B,KAAM,WAAW,MAAM,KAAK;;cAAiC,YAAY;;AAEzE,KAAM,IAAI,GAAG,MAAM;AAEnB,KAAM,QAAQ,GAAG,MAAM;AAEvB,UAAW,aAAa;;;;;;oBAMZ,gBAAgB,QAAQ,OAAO,CAAC,IAAI,IAAI,IAAI;sBAC1C,gBAAgB,QAAQ,OAAO;wBAC7B,gBAAgB,QAAQ,OAAO,CAAC,QAAQ,IAAI,QAAQ;;;uBAGrD,gBAAgB,UAAU,OAAO;sBAClC,KAAK;;;ACnDzB,cAAM,iBAAkB,SAAQ,YAAY;;AAC5C,cAAM,EAAE,mBAA0B;;ACKlC,cAAa,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCZ,KAAM,IAAI,gBAAgB,KAAK;AAE/B,KAAM,QAAQ,sCAAsC,IAAI,kBAAkB,IAAI;;AC6FpF,cAAa,kBAAkB,QAAe,QAAQ,oBAAoB,KAAK,cAAY,OAAO;;AC1HlG,cAAa,kBAAkB,cAAe,MAAM,WAAW,aAAa","names":[]}
package/dist/index.d.mts CHANGED
@@ -13,6 +13,8 @@ interface History {
13
13
  reason?: string;
14
14
  metadata?: object;
15
15
  patch?: Operation[];
16
+ createdAt?: Date;
17
+ updatedAt?: Date;
16
18
  }
17
19
  interface PatchEvent<T> {
18
20
  oldDoc?: HydratedDocument<T>;
@@ -47,6 +49,7 @@ interface PluginOptions<T> {
47
49
  omit?: string[];
48
50
  patchHistoryDisabled?: boolean;
49
51
  preDelete?: (docs: HydratedDocument<T>[]) => Promise<void>;
52
+ onError?: (error: Error) => void;
50
53
  }
51
54
 
52
55
  declare class PatchEventEmitter extends EventEmitter {
@@ -92,7 +95,7 @@ declare const UNITS: {
92
95
  type Unit = keyof typeof UNITS;
93
96
  type Duration = number | `${number}` | `${number}${Unit}` | `${number} ${Unit}`;
94
97
 
95
- declare const setPatchHistoryTTL: (ttl: Duration) => Promise<void>;
98
+ declare const setPatchHistoryTTL: (ttl: Duration, onError?: (error: Error) => void) => Promise<void>;
96
99
 
97
100
  declare const patchHistoryPlugin: <T>(schema: Schema<T>, opts: PluginOptions<T>) => void;
98
101
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","sources":["../src/types.ts","../src/em.ts","../src/ms.ts","../src/helpers.ts","../src/index.ts"],"mappings":";;;;AAGM,UAAW,OAAO;;;;kBAIR,KAAK,CAAC,QAAQ;;;;;;YAMpB,SAAS;;AAGb,UAAW,UAAU;aAChB,gBAAgB;UACnB,gBAAgB;YACd,SAAS;;AAGb,UAAW,YAAY;;;;;kBAKb,gBAAgB;kBAChB,gBAAgB;;;;AAK1B,KAAM,WAAW,MAAM,KAAK;;cAAiC,YAAY;;AAEzE,KAAM,IAAI,GAAG,MAAM;AAEnB,KAAM,QAAQ,GAAG,MAAM;AAEvB,UAAW,aAAa;;;;;;oBAMZ,gBAAgB,QAAQ,OAAO,CAAC,IAAI,IAAI,IAAI;sBAC1C,gBAAgB,QAAQ,OAAO;wBAC7B,gBAAgB,QAAQ,OAAO,CAAC,QAAQ,IAAI,QAAQ;;;uBAGrD,gBAAgB,UAAU,OAAO;;;AChDtD,cAAM,iBAAkB,SAAQ,YAAY;;AAC5C,cAAM,EAAE,mBAA0B;;ACKlC,cAAa,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCZ,KAAM,IAAI,gBAAgB,KAAK;AAE/B,KAAM,QAAQ,sCAAsC,IAAI,kBAAkB,IAAI;;ACiFpF,cAAa,kBAAkB,QAAe,QAAQ,KAAG,OAAO;;AC9GhE,cAAa,kBAAkB,cAAe,MAAM,WAAW,aAAa","names":[]}
1
+ {"version":3,"file":"index.d.mts","sources":["../src/types.ts","../src/em.ts","../src/ms.ts","../src/helpers.ts","../src/index.ts"],"mappings":";;;;AAGM,UAAW,OAAO;;;;kBAIR,KAAK,CAAC,QAAQ;;;;;;YAMpB,SAAS;gBACL,IAAI;gBACJ,IAAI;;AAGZ,UAAW,UAAU;aAChB,gBAAgB;UACnB,gBAAgB;YACd,SAAS;;AAGb,UAAW,YAAY;;;;;kBAKb,gBAAgB;kBAChB,gBAAgB;;;;AAK1B,KAAM,WAAW,MAAM,KAAK;;cAAiC,YAAY;;AAEzE,KAAM,IAAI,GAAG,MAAM;AAEnB,KAAM,QAAQ,GAAG,MAAM;AAEvB,UAAW,aAAa;;;;;;oBAMZ,gBAAgB,QAAQ,OAAO,CAAC,IAAI,IAAI,IAAI;sBAC1C,gBAAgB,QAAQ,OAAO;wBAC7B,gBAAgB,QAAQ,OAAO,CAAC,QAAQ,IAAI,QAAQ;;;uBAGrD,gBAAgB,UAAU,OAAO;sBAClC,KAAK;;;ACnDzB,cAAM,iBAAkB,SAAQ,YAAY;;AAC5C,cAAM,EAAE,mBAA0B;;ACKlC,cAAa,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCZ,KAAM,IAAI,gBAAgB,KAAK;AAE/B,KAAM,QAAQ,sCAAsC,IAAI,kBAAkB,IAAI;;AC6FpF,cAAa,kBAAkB,QAAe,QAAQ,oBAAoB,KAAK,cAAY,OAAO;;AC1HlG,cAAa,kBAAkB,cAAe,MAAM,WAAW,aAAa","names":[]}