ts-patch-mongoose 2.9.6 → 3.1.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 +2 -5
- package/dist/index.cjs +307 -93
- package/dist/index.d.cts +42 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +42 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +307 -93
- package/package.json +13 -19
- package/src/helpers.ts +132 -10
- package/src/hooks/delete-hooks.ts +5 -7
- package/src/hooks/update-hooks.ts +48 -34
- package/src/index.ts +4 -32
- package/src/ms.ts +66 -0
- package/src/omit-deep.ts +56 -0
- package/src/patch.ts +42 -47
- package/src/types.ts +1 -0
- package/src/version.ts +5 -4
- package/tests/em.test.ts +26 -8
- package/tests/helpers.test.ts +291 -2
- package/tests/ms.test.ts +113 -0
- package/tests/omit-deep.test.ts +235 -0
- package/tests/patch.test.ts +6 -5
- package/tests/plugin-all-features.test.ts +844 -0
- package/tests/plugin-complex-data.test.ts +2647 -0
- package/tests/plugin-event-created.test.ts +10 -10
- package/tests/plugin-event-deleted.test.ts +10 -10
- package/tests/plugin-event-updated.test.ts +9 -9
- package/tests/plugin-global.test.ts +6 -6
- package/tests/plugin-omit-all.test.ts +1 -1
- package/tests/plugin-patch-history-disabled.test.ts +1 -1
- package/tests/plugin-pre-delete.test.ts +8 -8
- package/tests/plugin-pre-save.test.ts +2 -2
- package/tests/plugin.test.ts +3 -3
- package/tsconfig.json +2 -3
- package/vite.config.mts +2 -1
- 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,166 @@ 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 Error]": {
|
|
142
|
+
const err = value;
|
|
143
|
+
const cloned = new err.constructor(err.message);
|
|
144
|
+
if (err.stack) cloned.stack = err.stack;
|
|
145
|
+
return cloned;
|
|
146
|
+
}
|
|
147
|
+
case "[object ArrayBuffer]":
|
|
148
|
+
return cloneArrayBuffer(value);
|
|
149
|
+
case "[object DataView]": {
|
|
150
|
+
const dv = value;
|
|
151
|
+
const buffer = cloneArrayBuffer(dv.buffer);
|
|
152
|
+
return new DataView(buffer, dv.byteOffset, dv.byteLength);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (ArrayBuffer.isView(value)) {
|
|
156
|
+
const ta = value;
|
|
157
|
+
const buffer = cloneArrayBuffer(ta.buffer);
|
|
158
|
+
return new value.constructor(buffer, ta.byteOffset, ta.length);
|
|
159
|
+
}
|
|
160
|
+
return void 0;
|
|
161
|
+
};
|
|
162
|
+
const cloneCollection = (value, seen) => {
|
|
163
|
+
if (value instanceof Map) {
|
|
164
|
+
const map = /* @__PURE__ */ new Map();
|
|
165
|
+
seen.set(value, map);
|
|
166
|
+
for (const [k, v] of value) map.set(k, cloneDeep(v, seen));
|
|
167
|
+
return map;
|
|
168
|
+
}
|
|
169
|
+
if (value instanceof Set) {
|
|
170
|
+
const set = /* @__PURE__ */ new Set();
|
|
171
|
+
seen.set(value, set);
|
|
172
|
+
for (const v of value) set.add(cloneDeep(v, seen));
|
|
173
|
+
return set;
|
|
174
|
+
}
|
|
175
|
+
if (Array.isArray(value)) {
|
|
176
|
+
const arr = new Array(value.length);
|
|
177
|
+
seen.set(value, arr);
|
|
178
|
+
for (let i = 0; i < value.length; i++) {
|
|
179
|
+
arr[i] = cloneDeep(value[i], seen);
|
|
180
|
+
}
|
|
181
|
+
return arr;
|
|
182
|
+
}
|
|
183
|
+
const result = typeof value.constructor === "function" ? Object.create(Object.getPrototypeOf(value)) : {};
|
|
184
|
+
seen.set(value, result);
|
|
185
|
+
for (const key of Object.keys(value)) {
|
|
186
|
+
result[key] = cloneDeep(value[key], seen);
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
};
|
|
190
|
+
const cloneDeep = (value, seen = /* @__PURE__ */ new WeakMap()) => {
|
|
191
|
+
if (value === null || typeof value !== "object") return value;
|
|
192
|
+
if (seen.has(value)) return seen.get(value);
|
|
193
|
+
const immutable = cloneImmutable(value);
|
|
194
|
+
if (immutable !== void 0) return immutable;
|
|
195
|
+
const record = value;
|
|
196
|
+
if (typeof record._bsontype === "string" && typeof record.toHexString === "function") {
|
|
197
|
+
return new value.constructor(record.toHexString());
|
|
198
|
+
}
|
|
199
|
+
if (typeof record.toJSON === "function") {
|
|
200
|
+
return JSON.parse(JSON.stringify(value));
|
|
201
|
+
}
|
|
202
|
+
return cloneCollection(value, seen);
|
|
203
|
+
};
|
|
204
|
+
const chunk = (array, size) => {
|
|
205
|
+
const result = [];
|
|
206
|
+
for (let i = 0; i < array.length; i += size) {
|
|
207
|
+
result.push(array.slice(i, i + size));
|
|
208
|
+
}
|
|
209
|
+
return result;
|
|
210
|
+
};
|
|
62
211
|
const isHookIgnored = (options) => {
|
|
63
212
|
return options.ignoreHook === true || options.ignoreEvent === true && options.ignorePatchHistory === true;
|
|
64
213
|
};
|
|
@@ -66,7 +215,7 @@ const toObjectOptions = {
|
|
|
66
215
|
depopulate: true,
|
|
67
216
|
virtuals: false
|
|
68
217
|
};
|
|
69
|
-
const setPatchHistoryTTL = async (ttl) => {
|
|
218
|
+
const setPatchHistoryTTL = async (ttl, onError) => {
|
|
70
219
|
const name = "createdAt_1_TTL";
|
|
71
220
|
try {
|
|
72
221
|
const indexes = await HistoryModel.collection.indexes();
|
|
@@ -75,7 +224,7 @@ const setPatchHistoryTTL = async (ttl) => {
|
|
|
75
224
|
await HistoryModel.collection.dropIndex(name);
|
|
76
225
|
return;
|
|
77
226
|
}
|
|
78
|
-
const milliseconds =
|
|
227
|
+
const milliseconds = ms(ttl);
|
|
79
228
|
if (milliseconds < 1e3 && existingIndex) {
|
|
80
229
|
await HistoryModel.collection.dropIndex(name);
|
|
81
230
|
return;
|
|
@@ -89,7 +238,8 @@ const setPatchHistoryTTL = async (ttl) => {
|
|
|
89
238
|
}
|
|
90
239
|
await HistoryModel.collection.createIndex({ createdAt: 1 }, { expireAfterSeconds, name });
|
|
91
240
|
} catch (err) {
|
|
92
|
-
|
|
241
|
+
const handler = onError ?? console.error;
|
|
242
|
+
handler(err);
|
|
93
243
|
}
|
|
94
244
|
};
|
|
95
245
|
|
|
@@ -97,64 +247,101 @@ class PatchEventEmitter extends EventEmitter {
|
|
|
97
247
|
}
|
|
98
248
|
const em = new PatchEventEmitter();
|
|
99
249
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
250
|
+
const isPlainObject = (val) => {
|
|
251
|
+
if (Object.prototype.toString.call(val) !== "[object Object]") return false;
|
|
252
|
+
const prot = Object.getPrototypeOf(val);
|
|
253
|
+
return prot === null || prot === Object.prototype;
|
|
254
|
+
};
|
|
255
|
+
const isUnsafeKey = (key) => {
|
|
256
|
+
return key === "__proto__" || key === "constructor" || key === "prototype";
|
|
257
|
+
};
|
|
258
|
+
const classifyKeys = (omitKeys) => {
|
|
259
|
+
const topLevel = /* @__PURE__ */ new Set();
|
|
260
|
+
const nested = /* @__PURE__ */ new Map();
|
|
261
|
+
for (const key of omitKeys) {
|
|
262
|
+
const dotIdx = key.indexOf(".");
|
|
263
|
+
if (dotIdx === -1) {
|
|
264
|
+
topLevel.add(key);
|
|
265
|
+
} else {
|
|
266
|
+
const head = key.slice(0, dotIdx);
|
|
267
|
+
const tail = key.slice(dotIdx + 1);
|
|
268
|
+
if (!isUnsafeKey(head)) {
|
|
269
|
+
const existing = nested.get(head) ?? [];
|
|
270
|
+
existing.push(tail);
|
|
271
|
+
nested.set(head, existing);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
113
274
|
}
|
|
114
|
-
return
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
|
|
275
|
+
return { topLevel, nested };
|
|
276
|
+
};
|
|
277
|
+
const omitDeep = (value, keys) => {
|
|
278
|
+
if (value === void 0) return {};
|
|
279
|
+
if (Array.isArray(value)) {
|
|
280
|
+
return value.map((item) => omitDeep(item, keys));
|
|
119
281
|
}
|
|
120
|
-
return
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
282
|
+
if (!isPlainObject(value)) return value;
|
|
283
|
+
const omitKeys = typeof keys === "string" ? [keys] : keys;
|
|
284
|
+
if (!Array.isArray(omitKeys)) return value;
|
|
285
|
+
const { topLevel, nested } = classifyKeys(omitKeys);
|
|
286
|
+
const result = {};
|
|
287
|
+
for (const key of Object.keys(value)) {
|
|
288
|
+
if (topLevel.has(key)) continue;
|
|
289
|
+
const nestedKeys = nested.get(key);
|
|
290
|
+
result[key] = omitDeep(value[key], nestedKeys ?? omitKeys);
|
|
125
291
|
}
|
|
126
|
-
return
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
292
|
+
return result;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const isPatchHistoryEnabled = (opts, context) => {
|
|
296
|
+
return !opts.patchHistoryDisabled && !context.ignorePatchHistory;
|
|
297
|
+
};
|
|
298
|
+
const applyOmit = (object, opts) => {
|
|
299
|
+
return opts.omit ? omitDeep(object, opts.omit) : object;
|
|
300
|
+
};
|
|
301
|
+
const replacer = (_key, value) => typeof value === "bigint" ? value.toString() : value;
|
|
302
|
+
const getJsonOmit = (opts, doc) => {
|
|
303
|
+
return applyOmit(JSON.parse(JSON.stringify(doc, replacer)), opts);
|
|
304
|
+
};
|
|
305
|
+
const getObjectOmit = (opts, doc) => {
|
|
306
|
+
return applyOmit(isFunction(doc?.toObject) ? doc.toObject() : doc, opts);
|
|
307
|
+
};
|
|
308
|
+
const getOptionalField = async (fn, doc) => {
|
|
309
|
+
if (isFunction(fn)) {
|
|
310
|
+
return await fn(doc);
|
|
131
311
|
}
|
|
132
312
|
return void 0;
|
|
133
|
-
}
|
|
134
|
-
|
|
313
|
+
};
|
|
314
|
+
const getUser = async (opts, doc) => getOptionalField(opts.getUser, doc);
|
|
315
|
+
const getReason = async (opts, doc) => getOptionalField(opts.getReason, doc);
|
|
316
|
+
const getMetadata = async (opts, doc) => getOptionalField(opts.getMetadata, doc);
|
|
317
|
+
const getValue = (item) => {
|
|
135
318
|
return item.status === "fulfilled" ? item.value : void 0;
|
|
136
|
-
}
|
|
137
|
-
async
|
|
319
|
+
};
|
|
320
|
+
const getData = async (opts, doc) => {
|
|
138
321
|
return Promise.allSettled([getUser(opts, doc), getReason(opts, doc), getMetadata(opts, doc)]).then(([user, reason, metadata]) => {
|
|
139
322
|
return [getValue(user), getValue(reason), getValue(metadata)];
|
|
140
323
|
});
|
|
141
|
-
}
|
|
142
|
-
|
|
324
|
+
};
|
|
325
|
+
const emitEvent = (context, event, data) => {
|
|
143
326
|
if (event && !context.ignoreEvent) {
|
|
144
|
-
|
|
327
|
+
try {
|
|
328
|
+
em.emit(event, data);
|
|
329
|
+
} catch {
|
|
330
|
+
}
|
|
145
331
|
}
|
|
146
|
-
}
|
|
147
|
-
async
|
|
332
|
+
};
|
|
333
|
+
const bulkPatch = async (opts, context, eventKey, docsKey) => {
|
|
148
334
|
const history = isPatchHistoryEnabled(opts, context);
|
|
149
335
|
const event = opts[eventKey];
|
|
150
336
|
const docs = context[docsKey];
|
|
151
337
|
const key = eventKey === "eventCreated" ? "doc" : "oldDoc";
|
|
152
|
-
if (isEmpty(docs) || !event && !history) return;
|
|
338
|
+
if (isEmpty(docs) || !docs || !event && !history) return;
|
|
153
339
|
const chunks = chunk(docs, 1e3);
|
|
154
|
-
for (const
|
|
340
|
+
for (const batch of chunks) {
|
|
155
341
|
const bulk = [];
|
|
156
|
-
for (const doc of
|
|
157
|
-
|
|
342
|
+
for (const doc of batch) {
|
|
343
|
+
const omitted = getObjectOmit(opts, doc);
|
|
344
|
+
emitEvent(context, event, { [key]: omitted });
|
|
158
345
|
if (history) {
|
|
159
346
|
const [user, reason, metadata] = await getData(opts, doc);
|
|
160
347
|
bulk.push({
|
|
@@ -164,7 +351,7 @@ async function bulkPatch(opts, context, eventKey, docsKey) {
|
|
|
164
351
|
modelName: context.modelName,
|
|
165
352
|
collectionName: context.collectionName,
|
|
166
353
|
collectionId: doc._id,
|
|
167
|
-
doc:
|
|
354
|
+
doc: omitted,
|
|
168
355
|
version: 0,
|
|
169
356
|
...user !== void 0 && { user },
|
|
170
357
|
...reason !== void 0 && { reason },
|
|
@@ -175,16 +362,17 @@ async function bulkPatch(opts, context, eventKey, docsKey) {
|
|
|
175
362
|
}
|
|
176
363
|
}
|
|
177
364
|
if (history && !isEmpty(bulk)) {
|
|
365
|
+
const onError = opts.onError ?? console.error;
|
|
178
366
|
await HistoryModel.bulkWrite(bulk, { ordered: false }).catch((error) => {
|
|
179
|
-
|
|
367
|
+
onError(error);
|
|
180
368
|
});
|
|
181
369
|
}
|
|
182
370
|
}
|
|
183
|
-
}
|
|
184
|
-
async
|
|
371
|
+
};
|
|
372
|
+
const createPatch = async (opts, context) => {
|
|
185
373
|
await bulkPatch(opts, context, "eventCreated", "createdDocs");
|
|
186
|
-
}
|
|
187
|
-
async
|
|
374
|
+
};
|
|
375
|
+
const updatePatch = async (opts, context, current, original) => {
|
|
188
376
|
const history = isPatchHistoryEnabled(opts, context);
|
|
189
377
|
const currentObject = getJsonOmit(opts, current);
|
|
190
378
|
const originalObject = getJsonOmit(opts, original);
|
|
@@ -199,6 +387,7 @@ async function updatePatch(opts, context, current, original) {
|
|
|
199
387
|
version = lastHistory.version + 1;
|
|
200
388
|
}
|
|
201
389
|
const [user, reason, metadata] = await getData(opts, current);
|
|
390
|
+
const onError = opts.onError ?? console.error;
|
|
202
391
|
await HistoryModel.create({
|
|
203
392
|
op: context.op,
|
|
204
393
|
modelName: context.modelName,
|
|
@@ -209,12 +398,14 @@ async function updatePatch(opts, context, current, original) {
|
|
|
209
398
|
...user !== void 0 && { user },
|
|
210
399
|
...reason !== void 0 && { reason },
|
|
211
400
|
...metadata !== void 0 && { metadata }
|
|
401
|
+
}).catch((error) => {
|
|
402
|
+
onError(error);
|
|
212
403
|
});
|
|
213
404
|
}
|
|
214
|
-
}
|
|
215
|
-
async
|
|
405
|
+
};
|
|
406
|
+
const deletePatch = async (opts, context) => {
|
|
216
407
|
await bulkPatch(opts, context, "eventDeleted", "deletedDocs");
|
|
217
|
-
}
|
|
408
|
+
};
|
|
218
409
|
|
|
219
410
|
const deleteMethods = ["remove", "findOneAndDelete", "findOneAndRemove", "findByIdAndDelete", "findByIdAndRemove", "deleteOne", "deleteMany"];
|
|
220
411
|
const deleteHooksInitialize = (schema, opts) => {
|
|
@@ -225,8 +416,8 @@ const deleteHooksInitialize = (schema, opts) => {
|
|
|
225
416
|
const filter = this.getFilter();
|
|
226
417
|
this._context = {
|
|
227
418
|
op: this.op,
|
|
228
|
-
modelName: opts.modelName ??
|
|
229
|
-
collectionName: opts.collectionName ??
|
|
419
|
+
modelName: opts.modelName ?? model.modelName,
|
|
420
|
+
collectionName: opts.collectionName ?? model.collection.collectionName,
|
|
230
421
|
ignoreEvent: options.ignoreEvent,
|
|
231
422
|
ignorePatchHistory: options.ignorePatchHistory
|
|
232
423
|
};
|
|
@@ -242,12 +433,13 @@ const deleteHooksInitialize = (schema, opts) => {
|
|
|
242
433
|
}
|
|
243
434
|
}
|
|
244
435
|
if (opts.preDelete && isArray(this._context.deletedDocs) && !isEmpty(this._context.deletedDocs)) {
|
|
245
|
-
await opts.preDelete(this._context.deletedDocs);
|
|
436
|
+
await opts.preDelete(cloneDeep(this._context.deletedDocs));
|
|
246
437
|
}
|
|
247
438
|
});
|
|
248
439
|
schema.post(deleteMethods, { document: false, query: true }, async function() {
|
|
249
440
|
const options = this.getOptions();
|
|
250
441
|
if (isHookIgnored(options)) return;
|
|
442
|
+
if (!this._context) return;
|
|
251
443
|
await deletePatch(opts, this._context);
|
|
252
444
|
});
|
|
253
445
|
};
|
|
@@ -275,15 +467,42 @@ const saveHooksInitialize = (schema, opts) => {
|
|
|
275
467
|
};
|
|
276
468
|
|
|
277
469
|
const updateMethods = ["update", "updateOne", "replaceOne", "updateMany", "findOneAndUpdate", "findOneAndReplace", "findByIdAndUpdate"];
|
|
470
|
+
const trackChangedFields = (fields, updated, changed) => {
|
|
471
|
+
if (!fields) return;
|
|
472
|
+
for (const key of Object.keys(fields)) {
|
|
473
|
+
const root = key.split(".")[0];
|
|
474
|
+
changed.set(root, updated[root]);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
const applyPullAll = (updated, fields, changed) => {
|
|
478
|
+
for (const [field, values] of Object.entries(fields)) {
|
|
479
|
+
const arr = updated[field];
|
|
480
|
+
if (Array.isArray(arr)) {
|
|
481
|
+
const filtered = arr.filter((item) => !values.some((v) => JSON.stringify(v) === JSON.stringify(item)));
|
|
482
|
+
updated[field] = filtered;
|
|
483
|
+
changed.set(field, filtered);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
};
|
|
278
487
|
const assignUpdate = (document, update, commands) => {
|
|
279
488
|
let updated = assign(document.toObject(toObjectOptions), update);
|
|
280
|
-
|
|
489
|
+
const changedByCommand = /* @__PURE__ */ new Map();
|
|
490
|
+
for (const command of commands) {
|
|
491
|
+
const op = Object.keys(command)[0];
|
|
492
|
+
const fields = command[op];
|
|
281
493
|
try {
|
|
282
494
|
updated = assign(updated, command);
|
|
495
|
+
trackChangedFields(fields, updated, changedByCommand);
|
|
283
496
|
} catch {
|
|
497
|
+
if (op === "$pullAll" && fields) {
|
|
498
|
+
applyPullAll(updated, fields, changedByCommand);
|
|
499
|
+
}
|
|
284
500
|
}
|
|
285
|
-
}
|
|
501
|
+
}
|
|
286
502
|
const doc = document.set(updated).toObject(toObjectOptions);
|
|
503
|
+
for (const [field, value] of changedByCommand) {
|
|
504
|
+
doc[field] = value;
|
|
505
|
+
}
|
|
287
506
|
if (update.createdAt) doc.createdAt = update.createdAt;
|
|
288
507
|
return doc;
|
|
289
508
|
};
|
|
@@ -292,28 +511,27 @@ const splitUpdateAndCommands = (updateQuery) => {
|
|
|
292
511
|
const commands = [];
|
|
293
512
|
if (!isEmpty(updateQuery) && !isArray(updateQuery) && isObjectLike(updateQuery)) {
|
|
294
513
|
update = cloneDeep(updateQuery);
|
|
295
|
-
const keysWithDollarSign = keys(update).filter((key) => key.startsWith("$"));
|
|
514
|
+
const keysWithDollarSign = Object.keys(update).filter((key) => key.startsWith("$"));
|
|
296
515
|
if (!isEmpty(keysWithDollarSign)) {
|
|
297
|
-
|
|
516
|
+
for (const key of keysWithDollarSign) {
|
|
298
517
|
commands.push({ [key]: update[key] });
|
|
299
518
|
delete update[key];
|
|
300
|
-
}
|
|
519
|
+
}
|
|
301
520
|
}
|
|
302
521
|
}
|
|
303
522
|
return { update, commands };
|
|
304
523
|
};
|
|
305
524
|
const updateHooksInitialize = (schema, opts) => {
|
|
306
|
-
schema.pre(updateMethods, async function() {
|
|
525
|
+
schema.pre(updateMethods, { document: false, query: true }, async function() {
|
|
307
526
|
const options = this.getOptions();
|
|
308
527
|
if (isHookIgnored(options)) return;
|
|
309
528
|
const model = this.model;
|
|
310
529
|
const filter = this.getFilter();
|
|
311
|
-
const count = await this.model.countDocuments(filter).exec();
|
|
312
530
|
this._context = {
|
|
313
531
|
op: this.op,
|
|
314
|
-
modelName: opts.modelName ??
|
|
315
|
-
collectionName: opts.collectionName ??
|
|
316
|
-
isNew: Boolean(options.upsert) &&
|
|
532
|
+
modelName: opts.modelName ?? model.modelName,
|
|
533
|
+
collectionName: opts.collectionName ?? model.collection.collectionName,
|
|
534
|
+
isNew: Boolean(options.upsert) && await model.countDocuments(filter).exec() === 0,
|
|
317
535
|
ignoreEvent: options.ignoreEvent,
|
|
318
536
|
ignorePatchHistory: options.ignorePatchHistory
|
|
319
537
|
};
|
|
@@ -325,25 +543,20 @@ const updateHooksInitialize = (schema, opts) => {
|
|
|
325
543
|
await updatePatch(opts, this._context, assignUpdate(doc, update, commands), origDoc);
|
|
326
544
|
});
|
|
327
545
|
});
|
|
328
|
-
schema.post(updateMethods, async function() {
|
|
546
|
+
schema.post(updateMethods, { document: false, query: true }, async function() {
|
|
329
547
|
const options = this.getOptions();
|
|
330
548
|
if (isHookIgnored(options)) return;
|
|
549
|
+
if (!this._context) return;
|
|
331
550
|
if (!this._context.isNew) return;
|
|
332
551
|
const model = this.model;
|
|
333
552
|
const updateQuery = this.getUpdate();
|
|
334
553
|
const { update, commands } = splitUpdateAndCommands(updateQuery);
|
|
335
|
-
let current = null;
|
|
336
554
|
const filter = this.getFilter();
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
current = await model.findOne(combined).sort("desc").lean().exec();
|
|
343
|
-
}
|
|
344
|
-
if (!isEmpty(filter) && !current) {
|
|
345
|
-
console.log("filter", filter);
|
|
346
|
-
current = await model.findOne(filter).sort("desc").lean().exec();
|
|
555
|
+
const candidates = [update, assignUpdate(model.hydrate({}), update, commands), filter];
|
|
556
|
+
let current = null;
|
|
557
|
+
for (const query of candidates) {
|
|
558
|
+
if (current || isEmpty(query)) continue;
|
|
559
|
+
current = await model.findOne(query).sort({ _id: -1 }).lean().exec();
|
|
347
560
|
}
|
|
348
561
|
if (current) {
|
|
349
562
|
this._context.createdDocs = [current];
|
|
@@ -352,15 +565,16 @@ const updateHooksInitialize = (schema, opts) => {
|
|
|
352
565
|
});
|
|
353
566
|
};
|
|
354
567
|
|
|
355
|
-
const
|
|
356
|
-
const
|
|
357
|
-
const
|
|
568
|
+
const major = Number.parseInt(mongoose.version, 10);
|
|
569
|
+
const isMongooseLessThan8 = major < 8;
|
|
570
|
+
const isMongooseLessThan7 = major < 7;
|
|
571
|
+
const isMongoose6 = major === 6;
|
|
358
572
|
if (isMongoose6) {
|
|
359
573
|
mongoose.set("strictQuery", false);
|
|
360
574
|
}
|
|
361
575
|
|
|
362
576
|
const remove = isMongooseLessThan7 ? "remove" : "deleteOne";
|
|
363
|
-
const patchHistoryPlugin =
|
|
577
|
+
const patchHistoryPlugin = (schema, opts) => {
|
|
364
578
|
saveHooksInitialize(schema, opts);
|
|
365
579
|
updateHooksInitialize(schema, opts);
|
|
366
580
|
deleteHooksInitialize(schema, opts);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-patch-mongoose",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.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.
|
|
78
|
+
"@biomejs/biome": "2.4.9",
|
|
79
|
+
"@types/node": "25.5.0",
|
|
80
|
+
"@vitest/coverage-v8": "4.1.2",
|
|
88
81
|
"mongodb-memory-server": "11.0.1",
|
|
89
|
-
"mongoose": "9.
|
|
90
|
-
"
|
|
91
|
-
"
|
|
82
|
+
"mongoose": "9.3.3",
|
|
83
|
+
"np": "11.0.2",
|
|
84
|
+
"open-cli": "9.0.0",
|
|
85
|
+
"pkgroll": "2.27.0",
|
|
92
86
|
"simple-git-hooks": "2.13.1",
|
|
93
87
|
"typescript": "5.9.3",
|
|
94
|
-
"vitest": "4.
|
|
95
|
-
"np": "11.0.2"
|
|
88
|
+
"vitest": "4.1.2"
|
|
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
|
}
|