ts-patch-mongoose 3.0.0 → 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/biome.json +2 -5
- package/dist/index.cjs +103 -109
- package/dist/index.d.cts +2 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +103 -109
- package/package.json +6 -6
- package/src/helpers.ts +16 -3
- package/src/hooks/delete-hooks.ts +5 -4
- package/src/hooks/update-hooks.ts +42 -19
- package/src/omit-deep.ts +24 -63
- package/src/patch.ts +28 -31
- package/src/types.ts +1 -0
- package/tests/em.test.ts +24 -8
- package/tests/helpers.test.ts +62 -0
- package/tests/omit-deep.test.ts +25 -10
- package/tests/patch.test.ts +6 -5
- package/tests/plugin-all-features.test.ts +104 -1
- package/tests/plugin-complex-data.test.ts +1315 -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/vite.config.mts +2 -1
package/dist/index.mjs
CHANGED
|
@@ -138,6 +138,12 @@ const cloneImmutable = (value) => {
|
|
|
138
138
|
cloned.lastIndex = re.lastIndex;
|
|
139
139
|
return cloned;
|
|
140
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
|
+
}
|
|
141
147
|
case "[object ArrayBuffer]":
|
|
142
148
|
return cloneArrayBuffer(value);
|
|
143
149
|
case "[object DataView]": {
|
|
@@ -186,7 +192,11 @@ const cloneDeep = (value, seen = /* @__PURE__ */ new WeakMap()) => {
|
|
|
186
192
|
if (seen.has(value)) return seen.get(value);
|
|
187
193
|
const immutable = cloneImmutable(value);
|
|
188
194
|
if (immutable !== void 0) return immutable;
|
|
189
|
-
|
|
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") {
|
|
190
200
|
return JSON.parse(JSON.stringify(value));
|
|
191
201
|
}
|
|
192
202
|
return cloneCollection(value, seen);
|
|
@@ -205,7 +215,7 @@ const toObjectOptions = {
|
|
|
205
215
|
depopulate: true,
|
|
206
216
|
virtuals: false
|
|
207
217
|
};
|
|
208
|
-
const setPatchHistoryTTL = async (ttl) => {
|
|
218
|
+
const setPatchHistoryTTL = async (ttl, onError) => {
|
|
209
219
|
const name = "createdAt_1_TTL";
|
|
210
220
|
try {
|
|
211
221
|
const indexes = await HistoryModel.collection.indexes();
|
|
@@ -228,7 +238,8 @@ const setPatchHistoryTTL = async (ttl) => {
|
|
|
228
238
|
}
|
|
229
239
|
await HistoryModel.collection.createIndex({ createdAt: 1 }, { expireAfterSeconds, name });
|
|
230
240
|
} catch (err) {
|
|
231
|
-
|
|
241
|
+
const handler = onError ?? console.error;
|
|
242
|
+
handler(err);
|
|
232
243
|
}
|
|
233
244
|
};
|
|
234
245
|
|
|
@@ -244,113 +255,65 @@ const isPlainObject = (val) => {
|
|
|
244
255
|
const isUnsafeKey = (key) => {
|
|
245
256
|
return key === "__proto__" || key === "constructor" || key === "prototype";
|
|
246
257
|
};
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
for (const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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];
|
|
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
|
+
}
|
|
295
273
|
}
|
|
296
|
-
return delete target[last ?? ""];
|
|
297
274
|
}
|
|
298
|
-
return
|
|
275
|
+
return { topLevel, nested };
|
|
299
276
|
};
|
|
300
277
|
const omitDeep = (value, keys) => {
|
|
301
278
|
if (value === void 0) return {};
|
|
302
279
|
if (Array.isArray(value)) {
|
|
303
|
-
|
|
304
|
-
value[i] = omitDeep(value[i], keys);
|
|
305
|
-
}
|
|
306
|
-
return value;
|
|
280
|
+
return value.map((item) => omitDeep(item, keys));
|
|
307
281
|
}
|
|
308
282
|
if (!isPlainObject(value)) return value;
|
|
309
283
|
const omitKeys = typeof keys === "string" ? [keys] : keys;
|
|
310
284
|
if (!Array.isArray(omitKeys)) return value;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
285
|
+
const { topLevel, nested } = classifyKeys(omitKeys);
|
|
286
|
+
const result = {};
|
|
314
287
|
for (const key of Object.keys(value)) {
|
|
315
|
-
|
|
288
|
+
if (topLevel.has(key)) continue;
|
|
289
|
+
const nestedKeys = nested.get(key);
|
|
290
|
+
result[key] = omitDeep(value[key], nestedKeys ?? omitKeys);
|
|
316
291
|
}
|
|
317
|
-
return
|
|
292
|
+
return result;
|
|
318
293
|
};
|
|
319
294
|
|
|
320
295
|
const isPatchHistoryEnabled = (opts, context) => {
|
|
321
296
|
return !opts.patchHistoryDisabled && !context.ignorePatchHistory;
|
|
322
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;
|
|
323
302
|
const getJsonOmit = (opts, doc) => {
|
|
324
|
-
|
|
325
|
-
if (opts.omit) {
|
|
326
|
-
return omitDeep(object, opts.omit);
|
|
327
|
-
}
|
|
328
|
-
return object;
|
|
303
|
+
return applyOmit(JSON.parse(JSON.stringify(doc, replacer)), opts);
|
|
329
304
|
};
|
|
330
305
|
const getObjectOmit = (opts, doc) => {
|
|
331
|
-
|
|
332
|
-
return omitDeep(isFunction(doc?.toObject) ? doc.toObject() : doc, opts.omit);
|
|
333
|
-
}
|
|
334
|
-
return doc;
|
|
335
|
-
};
|
|
336
|
-
const getUser = async (opts, doc) => {
|
|
337
|
-
if (isFunction(opts.getUser)) {
|
|
338
|
-
return await opts.getUser(doc);
|
|
339
|
-
}
|
|
340
|
-
return void 0;
|
|
341
|
-
};
|
|
342
|
-
const getReason = async (opts, doc) => {
|
|
343
|
-
if (isFunction(opts.getReason)) {
|
|
344
|
-
return await opts.getReason(doc);
|
|
345
|
-
}
|
|
346
|
-
return void 0;
|
|
306
|
+
return applyOmit(isFunction(doc?.toObject) ? doc.toObject() : doc, opts);
|
|
347
307
|
};
|
|
348
|
-
const
|
|
349
|
-
if (isFunction(
|
|
350
|
-
return await
|
|
308
|
+
const getOptionalField = async (fn, doc) => {
|
|
309
|
+
if (isFunction(fn)) {
|
|
310
|
+
return await fn(doc);
|
|
351
311
|
}
|
|
352
312
|
return void 0;
|
|
353
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);
|
|
354
317
|
const getValue = (item) => {
|
|
355
318
|
return item.status === "fulfilled" ? item.value : void 0;
|
|
356
319
|
};
|
|
@@ -361,7 +324,10 @@ const getData = async (opts, doc) => {
|
|
|
361
324
|
};
|
|
362
325
|
const emitEvent = (context, event, data) => {
|
|
363
326
|
if (event && !context.ignoreEvent) {
|
|
364
|
-
|
|
327
|
+
try {
|
|
328
|
+
em.emit(event, data);
|
|
329
|
+
} catch {
|
|
330
|
+
}
|
|
365
331
|
}
|
|
366
332
|
};
|
|
367
333
|
const bulkPatch = async (opts, context, eventKey, docsKey) => {
|
|
@@ -374,7 +340,8 @@ const bulkPatch = async (opts, context, eventKey, docsKey) => {
|
|
|
374
340
|
for (const batch of chunks) {
|
|
375
341
|
const bulk = [];
|
|
376
342
|
for (const doc of batch) {
|
|
377
|
-
|
|
343
|
+
const omitted = getObjectOmit(opts, doc);
|
|
344
|
+
emitEvent(context, event, { [key]: omitted });
|
|
378
345
|
if (history) {
|
|
379
346
|
const [user, reason, metadata] = await getData(opts, doc);
|
|
380
347
|
bulk.push({
|
|
@@ -384,7 +351,7 @@ const bulkPatch = async (opts, context, eventKey, docsKey) => {
|
|
|
384
351
|
modelName: context.modelName,
|
|
385
352
|
collectionName: context.collectionName,
|
|
386
353
|
collectionId: doc._id,
|
|
387
|
-
doc:
|
|
354
|
+
doc: omitted,
|
|
388
355
|
version: 0,
|
|
389
356
|
...user !== void 0 && { user },
|
|
390
357
|
...reason !== void 0 && { reason },
|
|
@@ -395,8 +362,9 @@ const bulkPatch = async (opts, context, eventKey, docsKey) => {
|
|
|
395
362
|
}
|
|
396
363
|
}
|
|
397
364
|
if (history && !isEmpty(bulk)) {
|
|
365
|
+
const onError = opts.onError ?? console.error;
|
|
398
366
|
await HistoryModel.bulkWrite(bulk, { ordered: false }).catch((error) => {
|
|
399
|
-
|
|
367
|
+
onError(error);
|
|
400
368
|
});
|
|
401
369
|
}
|
|
402
370
|
}
|
|
@@ -419,6 +387,7 @@ const updatePatch = async (opts, context, current, original) => {
|
|
|
419
387
|
version = lastHistory.version + 1;
|
|
420
388
|
}
|
|
421
389
|
const [user, reason, metadata] = await getData(opts, current);
|
|
390
|
+
const onError = opts.onError ?? console.error;
|
|
422
391
|
await HistoryModel.create({
|
|
423
392
|
op: context.op,
|
|
424
393
|
modelName: context.modelName,
|
|
@@ -429,6 +398,8 @@ const updatePatch = async (opts, context, current, original) => {
|
|
|
429
398
|
...user !== void 0 && { user },
|
|
430
399
|
...reason !== void 0 && { reason },
|
|
431
400
|
...metadata !== void 0 && { metadata }
|
|
401
|
+
}).catch((error) => {
|
|
402
|
+
onError(error);
|
|
432
403
|
});
|
|
433
404
|
}
|
|
434
405
|
};
|
|
@@ -445,8 +416,8 @@ const deleteHooksInitialize = (schema, opts) => {
|
|
|
445
416
|
const filter = this.getFilter();
|
|
446
417
|
this._context = {
|
|
447
418
|
op: this.op,
|
|
448
|
-
modelName: opts.modelName ??
|
|
449
|
-
collectionName: opts.collectionName ??
|
|
419
|
+
modelName: opts.modelName ?? model.modelName,
|
|
420
|
+
collectionName: opts.collectionName ?? model.collection.collectionName,
|
|
450
421
|
ignoreEvent: options.ignoreEvent,
|
|
451
422
|
ignorePatchHistory: options.ignorePatchHistory
|
|
452
423
|
};
|
|
@@ -462,12 +433,13 @@ const deleteHooksInitialize = (schema, opts) => {
|
|
|
462
433
|
}
|
|
463
434
|
}
|
|
464
435
|
if (opts.preDelete && isArray(this._context.deletedDocs) && !isEmpty(this._context.deletedDocs)) {
|
|
465
|
-
await opts.preDelete(this._context.deletedDocs);
|
|
436
|
+
await opts.preDelete(cloneDeep(this._context.deletedDocs));
|
|
466
437
|
}
|
|
467
438
|
});
|
|
468
439
|
schema.post(deleteMethods, { document: false, query: true }, async function() {
|
|
469
440
|
const options = this.getOptions();
|
|
470
441
|
if (isHookIgnored(options)) return;
|
|
442
|
+
if (!this._context) return;
|
|
471
443
|
await deletePatch(opts, this._context);
|
|
472
444
|
});
|
|
473
445
|
};
|
|
@@ -495,15 +467,42 @@ const saveHooksInitialize = (schema, opts) => {
|
|
|
495
467
|
};
|
|
496
468
|
|
|
497
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
|
+
};
|
|
498
487
|
const assignUpdate = (document, update, commands) => {
|
|
499
488
|
let updated = assign(document.toObject(toObjectOptions), update);
|
|
489
|
+
const changedByCommand = /* @__PURE__ */ new Map();
|
|
500
490
|
for (const command of commands) {
|
|
491
|
+
const op = Object.keys(command)[0];
|
|
492
|
+
const fields = command[op];
|
|
501
493
|
try {
|
|
502
494
|
updated = assign(updated, command);
|
|
495
|
+
trackChangedFields(fields, updated, changedByCommand);
|
|
503
496
|
} catch {
|
|
497
|
+
if (op === "$pullAll" && fields) {
|
|
498
|
+
applyPullAll(updated, fields, changedByCommand);
|
|
499
|
+
}
|
|
504
500
|
}
|
|
505
501
|
}
|
|
506
502
|
const doc = document.set(updated).toObject(toObjectOptions);
|
|
503
|
+
for (const [field, value] of changedByCommand) {
|
|
504
|
+
doc[field] = value;
|
|
505
|
+
}
|
|
507
506
|
if (update.createdAt) doc.createdAt = update.createdAt;
|
|
508
507
|
return doc;
|
|
509
508
|
};
|
|
@@ -523,17 +522,16 @@ const splitUpdateAndCommands = (updateQuery) => {
|
|
|
523
522
|
return { update, commands };
|
|
524
523
|
};
|
|
525
524
|
const updateHooksInitialize = (schema, opts) => {
|
|
526
|
-
schema.pre(updateMethods, async function() {
|
|
525
|
+
schema.pre(updateMethods, { document: false, query: true }, async function() {
|
|
527
526
|
const options = this.getOptions();
|
|
528
527
|
if (isHookIgnored(options)) return;
|
|
529
528
|
const model = this.model;
|
|
530
529
|
const filter = this.getFilter();
|
|
531
|
-
const count = await this.model.countDocuments(filter).exec();
|
|
532
530
|
this._context = {
|
|
533
531
|
op: this.op,
|
|
534
|
-
modelName: opts.modelName ??
|
|
535
|
-
collectionName: opts.collectionName ??
|
|
536
|
-
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,
|
|
537
535
|
ignoreEvent: options.ignoreEvent,
|
|
538
536
|
ignorePatchHistory: options.ignorePatchHistory
|
|
539
537
|
};
|
|
@@ -545,24 +543,20 @@ const updateHooksInitialize = (schema, opts) => {
|
|
|
545
543
|
await updatePatch(opts, this._context, assignUpdate(doc, update, commands), origDoc);
|
|
546
544
|
});
|
|
547
545
|
});
|
|
548
|
-
schema.post(updateMethods, async function() {
|
|
546
|
+
schema.post(updateMethods, { document: false, query: true }, async function() {
|
|
549
547
|
const options = this.getOptions();
|
|
550
548
|
if (isHookIgnored(options)) return;
|
|
549
|
+
if (!this._context) return;
|
|
551
550
|
if (!this._context.isNew) return;
|
|
552
551
|
const model = this.model;
|
|
553
552
|
const updateQuery = this.getUpdate();
|
|
554
553
|
const { update, commands } = splitUpdateAndCommands(updateQuery);
|
|
555
|
-
let current = null;
|
|
556
554
|
const filter = this.getFilter();
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
current = await model.findOne(combined).sort("desc").lean().exec();
|
|
563
|
-
}
|
|
564
|
-
if (!isEmpty(filter) && !current) {
|
|
565
|
-
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();
|
|
566
560
|
}
|
|
567
561
|
if (current) {
|
|
568
562
|
this._context.createdDocs = [current];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-patch-mongoose",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Patch history & events for mongoose models",
|
|
5
5
|
"author": "ilovepixelart",
|
|
6
6
|
"license": "MIT",
|
|
@@ -75,17 +75,17 @@
|
|
|
75
75
|
"power-assign": "0.2.10"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
|
-
"@biomejs/biome": "2.4.
|
|
78
|
+
"@biomejs/biome": "2.4.9",
|
|
79
79
|
"@types/node": "25.5.0",
|
|
80
|
-
"@vitest/coverage-v8": "4.1.
|
|
80
|
+
"@vitest/coverage-v8": "4.1.2",
|
|
81
81
|
"mongodb-memory-server": "11.0.1",
|
|
82
|
-
"mongoose": "9.3.
|
|
82
|
+
"mongoose": "9.3.3",
|
|
83
83
|
"np": "11.0.2",
|
|
84
|
-
"open-cli": "
|
|
84
|
+
"open-cli": "9.0.0",
|
|
85
85
|
"pkgroll": "2.27.0",
|
|
86
86
|
"simple-git-hooks": "2.13.1",
|
|
87
87
|
"typescript": "5.9.3",
|
|
88
|
-
"vitest": "4.1.
|
|
88
|
+
"vitest": "4.1.2"
|
|
89
89
|
},
|
|
90
90
|
"peerDependencies": {
|
|
91
91
|
"mongoose": ">=6.6.0 < 10"
|
package/src/helpers.ts
CHANGED
|
@@ -44,6 +44,12 @@ const cloneImmutable = <T>(value: T): T | undefined => {
|
|
|
44
44
|
cloned.lastIndex = re.lastIndex
|
|
45
45
|
return cloned as T
|
|
46
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
|
+
}
|
|
47
53
|
case '[object ArrayBuffer]':
|
|
48
54
|
return cloneArrayBuffer(value as unknown as ArrayBuffer) as T
|
|
49
55
|
case '[object DataView]': {
|
|
@@ -101,7 +107,13 @@ export const cloneDeep = <T>(value: T, seen = new WeakMap<object, unknown>()): T
|
|
|
101
107
|
const immutable = cloneImmutable(value)
|
|
102
108
|
if (immutable !== undefined) return immutable
|
|
103
109
|
|
|
104
|
-
|
|
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') {
|
|
105
117
|
// NOSONAR — structuredClone cannot handle objects with non-cloneable methods (e.g. mongoose documents)
|
|
106
118
|
return JSON.parse(JSON.stringify(value)) as T
|
|
107
119
|
}
|
|
@@ -126,7 +138,7 @@ export const toObjectOptions: ToObjectOptions = {
|
|
|
126
138
|
virtuals: false,
|
|
127
139
|
}
|
|
128
140
|
|
|
129
|
-
export const setPatchHistoryTTL = async (ttl: Duration): Promise<void> => {
|
|
141
|
+
export const setPatchHistoryTTL = async (ttl: Duration, onError?: (error: Error) => void): Promise<void> => {
|
|
130
142
|
const name = 'createdAt_1_TTL'
|
|
131
143
|
try {
|
|
132
144
|
const indexes = await HistoryModel.collection.indexes()
|
|
@@ -156,6 +168,7 @@ export const setPatchHistoryTTL = async (ttl: Duration): Promise<void> => {
|
|
|
156
168
|
|
|
157
169
|
await HistoryModel.collection.createIndex({ createdAt: 1 }, { expireAfterSeconds, name })
|
|
158
170
|
} catch (err) {
|
|
159
|
-
|
|
171
|
+
const handler = onError ?? console.error
|
|
172
|
+
handler(err as Error)
|
|
160
173
|
}
|
|
161
174
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isArray, isEmpty, isHookIgnored } from '../helpers'
|
|
1
|
+
import { cloneDeep, isArray, isEmpty, isHookIgnored } from '../helpers'
|
|
2
2
|
import { deletePatch } from '../patch'
|
|
3
3
|
|
|
4
4
|
import type { HydratedDocument, Model, MongooseQueryMiddleware, Schema } from 'mongoose'
|
|
@@ -16,8 +16,8 @@ export const deleteHooksInitialize = <T>(schema: Schema<T>, opts: PluginOptions<
|
|
|
16
16
|
|
|
17
17
|
this._context = {
|
|
18
18
|
op: this.op,
|
|
19
|
-
modelName: opts.modelName ??
|
|
20
|
-
collectionName: opts.collectionName ??
|
|
19
|
+
modelName: opts.modelName ?? model.modelName,
|
|
20
|
+
collectionName: opts.collectionName ?? model.collection.collectionName,
|
|
21
21
|
ignoreEvent: options.ignoreEvent as boolean,
|
|
22
22
|
ignorePatchHistory: options.ignorePatchHistory as boolean,
|
|
23
23
|
}
|
|
@@ -35,13 +35,14 @@ export const deleteHooksInitialize = <T>(schema: Schema<T>, opts: PluginOptions<
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
if (opts.preDelete && isArray(this._context.deletedDocs) && !isEmpty(this._context.deletedDocs)) {
|
|
38
|
-
await opts.preDelete(this._context.deletedDocs)
|
|
38
|
+
await opts.preDelete(cloneDeep(this._context.deletedDocs))
|
|
39
39
|
}
|
|
40
40
|
})
|
|
41
41
|
|
|
42
42
|
schema.post(deleteMethods as MongooseQueryMiddleware[], { document: false, query: true }, async function (this: HookContext<T>) {
|
|
43
43
|
const options = this.getOptions()
|
|
44
44
|
if (isHookIgnored(options)) return
|
|
45
|
+
if (!this._context) return
|
|
45
46
|
|
|
46
47
|
await deletePatch(opts, this._context)
|
|
47
48
|
})
|
|
@@ -7,17 +7,46 @@ import type { HookContext, PluginOptions } from '../types'
|
|
|
7
7
|
|
|
8
8
|
const updateMethods = ['update', 'updateOne', 'replaceOne', 'updateMany', 'findOneAndUpdate', 'findOneAndReplace', 'findByIdAndUpdate']
|
|
9
9
|
|
|
10
|
+
const trackChangedFields = (fields: Record<string, unknown> | undefined, updated: Record<string, unknown>, changed: Map<string, unknown>): void => {
|
|
11
|
+
if (!fields) return
|
|
12
|
+
for (const key of Object.keys(fields)) {
|
|
13
|
+
const root = key.split('.')[0] as string
|
|
14
|
+
changed.set(root, updated[root])
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const applyPullAll = (updated: Record<string, unknown>, fields: Record<string, unknown[]>, changed: Map<string, unknown>): void => {
|
|
19
|
+
for (const [field, values] of Object.entries(fields)) {
|
|
20
|
+
const arr = updated[field]
|
|
21
|
+
if (Array.isArray(arr)) {
|
|
22
|
+
const filtered = arr.filter((item: unknown) => !values.some((v) => JSON.stringify(v) === JSON.stringify(item)))
|
|
23
|
+
updated[field] = filtered
|
|
24
|
+
changed.set(field, filtered)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
10
29
|
export const assignUpdate = <T>(document: HydratedDocument<T>, update: UpdateQuery<T>, commands: Record<string, unknown>[]): HydratedDocument<T> => {
|
|
11
|
-
let updated = assign(document.toObject(toObjectOptions), update)
|
|
30
|
+
let updated = assign(document.toObject(toObjectOptions), update) as Record<string, unknown>
|
|
31
|
+
const changedByCommand = new Map<string, unknown>()
|
|
32
|
+
|
|
12
33
|
for (const command of commands) {
|
|
34
|
+
const op = Object.keys(command)[0] as string
|
|
35
|
+
const fields = command[op] as Record<string, unknown> | undefined
|
|
13
36
|
try {
|
|
14
37
|
updated = assign(updated, command)
|
|
38
|
+
trackChangedFields(fields, updated, changedByCommand)
|
|
15
39
|
} catch {
|
|
16
|
-
|
|
40
|
+
if (op === '$pullAll' && fields) {
|
|
41
|
+
applyPullAll(updated, fields as Record<string, unknown[]>, changedByCommand)
|
|
42
|
+
}
|
|
17
43
|
}
|
|
18
44
|
}
|
|
19
45
|
|
|
20
46
|
const doc = document.set(updated).toObject(toObjectOptions) as HydratedDocument<T> & { createdAt?: Date }
|
|
47
|
+
for (const [field, value] of changedByCommand) {
|
|
48
|
+
;(doc as unknown as Record<string, unknown>)[field] = value
|
|
49
|
+
}
|
|
21
50
|
if (update.createdAt) doc.createdAt = update.createdAt
|
|
22
51
|
return doc
|
|
23
52
|
}
|
|
@@ -41,19 +70,18 @@ export const splitUpdateAndCommands = <T>(updateQuery: UpdateWithAggregationPipe
|
|
|
41
70
|
}
|
|
42
71
|
|
|
43
72
|
export const updateHooksInitialize = <T>(schema: Schema<T>, opts: PluginOptions<T>): void => {
|
|
44
|
-
schema.pre(updateMethods as MongooseQueryMiddleware[], async function (this: HookContext<T>) {
|
|
73
|
+
schema.pre(updateMethods as MongooseQueryMiddleware[], { document: false, query: true }, async function (this: HookContext<T>) {
|
|
45
74
|
const options = this.getOptions()
|
|
46
75
|
if (isHookIgnored(options)) return
|
|
47
76
|
|
|
48
77
|
const model = this.model as Model<T>
|
|
49
78
|
const filter = this.getFilter()
|
|
50
|
-
const count = await this.model.countDocuments(filter).exec()
|
|
51
79
|
|
|
52
80
|
this._context = {
|
|
53
81
|
op: this.op,
|
|
54
|
-
modelName: opts.modelName ??
|
|
55
|
-
collectionName: opts.collectionName ??
|
|
56
|
-
isNew: Boolean(options.upsert) &&
|
|
82
|
+
modelName: opts.modelName ?? model.modelName,
|
|
83
|
+
collectionName: opts.collectionName ?? model.collection.collectionName,
|
|
84
|
+
isNew: Boolean(options.upsert) && (await model.countDocuments(filter).exec()) === 0,
|
|
57
85
|
ignoreEvent: options.ignoreEvent as boolean,
|
|
58
86
|
ignorePatchHistory: options.ignorePatchHistory as boolean,
|
|
59
87
|
}
|
|
@@ -68,9 +96,10 @@ export const updateHooksInitialize = <T>(schema: Schema<T>, opts: PluginOptions<
|
|
|
68
96
|
})
|
|
69
97
|
})
|
|
70
98
|
|
|
71
|
-
schema.post(updateMethods as MongooseQueryMiddleware[], async function (this: HookContext<T>) {
|
|
99
|
+
schema.post(updateMethods as MongooseQueryMiddleware[], { document: false, query: true }, async function (this: HookContext<T>) {
|
|
72
100
|
const options = this.getOptions()
|
|
73
101
|
if (isHookIgnored(options)) return
|
|
102
|
+
if (!this._context) return
|
|
74
103
|
|
|
75
104
|
if (!this._context.isNew) return
|
|
76
105
|
|
|
@@ -78,19 +107,13 @@ export const updateHooksInitialize = <T>(schema: Schema<T>, opts: PluginOptions<
|
|
|
78
107
|
const updateQuery = this.getUpdate()
|
|
79
108
|
const { update, commands } = splitUpdateAndCommands(updateQuery)
|
|
80
109
|
|
|
81
|
-
let current: HydratedDocument<T> | null = null
|
|
82
110
|
const filter = this.getFilter()
|
|
83
|
-
const
|
|
84
|
-
if (!isEmpty(update) && !current) {
|
|
85
|
-
current = (await model.findOne(update).sort('desc').lean().exec()) as HydratedDocument<T>
|
|
86
|
-
}
|
|
111
|
+
const candidates = [update, assignUpdate(model.hydrate({}), update, commands), filter]
|
|
87
112
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (!isEmpty(filter) && !current) {
|
|
93
|
-
current = (await model.findOne(filter).sort('desc').lean().exec()) as HydratedDocument<T>
|
|
113
|
+
let current: HydratedDocument<T> | null = null
|
|
114
|
+
for (const query of candidates) {
|
|
115
|
+
if (current || isEmpty(query)) continue
|
|
116
|
+
current = (await model.findOne(query).sort({ _id: -1 }).lean().exec()) as HydratedDocument<T>
|
|
94
117
|
}
|
|
95
118
|
|
|
96
119
|
if (current) {
|