ts-patch-mongoose 3.1.2 → 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.
package/dist/index.mjs CHANGED
@@ -1,7 +1,5 @@
1
1
  import mongoose, { Schema, model } from 'mongoose';
2
- import jsonpatch from 'fast-json-patch';
3
2
  import EventEmitter from 'node:events';
4
- import { assign } from 'power-assign';
5
3
 
6
4
  const HistorySchema = new Schema(
7
5
  {
@@ -248,6 +246,94 @@ class PatchEventEmitter extends EventEmitter {
248
246
  }
249
247
  const em = new PatchEventEmitter();
250
248
 
249
+ const escapeToken = (key) => {
250
+ if (!key.includes("/") && !key.includes("~")) return key;
251
+ return key.replaceAll("~", "~0").replaceAll("/", "~1");
252
+ };
253
+ const joinPath = (base, key) => `${base}/${escapeToken(key)}`;
254
+ const cloneValue = (value) => {
255
+ if (value === void 0) return null;
256
+ if (value === null || typeof value !== "object") return value;
257
+ return JSON.parse(JSON.stringify(value));
258
+ };
259
+ const isContainer = (value) => {
260
+ return typeof value === "object" && value !== null;
261
+ };
262
+ const keysOf = (value) => {
263
+ if (Array.isArray(value)) {
264
+ const indices = [];
265
+ for (let i = 0; i < value.length; i++) indices.push(String(i));
266
+ return indices;
267
+ }
268
+ return Object.keys(value);
269
+ };
270
+ const normalizeTarget = (target) => {
271
+ if (!isContainer(target) || Array.isArray(target)) return target;
272
+ const withToJSON = target;
273
+ return typeof withToJSON.toJSON === "function" ? withToJSON.toJSON() : target;
274
+ };
275
+ const emitTest = (path, value, invertible, out) => {
276
+ if (invertible) out.push({ op: "test", path, value: cloneValue(value) });
277
+ };
278
+ const emitReplace = (path, source, target, invertible, out) => {
279
+ emitTest(path, source, invertible, out);
280
+ out.push({ op: "replace", path, value: cloneValue(target) });
281
+ };
282
+ const emitRemove = (path, source, invertible, out) => {
283
+ emitTest(path, source, invertible, out);
284
+ out.push({ op: "remove", path });
285
+ };
286
+ const shouldTreatAsRemoval = (sourceChild, targetChild, sourceIsArray) => {
287
+ return targetChild === void 0 && sourceChild !== void 0 && !sourceIsArray;
288
+ };
289
+ const diffSourceKey = (scope, key) => {
290
+ const { source, target, targetKeySet, basePath, sourceIsArray, invertible, out } = scope;
291
+ const childPath = joinPath(basePath, key);
292
+ const sourceChild = source[key];
293
+ if (!targetKeySet.has(key)) {
294
+ emitRemove(childPath, sourceChild, invertible, out);
295
+ return;
296
+ }
297
+ const targetChild = target[key];
298
+ if (shouldTreatAsRemoval(sourceChild, targetChild, sourceIsArray)) {
299
+ emitRemove(childPath, sourceChild, invertible, out);
300
+ return;
301
+ }
302
+ diff(sourceChild, targetChild, childPath, invertible, out);
303
+ };
304
+ const diffAddedKeys = (target, sourceKeys, targetKeys, basePath, out) => {
305
+ const sourceKeySet = new Set(sourceKeys);
306
+ for (const key of targetKeys) {
307
+ if (sourceKeySet.has(key)) continue;
308
+ const targetChild = target[key];
309
+ if (targetChild === void 0) continue;
310
+ out.push({ op: "add", path: joinPath(basePath, key), value: cloneValue(targetChild) });
311
+ }
312
+ };
313
+ const diff = (source, target, basePath, invertible, out) => {
314
+ if (source === target) return;
315
+ const resolvedTarget = normalizeTarget(target);
316
+ const sourceIsArray = Array.isArray(source);
317
+ const targetIsArray = Array.isArray(resolvedTarget);
318
+ if (!isContainer(source) || !isContainer(resolvedTarget) || sourceIsArray !== targetIsArray) {
319
+ emitReplace(basePath, source, resolvedTarget, invertible, out);
320
+ return;
321
+ }
322
+ const sourceKeys = keysOf(source);
323
+ const targetKeys = keysOf(resolvedTarget);
324
+ const targetKeySet = new Set(targetKeys);
325
+ const scope = { source, target: resolvedTarget, targetKeySet, basePath, sourceIsArray, invertible, out };
326
+ for (const key of Array.from(sourceKeys).reverse()) {
327
+ diffSourceKey(scope, key);
328
+ }
329
+ diffAddedKeys(resolvedTarget, sourceKeys, targetKeys, basePath, out);
330
+ };
331
+ const compare = (source, target, invertible = false) => {
332
+ const out = [];
333
+ diff(source, target, "", invertible, out);
334
+ return out;
335
+ };
336
+
251
337
  const isPlainObject = (val) => {
252
338
  if (Object.prototype.toString.call(val) !== "[object Object]") return false;
253
339
  const prot = Object.getPrototypeOf(val);
@@ -378,7 +464,7 @@ const updatePatch = async (opts, context, current, original) => {
378
464
  const currentObject = getJsonOmit(opts, current);
379
465
  const originalObject = getJsonOmit(opts, original);
380
466
  if (isEmpty(originalObject) || isEmpty(currentObject)) return;
381
- const patch = jsonpatch.compare(originalObject, currentObject, true);
467
+ const patch = compare(originalObject, currentObject, true);
382
468
  if (isEmpty(patch)) return;
383
469
  emitEvent(context, opts.eventUpdated, { oldDoc: original, doc: current, patch });
384
470
  if (history) {
@@ -467,6 +553,186 @@ const saveHooksInitialize = (schema, opts) => {
467
553
  });
468
554
  };
469
555
 
556
+ const hasOwn = Object.prototype.hasOwnProperty;
557
+ const parseSegment = (segment) => {
558
+ const asNumber2 = Number(segment);
559
+ return Number.isInteger(asNumber2) && String(asNumber2) === segment ? asNumber2 : segment;
560
+ };
561
+ const parsePath = (path) => {
562
+ const [first, ...rest] = path.split(".").map(parseSegment);
563
+ let leaf = first ?? path;
564
+ const crumbs = [];
565
+ for (const key of rest) {
566
+ crumbs.push({ key: leaf, nextNumeric: typeof key === "number" });
567
+ leaf = key;
568
+ }
569
+ return { leaf, crumbs };
570
+ };
571
+ const deepEqualJson = (a, b) => {
572
+ try {
573
+ return JSON.stringify(a) === JSON.stringify(b);
574
+ } catch {
575
+ return a === b;
576
+ }
577
+ };
578
+ const ensureContainer = (parent, key, hintNumeric) => {
579
+ const existing = parent[key];
580
+ if (existing !== null && typeof existing === "object") {
581
+ return existing;
582
+ }
583
+ const created = hintNumeric ? [] : {};
584
+ parent[key] = created;
585
+ return created;
586
+ };
587
+ const setAtPath = (doc, path, value) => {
588
+ const { leaf, crumbs } = parsePath(path);
589
+ let cursor = doc;
590
+ for (const crumb of crumbs) {
591
+ cursor = ensureContainer(cursor, crumb.key, crumb.nextNumeric);
592
+ }
593
+ cursor[leaf] = value;
594
+ };
595
+ const getAtPath = (doc, path) => {
596
+ const { leaf, crumbs } = parsePath(path);
597
+ let cursor = doc;
598
+ for (const crumb of crumbs) {
599
+ const next = cursor[crumb.key];
600
+ if (next === null || typeof next !== "object") return void 0;
601
+ cursor = next;
602
+ }
603
+ return { container: cursor, leaf, exists: hasOwn.call(cursor, leaf) };
604
+ };
605
+ const unsetAtPath = (doc, path) => {
606
+ const located = getAtPath(doc, path);
607
+ if (!located) return;
608
+ if (Array.isArray(located.container)) {
609
+ located.container[located.leaf] = void 0;
610
+ } else {
611
+ delete located.container[located.leaf];
612
+ }
613
+ };
614
+ const asNumber = (value) => typeof value === "number" ? value : 0;
615
+ const toArray = (value) => Array.isArray(value) ? value : void 0;
616
+ const shouldReplaceForMin = (current, candidate) => {
617
+ if (candidate === void 0) return false;
618
+ if (current === void 0) return true;
619
+ return candidate < current;
620
+ };
621
+ const shouldReplaceForMax = (current, candidate) => {
622
+ if (candidate === void 0) return false;
623
+ if (current === void 0) return true;
624
+ return candidate > current;
625
+ };
626
+ const operators = {
627
+ $set: (doc, path, value) => setAtPath(doc, path, value),
628
+ $unset: (doc, path) => unsetAtPath(doc, path),
629
+ $inc: (doc, path, delta) => {
630
+ const located = getAtPath(doc, path);
631
+ const current = located?.exists ? asNumber(located.container[located.leaf]) : 0;
632
+ setAtPath(doc, path, current + asNumber(delta));
633
+ },
634
+ $mul: (doc, path, factor) => {
635
+ const located = getAtPath(doc, path);
636
+ const current = located?.exists ? asNumber(located.container[located.leaf]) : 0;
637
+ setAtPath(doc, path, current * asNumber(factor));
638
+ },
639
+ $min: (doc, path, candidate) => {
640
+ const located = getAtPath(doc, path);
641
+ if (!located?.exists) {
642
+ setAtPath(doc, path, candidate);
643
+ return;
644
+ }
645
+ if (shouldReplaceForMin(located.container[located.leaf], candidate)) {
646
+ setAtPath(doc, path, candidate);
647
+ }
648
+ },
649
+ $max: (doc, path, candidate) => {
650
+ const located = getAtPath(doc, path);
651
+ if (!located?.exists) {
652
+ setAtPath(doc, path, candidate);
653
+ return;
654
+ }
655
+ if (shouldReplaceForMax(located.container[located.leaf], candidate)) {
656
+ setAtPath(doc, path, candidate);
657
+ }
658
+ },
659
+ $rename: (doc, path, newPath) => {
660
+ if (typeof newPath !== "string") return;
661
+ const located = getAtPath(doc, path);
662
+ if (!located?.exists) return;
663
+ const value = located.container[located.leaf];
664
+ unsetAtPath(doc, path);
665
+ setAtPath(doc, newPath, value);
666
+ },
667
+ $currentDate: (doc, path, spec) => {
668
+ const wantTimestamp = typeof spec === "object" && spec !== null && spec.$type === "timestamp";
669
+ setAtPath(doc, path, wantTimestamp ? Date.now() : /* @__PURE__ */ new Date());
670
+ },
671
+ $push: (doc, path, pushSpec) => {
672
+ const located = getAtPath(doc, path);
673
+ const existing = located?.exists ? toArray(located.container[located.leaf]) ?? [] : [];
674
+ const next = [...existing];
675
+ if (typeof pushSpec === "object" && pushSpec !== null && "$each" in pushSpec && Array.isArray(pushSpec.$each)) {
676
+ next.push(...pushSpec.$each);
677
+ } else {
678
+ next.push(pushSpec);
679
+ }
680
+ setAtPath(doc, path, next);
681
+ },
682
+ $addToSet: (doc, path, item) => {
683
+ const located = getAtPath(doc, path);
684
+ const existing = located?.exists ? toArray(located.container[located.leaf]) ?? [] : [];
685
+ const next = [...existing];
686
+ const rawItems = typeof item === "object" && item !== null && "$each" in item ? item.$each : item;
687
+ const toAdd = Array.isArray(rawItems) ? rawItems : [rawItems];
688
+ for (const candidate of toAdd) {
689
+ if (!next.some((existingEntry) => deepEqualJson(existingEntry, candidate))) {
690
+ next.push(candidate);
691
+ }
692
+ }
693
+ setAtPath(doc, path, next);
694
+ },
695
+ $pull: (doc, path, matcher) => {
696
+ const located = getAtPath(doc, path);
697
+ const existing = located?.exists ? toArray(located.container[located.leaf]) : void 0;
698
+ if (!existing) return;
699
+ const filtered = existing.filter((entry) => !deepEqualJson(entry, matcher));
700
+ setAtPath(doc, path, filtered);
701
+ },
702
+ $pullAll: (doc, path, values) => {
703
+ const located = getAtPath(doc, path);
704
+ const existing = located?.exists ? toArray(located.container[located.leaf]) : void 0;
705
+ if (!existing || !Array.isArray(values)) return;
706
+ const filtered = existing.filter((entry) => !values.some((target) => deepEqualJson(entry, target)));
707
+ setAtPath(doc, path, filtered);
708
+ },
709
+ $pop: (doc, path, direction) => {
710
+ const located = getAtPath(doc, path);
711
+ const existing = located?.exists ? toArray(located.container[located.leaf]) : void 0;
712
+ if (!existing || existing.length === 0) return;
713
+ const next = direction === -1 ? existing.slice(1) : existing.slice(0, -1);
714
+ setAtPath(doc, path, next);
715
+ }
716
+ };
717
+ const applyOperator = (doc, operator, fields) => {
718
+ const fn = operators[operator];
719
+ if (!fn || fields === null || typeof fields !== "object") return;
720
+ for (const [path, argument] of Object.entries(fields)) {
721
+ fn(doc, path, argument);
722
+ }
723
+ };
724
+ const applyUpdate = (doc, update) => {
725
+ const result = { ...doc };
726
+ for (const [key, value] of Object.entries(update)) {
727
+ if (key.startsWith("$")) {
728
+ applyOperator(result, key, value);
729
+ } else {
730
+ setAtPath(result, key, value);
731
+ }
732
+ }
733
+ return result;
734
+ };
735
+
470
736
  const updateMethods = ["update", "updateOne", "replaceOne", "updateMany", "findOneAndUpdate", "findOneAndReplace", "findByIdAndUpdate"];
471
737
  const trackChangedFields = (fields, updated, changed) => {
472
738
  if (!fields) return;
@@ -475,30 +741,14 @@ const trackChangedFields = (fields, updated, changed) => {
475
741
  changed.set(root, updated[root]);
476
742
  }
477
743
  };
478
- const applyPullAll = (updated, fields, changed) => {
479
- for (const [field, values] of Object.entries(fields)) {
480
- const arr = updated[field];
481
- if (Array.isArray(arr)) {
482
- const filtered = arr.filter((item) => !values.some((v) => JSON.stringify(v) === JSON.stringify(item)));
483
- updated[field] = filtered;
484
- changed.set(field, filtered);
485
- }
486
- }
487
- };
488
744
  const assignUpdate = (document, update, commands) => {
489
- let updated = assign(document.toObject(toObjectOptions), update);
745
+ let updated = applyUpdate(document.toObject(toObjectOptions), update);
490
746
  const changedByCommand = /* @__PURE__ */ new Map();
491
747
  for (const command of commands) {
492
748
  const [op = ""] = Object.keys(command);
493
749
  const fields = command[op];
494
- try {
495
- updated = assign(updated, command);
496
- trackChangedFields(fields, updated, changedByCommand);
497
- } catch {
498
- if (op === "$pullAll" && fields) {
499
- applyPullAll(updated, fields, changedByCommand);
500
- }
501
- }
750
+ updated = applyUpdate(updated, command);
751
+ trackChangedFields(fields, updated, changedByCommand);
502
752
  }
503
753
  const doc = document.set(updated).toObject(toObjectOptions);
504
754
  for (const [field, value] of changedByCommand) {
@@ -553,7 +803,9 @@ const updateHooksInitialize = (schema, opts) => {
553
803
  const updateQuery = this.getUpdate();
554
804
  const { update, commands } = splitUpdateAndCommands(updateQuery);
555
805
  const filter = this.getFilter();
556
- const candidates = [update, assignUpdate(model.hydrate({}), update, commands), filter];
806
+ const simulated = assignUpdate(model.hydrate({}), update, commands);
807
+ const simulatedFilter = Object.fromEntries(Object.entries(simulated).filter(([, v]) => v !== void 0));
808
+ const candidates = [update, simulatedFilter, filter];
557
809
  let current = null;
558
810
  for (const query of candidates) {
559
811
  if (current || isEmpty(query)) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-patch-mongoose",
3
- "version": "3.1.2",
3
+ "version": "4.0.0",
4
4
  "description": "Patch history & events for mongoose models",
5
5
  "author": "ilovepixelart",
6
6
  "license": "MIT",
@@ -12,9 +12,6 @@
12
12
  "url": "https://github.com/ilovepixelart/ts-patch-mongoose/issues"
13
13
  },
14
14
  "homepage": "https://github.com/ilovepixelart/ts-patch-mongoose#readme",
15
- "directories": {
16
- "examples": "examples"
17
- },
18
15
  "keywords": [
19
16
  "backend",
20
17
  "mongoose",
@@ -36,11 +33,10 @@
36
33
  "log"
37
34
  ],
38
35
  "engines": {
39
- "node": ">=18"
36
+ "node": ">=20"
40
37
  },
41
38
  "files": [
42
- "dist",
43
- "src"
39
+ "dist"
44
40
  ],
45
41
  "type": "module",
46
42
  "exports": {
@@ -56,8 +52,11 @@
56
52
  "main": "./dist/index.cjs",
57
53
  "module": "./dist/index.mjs",
58
54
  "types": "./dist/index.d.cts",
55
+ "publishConfig": {
56
+ "access": "public",
57
+ "provenance": true
58
+ },
59
59
  "scripts": {
60
- "prepare": "simple-git-hooks",
61
60
  "biome": "npx @biomejs/biome check",
62
61
  "biome:fix": "npx @biomejs/biome check --write .",
63
62
  "test": "vitest run --coverage",
@@ -67,14 +66,11 @@
67
66
  "build": "pkgroll --clean-dist",
68
67
  "release": "npm install && npm run biome && npm run type:check && npm run type:check:tests && npm run build && np --no-publish"
69
68
  },
70
- "dependencies": {
71
- "fast-json-patch": "3.1.1",
72
- "power-assign": "0.2.10"
73
- },
74
69
  "devDependencies": {
75
70
  "@biomejs/biome": "2.4.11",
76
71
  "@types/node": "25.6.0",
77
72
  "@vitest/coverage-v8": "4.1.4",
73
+ "fast-check": "4.6.0",
78
74
  "mongodb-memory-server": "11.0.1",
79
75
  "mongoose": "9.4.1",
80
76
  "np": "11.0.3",
package/src/em.ts DELETED
@@ -1,6 +0,0 @@
1
- import EventEmitter from 'node:events'
2
-
3
- class PatchEventEmitter extends EventEmitter {}
4
- const em = new PatchEventEmitter()
5
-
6
- export default em
package/src/helpers.ts DELETED
@@ -1,174 +0,0 @@
1
- import { HistoryModel } from './model'
2
- import { type Duration, ms } from './ms'
3
-
4
- import type { QueryOptions, ToObjectOptions } from 'mongoose'
5
-
6
- export const isArray = Array.isArray
7
-
8
- export const isEmpty = (value: unknown): boolean => {
9
- if (value == null) return true
10
- if (Array.isArray(value) || typeof value === 'string') return value.length === 0
11
- if (value instanceof Map || value instanceof Set) return value.size === 0
12
- if (typeof value === 'object') {
13
- for (const key in value) {
14
- if (Object.hasOwn(value, key)) return false
15
- }
16
- return true
17
- }
18
- return true
19
- }
20
-
21
- export const isFunction = (value: unknown): value is (...args: unknown[]) => unknown => {
22
- return typeof value === 'function'
23
- }
24
-
25
- export const isObjectLike = (value: unknown): value is Record<string, unknown> => {
26
- return typeof value === 'object' && value !== null
27
- }
28
-
29
- const cloneArrayBuffer = (arrayBuffer: ArrayBuffer): ArrayBuffer => {
30
- const result = new ArrayBuffer(arrayBuffer.byteLength)
31
- new Uint8Array(result).set(new Uint8Array(arrayBuffer))
32
- return result
33
- }
34
-
35
- const cloneImmutable = <T>(value: T): T | undefined => {
36
- const tag = Object.prototype.toString.call(value)
37
-
38
- switch (tag) {
39
- case '[object Date]':
40
- return new Date(+(value as unknown as Date)) as T
41
- case '[object RegExp]': {
42
- const re = value as unknown as RegExp
43
- const cloned = new RegExp(re.source, re.flags)
44
- cloned.lastIndex = re.lastIndex
45
- return cloned as T
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
- }
53
- case '[object ArrayBuffer]':
54
- return cloneArrayBuffer(value as unknown as ArrayBuffer) as T
55
- case '[object DataView]': {
56
- const dv = value as unknown as DataView
57
- const buffer = cloneArrayBuffer(dv.buffer as ArrayBuffer)
58
- return new DataView(buffer, dv.byteOffset, dv.byteLength) as T
59
- }
60
- }
61
-
62
- if (ArrayBuffer.isView(value)) {
63
- const ta = value as unknown as { buffer: ArrayBuffer; byteOffset: number; length: number }
64
- const buffer = cloneArrayBuffer(ta.buffer)
65
- return new (value.constructor as new (buffer: ArrayBuffer, byteOffset: number, length: number) => T)(buffer, ta.byteOffset, ta.length)
66
- }
67
-
68
- return undefined
69
- }
70
-
71
- const cloneCollection = <T extends object>(value: T, seen: WeakMap<object, unknown>): T => {
72
- if (value instanceof Map) {
73
- const map = new Map()
74
- seen.set(value, map)
75
- for (const [k, v] of value) map.set(k, cloneDeep(v, seen))
76
- return map as T
77
- }
78
-
79
- if (value instanceof Set) {
80
- const set = new Set()
81
- seen.set(value, set)
82
- for (const v of value) set.add(cloneDeep(v, seen))
83
- return set as T
84
- }
85
-
86
- if (Array.isArray(value)) {
87
- const arr = new Array(value.length) as unknown[]
88
- seen.set(value, arr)
89
- for (let i = 0; i < value.length; i++) {
90
- arr[i] = cloneDeep(value[i], seen)
91
- }
92
- return arr as T
93
- }
94
-
95
- const result = typeof value.constructor === 'function' ? (Object.create(Object.getPrototypeOf(value) as object) as T) : ({} as T)
96
- seen.set(value, result)
97
- for (const key of Object.keys(value)) {
98
- ;(result as Record<string, unknown>)[key] = cloneDeep((value as Record<string, unknown>)[key], seen)
99
- }
100
- return result
101
- }
102
-
103
- export const cloneDeep = <T>(value: T, seen = new WeakMap<object, unknown>()): T => {
104
- if (value === null || typeof value !== 'object') return value
105
- if (seen.has(value)) return seen.get(value) as T
106
-
107
- const immutable = cloneImmutable(value)
108
- if (immutable !== undefined) return immutable
109
-
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') {
117
- // NOSONAR — structuredClone cannot handle objects with non-cloneable methods (e.g. mongoose documents)
118
- return JSON.parse(JSON.stringify(value)) as T
119
- }
120
-
121
- return cloneCollection(value, seen)
122
- }
123
-
124
- export const chunk = <T>(array: T[], size: number): T[][] => {
125
- const result: T[][] = []
126
- for (let i = 0; i < array.length; i += size) {
127
- result.push(array.slice(i, i + size))
128
- }
129
- return result
130
- }
131
-
132
- export const isHookIgnored = <T>(options: QueryOptions<T>): boolean => {
133
- return options.ignoreHook === true || (options.ignoreEvent === true && options.ignorePatchHistory === true)
134
- }
135
-
136
- export const toObjectOptions: ToObjectOptions = {
137
- depopulate: true,
138
- virtuals: false,
139
- }
140
-
141
- export const setPatchHistoryTTL = async (ttl: Duration, onError?: (error: Error) => void): Promise<void> => {
142
- const name = 'createdAt_1_TTL'
143
- try {
144
- const indexes = await HistoryModel.collection.indexes()
145
- const existingIndex = indexes?.find((index) => index.name === name)
146
-
147
- if (!ttl && existingIndex) {
148
- await HistoryModel.collection.dropIndex(name)
149
- return
150
- }
151
-
152
- const milliseconds = ms(ttl)
153
-
154
- if (milliseconds < 1000 && existingIndex) {
155
- await HistoryModel.collection.dropIndex(name)
156
- return
157
- }
158
-
159
- const expireAfterSeconds = milliseconds / 1000
160
-
161
- if (existingIndex && existingIndex.expireAfterSeconds === expireAfterSeconds) {
162
- return
163
- }
164
-
165
- if (existingIndex) {
166
- await HistoryModel.collection.dropIndex(name)
167
- }
168
-
169
- await HistoryModel.collection.createIndex({ createdAt: 1 }, { expireAfterSeconds, name })
170
- } catch (err) {
171
- const handler = onError ?? console.error
172
- handler(err as Error)
173
- }
174
- }
@@ -1,49 +0,0 @@
1
- import { cloneDeep, isArray, isEmpty, isHookIgnored } from '../helpers'
2
- import { deletePatch } from '../patch'
3
-
4
- import type { HydratedDocument, Model, MongooseQueryMiddleware, Schema } from 'mongoose'
5
- import type { HookContext, PluginOptions } from '../types'
6
-
7
- const deleteMethods = ['remove', 'findOneAndDelete', 'findOneAndRemove', 'findByIdAndDelete', 'findByIdAndRemove', 'deleteOne', 'deleteMany']
8
-
9
- export const deleteHooksInitialize = <T>(schema: Schema<T>, opts: PluginOptions<T>): void => {
10
- schema.pre(deleteMethods as MongooseQueryMiddleware[], { document: false, query: true }, async function (this: HookContext<T>) {
11
- const options = this.getOptions()
12
- if (isHookIgnored(options)) return
13
-
14
- const model = this.model as Model<T>
15
- const filter = this.getFilter()
16
-
17
- this._context = {
18
- op: this.op,
19
- modelName: opts.modelName ?? model.modelName,
20
- collectionName: opts.collectionName ?? model.collection.collectionName,
21
- ignoreEvent: options.ignoreEvent as boolean,
22
- ignorePatchHistory: options.ignorePatchHistory as boolean,
23
- }
24
-
25
- if (['remove', 'deleteMany'].includes(this._context.op) && !options.single) {
26
- const docs = await model.find<T>(filter).lean().exec()
27
- if (!isEmpty(docs)) {
28
- this._context.deletedDocs = docs as HydratedDocument<T>[]
29
- }
30
- } else {
31
- const doc = await model.findOne<T>(filter).lean().exec()
32
- if (!isEmpty(doc)) {
33
- this._context.deletedDocs = [doc] as HydratedDocument<T>[]
34
- }
35
- }
36
-
37
- if (opts.preDelete && isArray(this._context.deletedDocs) && !isEmpty(this._context.deletedDocs)) {
38
- await opts.preDelete(cloneDeep(this._context.deletedDocs))
39
- }
40
- })
41
-
42
- schema.post(deleteMethods as MongooseQueryMiddleware[], { document: false, query: true }, async function (this: HookContext<T>) {
43
- const options = this.getOptions()
44
- if (isHookIgnored(options)) return
45
- if (!this._context) return
46
-
47
- await deletePatch(opts, this._context)
48
- })
49
- }
@@ -1,30 +0,0 @@
1
- import { toObjectOptions } from '../helpers'
2
- import { createPatch, updatePatch } from '../patch'
3
-
4
- import type { HydratedDocument, Model, Schema } from 'mongoose'
5
- import type { PatchContext, PluginOptions } from '../types'
6
-
7
- export const saveHooksInitialize = <T>(schema: Schema<T>, opts: PluginOptions<T>): void => {
8
- schema.pre('save', async function () {
9
- if (this.constructor.name !== 'model') return
10
-
11
- const current = this.toObject(toObjectOptions) as HydratedDocument<T>
12
- const model = this.constructor as Model<T>
13
-
14
- const context: PatchContext<T> = {
15
- op: this.isNew ? 'create' : 'update',
16
- modelName: opts.modelName ?? model.modelName,
17
- collectionName: opts.collectionName ?? model.collection.collectionName,
18
- createdDocs: [current],
19
- }
20
-
21
- if (this.isNew) {
22
- await createPatch(opts, context)
23
- } else {
24
- const original = await model.findById(current._id).lean().exec()
25
- if (original) {
26
- await updatePatch(opts, context, current, original as HydratedDocument<T>)
27
- }
28
- }
29
- })
30
- }