ts-patch-mongoose 3.1.0 → 4.0.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 (46) hide show
  1. package/README.md +39 -27
  2. package/dist/index.cjs +287 -32
  3. package/dist/index.d.cts +23 -1
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.mts +23 -1
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +287 -32
  8. package/package.json +19 -24
  9. package/biome.json +0 -47
  10. package/src/em.ts +0 -6
  11. package/src/helpers.ts +0 -174
  12. package/src/hooks/delete-hooks.ts +0 -49
  13. package/src/hooks/save-hooks.ts +0 -30
  14. package/src/hooks/update-hooks.ts +0 -125
  15. package/src/index.ts +0 -65
  16. package/src/model.ts +0 -50
  17. package/src/modules/power-assign.d.ts +0 -3
  18. package/src/ms.ts +0 -66
  19. package/src/omit-deep.ts +0 -56
  20. package/src/patch.ts +0 -154
  21. package/src/types.ts +0 -53
  22. package/src/version.ts +0 -13
  23. package/tests/constants/events.ts +0 -7
  24. package/tests/em.test.ts +0 -70
  25. package/tests/helpers.test.ts +0 -373
  26. package/tests/mongo/.gitignore +0 -3
  27. package/tests/mongo/server.ts +0 -31
  28. package/tests/ms.test.ts +0 -113
  29. package/tests/omit-deep.test.ts +0 -235
  30. package/tests/patch.test.ts +0 -200
  31. package/tests/plugin-all-features.test.ts +0 -844
  32. package/tests/plugin-complex-data.test.ts +0 -2647
  33. package/tests/plugin-event-created.test.ts +0 -371
  34. package/tests/plugin-event-deleted.test.ts +0 -400
  35. package/tests/plugin-event-updated.test.ts +0 -503
  36. package/tests/plugin-global.test.ts +0 -545
  37. package/tests/plugin-omit-all.test.ts +0 -349
  38. package/tests/plugin-patch-history-disabled.test.ts +0 -162
  39. package/tests/plugin-pre-delete.test.ts +0 -160
  40. package/tests/plugin-pre-save.test.ts +0 -54
  41. package/tests/plugin.test.ts +0 -576
  42. package/tests/schemas/Description.ts +0 -15
  43. package/tests/schemas/Product.ts +0 -38
  44. package/tests/schemas/User.ts +0 -22
  45. package/tsconfig.json +0 -32
  46. package/vite.config.mts +0 -24
package/README.md CHANGED
@@ -12,22 +12,28 @@ Patch history (audit log) & events plugin for mongoose
12
12
  [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=ilovepixelart_ts-patch-mongoose&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=ilovepixelart_ts-patch-mongoose)
13
13
  [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=ilovepixelart_ts-patch-mongoose&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=ilovepixelart_ts-patch-mongoose)
14
14
  [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=ilovepixelart_ts-patch-mongoose&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=ilovepixelart_ts-patch-mongoose)
