ts-patch-mongoose 3.1.0 → 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 +39 -27
- package/dist/index.cjs +287 -32
- package/dist/index.d.cts +23 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +23 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +287 -32
- package/package.json +19 -24
- package/biome.json +0 -47
- 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 -125
- package/src/index.ts +0 -65
- package/src/model.ts +0 -50
- package/src/modules/power-assign.d.ts +0 -3
- package/src/ms.ts +0 -66
- package/src/omit-deep.ts +0 -56
- package/src/patch.ts +0 -154
- package/src/types.ts +0 -53
- package/src/version.ts +0 -13
- package/tests/constants/events.ts +0 -7
- package/tests/em.test.ts +0 -70
- package/tests/helpers.test.ts +0 -373
- package/tests/mongo/.gitignore +0 -3
- package/tests/mongo/server.ts +0 -31
- package/tests/ms.test.ts +0 -113
- package/tests/omit-deep.test.ts +0 -235
- package/tests/patch.test.ts +0 -200
- package/tests/plugin-all-features.test.ts +0 -844
- package/tests/plugin-complex-data.test.ts +0 -2647
- package/tests/plugin-event-created.test.ts +0 -371
- package/tests/plugin-event-deleted.test.ts +0 -400
- package/tests/plugin-event-updated.test.ts +0 -503
- package/tests/plugin-global.test.ts +0 -545
- package/tests/plugin-omit-all.test.ts +0 -349
- package/tests/plugin-patch-history-disabled.test.ts +0 -162
- package/tests/plugin-pre-delete.test.ts +0 -160
- package/tests/plugin-pre-save.test.ts +0 -54
- package/tests/plugin.test.ts +0 -576
- package/tests/schemas/Description.ts +0 -15
- package/tests/schemas/Product.ts +0 -38
- package/tests/schemas/User.ts +0 -22
- package/tsconfig.json +0 -32
- package/vite.config.mts +0 -24
package/README.md
CHANGED
|
@@ -12,22 +12,28 @@ Patch history (audit log) & events plugin for mongoose
|
|
|
12
12
|
[](https://sonarcloud.io/summary/new_code?id=ilovepixelart_ts-patch-mongoose)
|
|
13
13
|
[](https://sonarcloud.io/summary/new_code?id=ilovepixelart_ts-patch-mongoose)
|
|
14
14
|
[](https://sonarcloud.io/summary/new_code?id=ilovepixelart_ts-patch-mongoose)
|
|
15
|
+
\
|
|
16
|
+
[](https://socket.dev/npm/package/ts-patch-mongoose)
|
|
17
|
+
[](https://securityscorecards.dev/viewer/?uri=github.com/ilovepixelart/ts-patch-mongoose)
|
|
18
|
+
[](https://www.bestpractices.dev/en/projects/12473)
|
|
15
19
|
|
|
16
20
|
## Motivation
|
|
17
21
|
|
|
18
|
-
ts-patch-mongoose is a plugin for mongoose
|
|
22
|
+
ts-patch-mongoose is a plugin for mongoose.
|
|
19
23
|
\
|
|
20
|
-
I need to track changes of mongoose models and save them as patch history (audit log) in separate collection. Changes must also emit events that I can subscribe to and react in other parts of my application. I also want to omit some fields from patch history.
|
|
24
|
+
I need to track changes of mongoose models and save them as patch history (audit log) in a separate collection. Changes must also emit events that I can subscribe to and react in other parts of my application. I also want to omit some fields from patch history.
|
|
21
25
|
|
|
22
26
|
## Supports and tested with
|
|
23
27
|
|
|
24
28
|
```json
|
|
25
29
|
{
|
|
26
30
|
"node": "20.x || 22.x || 24.x",
|
|
27
|
-
"mongoose": ">=6.6.
|
|
31
|
+
"mongoose": ">=6.6.0 <10"
|
|
28
32
|
}
|
|
29
33
|
```
|
|
30
34
|
|
|
35
|
+
CI tests against mongoose `6.12.2`, `7.6.4`, `8.23.0`, and `9.4.1`.
|
|
36
|
+
|
|
31
37
|
## Features
|
|
32
38
|
|
|
33
39
|
- Track changes in mongoose models
|
|
@@ -53,9 +59,9 @@ bun add ts-patch-mongoose mongoose
|
|
|
53
59
|
|
|
54
60
|
Works with any Node.js framework — Express, Fastify, Koa, Hono, Nest, etc.
|
|
55
61
|
\
|
|
56
|
-
How to use it with
|
|
62
|
+
How to use it with Express: [ts-express-tsx](https://github.com/ilovepixelart/ts-express-tsx)
|
|
57
63
|
|
|
58
|
-
Create your event constants `events.ts`
|
|
64
|
+
Create your event constants in `events.ts`
|
|
59
65
|
|
|
60
66
|
```typescript
|
|
61
67
|
export const BOOK_CREATED = 'book-created'
|
|
@@ -77,65 +83,66 @@ export type Book = {
|
|
|
77
83
|
}
|
|
78
84
|
```
|
|
79
85
|
|
|
80
|
-
|
|
86
|
+
Set up your mongoose model in `Book.ts`
|
|
81
87
|
|
|
82
88
|
```typescript
|
|
83
89
|
import { Schema, model } from 'mongoose'
|
|
84
90
|
|
|
85
|
-
import type { HydratedDocument
|
|
91
|
+
import type { HydratedDocument } from 'mongoose'
|
|
86
92
|
import type { Book } from '../types'
|
|
87
93
|
|
|
88
94
|
import { patchHistoryPlugin, setPatchHistoryTTL } from 'ts-patch-mongoose'
|
|
89
95
|
import { BOOK_CREATED, BOOK_UPDATED, BOOK_DELETED } from '../constants/events'
|
|
90
96
|
|
|
91
|
-
// You can set patch history TTL in plain
|
|
97
|
+
// You can set patch history TTL in plain English or in milliseconds as you wish.
|
|
92
98
|
// This will determine how long you want to keep patch history.
|
|
93
99
|
// You don't need to use this global config in case you want to keep patch history forever.
|
|
94
|
-
// Execute this method after you connected to
|
|
95
|
-
|
|
100
|
+
// Execute this method after you connected to your database somewhere in your application.
|
|
101
|
+
// Optional second argument for custom error handling
|
|
102
|
+
setPatchHistoryTTL('1 month', (error) => console.error('TTL setup failed:', error))
|
|
96
103
|
|
|
97
104
|
const BookSchema = new Schema<Book>({
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
title: {
|
|
106
|
+
type: String,
|
|
100
107
|
required: true
|
|
101
108
|
},
|
|
102
109
|
description: {
|
|
103
110
|
type: String,
|
|
104
111
|
},
|
|
105
112
|
authorId: {
|
|
106
|
-
type: Types.ObjectId,
|
|
113
|
+
type: Schema.Types.ObjectId,
|
|
107
114
|
required: true
|
|
108
115
|
}
|
|
109
116
|
}, { timestamps: true })
|
|
110
117
|
|
|
111
|
-
BookSchema.plugin(patchHistoryPlugin, {
|
|
118
|
+
BookSchema.plugin(patchHistoryPlugin, {
|
|
112
119
|
// Provide your event constants to plugin
|
|
113
120
|
eventCreated: BOOK_CREATED,
|
|
114
121
|
eventUpdated: BOOK_UPDATED,
|
|
115
122
|
eventDeleted: BOOK_DELETED,
|
|
116
|
-
|
|
123
|
+
|
|
117
124
|
// You can omit some properties in case you don't want to save them to patch history
|
|
118
125
|
omit: ['__v', 'createdAt', 'updatedAt'],
|
|
119
126
|
|
|
120
|
-
//
|
|
121
|
-
// Everything
|
|
127
|
+
// Additional options for patchHistoryPlugin
|
|
128
|
+
// Everything below is optional and just shows you what you can do:
|
|
122
129
|
|
|
123
|
-
// Code
|
|
124
|
-
// These three properties will be added to patch history document automatically and
|
|
130
|
+
// Code below is an abstract example, you can use any other way to get user, reason, metadata
|
|
131
|
+
// These three properties will be added to patch history document automatically and gives you flexibility to track who, why and when made changes to your documents
|
|
125
132
|
getUser: async (doc: HydratedDocument<Book>) => {
|
|
126
133
|
// For example: get user from http context
|
|
127
134
|
// You should return an object, in case you want to save user to patch history
|
|
128
135
|
return httpContext.get('user') as Record<string, unknown>
|
|
129
136
|
},
|
|
130
137
|
|
|
131
|
-
// Reason
|
|
138
|
+
// Reason for the document change (create/update/delete) like: 'Excel upload', 'Manual update', 'API call', etc.
|
|
132
139
|
getReason: async (doc: HydratedDocument<Book>) => {
|
|
133
140
|
// For example: get reason from http context, or any other place of your application
|
|
134
|
-
// You
|
|
141
|
+
// You should return a string, in case you want to save reason to patch history
|
|
135
142
|
return httpContext.get('reason') as string
|
|
136
143
|
},
|
|
137
144
|
|
|
138
|
-
// You can provide any information you want to save
|
|
145
|
+
// You can provide any information you want to save along with patch history
|
|
139
146
|
getMetadata: async (doc: HydratedDocument<Book>) => {
|
|
140
147
|
// For example: get metadata from http context, or any other place of your application
|
|
141
148
|
// You should return an object, in case you want to save metadata to patch history
|
|
@@ -143,15 +150,20 @@ BookSchema.plugin(patchHistoryPlugin, {
|
|
|
143
150
|
},
|
|
144
151
|
|
|
145
152
|
// Do something before deleting documents
|
|
146
|
-
// This method will be executed before deleting document or documents and always returns a
|
|
153
|
+
// This method will be executed before deleting document or documents and always returns a non-empty array of documents
|
|
147
154
|
preDelete: async (docs) => {
|
|
148
155
|
const bookIds = docs.map((doc) => doc._id)
|
|
149
156
|
await SomeOtherModel.deleteMany({ bookId: { $in: bookIds } })
|
|
150
157
|
},
|
|
151
158
|
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
159
|
+
// Custom error handler for history write failures (defaults to console.error)
|
|
160
|
+
onError: (error) => {
|
|
161
|
+
console.error('Patch history error:', error)
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
// In case you just want to track changes in your models using events
|
|
165
|
+
// and don't want to save changes to patch history collection
|
|
166
|
+
// patchHistoryDisabled: true,
|
|
155
167
|
})
|
|
156
168
|
|
|
157
169
|
const Book = model('Book', BookSchema)
|
|
@@ -188,7 +200,7 @@ patchEventEmitter.on(BOOK_UPDATED, ({ doc, oldDoc, patch }) => {
|
|
|
188
200
|
patchEventEmitter.on(BOOK_DELETED, ({ oldDoc }) => {
|
|
189
201
|
try {
|
|
190
202
|
console.log('Event - book deleted', oldDoc)
|
|
191
|
-
// Do something with
|
|
203
|
+
// Do something with oldDoc here
|
|
192
204
|
} catch (error) {
|
|
193
205
|
console.error(error)
|
|
194
206
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var mongoose = require('mongoose');
|
|
4
|
-
var jsonpatch = require('fast-json-patch');
|
|
5
4
|
var EventEmitter = require('node:events');
|
|
6
|
-
var powerAssign = require('power-assign');
|
|
7
5
|
|
|
8
6
|
const HistorySchema = new mongoose.Schema(
|
|
9
7
|
{
|
|
@@ -100,9 +98,10 @@ const ms = (val) => {
|
|
|
100
98
|
if (str.length > 100) return Number.NaN;
|
|
101
99
|
const match = RE.exec(str);
|
|
102
100
|
if (!match) return Number.NaN;
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
101
|
+
const [, numStr, unitStr] = match;
|
|
102
|
+
const n = Number.parseFloat(String(numStr));
|
|
103
|
+
const type = (unitStr ?? "ms").toLowerCase();
|
|
104
|
+
return n * UNITS[type];
|
|
106
105
|
};
|
|
107
106
|
|
|
108
107
|
const isArray = Array.isArray;
|
|
@@ -249,6 +248,94 @@ class PatchEventEmitter extends EventEmitter {
|
|
|
249
248
|
}
|
|
250
249
|
const em = new PatchEventEmitter();
|
|
251
250
|
|
|
251
|
+
const escapeToken = (key) => {
|
|
252
|
+
if (!key.includes("/") && !key.includes("~")) return key;
|
|
253
|
+
return key.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
254
|
+
};
|
|
255
|
+
const joinPath = (base, key) => `${base}/${escapeToken(key)}`;
|
|
256
|
+
const cloneValue = (value) => {
|
|
257
|
+
if (value === void 0) return null;
|
|
258
|
+
if (value === null || typeof value !== "object") return value;
|
|
259
|
+
return JSON.parse(JSON.stringify(value));
|
|
260
|
+
};
|
|
261
|
+
const isContainer = (value) => {
|
|
262
|
+
return typeof value === "object" && value !== null;
|
|
263
|
+
};
|
|
264
|
+
const keysOf = (value) => {
|
|
265
|
+
if (Array.isArray(value)) {
|
|
266
|
+
const indices = [];
|
|
267
|
+
for (let i = 0; i < value.length; i++) indices.push(String(i));
|
|
268
|
+
return indices;
|
|
269
|
+
}
|
|
270
|
+
return Object.keys(value);
|
|
271
|
+
};
|
|
272
|
+
const normalizeTarget = (target) => {
|
|
273
|
+
if (!isContainer(target) || Array.isArray(target)) return target;
|
|
274
|
+
const withToJSON = target;
|
|
275
|
+
return typeof withToJSON.toJSON === "function" ? withToJSON.toJSON() : target;
|
|
276
|
+
};
|
|
277
|
+
const emitTest = (path, value, invertible, out) => {
|
|
278
|
+
if (invertible) out.push({ op: "test", path, value: cloneValue(value) });
|
|
279
|
+
};
|
|
280
|
+
const emitReplace = (path, source, target, invertible, out) => {
|
|
281
|
+
emitTest(path, source, invertible, out);
|
|
282
|
+
out.push({ op: "replace", path, value: cloneValue(target) });
|
|
283
|
+
};
|
|
284
|
+
const emitRemove = (path, source, invertible, out) => {
|
|
285
|
+
emitTest(path, source, invertible, out);
|
|
286
|
+
out.push({ op: "remove", path });
|
|
287
|
+
};
|
|
288
|
+
const shouldTreatAsRemoval = (sourceChild, targetChild, sourceIsArray) => {
|
|
289
|
+
return targetChild === void 0 && sourceChild !== void 0 && !sourceIsArray;
|
|
290
|
+
};
|
|
291
|
+
const diffSourceKey = (scope, key) => {
|
|
292
|
+
const { source, target, targetKeySet, basePath, sourceIsArray, invertible, out } = scope;
|
|
293
|
+
const childPath = joinPath(basePath, key);
|
|
294
|
+
const sourceChild = source[key];
|
|
295
|
+
if (!targetKeySet.has(key)) {
|
|
296
|
+
emitRemove(childPath, sourceChild, invertible, out);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const targetChild = target[key];
|
|
300
|
+
if (shouldTreatAsRemoval(sourceChild, targetChild, sourceIsArray)) {
|
|
301
|
+
emitRemove(childPath, sourceChild, invertible, out);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
diff(sourceChild, targetChild, childPath, invertible, out);
|
|
305
|
+
};
|
|
306
|
+
const diffAddedKeys = (target, sourceKeys, targetKeys, basePath, out) => {
|
|
307
|
+
const sourceKeySet = new Set(sourceKeys);
|
|
308
|
+
for (const key of targetKeys) {
|
|
309
|
+
if (sourceKeySet.has(key)) continue;
|
|
310
|
+
const targetChild = target[key];
|
|
311
|
+
if (targetChild === void 0) continue;
|
|
312
|
+
out.push({ op: "add", path: joinPath(basePath, key), value: cloneValue(targetChild) });
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const diff = (source, target, basePath, invertible, out) => {
|
|
316
|
+
if (source === target) return;
|
|
317
|
+
const resolvedTarget = normalizeTarget(target);
|
|
318
|
+
const sourceIsArray = Array.isArray(source);
|
|
319
|
+
const targetIsArray = Array.isArray(resolvedTarget);
|
|
320
|
+
if (!isContainer(source) || !isContainer(resolvedTarget) || sourceIsArray !== targetIsArray) {
|
|
321
|
+
emitReplace(basePath, source, resolvedTarget, invertible, out);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const sourceKeys = keysOf(source);
|
|
325
|
+
const targetKeys = keysOf(resolvedTarget);
|
|
326
|
+
const targetKeySet = new Set(targetKeys);
|
|
327
|
+
const scope = { source, target: resolvedTarget, targetKeySet, basePath, sourceIsArray, invertible, out };
|
|
328
|
+
for (const key of Array.from(sourceKeys).reverse()) {
|
|
329
|
+
diffSourceKey(scope, key);
|
|
330
|
+
}
|
|
331
|
+
diffAddedKeys(resolvedTarget, sourceKeys, targetKeys, basePath, out);
|
|
332
|
+
};
|
|
333
|
+
const compare = (source, target, invertible = false) => {
|
|
334
|
+
const out = [];
|
|
335
|
+
diff(source, target, "", invertible, out);
|
|
336
|
+
return out;
|
|
337
|
+
};
|
|
338
|
+
|
|
252
339
|
const isPlainObject = (val) => {
|
|
253
340
|
if (Object.prototype.toString.call(val) !== "[object Object]") return false;
|
|
254
341
|
const prot = Object.getPrototypeOf(val);
|
|
@@ -379,13 +466,13 @@ const updatePatch = async (opts, context, current, original) => {
|
|
|
379
466
|
const currentObject = getJsonOmit(opts, current);
|
|
380
467
|
const originalObject = getJsonOmit(opts, original);
|
|
381
468
|
if (isEmpty(originalObject) || isEmpty(currentObject)) return;
|
|
382
|
-
const patch =
|
|
469
|
+
const patch = compare(originalObject, currentObject, true);
|
|
383
470
|
if (isEmpty(patch)) return;
|
|
384
471
|
emitEvent(context, opts.eventUpdated, { oldDoc: original, doc: current, patch });
|
|
385
472
|
if (history) {
|
|
386
473
|
let version = 0;
|
|
387
474
|
const lastHistory = await HistoryModel.findOne({ collectionId: original._id }).sort("-version").exec();
|
|
388
|
-
if (lastHistory
|
|
475
|
+
if (lastHistory) {
|
|
389
476
|
version = lastHistory.version + 1;
|
|
390
477
|
}
|
|
391
478
|
const [user, reason, metadata] = await getData(opts, current);
|
|
@@ -468,38 +555,202 @@ const saveHooksInitialize = (schema, opts) => {
|
|
|
468
555
|
});
|
|
469
556
|
};
|
|
470
557
|
|
|
558
|
+
const hasOwn = Object.prototype.hasOwnProperty;
|
|
559
|
+
const parseSegment = (segment) => {
|
|
560
|
+
const asNumber2 = Number(segment);
|
|
561
|
+
return Number.isInteger(asNumber2) && String(asNumber2) === segment ? asNumber2 : segment;
|
|
562
|
+
};
|
|
563
|
+
const parsePath = (path) => {
|
|
564
|
+
const [first, ...rest] = path.split(".").map(parseSegment);
|
|
565
|
+
let leaf = first ?? path;
|
|
566
|
+
const crumbs = [];
|
|
567
|
+
for (const key of rest) {
|
|
568
|
+
crumbs.push({ key: leaf, nextNumeric: typeof key === "number" });
|
|
569
|
+
leaf = key;
|
|
570
|
+
}
|
|
571
|
+
return { leaf, crumbs };
|
|
572
|
+
};
|
|
573
|
+
const deepEqualJson = (a, b) => {
|
|
574
|
+
try {
|
|
575
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
576
|
+
} catch {
|
|
577
|
+
return a === b;
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
const ensureContainer = (parent, key, hintNumeric) => {
|
|
581
|
+
const existing = parent[key];
|
|
582
|
+
if (existing !== null && typeof existing === "object") {
|
|
583
|
+
return existing;
|
|
584
|
+
}
|
|
585
|
+
const created = hintNumeric ? [] : {};
|
|
586
|
+
parent[key] = created;
|
|
587
|
+
return created;
|
|
588
|
+
};
|
|
589
|
+
const setAtPath = (doc, path, value) => {
|
|
590
|
+
const { leaf, crumbs } = parsePath(path);
|
|
591
|
+
let cursor = doc;
|
|
592
|
+
for (const crumb of crumbs) {
|
|
593
|
+
cursor = ensureContainer(cursor, crumb.key, crumb.nextNumeric);
|
|
594
|
+
}
|
|
595
|
+
cursor[leaf] = value;
|
|
596
|
+
};
|
|
597
|
+
const getAtPath = (doc, path) => {
|
|
598
|
+
const { leaf, crumbs } = parsePath(path);
|
|
599
|
+
let cursor = doc;
|
|
600
|
+
for (const crumb of crumbs) {
|
|
601
|
+
const next = cursor[crumb.key];
|
|
602
|
+
if (next === null || typeof next !== "object") return void 0;
|
|
603
|
+
cursor = next;
|
|
604
|
+
}
|
|
605
|
+
return { container: cursor, leaf, exists: hasOwn.call(cursor, leaf) };
|
|
606
|
+
};
|
|
607
|
+
const unsetAtPath = (doc, path) => {
|
|
608
|
+
const located = getAtPath(doc, path);
|
|
609
|
+
if (!located) return;
|
|
610
|
+
if (Array.isArray(located.container)) {
|
|
611
|
+
located.container[located.leaf] = void 0;
|
|
612
|
+
} else {
|
|
613
|
+
delete located.container[located.leaf];
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
const asNumber = (value) => typeof value === "number" ? value : 0;
|
|
617
|
+
const toArray = (value) => Array.isArray(value) ? value : void 0;
|
|
618
|
+
const shouldReplaceForMin = (current, candidate) => {
|
|
619
|
+
if (candidate === void 0) return false;
|
|
620
|
+
if (current === void 0) return true;
|
|
621
|
+
return candidate < current;
|
|
622
|
+
};
|
|
623
|
+
const shouldReplaceForMax = (current, candidate) => {
|
|
624
|
+
if (candidate === void 0) return false;
|
|
625
|
+
if (current === void 0) return true;
|
|
626
|
+
return candidate > current;
|
|
627
|
+
};
|
|
628
|
+
const operators = {
|
|
629
|
+
$set: (doc, path, value) => setAtPath(doc, path, value),
|
|
630
|
+
$unset: (doc, path) => unsetAtPath(doc, path),
|
|
631
|
+
$inc: (doc, path, delta) => {
|
|
632
|
+
const located = getAtPath(doc, path);
|
|
633
|
+
const current = located?.exists ? asNumber(located.container[located.leaf]) : 0;
|
|
634
|
+
setAtPath(doc, path, current + asNumber(delta));
|
|
635
|
+
},
|
|
636
|
+
$mul: (doc, path, factor) => {
|
|
637
|
+
const located = getAtPath(doc, path);
|
|
638
|
+
const current = located?.exists ? asNumber(located.container[located.leaf]) : 0;
|
|
639
|
+
setAtPath(doc, path, current * asNumber(factor));
|
|
640
|
+
},
|
|
641
|
+
$min: (doc, path, candidate) => {
|
|
642
|
+
const located = getAtPath(doc, path);
|
|
643
|
+
if (!located?.exists) {
|
|
644
|
+
setAtPath(doc, path, candidate);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (shouldReplaceForMin(located.container[located.leaf], candidate)) {
|
|
648
|
+
setAtPath(doc, path, candidate);
|
|
649
|
+
}
|
|
650
|
+
},
|
|
651
|
+
$max: (doc, path, candidate) => {
|
|
652
|
+
const located = getAtPath(doc, path);
|
|
653
|
+
if (!located?.exists) {
|
|
654
|
+
setAtPath(doc, path, candidate);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (shouldReplaceForMax(located.container[located.leaf], candidate)) {
|
|
658
|
+
setAtPath(doc, path, candidate);
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
$rename: (doc, path, newPath) => {
|
|
662
|
+
if (typeof newPath !== "string") return;
|
|
663
|
+
const located = getAtPath(doc, path);
|
|
664
|
+
if (!located?.exists) return;
|
|
665
|
+
const value = located.container[located.leaf];
|
|
666
|
+
unsetAtPath(doc, path);
|
|
667
|
+
setAtPath(doc, newPath, value);
|
|
668
|
+
},
|
|
669
|
+
$currentDate: (doc, path, spec) => {
|
|
670
|
+
const wantTimestamp = typeof spec === "object" && spec !== null && spec.$type === "timestamp";
|
|
671
|
+
setAtPath(doc, path, wantTimestamp ? Date.now() : /* @__PURE__ */ new Date());
|
|
672
|
+
},
|
|
673
|
+
$push: (doc, path, pushSpec) => {
|
|
674
|
+
const located = getAtPath(doc, path);
|
|
675
|
+
const existing = located?.exists ? toArray(located.container[located.leaf]) ?? [] : [];
|
|
676
|
+
const next = [...existing];
|
|
677
|
+
if (typeof pushSpec === "object" && pushSpec !== null && "$each" in pushSpec && Array.isArray(pushSpec.$each)) {
|
|
678
|
+
next.push(...pushSpec.$each);
|
|
679
|
+
} else {
|
|
680
|
+
next.push(pushSpec);
|
|
681
|
+
}
|
|
682
|
+
setAtPath(doc, path, next);
|
|
683
|
+
},
|
|
684
|
+
$addToSet: (doc, path, item) => {
|
|
685
|
+
const located = getAtPath(doc, path);
|
|
686
|
+
const existing = located?.exists ? toArray(located.container[located.leaf]) ?? [] : [];
|
|
687
|
+
const next = [...existing];
|
|
688
|
+
const rawItems = typeof item === "object" && item !== null && "$each" in item ? item.$each : item;
|
|
689
|
+
const toAdd = Array.isArray(rawItems) ? rawItems : [rawItems];
|
|
690
|
+
for (const candidate of toAdd) {
|
|
691
|
+
if (!next.some((existingEntry) => deepEqualJson(existingEntry, candidate))) {
|
|
692
|
+
next.push(candidate);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
setAtPath(doc, path, next);
|
|
696
|
+
},
|
|
697
|
+
$pull: (doc, path, matcher) => {
|
|
698
|
+
const located = getAtPath(doc, path);
|
|
699
|
+
const existing = located?.exists ? toArray(located.container[located.leaf]) : void 0;
|
|
700
|
+
if (!existing) return;
|
|
701
|
+
const filtered = existing.filter((entry) => !deepEqualJson(entry, matcher));
|
|
702
|
+
setAtPath(doc, path, filtered);
|
|
703
|
+
},
|
|
704
|
+
$pullAll: (doc, path, values) => {
|
|
705
|
+
const located = getAtPath(doc, path);
|
|
706
|
+
const existing = located?.exists ? toArray(located.container[located.leaf]) : void 0;
|
|
707
|
+
if (!existing || !Array.isArray(values)) return;
|
|
708
|
+
const filtered = existing.filter((entry) => !values.some((target) => deepEqualJson(entry, target)));
|
|
709
|
+
setAtPath(doc, path, filtered);
|
|
710
|
+
},
|
|
711
|
+
$pop: (doc, path, direction) => {
|
|
712
|
+
const located = getAtPath(doc, path);
|
|
713
|
+
const existing = located?.exists ? toArray(located.container[located.leaf]) : void 0;
|
|
714
|
+
if (!existing || existing.length === 0) return;
|
|
715
|
+
const next = direction === -1 ? existing.slice(1) : existing.slice(0, -1);
|
|
716
|
+
setAtPath(doc, path, next);
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
const applyOperator = (doc, operator, fields) => {
|
|
720
|
+
const fn = operators[operator];
|
|
721
|
+
if (!fn || fields === null || typeof fields !== "object") return;
|
|
722
|
+
for (const [path, argument] of Object.entries(fields)) {
|
|
723
|
+
fn(doc, path, argument);
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
const applyUpdate = (doc, update) => {
|
|
727
|
+
const result = { ...doc };
|
|
728
|
+
for (const [key, value] of Object.entries(update)) {
|
|
729
|
+
if (key.startsWith("$")) {
|
|
730
|
+
applyOperator(result, key, value);
|
|
731
|
+
} else {
|
|
732
|
+
setAtPath(result, key, value);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return result;
|
|
736
|
+
};
|
|
737
|
+
|
|
471
738
|
const updateMethods = ["update", "updateOne", "replaceOne", "updateMany", "findOneAndUpdate", "findOneAndReplace", "findByIdAndUpdate"];
|
|
472
739
|
const trackChangedFields = (fields, updated, changed) => {
|
|
473
740
|
if (!fields) return;
|
|
474
741
|
for (const key of Object.keys(fields)) {
|
|
475
|
-
const root = key.split(".")
|
|
742
|
+
const [root = key] = key.split(".");
|
|
476
743
|
changed.set(root, updated[root]);
|
|
477
744
|
}
|
|
478
745
|
};
|
|
479
|
-
const applyPullAll = (updated, fields, changed) => {
|
|
480
|
-
for (const [field, values] of Object.entries(fields)) {
|
|
481
|
-
const arr = updated[field];
|
|
482
|
-
if (Array.isArray(arr)) {
|
|
483
|
-
const filtered = arr.filter((item) => !values.some((v) => JSON.stringify(v) === JSON.stringify(item)));
|
|
484
|
-
updated[field] = filtered;
|
|
485
|
-
changed.set(field, filtered);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
};
|
|
489
746
|
const assignUpdate = (document, update, commands) => {
|
|
490
|
-
let updated =
|
|
747
|
+
let updated = applyUpdate(document.toObject(toObjectOptions), update);
|
|
491
748
|
const changedByCommand = /* @__PURE__ */ new Map();
|
|
492
749
|
for (const command of commands) {
|
|
493
|
-
const op = Object.keys(command)
|
|
750
|
+
const [op = ""] = Object.keys(command);
|
|
494
751
|
const fields = command[op];
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
trackChangedFields(fields, updated, changedByCommand);
|
|
498
|
-
} catch {
|
|
499
|
-
if (op === "$pullAll" && fields) {
|
|
500
|
-
applyPullAll(updated, fields, changedByCommand);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
752
|
+
updated = applyUpdate(updated, command);
|
|
753
|
+
trackChangedFields(fields, updated, changedByCommand);
|
|
503
754
|
}
|
|
504
755
|
const doc = document.set(updated).toObject(toObjectOptions);
|
|
505
756
|
for (const [field, value] of changedByCommand) {
|
|
@@ -554,11 +805,14 @@ const updateHooksInitialize = (schema, opts) => {
|
|
|
554
805
|
const updateQuery = this.getUpdate();
|
|
555
806
|
const { update, commands } = splitUpdateAndCommands(updateQuery);
|
|
556
807
|
const filter = this.getFilter();
|
|
557
|
-
const
|
|
808
|
+
const simulated = assignUpdate(model.hydrate({}), update, commands);
|
|
809
|
+
const simulatedFilter = Object.fromEntries(Object.entries(simulated).filter(([, v]) => v !== void 0));
|
|
810
|
+
const candidates = [update, simulatedFilter, filter];
|
|
558
811
|
let current = null;
|
|
559
812
|
for (const query of candidates) {
|
|
560
813
|
if (current || isEmpty(query)) continue;
|
|
561
|
-
|
|
814
|
+
const found = await model.findOne(query).sort({ _id: -1 }).lean().exec();
|
|
815
|
+
current = found;
|
|
562
816
|
}
|
|
563
817
|
if (current) {
|
|
564
818
|
this._context.createdDocs = [current];
|
|
@@ -590,13 +844,14 @@ const patchHistoryPlugin = (schema, opts) => {
|
|
|
590
844
|
await createPatch(opts, context);
|
|
591
845
|
});
|
|
592
846
|
if (isMongooseLessThan8) {
|
|
593
|
-
|
|
847
|
+
const legacySchema = schema;
|
|
848
|
+
legacySchema.pre(remove, { document: true, query: false }, async function() {
|
|
594
849
|
const original = this.toObject(toObjectOptions);
|
|
595
850
|
if (opts.preDelete && !isEmpty(original)) {
|
|
596
851
|
await opts.preDelete([original]);
|
|
597
852
|
}
|
|
598
853
|
});
|
|
599
|
-
|
|
854
|
+
legacySchema.post(remove, { document: true, query: false }, async function() {
|
|
600
855
|
const original = this.toObject(toObjectOptions);
|
|
601
856
|
const model = this.constructor;
|
|
602
857
|
const context = {
|
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
import { Types, Query, HydratedDocument, Schema } from 'mongoose';
|
|
2
|
-
import { Operation } from 'fast-json-patch';
|
|
3
2
|
import EventEmitter from 'node:events';
|
|
4
3
|
|
|
4
|
+
interface AddOperation<T> {
|
|
5
|
+
op: 'add';
|
|
6
|
+
path: string;
|
|
7
|
+
value: T;
|
|
8
|
+
}
|
|
9
|
+
interface RemoveOperation {
|
|
10
|
+
op: 'remove';
|
|
11
|
+
path: string;
|
|
12
|
+
}
|
|
13
|
+
interface ReplaceOperation<T> {
|
|
14
|
+
op: 'replace';
|
|
15
|
+
path: string;
|
|
16
|
+
value: T;
|
|
17
|
+
}
|
|
18
|
+
interface TestOperation<T> {
|
|
19
|
+
op: 'test';
|
|
20
|
+
path: string;
|
|
21
|
+
value: T;
|
|
22
|
+
}
|
|
23
|
+
type Operation = AddOperation<unknown> | RemoveOperation | ReplaceOperation<unknown> | TestOperation<unknown>;
|
|
24
|
+
|
|
5
25
|
interface History {
|
|
6
26
|
op: string;
|
|
7
27
|
modelName: string;
|
|
@@ -13,6 +33,8 @@ interface History {
|
|
|
13
33
|
reason?: string;
|
|
14
34
|
metadata?: object;
|
|
15
35
|
patch?: Operation[];
|
|
36
|
+
createdAt?: Date;
|
|
37
|
+
updatedAt?: Date;
|
|
16
38
|
}
|
|
17
39
|
interface PatchEvent<T> {
|
|
18
40
|
oldDoc?: HydratedDocument<T>;
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","sources":["../src/types.ts","../src/em.ts","../src/ms.ts","../src/helpers.ts","../src/index.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"index.d.cts","sources":["../src/json-patch.ts","../src/types.ts","../src/em.ts","../src/ms.ts","../src/helpers.ts","../src/index.ts"],"mappings":";;;AAAM,UAAW,YAAY;;;;;AAMvB,UAAW,eAAe;;;;AAK1B,UAAW,gBAAgB;;;;;AAM3B,UAAW,aAAa;;;;;AAMxB,KAAM,SAAS,GAAG,YAAY,YAAY,eAAe,GAAG,gBAAgB,YAAY,aAAa;;ACpBrG,UAAW,OAAO;;;;kBAIR,KAAK,CAAC,QAAQ;;;;;;YAMpB,SAAS;gBACL,IAAI;gBACJ,IAAI;;AAGZ,UAAW,UAAU;aAChB,gBAAgB;UACnB,gBAAgB;YACd,SAAS;;AAGb,UAAW,YAAY;;;;;kBAKb,gBAAgB;kBAChB,gBAAgB;;;;AAK1B,KAAM,WAAW,MAAM,KAAK;;cAAiC,YAAY;;AAEzE,KAAM,IAAI,GAAG,MAAM;AAEnB,KAAM,QAAQ,GAAG,MAAM;AAEvB,UAAW,aAAa;;;;;;oBAMZ,gBAAgB,QAAQ,OAAO,CAAC,IAAI,IAAI,IAAI;sBAC1C,gBAAgB,QAAQ,OAAO;wBAC7B,gBAAgB,QAAQ,OAAO,CAAC,QAAQ,IAAI,QAAQ;;;uBAGrD,gBAAgB,UAAU,OAAO;sBAClC,KAAK;;;ACnDzB,cAAM,iBAAkB,SAAQ,YAAY;;AAC5C,cAAM,EAAE,mBAA0B;;ACKlC,cAAa,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCZ,KAAM,IAAI,gBAAgB,KAAK;AAE/B,KAAM,QAAQ,sCAAsC,IAAI,kBAAkB,IAAI;;AC6FpF,cAAa,kBAAkB,QAAe,QAAQ,oBAAoB,KAAK,cAAY,OAAO;;AC1HlG,cAAa,kBAAkB,cAAe,MAAM,WAAW,aAAa","names":[]}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
import { Types, Query, HydratedDocument, Schema } from 'mongoose';
|
|
2
|
-
import { Operation } from 'fast-json-patch';
|
|
3
2
|
import EventEmitter from 'node:events';
|
|
4
3
|
|
|
4
|
+
interface AddOperation<T> {
|
|
5
|
+
op: 'add';
|
|
6
|
+
path: string;
|
|
7
|
+
value: T;
|
|
8
|
+
}
|
|
9
|
+
interface RemoveOperation {
|
|
10
|
+
op: 'remove';
|
|
11
|
+
path: string;
|
|
12
|
+
}
|
|
13
|
+
interface ReplaceOperation<T> {
|
|
14
|
+
op: 'replace';
|
|
15
|
+
path: string;
|
|
16
|
+
value: T;
|
|
17
|
+
}
|
|
18
|
+
interface TestOperation<T> {
|
|
19
|
+
op: 'test';
|
|
20
|
+
path: string;
|
|
21
|
+
value: T;
|
|
22
|
+
}
|
|
23
|
+
type Operation = AddOperation<unknown> | RemoveOperation | ReplaceOperation<unknown> | TestOperation<unknown>;
|
|
24
|
+
|
|
5
25
|
interface History {
|
|
6
26
|
op: string;
|
|
7
27
|
modelName: string;
|
|
@@ -13,6 +33,8 @@ interface History {
|
|
|
13
33
|
reason?: string;
|
|
14
34
|
metadata?: object;
|
|
15
35
|
patch?: Operation[];
|
|
36
|
+
createdAt?: Date;
|
|
37
|
+
updatedAt?: Date;
|
|
16
38
|
}
|
|
17
39
|
interface PatchEvent<T> {
|
|
18
40
|
oldDoc?: HydratedDocument<T>;
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","sources":["../src/types.ts","../src/em.ts","../src/ms.ts","../src/helpers.ts","../src/index.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","sources":["../src/json-patch.ts","../src/types.ts","../src/em.ts","../src/ms.ts","../src/helpers.ts","../src/index.ts"],"mappings":";;;AAAM,UAAW,YAAY;;;;;AAMvB,UAAW,eAAe;;;;AAK1B,UAAW,gBAAgB;;;;;AAM3B,UAAW,aAAa;;;;;AAMxB,KAAM,SAAS,GAAG,YAAY,YAAY,eAAe,GAAG,gBAAgB,YAAY,aAAa;;ACpBrG,UAAW,OAAO;;;;kBAIR,KAAK,CAAC,QAAQ;;;;;;YAMpB,SAAS;gBACL,IAAI;gBACJ,IAAI;;AAGZ,UAAW,UAAU;aAChB,gBAAgB;UACnB,gBAAgB;YACd,SAAS;;AAGb,UAAW,YAAY;;;;;kBAKb,gBAAgB;kBAChB,gBAAgB;;;;AAK1B,KAAM,WAAW,MAAM,KAAK;;cAAiC,YAAY;;AAEzE,KAAM,IAAI,GAAG,MAAM;AAEnB,KAAM,QAAQ,GAAG,MAAM;AAEvB,UAAW,aAAa;;;;;;oBAMZ,gBAAgB,QAAQ,OAAO,CAAC,IAAI,IAAI,IAAI;sBAC1C,gBAAgB,QAAQ,OAAO;wBAC7B,gBAAgB,QAAQ,OAAO,CAAC,QAAQ,IAAI,QAAQ;;;uBAGrD,gBAAgB,UAAU,OAAO;sBAClC,KAAK;;;ACnDzB,cAAM,iBAAkB,SAAQ,YAAY;;AAC5C,cAAM,EAAE,mBAA0B;;ACKlC,cAAa,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCZ,KAAM,IAAI,gBAAgB,KAAK;AAE/B,KAAM,QAAQ,sCAAsC,IAAI,kBAAkB,IAAI;;AC6FpF,cAAa,kBAAkB,QAAe,QAAQ,oBAAoB,KAAK,cAAY,OAAO;;AC1HlG,cAAa,kBAAkB,cAAe,MAAM,WAAW,aAAa","names":[]}
|