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/README.md +42 -27
- package/biome.json +1 -1
- package/dist/index.cjs +273 -53
- package/dist/index.d.cts +41 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +41 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +273 -53
- package/package.json +12 -18
- package/src/helpers.ts +118 -9
- package/src/hooks/delete-hooks.ts +1 -4
- package/src/hooks/update-hooks.ts +6 -15
- package/src/index.ts +4 -32
- package/src/ms.ts +66 -0
- package/src/omit-deep.ts +95 -0
- package/src/patch.ts +19 -21
- package/src/version.ts +5 -4
- package/tests/em.test.ts +2 -0
- package/tests/helpers.test.ts +229 -2
- package/tests/ms.test.ts +113 -0
- package/tests/omit-deep.test.ts +220 -0
- package/tests/plugin-all-features.test.ts +741 -0
- package/tests/plugin-complex-data.test.ts +1332 -0
- package/tsconfig.json +2 -3
- package/src/modules/omit-deep.d.ts +0 -3
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 =
|
|
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
|
-
|
|
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
|
-
|
|
322
|
+
};
|
|
323
|
+
const getJsonOmit = (opts, doc) => {
|
|
104
324
|
const object = JSON.parse(JSON.stringify(doc));
|
|
105
325
|
if (opts.omit) {
|
|
106
|
-
return
|
|
326
|
+
return omitDeep(object, opts.omit);
|
|
107
327
|
}
|
|
108
328
|
return object;
|
|
109
|
-
}
|
|
110
|
-
|
|
329
|
+
};
|
|
330
|
+
const getObjectOmit = (opts, doc) => {
|
|
111
331
|
if (opts.omit) {
|
|
112
|
-
return
|
|
332
|
+
return omitDeep(isFunction(doc?.toObject) ? doc.toObject() : doc, opts.omit);
|
|
113
333
|
}
|
|
114
334
|
return doc;
|
|
115
|
-
}
|
|
116
|
-
async
|
|
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
|
|
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
|
|
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
|
-
|
|
353
|
+
};
|
|
354
|
+
const getValue = (item) => {
|
|
135
355
|
return item.status === "fulfilled" ? item.value : void 0;
|
|
136
|
-
}
|
|
137
|
-
async
|
|
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
|
-
|
|
361
|
+
};
|
|
362
|
+
const emitEvent = (context, event, data) => {
|
|
143
363
|
if (event && !context.ignoreEvent) {
|
|
144
364
|
em.emit(event, data);
|
|
145
365
|
}
|
|
146
|
-
}
|
|
147
|
-
async
|
|
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
|
|
374
|
+
for (const batch of chunks) {
|
|
155
375
|
const bulk = [];
|
|
156
|
-
for (const doc of
|
|
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
|
|
403
|
+
};
|
|
404
|
+
const createPatch = async (opts, context) => {
|
|
185
405
|
await bulkPatch(opts, context, "eventCreated", "createdDocs");
|
|
186
|
-
}
|
|
187
|
-
async
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
356
|
-
const
|
|
357
|
-
const
|
|
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 =
|
|
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": "
|
|
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": ">=
|
|
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
|
-
"
|
|
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.
|
|
86
|
-
"@types/node": "25.
|
|
87
|
-
"@vitest/coverage-v8": "4.0
|
|
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.
|
|
82
|
+
"mongoose": "9.3.0",
|
|
83
|
+
"np": "11.0.2",
|
|
90
84
|
"open-cli": "8.0.0",
|
|
91
|
-
"pkgroll": "2.
|
|
85
|
+
"pkgroll": "2.27.0",
|
|
92
86
|
"simple-git-hooks": "2.13.1",
|
|
93
87
|
"typescript": "5.9.3",
|
|
94
|
-
"vitest": "4.0
|
|
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
|
-
"
|
|
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:
|
|
16
|
-
const name = 'createdAt_1_TTL'
|
|
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 =
|
|
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
|
-
|
|
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'
|