15
+ \
16
+ [![Socket Badge](https://badge.socket.dev/npm/package/ts-patch-mongoose)](https://socket.dev/npm/package/ts-patch-mongoose)
17
+ [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/ilovepixelart/ts-patch-mongoose/badge)](https://securityscorecards.dev/viewer/?uri=github.com/ilovepixelart/ts-patch-mongoose)
18
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/12473/badge)](https://www.bestpractices.dev/en/projects/12473)
15
19
 
16
20
  ## Motivation
17
21
 
18
- ts-patch-mongoose is a plugin for mongoose
22
+ ts-patch-mongoose is a plugin for mongoose.
19
23
  \
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.
24
+ 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
25
 
22
26
  ## Supports and tested with
23
27
 
24
28
  ```json
25
29
  {
26
30
  "node": "20.x || 22.x || 24.x",
27
- "mongoose": ">=6.6.x || 7.x || 8.x || 9.x",
31
+ "mongoose": ">=6.6.0 <10"
28
32
  }
29
33
  ```
30
34
 
35
+ CI tests against mongoose `6.12.2`, `7.6.4`, `8.23.0`, and `9.4.1`.
36
+
31
37
  ## Features
32
38
 
33
39
  - Track changes in mongoose models
@@ -53,9 +59,9 @@ bun add ts-patch-mongoose mongoose
53
59
 
54
60
  Works with any Node.js framework — Express, Fastify, Koa, Hono, Nest, etc.
55
61
  \
56
- How to use it with express [ts-express-tsx](https://github.com/ilovepixelart/ts-express-tsx)
62
+ How to use it with Express: [ts-express-tsx](https://github.com/ilovepixelart/ts-express-tsx)
57
63
 
58
- Create your event constants `events.ts`
64
+ Create your event constants in `events.ts`
59
65
 
60
66
  ```typescript
61
67
  export const BOOK_CREATED = 'book-created'
@@ -77,65 +83,66 @@ export type Book = {
77
83
  }
78
84
  ```
79
85
 
80
- Setup your mongoose model `Book.ts`
86
+ Set up your mongoose model in `Book.ts`
81
87
 
82
88
  ```typescript
83
89
  import { Schema, model } from 'mongoose'
84
90
 
85
- import type { HydratedDocument, Types } from 'mongoose'
91
+ import type { HydratedDocument } from 'mongoose'
86
92
  import type { Book } from '../types'
87
93
 
88
94
  import { patchHistoryPlugin, setPatchHistoryTTL } from 'ts-patch-mongoose'
89
95
  import { BOOK_CREATED, BOOK_UPDATED, BOOK_DELETED } from '../constants/events'
90
96
 
91
- // You can set patch history TTL in plain english or in milliseconds as you wish.
97
+ // You can set patch history TTL in plain English or in milliseconds as you wish.
92
98
  // This will determine how long you want to keep patch history.
93
99
  // 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')
100
+ // Execute this method after you connected to your database somewhere in your application.
101
+ // Optional second argument for custom error handling
102
+ setPatchHistoryTTL('1 month', (error) => console.error('TTL setup failed:', error))
96
103
 
97
104
  const BookSchema = new Schema<Book>({
98
- name: {
99
- title: String,
105
+ title: {
106
+ type: String,
100
107
  required: true
101
108
  },
102
109
  description: {
103
110
  type: String,
104
111
  },
105
112
  authorId: {
106
- type: Types.ObjectId,
113
+ type: Schema.Types.ObjectId,
107
114
  required: true
108
115
  }
109
116
  }, { timestamps: true })
110
117
 
111
- BookSchema.plugin(patchHistoryPlugin, {
118
+ BookSchema.plugin(patchHistoryPlugin, {
112
119
  // Provide your event constants to plugin
113
120
  eventCreated: BOOK_CREATED,
114
121
  eventUpdated: BOOK_UPDATED,
115
122
  eventDeleted: BOOK_DELETED,
116
-
123
+
117
124
  // You can omit some properties in case you don't want to save them to patch history
118
125
  omit: ['__v', 'createdAt', 'updatedAt'],
119
126
 
120
- // Addition options for patchHistoryPlugin plugin
121
- // Everything bellow is optional and just shows you what you can do:
127
+ // Additional options for patchHistoryPlugin
128
+ // Everything below is optional and just shows you what you can do:
122
129
 
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
130
+ // Code below is an abstract example, you can use any other way to get user, reason, metadata
131
+ // 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
132
  getUser: async (doc: HydratedDocument<Book>) => {
126
133
  // For example: get user from http context
127
134
  // You should return an object, in case you want to save user to patch history
128
135
  return httpContext.get('user') as Record<string, unknown>
129
136
  },
130
137
 
131
- // Reason of document (create/update/delete) like: 'Excel upload', 'Manual update', 'API call', etc.
138
+ // Reason for the document change (create/update/delete) like: 'Excel upload', 'Manual update', 'API call', etc.
132
139
  getReason: async (doc: HydratedDocument<Book>) => {
133
140
  // 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
141
+ // You should return a string, in case you want to save reason to patch history
135
142
  return httpContext.get('reason') as string
136
143
  },
137
144
 
138
- // You can provide any information you want to save in along with patch history
145
+ // You can provide any information you want to save along with patch history
139
146
  getMetadata: async (doc: HydratedDocument<Book>) => {
140
147
  // For example: get metadata from http context, or any other place of your application
141
148
  // You should return an object, in case you want to save metadata to patch history
@@ -143,15 +150,20 @@ BookSchema.plugin(patchHistoryPlugin, {
143
150
  },
144
151
 
145
152
  // Do something before deleting documents
146
- // This method will be executed before deleting document or documents and always returns a nonempty array of documents
153
+ // This method will be executed before deleting document or documents and always returns a non-empty array of documents
147
154
  preDelete: async (docs) => {
148
155
  const bookIds = docs.map((doc) => doc._id)
149
156
  await SomeOtherModel.deleteMany({ bookId: { $in: bookIds } })
150
157
  },
151
158
 
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,
159
+ // Custom error handler for history write failures (defaults to console.error)
160
+ onError: (error) => {
161
+ console.error('Patch history error:', error)
162
+ },
163
+
164
+ // In case you just want to track changes in your models using events
165
+ // and don't want to save changes to patch history collection
166
+ // patchHistoryDisabled: true,
155
167
  })
156
168
 
157
169
  const Book = model('Book', BookSchema)
@@ -188,7 +200,7 @@ patchEventEmitter.on(BOOK_UPDATED, ({ doc, oldDoc, patch }) => {
188
200
  patchEventEmitter.on(BOOK_DELETED, ({ oldDoc }) => {
189
201
  try {
190
202
  console.log('Event - book deleted', oldDoc)
191
- // Do something with doc here
203
+ // Do something with oldDoc here
192
204
  } catch (error) {
193
205
  console.error(error)
194
206
  }
package/dist/index.cjs CHANGED
@@ -1,9 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var mongoose = require('mongoose');
4
- var jsonpatch = require('fast-json-patch');
5
4
  var EventEmitter = require('node:events');
6
- var powerAssign = require('power-assign');
7
5
 
8
6
  const HistorySchema = new mongoose.Schema(
9
7
  {
@@ -100,9 +98,10 @@ const ms = (val) => {
100
98
  if (str.length > 100) return Number.NaN;
101
99
  const match = RE.exec(str);
102
100
  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);
101
+ const [, numStr, unitStr] = match;
102
+ const n = Number.parseFloat(String(numStr));
103
+ const type = (unitStr ?? "ms").toLowerCase();
104
+ return n * UNITS[type];
106
105
  };
107
106
 
108
107
  const isArray = Array.isArray;
@@ -249,6 +248,94 @@ class PatchEventEmitter extends EventEmitter {
249
248
  }
250
249
  const em = new PatchEventEmitter();
251
250
 
251
+ const escapeToken = (key) => {
252
+ if (!key.includes("/") && !key.includes("~")) return key;
253
+ return key.replaceAll("~", "~0").replaceAll("/", "~1");
254
+ };
255
+ const joinPath = (base, key) => `${base}/${escapeToken(key)}`;
256
+ const cloneValue = (value) => {
257
+ if (value === void 0) return null;
258
+ if (value === null || typeof value !== "object") return value;
259
+ return JSON.parse(JSON.stringify(value));
260
+ };
261
+ const isContainer = (value) => {
262
+ return typeof value === "object" && value !== null;
263
+ };
264
+ const keysOf = (value) => {
265
+ if (Array.isArray(value)) {
266
+ const indices = [];
267
+ for (let i = 0; i < value.length; i++) indices.push(String(i));
268
+ return indices;
269
+ }
270
+ return Object.keys(value);
271
+ };
272
+ const normalizeTarget = (target) => {
273
+ if (!isContainer(target) || Array.isArray(target)) return target;
274
+ const withToJSON = target;
275
+ return typeof withToJSON.toJSON === "function" ? withToJSON.toJSON() : target;
276
+ };
277
+ const emitTest = (path, value, invertible, out) => {
278
+ if (invertible) out.push({ op: "test", path, value: cloneValue(value) });
279
+ };
280
+ const emitReplace = (path, source, target, invertible, out) => {
281
+ emitTest(path, source, invertible, out);
282
+ out.push({ op: "replace", path, value: cloneValue(target) });
283
+ };
284
+ const emitRemove = (path, source, invertible, out) => {
285
+ emitTest(path, source, invertible, out);
286
+ out.push({ op: "remove", path });
287
+ };
288
+ const shouldTreatAsRemoval = (sourceChild, targetChild, sourceIsArray) => {
289
+ return targetChild === void 0 && sourceChild !== void 0 && !sourceIsArray;
290
+ };
291
+ const diffSourceKey = (scope, key) => {
292
+ const { source, target, targetKeySet, basePath, sourceIsArray, invertible, out } = scope;
293
+ const childPath = joinPath(basePath, key);
294
+ const sourceChild = source[key];
295
+ if (!targetKeySet.has(key)) {
296
+ emitRemove(childPath, sourceChild, invertible, out);
297
+ return;
298
+ }
299
+ const targetChild = target[key];
300
+ if (shouldTreatAsRemoval(sourceChild, targetChild, sourceIsArray)) {
301
+ emitRemove(childPath, sourceChild, invertible, out);
302
+ return;
303
+ }
304
+ diff(sourceChild, targetChild, childPath, invertible, out);
305
+ };
306
+ const diffAddedKeys = (target, sourceKeys, targetKeys, basePath, out) => {
307
+ const sourceKeySet = new Set(sourceKeys);
308
+ for (const key of targetKeys) {
309
+ if (sourceKeySet.has(key)) continue;
310
+ const targetChild = target[key];
311
+ if (targetChild === void 0) continue;
312
+ out.push({ op: "add", path: joinPath(basePath, key), value: cloneValue(targetChild) });
313
+ }
314
+ };
315
+ const diff = (source, target, basePath, invertible, out) => {
316
+ if (source === target) return;
317
+ const resolvedTarget = normalizeTarget(target);
318
+ const sourceIsArray = Array.isArray(source);
319
+ const targetIsArray = Array.isArray(resolvedTarget);
320
+ if (!isContainer(source) || !isContainer(resolvedTarget) || sourceIsArray !== targetIsArray) {
321
+ emitReplace(basePath, source, resolvedTarget, invertible, out);
322
+ return;
323
+ }
324
+ const sourceKeys = keysOf(source);
325
+ const targetKeys = keysOf(resolvedTarget);
326
+ const targetKeySet = new Set(targetKeys);
327
+ const scope = { source, target: resolvedTarget, targetKeySet, basePath, sourceIsArray, invertible, out };
328
+ for (const key of Array.from(sourceKeys).reverse()) {
329
+ diffSourceKey(scope, key);
330
+ }
331
+ diffAddedKeys(resolvedTarget, sourceKeys, targetKeys, basePath, out);
332
+ };
333
+ const compare = (source, target, invertible = false) => {
334
+ const out = [];
335
+ diff(source, target, "", invertible, out);
336
+ return out;
337
+ };
338
+
252
339
  const isPlainObject = (val) => {
253
340
  if (Object.prototype.toString.call(val) !== "[object Object]") return false;
254
341
  const prot = Object.getPrototypeOf(val);
@@ -379,13 +466,13 @@ const updatePatch = async (opts, context, current, original) => {
379
466
  const currentObject = getJsonOmit(opts, current);
380
467
  const originalObject = getJsonOmit(opts, original);
381
468
  if (isEmpty(originalObject) || isEmpty(currentObject)) return;
382
- const patch = jsonpatch.compare(originalObject, currentObject, true);
469
+ const patch = compare(originalObject, currentObject, true);
383
470
  if (isEmpty(patch)) return;
384
471
  emitEvent(context, opts.eventUpdated, { oldDoc: original, doc: current, patch });
385
472
  if (history) {
386
473
  let version = 0;
387
474
  const lastHistory = await HistoryModel.findOne({ collectionId: original._id }).sort("-version").exec();
388
- if (lastHistory && lastHistory.version >= 0) {
475
+ if (lastHistory) {
389
476
  version = lastHistory.version + 1;
390
477
  }
391
478
  const [user, reason, metadata] = await getData(opts, current);
@@ -468,38 +555,202 @@ const saveHooksInitialize = (schema, opts) => {
468
555
  });
469
556
  };
470
557
 
558
+ const hasOwn = Object.prototype.hasOwnProperty;
559
+ const parseSegment = (segment) => {
560
+ const asNumber2 = Number(segment);
561
+ return Number.isInteger(asNumber2) && String(asNumber2) === segment ? asNumber2 : segment;
562
+ };
563
+ const parsePath = (path) => {
564
+ const [first, ...rest] = path.split(".").map(parseSegment);
565
+ let leaf = first ?? path;
566
+ const crumbs = [];
567
+ for (const key of rest) {
568
+ crumbs.push({ key: leaf, nextNumeric: typeof key === "number" });
569
+ leaf = key;
570
+ }
571
+ return { leaf, crumbs };
572
+ };
573
+ const deepEqualJson = (a, b) => {
574
+ try {
575
+ return JSON.stringify(a) === JSON.stringify(b);
576
+ } catch {
577
+ return a === b;
578
+ }
579
+ };
580
+ const ensureContainer = (parent, key, hintNumeric) => {
581
+ const existing = parent[key];
582
+ if (existing !== null && typeof existing === "object") {
583
+ return existing;
584
+ }
585
+ const created = hintNumeric ? [] : {};
586
+ parent[key] = created;
587
+ return created;
588
+ };
589
+ const setAtPath = (doc, path, value) => {
590
+ const { leaf, crumbs } = parsePath(path);
591
+ let cursor = doc;
592
+ for (const crumb of crumbs) {
593
+ cursor = ensureContainer(cursor, crumb.key, crumb.nextNumeric);
594
+ }
595
+ cursor[leaf] = value;
596
+ };
597
+ const getAtPath = (doc, path) => {
598
+ const { leaf, crumbs } = parsePath(path);
599
+ let cursor = doc;
600
+ for (const crumb of crumbs) {
601
+ const next = cursor[crumb.key];
602
+ if (next === null || typeof next !== "object") return void 0;
603
+ cursor = next;
604
+ }
605
+ return { container: cursor, leaf, exists: hasOwn.call(cursor, leaf) };
606
+ };
607
+ const unsetAtPath = (doc, path) => {
608
+ const located = getAtPath(doc, path);
609
+ if (!located) return;
610
+ if (Array.isArray(located.container)) {
611
+ located.container[located.leaf] = void 0;
612
+ } else {
613
+ delete located.container[located.leaf];
614
+ }
615
+ };
616
+ const asNumber = (value) => typeof value === "number" ? value : 0;
617
+ const toArray = (value) => Array.isArray(value) ? value : void 0;
618
+ const shouldReplaceForMin = (current, candidate) => {
619
+ if (candidate === void 0) return false;
620
+ if (current === void 0) return true;
621
+ return candidate < current;
622
+ };
623
+ const shouldReplaceForMax = (current, candidate) => {
624
+ if (candidate === void 0) return false;
625
+ if (current === void 0) return true;
626
+ return candidate > current;
627
+ };
628
+ const operators = {
629
+ $set: (doc, path, value) => setAtPath(doc, path, value),
630
+ $unset: (doc, path) => unsetAtPath(doc, path),
631
+ $inc: (doc, path, delta) => {
632
+ const located = getAtPath(doc, path);
633
+ const current = located?.exists ? asNumber(located.container[located.leaf]) : 0;
634
+ setAtPath(doc, path, current + asNumber(delta));
635
+ },
636
+ $mul: (doc, path, factor) => {
637
+ const located = getAtPath(doc, path);
638
+ const current = located?.exists ? asNumber(located.container[located.leaf]) : 0;
639
+ setAtPath(doc, path, current * asNumber(factor));
640
+ },
641
+ $min: (doc, path, candidate) => {
642
+ const located = getAtPath(doc, path);
643
+ if (!located?.exists) {
644
+ setAtPath(doc, path, candidate);
645
+ return;
646
+ }
647
+ if (shouldReplaceForMin(located.container[located.leaf], candidate)) {
648
+ setAtPath(doc, path, candidate);
649
+ }
650
+ },
651
+ $max: (doc, path, candidate) => {
652
+ const located = getAtPath(doc, path);
653
+ if (!located?.exists) {
654
+ setAtPath(doc, path, candidate);
655
+ return;
656
+ }
657
+ if (shouldReplaceForMax(located.container[located.leaf], candidate)) {
658
+ setAtPath(doc, path, candidate);
659
+ }
660
+ },
661
+ $rename: (doc, path, newPath) => {
662
+ if (typeof newPath !== "string") return;
663
+ const located = getAtPath(doc, path);
664
+ if (!located?.exists) return;
665
+ const value = located.container[located.leaf];
666
+ unsetAtPath(doc, path);
667
+ setAtPath(doc, newPath, value);
668
+ },
669
+ $currentDate: (doc, path, spec) => {
670
+ const wantTimestamp = typeof spec === "object" && spec !== null && spec.$type === "timestamp";
671
+ setAtPath(doc, path, wantTimestamp ? Date.now() : /* @__PURE__ */ new Date());
672
+ },
673
+ $push: (doc, path, pushSpec) => {
674
+ const located = getAtPath(doc, path);
675
+ const existing = located?.exists ? toArray(located.container[located.leaf]) ?? [] : [];
676
+ const next = [...existing];
677
+ if (typeof pushSpec === "object" && pushSpec !== null && "$each" in pushSpec && Array.isArray(pushSpec.$each)) {
678
+ next.push(...pushSpec.$each);
679
+ } else {
680
+ next.push(pushSpec);
681
+ }
682
+ setAtPath(doc, path, next);
683
+ },
684
+ $addToSet: (doc, path, item) => {
685
+ const located = getAtPath(doc, path);
686
+ const existing = located?.exists ? toArray(located.container[located.leaf]) ?? [] : [];
687
+ const next = [...existing];
688
+ const rawItems = typeof item === "object" && item !== null && "$each" in item ? item.$each : item;
689
+ const toAdd = Array.isArray(rawItems) ? rawItems : [rawItems];
690
+ for (const candidate of toAdd) {
691
+ if (!next.some((existingEntry) => deepEqualJson(existingEntry, candidate))) {
692
+ next.push(candidate);
693
+ }
694
+ }
695
+ setAtPath(doc, path, next);
696
+ },
697
+ $pull: (doc, path, matcher) => {
698
+ const located = getAtPath(doc, path);
699
+ const existing = located?.exists ? toArray(located.container[located.leaf]) : void 0;
700
+ if (!existing) return;
701
+ const filtered = existing.filter((entry) => !deepEqualJson(entry, matcher));
702
+ setAtPath(doc, path, filtered);
703
+ },
704
+ $pullAll: (doc, path, values) => {
705
+ const located = getAtPath(doc, path);
706
+ const existing = located?.exists ? toArray(located.container[located.leaf]) : void 0;
707
+ if (!existing || !Array.isArray(values)) return;
708
+ const filtered = existing.filter((entry) => !values.some((target) => deepEqualJson(entry, target)));
709
+ setAtPath(doc, path, filtered);
710
+ },
711
+ $pop: (doc, path, direction) => {
712
+ const located = getAtPath(doc, path);
713
+ const existing = located?.exists ? toArray(located.container[located.leaf]) : void 0;
714
+ if (!existing || existing.length === 0) return;
715
+ const next = direction === -1 ? existing.slice(1) : existing.slice(0, -1);
716
+ setAtPath(doc, path, next);
717
+ }
718
+ };
719
+ const applyOperator = (doc, operator, fields) => {
720
+ const fn = operators[operator];
721
+ if (!fn || fields === null || typeof fields !== "object") return;
722
+ for (const [path, argument] of Object.entries(fields)) {
723
+ fn(doc, path, argument);
724
+ }
725
+ };
726
+ const applyUpdate = (doc, update) => {
727
+ const result = { ...doc };
728
+ for (const [key, value] of Object.entries(update)) {
729
+ if (key.startsWith("$")) {
730
+ applyOperator(result, key, value);
731
+ } else {
732
+ setAtPath(result, key, value);
733
+ }
734
+ }
735
+ return result;
736
+ };
737
+
471
738
  const updateMethods = ["update", "updateOne", "replaceOne", "updateMany", "findOneAndUpdate", "findOneAndReplace", "findByIdAndUpdate"];
472
739
  const trackChangedFields = (fields, updated, changed) => {
473
740
  if (!fields) return;
474
741
  for (const key of Object.keys(fields)) {
475
- const root = key.split(".")[0];
742
+ const [root = key] = key.split(".");
476
743
  changed.set(root, updated[root]);
477
744
  }
478
745
  };
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
- };
489
746
  const assignUpdate = (document, update, commands) => {
490
- let updated = powerAssign.assign(document.toObject(toObjectOptions), update);
747
+ let updated = applyUpdate(document.toObject(toObjectOptions), update);
491
748
  const changedByCommand = /* @__PURE__ */ new Map();
492
749
  for (const command of commands) {
493
- const op = Object.keys(command)[0];
750
+ const [op = ""] = Object.keys(command);
494
751
  const fields = command[op];
495
- try {
496
- updated = powerAssign.assign(updated, command);
497
- trackChangedFields(fields, updated, changedByCommand);
498
- } catch {
499
- if (op === "$pullAll" && fields) {
500
- applyPullAll(updated, fields, changedByCommand);
501
- }
502
- }
752
+ updated = applyUpdate(updated, command);
753
+ trackChangedFields(fields, updated, changedByCommand);
503
754
  }
504
755
  const doc = document.set(updated).toObject(toObjectOptions);
505
756
  for (const [field, value] of changedByCommand) {
@@ -554,11 +805,14 @@ const updateHooksInitialize = (schema, opts) => {
554
805
  const updateQuery = this.getUpdate();
555
806
  const { update, commands } = splitUpdateAndCommands(updateQuery);
556
807
  const filter = this.getFilter();
557
- const candidates = [update, assignUpdate(model.hydrate({}), update, commands), filter];
808
+ const simulated = assignUpdate(model.hydrate({}), update, commands);
809
+ const simulatedFilter = Object.fromEntries(Object.entries(simulated).filter(([, v]) => v !== void 0));
810
+ const candidates = [update, simulatedFilter, filter];
558
811
  let current = null;
559
812
  for (const query of candidates) {
560
813
  if (current || isEmpty(query)) continue;
561
- current = await model.findOne(query).sort({ _id: -1 }).lean().exec();
814
+ const found = await model.findOne(query).sort({ _id: -1 }).lean().exec();
815
+ current = found;
562
816
  }
563
817
  if (current) {
564
818
  this._context.createdDocs = [current];
@@ -590,13 +844,14 @@ const patchHistoryPlugin = (schema, opts) => {
590
844
  await createPatch(opts, context);
591
845
  });
592
846
  if (isMongooseLessThan8) {
593
- schema.pre(remove, { document: true, query: false }, async function() {
847
+ const legacySchema = schema;
848
+ legacySchema.pre(remove, { document: true, query: false }, async function() {
594
849
  const original = this.toObject(toObjectOptions);
595
850
  if (opts.preDelete && !isEmpty(original)) {
596
851
  await opts.preDelete([original]);
597
852
  }
598
853
  });
599
- schema.post(remove, { document: true, query: false }, async function() {
854
+ legacySchema.post(remove, { document: true, query: false }, async function() {
600
855
  const original = this.toObject(toObjectOptions);
601
856
  const model = this.constructor;
602
857
  const context = {
package/dist/index.d.cts CHANGED
@@ -1,7 +1,27 @@
1
1
  import { Types, Query, HydratedDocument, Schema } from 'mongoose';
2
- import { Operation } from 'fast-json-patch';
3
2
  import EventEmitter from 'node:events';
4
3
 
4
+ interface AddOperation<T> {
5
+ op: 'add';
6
+ path: string;
7
+ value: T;
8
+ }
9
+ interface RemoveOperation {
10
+ op: 'remove';
11
+ path: string;
12
+ }
13
+ interface ReplaceOperation<T> {
14
+ op: 'replace';
15
+ path: string;
16
+ value: T;
17
+ }
18
+ interface TestOperation<T> {
19
+ op: 'test';
20
+ path: string;
21
+ value: T;
22
+ }
23
+ type Operation = AddOperation<unknown> | RemoveOperation | ReplaceOperation<unknown> | TestOperation<unknown>;
24
+
5
25
  interface History {
6
26
  op: string;
7
27
  modelName: string;
@@ -13,6 +33,8 @@ interface History {
13
33
  reason?: string;
14
34
  metadata?: object;
15
35
  patch?: Operation[];
36
+ createdAt?: Date;
37
+ updatedAt?: Date;
16
38
  }
17
39
  interface PatchEvent<T> {
18
40
  oldDoc?: HydratedDocument<T>;
@@ -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;sBAClC,KAAK;;;ACjDzB,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":[]}
1
+ {"version":3,"file":"index.d.cts","sources":["../src/json-patch.ts","../src/types.ts","../src/em.ts","../src/ms.ts","../src/helpers.ts","../src/index.ts"],"mappings":";;;AAAM,UAAW,YAAY;;;;;AAMvB,UAAW,eAAe;;;;AAK1B,UAAW,gBAAgB;;;;;AAM3B,UAAW,aAAa;;;;;AAMxB,KAAM,SAAS,GAAG,YAAY,YAAY,eAAe,GAAG,gBAAgB,YAAY,aAAa;;ACpBrG,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
@@ -1,7 +1,27 @@
1
1
  import { Types, Query, HydratedDocument, Schema } from 'mongoose';
2
- import { Operation } from 'fast-json-patch';
3
2
  import EventEmitter from 'node:events';
4
3
 
4
+ interface AddOperation<T> {
5
+ op: 'add';
6
+ path: string;
7
+ value: T;
8
+ }
9
+ interface RemoveOperation {
10
+ op: 'remove';
11
+ path: string;
12
+ }
13
+ interface ReplaceOperation<T> {
14
+ op: 'replace';
15
+ path: string;
16
+ value: T;
17
+ }
18
+ interface TestOperation<T> {
19
+ op: 'test';
20
+ path: string;
21
+ value: T;
22
+ }
23
+ type Operation = AddOperation<unknown> | RemoveOperation | ReplaceOperation<unknown> | TestOperation<unknown>;
24
+
5
25
  interface History {
6
26
  op: string;
7
27
  modelName: string;
@@ -13,6 +33,8 @@ interface History {
13
33
  reason?: string;
14
34
  metadata?: object;
15
35
  patch?: Operation[];
36
+ createdAt?: Date;
37
+ updatedAt?: Date;
16
38
  }
17
39
  interface PatchEvent<T> {
18
40
  oldDoc?: HydratedDocument<T>;
@@ -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;sBAClC,KAAK;;;ACjDzB,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":[]}
1
+ {"version":3,"file":"index.d.mts","sources":["../src/json-patch.ts","../src/types.ts","../src/em.ts","../src/ms.ts","../src/helpers.ts","../src/index.ts"],"mappings":";;;AAAM,UAAW,YAAY;;;;;AAMvB,UAAW,eAAe;;;;AAK1B,UAAW,gBAAgB;;;;;AAM3B,UAAW,aAAa;;;;;AAMxB,KAAM,SAAS,GAAG,YAAY,YAAY,eAAe,GAAG,gBAAgB,YAAY,aAAa;;ACpBrG,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":[]}