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/README.md +9 -3
- package/dist/index.cjs +275 -23
- package/dist/index.d.cts +21 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +21 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +275 -23
- package/package.json +8 -12
- package/src/em.ts +0 -6
- package/src/helpers.ts +0 -174
- package/src/hooks/delete-hooks.ts +0 -49
- package/src/hooks/save-hooks.ts +0 -30
- package/src/hooks/update-hooks.ts +0 -130
- package/src/index.ts +0 -68
- package/src/model.ts +0 -50
- package/src/modules/power-assign.d.ts +0 -3
- package/src/ms.ts +0 -67
- package/src/omit-deep.ts +0 -56
- package/src/patch.ts +0 -154
- package/src/types.ts +0 -55
- package/src/version.ts +0 -13
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 =
|
|
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 =
|
|
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
|
-
|
|
495
|
-
|
|
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
|
|
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
|
+
"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": ">=
|
|
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
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
|
-
}
|
package/src/hooks/save-hooks.ts
DELETED
|
@@ -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
|
-
}
|