ts-patch-mongoose 2.9.6 → 3.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,18 +1,7 @@
1
- import isEmpty from 'lodash/isEmpty.js';
2
- import ms from 'ms';
3
1
  import mongoose, { Schema, model } from 'mongoose';
4
- import isArray from 'lodash/isArray.js';
5
2
  import jsonpatch from 'fast-json-patch';
6
- import chunk from 'lodash/chunk.js';
7
- import isFunction from 'lodash/isFunction.js';
8
- import omit from 'omit-deep';
9
3
  import EventEmitter from 'node:events';
10
- import cloneDeep from 'lodash/cloneDeep.js';
11
- import forEach from 'lodash/forEach.js';
12
- import isObjectLike from 'lodash/isObjectLike.js';
13
- import keys from 'lodash/keys.js';
14
4
  import { assign } from 'power-assign';
15
- import { satisfies } from 'semver';
16
5
 
17
6
  const HistorySchema = new Schema(
18
7
  {
@@ -59,6 +48,156 @@ HistorySchema.index({ collectionId: 1, version: -1 });
59
48
  HistorySchema.index({ op: 1, modelName: 1, collectionName: 1, collectionId: 1, reason: 1, version: 1 });
60
49
  const HistoryModel = model("History", HistorySchema, "history");
61
50
 
51
+ const s = 1e3;
52
+ const m = s * 60;
53
+ const h = m * 60;
54
+ const d = h * 24;
55
+ const w = d * 7;
56
+ const y = d * 365.25;
57
+ const mo = y / 12;
58
+ const UNITS = {
59
+ milliseconds: 1,
60
+ millisecond: 1,
61
+ msecs: 1,
62
+ msec: 1,
63
+ ms: 1,
64
+ seconds: s,
65
+ second: s,
66
+ secs: s,
67
+ sec: s,
68
+ s,
69
+ minutes: m,
70
+ minute: m,
71
+ mins: m,
72
+ min: m,
73
+ m,
74
+ hours: h,
75
+ hour: h,
76
+ hrs: h,
77
+ hr: h,
78
+ h,
79
+ days: d,
80
+ day: d,
81
+ d,
82
+ weeks: w,
83
+ week: w,
84
+ w,
85
+ months: mo,
86
+ month: mo,
87
+ mo,
88
+ years: y,
89
+ year: y,
90
+ yrs: y,
91
+ yr: y,
92
+ y
93
+ };
94
+ const unitPattern = Object.keys(UNITS).sort((a, b) => b.length - a.length).join("|");
95
+ const RE = new RegExp(String.raw`^(-?(?:\d+)?\.?\d+)\s*(${unitPattern})?$`, "i");
96
+ const ms = (val) => {
97
+ const str = String(val);
98
+ if (str.length > 100) return Number.NaN;
99
+ const match = RE.exec(str);
100
+ if (!match) return Number.NaN;
101
+ const n = Number.parseFloat(match[1] ?? "");
102
+ const type = (match[2] ?? "ms").toLowerCase();
103
+ return n * (UNITS[type] ?? 0);
104
+ };
105
+
106
+ const isArray = Array.isArray;
107
+ const isEmpty = (value) => {
108
+ if (value == null) return true;
109
+ if (Array.isArray(value) || typeof value === "string") return value.length === 0;
110
+ if (value instanceof Map || value instanceof Set) return value.size === 0;
111
+ if (typeof value === "object") {
112
+ for (const key in value) {
113
+ if (Object.hasOwn(value, key)) return false;
114
+ }
115
+ return true;
116
+ }
117
+ return true;
118
+ };
119
+ const isFunction = (value) => {
120
+ return typeof value === "function";
121
+ };
122
+ const isObjectLike = (value) => {
123
+ return typeof value === "object" && value !== null;
124
+ };
125
+ const cloneArrayBuffer = (arrayBuffer) => {
126
+ const result = new ArrayBuffer(arrayBuffer.byteLength);
127
+ new Uint8Array(result).set(new Uint8Array(arrayBuffer));
128
+ return result;
129
+ };
130
+ const cloneImmutable = (value) => {
131
+ const tag = Object.prototype.toString.call(value);
132
+ switch (tag) {
133
+ case "[object Date]":
134
+ return /* @__PURE__ */ new Date(+value);
135
+ case "[object RegExp]": {
136
+ const re = value;
137
+ const cloned = new RegExp(re.source, re.flags);
138
+ cloned.lastIndex = re.lastIndex;
139
+ return cloned;
140
+ }
141
+ case "[object ArrayBuffer]":
142
+ return cloneArrayBuffer(value);
143
+ case "[object DataView]": {
144
+ const dv = value;
145
+ const buffer = cloneArrayBuffer(dv.buffer);
146
+ return new DataView(buffer, dv.byteOffset, dv.byteLength);
147
+ }
148
+ }
149
+ if (ArrayBuffer.isView(value)) {
150
+ const ta = value;
151
+ const buffer = cloneArrayBuffer(ta.buffer);
152
+ return new value.constructor(buffer, ta.byteOffset, ta.length);
153
+ }
154
+ return void 0;
155
+ };
156
+ const cloneCollection = (value, seen) => {
157
+ if (value instanceof Map) {
158
+ const map = /* @__PURE__ */ new Map();
159
+ seen.set(value, map);
160
+ for (const [k, v] of value) map.set(k, cloneDeep(v, seen));
161
+ return map;
162
+ }
163
+ if (value instanceof Set) {
164
+ const set = /* @__PURE__ */ new Set();
165
+ seen.set(value, set);
166
+ for (const v of value) set.add(cloneDeep(v, seen));
167
+ return set;
168
+ }
169
+ if (Array.isArray(value)) {
170
+ const arr = new Array(value.length);
171
+ seen.set(value, arr);
172
+ for (let i = 0; i < value.length; i++) {
173
+ arr[i] = cloneDeep(value[i], seen);
174
+ }
175
+ return arr;
176
+ }
177
+ const result = typeof value.constructor === "function" ? Object.create(Object.getPrototypeOf(value)) : {};
178
+ seen.set(value, result);
179
+ for (const key of Object.keys(value)) {
180
+ result[key] = cloneDeep(value[key], seen);
181
+ }
182
+ return result;
183
+ };
184
+ const cloneDeep = (value, seen = /* @__PURE__ */ new WeakMap()) => {
185
+ if (value === null || typeof value !== "object") return value;
186
+ if (seen.has(value)) return seen.get(value);
187
+ const immutable = cloneImmutable(value);
188
+ if (immutable !== void 0) return immutable;
189
+ if ("toJSON" in value && typeof value.toJSON === "function") {
190
+ return JSON.parse(JSON.stringify(value));
191
+ }
192
+ return cloneCollection(value, seen);
193
+ };
194
+ const chunk = (array, size) => {
195
+ const result = [];
196
+ for (let i = 0; i < array.length; i += size) {
197
+ result.push(array.slice(i, i + size));
198
+ }
199
+ return result;
200
+ };
62
201
  const isHookIgnored = (options) => {
63
202
  return options.ignoreHook === true || options.ignoreEvent === true && options.ignorePatchHistory === true;
64
203
  };
@@ -75,7 +214,7 @@ const setPatchHistoryTTL = async (ttl) => {
75
214
  await HistoryModel.collection.dropIndex(name);
76
215
  return;
77
216
  }
78
- const milliseconds = typeof ttl === "string" ? ms(ttl) : ttl;
217
+ const milliseconds = ms(ttl);
79
218
  if (milliseconds < 1e3 && existingIndex) {
80
219
  await HistoryModel.collection.dropIndex(name);
81
220
  return;
@@ -97,63 +236,144 @@ class PatchEventEmitter extends EventEmitter {
97
236
  }
98
237
  const em = new PatchEventEmitter();
99
238
 
100
- function isPatchHistoryEnabled(opts, context) {
239
+ const isPlainObject = (val) => {
240
+ if (Object.prototype.toString.call(val) !== "[object Object]") return false;
241
+ const prot = Object.getPrototypeOf(val);
242
+ return prot === null || prot === Object.prototype;
243
+ };
244
+ const isUnsafeKey = (key) => {
245
+ return key === "__proto__" || key === "constructor" || key === "prototype";
246
+ };
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];
295
+ }
296
+ return delete target[last ?? ""];
297
+ }
298
+ return true;
299
+ };
300
+ const omitDeep = (value, keys) => {
301
+ if (value === void 0) return {};
302
+ if (Array.isArray(value)) {
303
+ for (let i = 0; i < value.length; i++) {
304
+ value[i] = omitDeep(value[i], keys);
305
+ }
306
+ return value;
307
+ }
308
+ if (!isPlainObject(value)) return value;
309
+ const omitKeys = typeof keys === "string" ? [keys] : keys;
310
+ if (!Array.isArray(omitKeys)) return value;
311
+ for (const key of omitKeys) {
312
+ unset(value, key);
313
+ }
314
+ for (const key of Object.keys(value)) {
315
+ value[key] = omitDeep(value[key], omitKeys);
316
+ }
317
+ return value;
318
+ };
319
+
320
+ const isPatchHistoryEnabled = (opts, context) => {
101
321
  return !opts.patchHistoryDisabled && !context.ignorePatchHistory;
102
- }
103
- function getJsonOmit(opts, doc) {
322
+ };
323
+ const getJsonOmit = (opts, doc) => {
104
324
  const object = JSON.parse(JSON.stringify(doc));
105
325
  if (opts.omit) {
106
- return omit(object, opts.omit);
326
+ return omitDeep(object, opts.omit);
107
327
  }
108
328
  return object;
109
- }
110
- function getObjectOmit(opts, doc) {
329
+ };
330
+ const getObjectOmit = (opts, doc) => {
111
331
  if (opts.omit) {
112
- return omit(isFunction(doc?.toObject) ? doc.toObject() : doc, opts.omit);
332
+ return omitDeep(isFunction(doc?.toObject) ? doc.toObject() : doc, opts.omit);
113
333
  }
114
334
  return doc;
115
- }
116
- async function getUser(opts, doc) {
335
+ };
336
+ const getUser = async (opts, doc) => {
117
337
  if (isFunction(opts.getUser)) {
118
338
  return await opts.getUser(doc);
119
339
  }
120
340
  return void 0;
121
- }
122
- async function getReason(opts, doc) {
341
+ };
342
+ const getReason = async (opts, doc) => {
123
343
  if (isFunction(opts.getReason)) {
124
344
  return await opts.getReason(doc);
125
345
  }
126
346
  return void 0;
127
- }
128
- async function getMetadata(opts, doc) {
347
+ };
348
+ const getMetadata = async (opts, doc) => {
129
349
  if (isFunction(opts.getMetadata)) {
130
350
  return await opts.getMetadata(doc);
131
351
  }
132
352
  return void 0;
133
- }
134
- function getValue(item) {
353
+ };
354
+ const getValue = (item) => {
135
355
  return item.status === "fulfilled" ? item.value : void 0;
136
- }
137
- async function getData(opts, doc) {
356
+ };
357
+ const getData = async (opts, doc) => {
138
358
  return Promise.allSettled([getUser(opts, doc), getReason(opts, doc), getMetadata(opts, doc)]).then(([user, reason, metadata]) => {
139
359
  return [getValue(user), getValue(reason), getValue(metadata)];
140
360
  });
141
- }
142
- function emitEvent(context, event, data) {
361
+ };
362
+ const emitEvent = (context, event, data) => {
143
363
  if (event && !context.ignoreEvent) {
144
364
  em.emit(event, data);
145
365
  }
146
- }
147
- async function bulkPatch(opts, context, eventKey, docsKey) {
366
+ };
367
+ const bulkPatch = async (opts, context, eventKey, docsKey) => {
148
368
  const history = isPatchHistoryEnabled(opts, context);
149
369
  const event = opts[eventKey];
150
370
  const docs = context[docsKey];
151
371
  const key = eventKey === "eventCreated" ? "doc" : "oldDoc";
152
- if (isEmpty(docs) || !event && !history) return;
372
+ if (isEmpty(docs) || !docs || !event && !history) return;
153
373
  const chunks = chunk(docs, 1e3);
154
- for (const chunk2 of chunks) {
374
+ for (const batch of chunks) {
155
375
  const bulk = [];
156
- for (const doc of chunk2) {
376
+ for (const doc of batch) {
157
377
  emitEvent(context, event, { [key]: doc });
158
378
  if (history) {
159
379
  const [user, reason, metadata] = await getData(opts, doc);
@@ -180,11 +400,11 @@ async function bulkPatch(opts, context, eventKey, docsKey) {
180
400
  });
181
401
  }
182
402
  }
183
- }
184
- async function createPatch(opts, context) {
403
+ };
404
+ const createPatch = async (opts, context) => {
185
405
  await bulkPatch(opts, context, "eventCreated", "createdDocs");
186
- }
187
- async function updatePatch(opts, context, current, original) {
406
+ };
407
+ const updatePatch = async (opts, context, current, original) => {
188
408
  const history = isPatchHistoryEnabled(opts, context);
189
409
  const currentObject = getJsonOmit(opts, current);
190
410
  const originalObject = getJsonOmit(opts, original);
@@ -211,10 +431,10 @@ async function updatePatch(opts, context, current, original) {
211
431
  ...metadata !== void 0 && { metadata }
212
432
  });
213
433
  }
214
- }
215
- async function deletePatch(opts, context) {
434
+ };
435
+ const deletePatch = async (opts, context) => {
216
436
  await bulkPatch(opts, context, "eventDeleted", "deletedDocs");
217
- }
437
+ };
218
438
 
219
439
  const deleteMethods = ["remove", "findOneAndDelete", "findOneAndRemove", "findByIdAndDelete", "findByIdAndRemove", "deleteOne", "deleteMany"];
220
440
  const deleteHooksInitialize = (schema, opts) => {
@@ -277,12 +497,12 @@ const saveHooksInitialize = (schema, opts) => {
277
497
  const updateMethods = ["update", "updateOne", "replaceOne", "updateMany", "findOneAndUpdate", "findOneAndReplace", "findByIdAndUpdate"];
278
498
  const assignUpdate = (document, update, commands) => {
279
499
  let updated = assign(document.toObject(toObjectOptions), update);
280
- forEach(commands, (command) => {
500
+ for (const command of commands) {
281
501
  try {
282
502
  updated = assign(updated, command);
283
503
  } catch {
284
504
  }
285
- });
505
+ }
286
506
  const doc = document.set(updated).toObject(toObjectOptions);
287
507
  if (update.createdAt) doc.createdAt = update.createdAt;
288
508
  return doc;
@@ -292,12 +512,12 @@ const splitUpdateAndCommands = (updateQuery) => {
292
512
  const commands = [];
293
513
  if (!isEmpty(updateQuery) && !isArray(updateQuery) && isObjectLike(updateQuery)) {
294
514
  update = cloneDeep(updateQuery);
295
- const keysWithDollarSign = keys(update).filter((key) => key.startsWith("$"));
515
+ const keysWithDollarSign = Object.keys(update).filter((key) => key.startsWith("$"));
296
516
  if (!isEmpty(keysWithDollarSign)) {
297
- forEach(keysWithDollarSign, (key) => {
517
+ for (const key of keysWithDollarSign) {
298
518
  commands.push({ [key]: update[key] });
299
519
  delete update[key];
300
- });
520
+ }
301
521
  }
302
522
  }
303
523
  return { update, commands };
@@ -342,7 +562,6 @@ const updateHooksInitialize = (schema, opts) => {
342
562
  current = await model.findOne(combined).sort("desc").lean().exec();
343
563
  }
344
564
  if (!isEmpty(filter) && !current) {
345
- console.log("filter", filter);
346
565
  current = await model.findOne(filter).sort("desc").lean().exec();
347
566
  }
348
567
  if (current) {
@@ -352,15 +571,16 @@ const updateHooksInitialize = (schema, opts) => {
352
571
  });
353
572
  };
354
573
 
355
- const isMongooseLessThan8 = satisfies(mongoose.version, "<8");
356
- const isMongooseLessThan7 = satisfies(mongoose.version, "<7");
357
- const isMongoose6 = satisfies(mongoose.version, "6");
574
+ const major = Number.parseInt(mongoose.version, 10);
575
+ const isMongooseLessThan8 = major < 8;
576
+ const isMongooseLessThan7 = major < 7;
577
+ const isMongoose6 = major === 6;
358
578
  if (isMongoose6) {
359
579
  mongoose.set("strictQuery", false);
360
580
  }
361
581
 
362
582
  const remove = isMongooseLessThan7 ? "remove" : "deleteOne";
363
- const patchHistoryPlugin = function plugin(schema, opts) {
583
+ const patchHistoryPlugin = (schema, opts) => {
364
584
  saveHooksInitialize(schema, opts);
365
585
  updateHooksInitialize(schema, opts);
366
586
  deleteHooksInitialize(schema, opts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-patch-mongoose",
3
- "version": "2.9.6",
3
+ "version": "3.0.0",
4
4
  "description": "Patch history & events for mongoose models",
5
5
  "author": "ilovepixelart",
6
6
  "license": "MIT",
@@ -36,7 +36,7 @@
36
36
  "log"
37
37
  ],
38
38
  "engines": {
39
- "node": ">=16"
39
+ "node": ">=18"
40
40
  },
41
41
  "files": [
42
42
  "dist",
@@ -71,28 +71,21 @@
71
71
  "release": "npm install && npm run biome && npm run type:check && npm run build && np --no-publish"
72
72
  },
73
73
  "dependencies": {
74
- "@types/lodash": "4.17.24",
75
- "@types/ms": "2.1.0",
76
- "@types/semver": "7.7.1",
77
74
  "fast-json-patch": "3.1.1",
78
- "lodash": "4.17.23",
79
- "ms": "2.1.3",
80
- "omit-deep": "0.3.0",
81
- "power-assign": "0.2.10",
82
- "semver": "7.7.4"
75
+ "power-assign": "0.2.10"
83
76
  },
84
77
  "devDependencies": {
85
- "@biomejs/biome": "2.4.4",
86
- "@types/node": "25.3.0",
87
- "@vitest/coverage-v8": "4.0.18",
78
+ "@biomejs/biome": "2.4.7",
79
+ "@types/node": "25.5.0",
80
+ "@vitest/coverage-v8": "4.1.0",
88
81
  "mongodb-memory-server": "11.0.1",
89
- "mongoose": "9.2.2",
82
+ "mongoose": "9.3.0",
83
+ "np": "11.0.2",
90
84
  "open-cli": "8.0.0",
91
- "pkgroll": "2.26.3",
85
+ "pkgroll": "2.27.0",
92
86
  "simple-git-hooks": "2.13.1",
93
87
  "typescript": "5.9.3",
94
- "vitest": "4.0.18",
95
- "np": "11.0.2"
88
+ "vitest": "4.1.0"
96
89
  },
97
90
  "peerDependencies": {
98
91
  "mongoose": ">=6.6.0 < 10"
@@ -105,6 +98,7 @@
105
98
  "publish": false
106
99
  },
107
100
  "overrides": {
108
- "esbuild": "0.25.0"
101
+ "tmp": "0.2.5",
102
+ "file-type": "21.3.2"
109
103
  }
110
104
  }
package/src/helpers.ts CHANGED
@@ -1,8 +1,122 @@
1
- import ms from 'ms'
2
1
  import { HistoryModel } from './model'
2
+ import { type Duration, ms } from './ms'
3
3
 
4
4
  import type { QueryOptions, ToObjectOptions } from 'mongoose'
5
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 ArrayBuffer]':
48
+ return cloneArrayBuffer(value as unknown as ArrayBuffer) as T
49
+ case '[object DataView]': {
50
+ const dv = value as unknown as DataView
51
+ const buffer = cloneArrayBuffer(dv.buffer as ArrayBuffer)
52
+ return new DataView(buffer, dv.byteOffset, dv.byteLength) as T
53
+ }
54
+ }
55
+
56
+ if (ArrayBuffer.isView(value)) {
57
+ const ta = value as unknown as { buffer: ArrayBuffer; byteOffset: number; length: number }
58
+ const buffer = cloneArrayBuffer(ta.buffer)
59
+ return new (value.constructor as new (buffer: ArrayBuffer, byteOffset: number, length: number) => T)(buffer, ta.byteOffset, ta.length)
60
+ }
61
+
62
+ return undefined
63
+ }
64
+
65
+ const cloneCollection = <T extends object>(value: T, seen: WeakMap<object, unknown>): T => {
66
+ if (value instanceof Map) {
67
+ const map = new Map()
68
+ seen.set(value, map)
69
+ for (const [k, v] of value) map.set(k, cloneDeep(v, seen))
70
+ return map as T
71
+ }
72
+
73
+ if (value instanceof Set) {
74
+ const set = new Set()
75
+ seen.set(value, set)
76
+ for (const v of value) set.add(cloneDeep(v, seen))
77
+ return set as T
78
+ }
79
+
80
+ if (Array.isArray(value)) {
81
+ const arr = new Array(value.length) as unknown[]
82
+ seen.set(value, arr)
83
+ for (let i = 0; i < value.length; i++) {
84
+ arr[i] = cloneDeep(value[i], seen)
85
+ }
86
+ return arr as T
87
+ }
88
+
89
+ const result = typeof value.constructor === 'function' ? (Object.create(Object.getPrototypeOf(value) as object) as T) : ({} as T)
90
+ seen.set(value, result)
91
+ for (const key of Object.keys(value)) {
92
+ ;(result as Record<string, unknown>)[key] = cloneDeep((value as Record<string, unknown>)[key], seen)
93
+ }
94
+ return result
95
+ }
96
+
97
+ export const cloneDeep = <T>(value: T, seen = new WeakMap<object, unknown>()): T => {
98
+ if (value === null || typeof value !== 'object') return value
99
+ if (seen.has(value)) return seen.get(value) as T
100
+
101
+ const immutable = cloneImmutable(value)
102
+ if (immutable !== undefined) return immutable
103
+
104
+ if ('toJSON' in value && typeof (value as Record<string, unknown>).toJSON === 'function') {
105
+ // NOSONAR — structuredClone cannot handle objects with non-cloneable methods (e.g. mongoose documents)
106
+ return JSON.parse(JSON.stringify(value)) as T
107
+ }
108
+
109
+ return cloneCollection(value, seen)
110
+ }
111
+
112
+ export const chunk = <T>(array: T[], size: number): T[][] => {
113
+ const result: T[][] = []
114
+ for (let i = 0; i < array.length; i += size) {
115
+ result.push(array.slice(i, i + size))
116
+ }
117
+ return result
118
+ }
119
+
6
120
  export const isHookIgnored = <T>(options: QueryOptions<T>): boolean => {
7
121
  return options.ignoreHook === true || (options.ignoreEvent === true && options.ignorePatchHistory === true)
8
122
  }
@@ -12,21 +126,19 @@ export const toObjectOptions: ToObjectOptions = {
12
126
  virtuals: false,
13
127
  }
14
128
 
15
- export const setPatchHistoryTTL = async (ttl: number | ms.StringValue): Promise<void> => {
16
- const name = 'createdAt_1_TTL' // To avoid collision with user defined index / manually created index
129
+ export const setPatchHistoryTTL = async (ttl: Duration): Promise<void> => {
130
+ const name = 'createdAt_1_TTL'
17
131
  try {
18
132
  const indexes = await HistoryModel.collection.indexes()
19
133
  const existingIndex = indexes?.find((index) => index.name === name)
20
134
 
21
- // Drop the index if historyTTL is not set and index exists
22
135
  if (!ttl && existingIndex) {
23
136
  await HistoryModel.collection.dropIndex(name)
24
137
  return
25
138
  }
26
139
 
27
- const milliseconds = typeof ttl === 'string' ? ms(ttl) : ttl
140
+ const milliseconds = ms(ttl)
28
141
 
29
- // Drop the index if historyTTL is less than 1 second and index exists
30
142
  if (milliseconds < 1000 && existingIndex) {
31
143
  await HistoryModel.collection.dropIndex(name)
32
144
  return
@@ -35,16 +147,13 @@ export const setPatchHistoryTTL = async (ttl: number | ms.StringValue): Promise<
35
147
  const expireAfterSeconds = milliseconds / 1000
36
148
 
37
149
  if (existingIndex && existingIndex.expireAfterSeconds === expireAfterSeconds) {
38
- // Index already exists with the correct TTL, no need to recreate
39
150
  return
40
151
  }
41
152
 
42
153
  if (existingIndex) {
43
- // Drop the existing index if it exists and TTL is different
44
154
  await HistoryModel.collection.dropIndex(name)
45
155
  }
46
156
 
47
- // Create a new index with the correct TTL if it doesn't exist or if the TTL is different
48
157
  await HistoryModel.collection.createIndex({ createdAt: 1 }, { expireAfterSeconds, name })
49
158
  } catch (err) {
50
159
  console.error("Couldn't create or update index for history collection", err)
@@ -1,7 +1,4 @@
1
- // Using CJS lodash with .js extensions for ESM compatibility
2
- import isArray from 'lodash/isArray.js'
3
- import isEmpty from 'lodash/isEmpty.js'
4
- import { isHookIgnored } from '../helpers'
1
+ import { isArray, isEmpty, isHookIgnored } from '../helpers'
5
2
  import { deletePatch } from '../patch'
6
3
 
7
4
  import type { HydratedDocument, Model, MongooseQueryMiddleware, Schema } from 'mongoose'