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/README.md
CHANGED
|
@@ -23,8 +23,8 @@ I need to track changes of mongoose models and save them as patch history (audit
|
|
|
23
23
|
|
|
24
24
|
```json
|
|
25
25
|
{
|
|
26
|
-
"node": "
|
|
27
|
-
"mongoose": ">=6.6.x || 7.x || 8.x",
|
|
26
|
+
"node": "20.x || 22.x || 24.x",
|
|
27
|
+
"mongoose": ">=6.6.x || 7.x || 8.x || 9.x",
|
|
28
28
|
}
|
|
29
29
|
```
|
|
30
30
|
|
|
@@ -40,37 +40,19 @@ I need to track changes of mongoose models and save them as patch history (audit
|
|
|
40
40
|
|
|
41
41
|
## Installation
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
`mongoose` is a peer dependency — install it alongside `ts-patch-mongoose`.
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
npm install ts-patch-mongoose
|
|
47
|
-
pnpm add ts-patch-mongoose
|
|
48
|
-
yarn add ts-patch-mongoose
|
|
49
|
-
bun add ts-patch-mongoose
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
- This plugin requires mongoose `>=6.6.x || 7.x || 8.x` to be installed as a peer dependency
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
# For latest mongoose 6
|
|
56
|
-
npm install mongoose@6
|
|
57
|
-
pnpm add mongoose@6
|
|
58
|
-
yarn add mongoose@6
|
|
59
|
-
bun add mongoose@6
|
|
60
|
-
# For latest mongoose 7
|
|
61
|
-
npm install mongoose@7
|
|
62
|
-
pnpm add mongoose@7
|
|
63
|
-
yarn add mongoose@7
|
|
64
|
-
bun add mongoose@7
|
|
65
|
-
# For latest mongoose 8
|
|
66
|
-
npm install mongoose@8
|
|
67
|
-
pnpm add mongoose@8
|
|
68
|
-
yarn add mongoose@8
|
|
69
|
-
bun add mongoose@8
|
|
46
|
+
npm install ts-patch-mongoose mongoose
|
|
47
|
+
pnpm add ts-patch-mongoose mongoose
|
|
48
|
+
yarn add ts-patch-mongoose mongoose
|
|
49
|
+
bun add ts-patch-mongoose mongoose
|
|
70
50
|
```
|
|
71
51
|
|
|
72
52
|
## Example
|
|
73
53
|
|
|
54
|
+
Works with any Node.js framework — Express, Fastify, Koa, Hono, Nest, etc.
|
|
55
|
+
\
|
|
74
56
|
How to use it with express [ts-express-tsx](https://github.com/ilovepixelart/ts-express-tsx)
|
|
75
57
|
|
|
76
58
|
Create your event constants `events.ts`
|
|
@@ -213,6 +195,39 @@ patchEventEmitter.on(BOOK_DELETED, ({ oldDoc }) => {
|
|
|
213
195
|
})
|
|
214
196
|
```
|
|
215
197
|
|
|
198
|
+
## NestJS
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
|
|
202
|
+
import { patchHistoryPlugin } from 'ts-patch-mongoose'
|
|
203
|
+
|
|
204
|
+
@Schema({ timestamps: true })
|
|
205
|
+
export class Book {
|
|
206
|
+
@Prop({ type: String, required: true })
|
|
207
|
+
title!: string
|
|
208
|
+
|
|
209
|
+
@Prop({ type: String })
|
|
210
|
+
description?: string
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export const BookSchema = SchemaFactory.createForClass(Book)
|
|
214
|
+
|
|
215
|
+
BookSchema.plugin(patchHistoryPlugin, {
|
|
216
|
+
eventCreated: 'book-created',
|
|
217
|
+
eventUpdated: 'book-updated',
|
|
218
|
+
eventDeleted: 'book-deleted',
|
|
219
|
+
omit: ['__v', 'createdAt', 'updatedAt'],
|
|
220
|
+
})
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Contributing
|
|
224
|
+
|
|
225
|
+
Check [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details
|
|
230
|
+
|
|
216
231
|
## Check my other projects
|
|
217
232
|
|
|
218
233
|
- [ts-migrate-mongoose](https://github.com/ilovepixelart/ts-migrate-mongoose) - Migration framework for mongoose
|
package/biome.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "https://biomejs.dev/schemas/2.4.
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.4.9/schema.json",
|
|
3
3
|
"vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false },
|
|
4
4
|
"files": {
|
|
5
5
|
"ignoreUnknown": false,
|
|
@@ -27,10 +27,7 @@
|
|
|
27
27
|
"linter": {
|
|
28
28
|
"enabled": true,
|
|
29
29
|
"rules": {
|
|
30
|
-
"recommended": true
|
|
31
|
-
"correctness": {
|
|
32
|
-
"noUnusedVariables": "off"
|
|
33
|
-
}
|
|
30
|
+
"recommended": true
|
|
34
31
|
}
|
|
35
32
|
},
|
|
36
33
|
"javascript": {
|
package/dist/index.cjs
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var isEmpty = require('lodash/isEmpty.js');
|
|
4
|
-
var ms = require('ms');
|
|
5
3
|
var mongoose = require('mongoose');
|
|
6
|
-
var isArray = require('lodash/isArray.js');
|
|
7
4
|
var jsonpatch = require('fast-json-patch');
|
|
8
|
-
var chunk = require('lodash/chunk.js');
|
|
9
|
-
var isFunction = require('lodash/isFunction.js');
|
|
10
|
-
var omit = require('omit-deep');
|
|
11
5
|
var EventEmitter = require('node:events');
|
|
12
|
-
var cloneDeep = require('lodash/cloneDeep.js');
|
|
13
|
-
var forEach = require('lodash/forEach.js');
|
|
14
|
-
var isObjectLike = require('lodash/isObjectLike.js');
|
|
15
|
-
var keys = require('lodash/keys.js');
|
|
16
6
|
var powerAssign = require('power-assign');
|
|
17
|
-
var semver = require('semver');
|
|
18
7
|
|
|
19
8
|
const HistorySchema = new mongoose.Schema(
|
|
20
9
|
{
|
|
@@ -61,6 +50,166 @@ HistorySchema.index({ collectionId: 1, version: -1 });
|
|
|
61
50
|
HistorySchema.index({ op: 1, modelName: 1, collectionName: 1, collectionId: 1, reason: 1, version: 1 });
|
|
62
51
|
const HistoryModel = mongoose.model("History", HistorySchema, "history");
|
|
63
52
|
|
|
53
|
+
const s = 1e3;
|
|
54
|
+
const m = s * 60;
|
|
55
|
+
const h = m * 60;
|
|
56
|
+
const d = h * 24;
|
|
57
|
+
const w = d * 7;
|
|
58
|
+
const y = d * 365.25;
|
|
59
|
+
const mo = y / 12;
|
|
60
|
+
const UNITS = {
|
|
61
|
+
milliseconds: 1,
|
|
62
|
+
millisecond: 1,
|
|
63
|
+
msecs: 1,
|
|
64
|
+
msec: 1,
|
|
65
|
+
ms: 1,
|
|
66
|
+
seconds: s,
|
|
67
|
+
second: s,
|
|
68
|
+
secs: s,
|
|
69
|
+
sec: s,
|
|
70
|
+
s,
|
|
71
|
+
minutes: m,
|
|
72
|
+
minute: m,
|
|
73
|
+
mins: m,
|
|
74
|
+
min: m,
|
|
75
|
+
m,
|
|
76
|
+
hours: h,
|
|
77
|
+
hour: h,
|
|
78
|
+
hrs: h,
|
|
79
|
+
hr: h,
|
|
80
|
+
h,
|
|
81
|
+
days: d,
|
|
82
|
+
day: d,
|
|
83
|
+
d,
|
|
84
|
+
weeks: w,
|
|
85
|
+
week: w,
|
|
86
|
+
w,
|
|
87
|
+
months: mo,
|
|
88
|
+
month: mo,
|
|
89
|
+
mo,
|
|
90
|
+
years: y,
|
|
91
|
+
year: y,
|
|
92
|
+
yrs: y,
|
|
93
|
+
yr: y,
|
|
94
|
+
y
|
|
95
|
+
};
|
|
96
|
+
const unitPattern = Object.keys(UNITS).sort((a, b) => b.length - a.length).join("|");
|
|
97
|
+
const RE = new RegExp(String.raw`^(-?(?:\d+)?\.?\d+)\s*(${unitPattern})?$`, "i");
|
|
98
|
+
const ms = (val) => {
|
|
99
|
+
const str = String(val);
|
|
100
|
+
if (str.length > 100) return Number.NaN;
|
|
101
|
+
const match = RE.exec(str);
|
|
102
|
+
if (!match) return Number.NaN;
|
|
103
|
+
const n = Number.parseFloat(match[1] ?? "");
|
|
104
|
+
const type = (match[2] ?? "ms").toLowerCase();
|
|
105
|
+
return n * (UNITS[type] ?? 0);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const isArray = Array.isArray;
|
|
109
|
+
const isEmpty = (value) => {
|
|
110
|
+
if (value == null) return true;
|
|
111
|
+
if (Array.isArray(value) || typeof value === "string") return value.length === 0;
|
|
112
|
+
if (value instanceof Map || value instanceof Set) return value.size === 0;
|
|
113
|
+
if (typeof value === "object") {
|
|
114
|
+
for (const key in value) {
|
|
115
|
+
if (Object.hasOwn(value, key)) return false;
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
};
|
|
121
|
+
const isFunction = (value) => {
|
|
122
|
+
return typeof value === "function";
|
|
123
|
+
};
|
|
124
|
+
const isObjectLike = (value) => {
|
|
125
|
+
return typeof value === "object" && value !== null;
|
|
126
|
+
};
|
|
127
|
+
const cloneArrayBuffer = (arrayBuffer) => {
|
|
128
|
+
const result = new ArrayBuffer(arrayBuffer.byteLength);
|
|
129
|
+
new Uint8Array(result).set(new Uint8Array(arrayBuffer));
|
|
130
|
+
return result;
|
|
131
|
+
};
|
|
132
|
+
const cloneImmutable = (value) => {
|
|
133
|
+
const tag = Object.prototype.toString.call(value);
|
|
134
|
+
switch (tag) {
|
|
135
|
+
case "[object Date]":
|
|
136
|
+
return /* @__PURE__ */ new Date(+value);
|
|
137
|
+
case "[object RegExp]": {
|
|
138
|
+
const re = value;
|
|
139
|
+
const cloned = new RegExp(re.source, re.flags);
|
|
140
|
+
cloned.lastIndex = re.lastIndex;
|
|
141
|
+
return cloned;
|
|
142
|
+
}
|
|
143
|
+
case "[object Error]": {
|
|
144
|
+
const err = value;
|
|
145
|
+
const cloned = new err.constructor(err.message);
|
|
146
|
+
if (err.stack) cloned.stack = err.stack;
|
|
147
|
+
return cloned;
|
|
148
|
+
}
|
|
149
|
+
case "[object ArrayBuffer]":
|
|
150
|
+
return cloneArrayBuffer(value);
|
|
151
|
+
case "[object DataView]": {
|
|
152
|
+
const dv = value;
|
|
153
|
+
const buffer = cloneArrayBuffer(dv.buffer);
|
|
154
|
+
return new DataView(buffer, dv.byteOffset, dv.byteLength);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (ArrayBuffer.isView(value)) {
|
|
158
|
+
const ta = value;
|
|
159
|
+
const buffer = cloneArrayBuffer(ta.buffer);
|
|
160
|
+
return new value.constructor(buffer, ta.byteOffset, ta.length);
|
|
161
|
+
}
|
|
162
|
+
return void 0;
|
|
163
|
+
};
|
|
164
|
+
const cloneCollection = (value, seen) => {
|
|
165
|
+
if (value instanceof Map) {
|
|
166
|
+
const map = /* @__PURE__ */ new Map();
|
|
167
|
+
seen.set(value, map);
|
|
168
|
+
for (const [k, v] of value) map.set(k, cloneDeep(v, seen));
|
|
169
|
+
return map;
|
|
170
|
+
}
|
|
171
|
+
if (value instanceof Set) {
|
|
172
|
+
const set = /* @__PURE__ */ new Set();
|
|
173
|
+
seen.set(value, set);
|
|
174
|
+
for (const v of value) set.add(cloneDeep(v, seen));
|
|
175
|
+
return set;
|
|
176
|
+
}
|
|
177
|
+
if (Array.isArray(value)) {
|
|
178
|
+
const arr = new Array(value.length);
|
|
179
|
+
seen.set(value, arr);
|
|
180
|
+
for (let i = 0; i < value.length; i++) {
|
|
181
|
+
arr[i] = cloneDeep(value[i], seen);
|
|
182
|
+
}
|
|
183
|
+
return arr;
|
|
184
|
+
}
|
|
185
|
+
const result = typeof value.constructor === "function" ? Object.create(Object.getPrototypeOf(value)) : {};
|
|
186
|
+
seen.set(value, result);
|
|
187
|
+
for (const key of Object.keys(value)) {
|
|
188
|
+
result[key] = cloneDeep(value[key], seen);
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
};
|
|
192
|
+
const cloneDeep = (value, seen = /* @__PURE__ */ new WeakMap()) => {
|
|
193
|
+
if (value === null || typeof value !== "object") return value;
|
|
194
|
+
if (seen.has(value)) return seen.get(value);
|
|
195
|
+
const immutable = cloneImmutable(value);
|
|
196
|
+
if (immutable !== void 0) return immutable;
|
|
197
|
+
const record = value;
|
|
198
|
+
if (typeof record._bsontype === "string" && typeof record.toHexString === "function") {
|
|
199
|
+
return new value.constructor(record.toHexString());
|
|
200
|
+
}
|
|
201
|
+
if (typeof record.toJSON === "function") {
|
|
202
|
+
return JSON.parse(JSON.stringify(value));
|
|
203
|
+
}
|
|
204
|
+
return cloneCollection(value, seen);
|
|
205
|
+
};
|
|
206
|
+
const chunk = (array, size) => {
|
|
207
|
+
const result = [];
|
|
208
|
+
for (let i = 0; i < array.length; i += size) {
|
|
209
|
+
result.push(array.slice(i, i + size));
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
};
|
|
64
213
|
const isHookIgnored = (options) => {
|
|
65
214
|
return options.ignoreHook === true || options.ignoreEvent === true && options.ignorePatchHistory === true;
|
|
66
215
|
};
|
|
@@ -68,7 +217,7 @@ const toObjectOptions = {
|
|
|
68
217
|
depopulate: true,
|
|
69
218
|
virtuals: false
|
|
70
219
|
};
|
|
71
|
-
const setPatchHistoryTTL = async (ttl) => {
|
|
220
|
+
const setPatchHistoryTTL = async (ttl, onError) => {
|
|
72
221
|
const name = "createdAt_1_TTL";
|
|
73
222
|
try {
|
|
74
223
|
const indexes = await HistoryModel.collection.indexes();
|
|
@@ -77,7 +226,7 @@ const setPatchHistoryTTL = async (ttl) => {
|
|
|
77
226
|
await HistoryModel.collection.dropIndex(name);
|
|
78
227
|
return;
|
|
79
228
|
}
|
|
80
|
-
const milliseconds =
|
|
229
|
+
const milliseconds = ms(ttl);
|
|
81
230
|
if (milliseconds < 1e3 && existingIndex) {
|
|
82
231
|
await HistoryModel.collection.dropIndex(name);
|
|
83
232
|
return;
|
|
@@ -91,7 +240,8 @@ const setPatchHistoryTTL = async (ttl) => {
|
|
|
91
240
|
}
|
|
92
241
|
await HistoryModel.collection.createIndex({ createdAt: 1 }, { expireAfterSeconds, name });
|
|
93
242
|
} catch (err) {
|
|
94
|
-
|
|
243
|
+
const handler = onError ?? console.error;
|
|
244
|
+
handler(err);
|
|
95
245
|
}
|
|
96
246
|
};
|
|
97
247
|
|
|
@@ -99,64 +249,101 @@ class PatchEventEmitter extends EventEmitter {
|
|
|
99
249
|
}
|
|
100
250
|
const em = new PatchEventEmitter();
|
|
101
251
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
252
|
+
const isPlainObject = (val) => {
|
|
253
|
+
if (Object.prototype.toString.call(val) !== "[object Object]") return false;
|
|
254
|
+
const prot = Object.getPrototypeOf(val);
|
|
255
|
+
return prot === null || prot === Object.prototype;
|
|
256
|
+
};
|
|
257
|
+
const isUnsafeKey = (key) => {
|
|
258
|
+
return key === "__proto__" || key === "constructor" || key === "prototype";
|
|
259
|
+
};
|
|
260
|
+
const classifyKeys = (omitKeys) => {
|
|
261
|
+
const topLevel = /* @__PURE__ */ new Set();
|
|
262
|
+
const nested = /* @__PURE__ */ new Map();
|
|
263
|
+
for (const key of omitKeys) {
|
|
264
|
+
const dotIdx = key.indexOf(".");
|
|
265
|
+
if (dotIdx === -1) {
|
|
266
|
+
topLevel.add(key);
|
|
267
|
+
} else {
|
|
268
|
+
const head = key.slice(0, dotIdx);
|
|
269
|
+
const tail = key.slice(dotIdx + 1);
|
|
270
|
+
if (!isUnsafeKey(head)) {
|
|
271
|
+
const existing = nested.get(head) ?? [];
|
|
272
|
+
existing.push(tail);
|
|
273
|
+
nested.set(head, existing);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
115
276
|
}
|
|
116
|
-
return
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
|
|
277
|
+
return { topLevel, nested };
|
|
278
|
+
};
|
|
279
|
+
const omitDeep = (value, keys) => {
|
|
280
|
+
if (value === void 0) return {};
|
|
281
|
+
if (Array.isArray(value)) {
|
|
282
|
+
return value.map((item) => omitDeep(item, keys));
|
|
121
283
|
}
|
|
122
|
-
return
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
284
|
+
if (!isPlainObject(value)) return value;
|
|
285
|
+
const omitKeys = typeof keys === "string" ? [keys] : keys;
|
|
286
|
+
if (!Array.isArray(omitKeys)) return value;
|
|
287
|
+
const { topLevel, nested } = classifyKeys(omitKeys);
|
|
288
|
+
const result = {};
|
|
289
|
+
for (const key of Object.keys(value)) {
|
|
290
|
+
if (topLevel.has(key)) continue;
|
|
291
|
+
const nestedKeys = nested.get(key);
|
|
292
|
+
result[key] = omitDeep(value[key], nestedKeys ?? omitKeys);
|
|
127
293
|
}
|
|
128
|
-
return
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
294
|
+
return result;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const isPatchHistoryEnabled = (opts, context) => {
|
|
298
|
+
return !opts.patchHistoryDisabled && !context.ignorePatchHistory;
|
|
299
|
+
};
|
|
300
|
+
const applyOmit = (object, opts) => {
|
|
301
|
+
return opts.omit ? omitDeep(object, opts.omit) : object;
|
|
302
|
+
};
|
|
303
|
+
const replacer = (_key, value) => typeof value === "bigint" ? value.toString() : value;
|
|
304
|
+
const getJsonOmit = (opts, doc) => {
|
|
305
|
+
return applyOmit(JSON.parse(JSON.stringify(doc, replacer)), opts);
|
|
306
|
+
};
|
|
307
|
+
const getObjectOmit = (opts, doc) => {
|
|
308
|
+
return applyOmit(isFunction(doc?.toObject) ? doc.toObject() : doc, opts);
|
|
309
|
+
};
|
|
310
|
+
const getOptionalField = async (fn, doc) => {
|
|
311
|
+
if (isFunction(fn)) {
|
|
312
|
+
return await fn(doc);
|
|
133
313
|
}
|
|
134
314
|
return void 0;
|
|
135
|
-
}
|
|
136
|
-
|
|
315
|
+
};
|
|
316
|
+
const getUser = async (opts, doc) => getOptionalField(opts.getUser, doc);
|
|
317
|
+
const getReason = async (opts, doc) => getOptionalField(opts.getReason, doc);
|
|
318
|
+
const getMetadata = async (opts, doc) => getOptionalField(opts.getMetadata, doc);
|
|
319
|
+
const getValue = (item) => {
|
|
137
320
|
return item.status === "fulfilled" ? item.value : void 0;
|
|
138
|
-
}
|
|
139
|
-
async
|
|
321
|
+
};
|
|
322
|
+
const getData = async (opts, doc) => {
|
|
140
323
|
return Promise.allSettled([getUser(opts, doc), getReason(opts, doc), getMetadata(opts, doc)]).then(([user, reason, metadata]) => {
|
|
141
324
|
return [getValue(user), getValue(reason), getValue(metadata)];
|
|
142
325
|
});
|
|
143
|
-
}
|
|
144
|
-
|
|
326
|
+
};
|
|
327
|
+
const emitEvent = (context, event, data) => {
|
|
145
328
|
if (event && !context.ignoreEvent) {
|
|
146
|
-
|
|
329
|
+
try {
|
|
330
|
+
em.emit(event, data);
|
|
331
|
+
} catch {
|
|
332
|
+
}
|
|
147
333
|
}
|
|
148
|
-
}
|
|
149
|
-
async
|
|
334
|
+
};
|
|
335
|
+
const bulkPatch = async (opts, context, eventKey, docsKey) => {
|
|
150
336
|
const history = isPatchHistoryEnabled(opts, context);
|
|
151
337
|
const event = opts[eventKey];
|
|
152
338
|
const docs = context[docsKey];
|
|
153
339
|
const key = eventKey === "eventCreated" ? "doc" : "oldDoc";
|
|
154
|
-
if (isEmpty(docs) || !event && !history) return;
|
|
340
|
+
if (isEmpty(docs) || !docs || !event && !history) return;
|
|
155
341
|
const chunks = chunk(docs, 1e3);
|
|
156
|
-
for (const
|
|
342
|
+
for (const batch of chunks) {
|
|
157
343
|
const bulk = [];
|
|
158
|
-
for (const doc of
|
|
159
|
-
|
|
344
|
+
for (const doc of batch) {
|
|
345
|
+
const omitted = getObjectOmit(opts, doc);
|
|
346
|
+
emitEvent(context, event, { [key]: omitted });
|
|
160
347
|
if (history) {
|
|
161
348
|
const [user, reason, metadata] = await getData(opts, doc);
|
|
162
349
|
bulk.push({
|
|
@@ -166,7 +353,7 @@ async function bulkPatch(opts, context, eventKey, docsKey) {
|
|
|
166
353
|
modelName: context.modelName,
|
|
167
354
|
collectionName: context.collectionName,
|
|
168
355
|
collectionId: doc._id,
|
|
169
|
-
doc:
|
|
356
|
+
doc: omitted,
|
|
170
357
|
version: 0,
|
|
171
358
|
...user !== void 0 && { user },
|
|
172
359
|
...reason !== void 0 && { reason },
|
|
@@ -177,16 +364,17 @@ async function bulkPatch(opts, context, eventKey, docsKey) {
|
|
|
177
364
|
}
|
|
178
365
|
}
|
|
179
366
|
if (history && !isEmpty(bulk)) {
|
|
367
|
+
const onError = opts.onError ?? console.error;
|
|
180
368
|
await HistoryModel.bulkWrite(bulk, { ordered: false }).catch((error) => {
|
|
181
|
-
|
|
369
|
+
onError(error);
|
|
182
370
|
});
|
|
183
371
|
}
|
|
184
372
|
}
|
|
185
|
-
}
|
|
186
|
-
async
|
|
373
|
+
};
|
|
374
|
+
const createPatch = async (opts, context) => {
|
|
187
375
|
await bulkPatch(opts, context, "eventCreated", "createdDocs");
|
|
188
|
-
}
|
|
189
|
-
async
|
|
376
|
+
};
|
|
377
|
+
const updatePatch = async (opts, context, current, original) => {
|
|
190
378
|
const history = isPatchHistoryEnabled(opts, context);
|
|
191
379
|
const currentObject = getJsonOmit(opts, current);
|
|
192
380
|
const originalObject = getJsonOmit(opts, original);
|
|
@@ -201,6 +389,7 @@ async function updatePatch(opts, context, current, original) {
|
|
|
201
389
|
version = lastHistory.version + 1;
|
|
202
390
|
}
|
|
203
391
|
const [user, reason, metadata] = await getData(opts, current);
|
|
392
|
+
const onError = opts.onError ?? console.error;
|
|
204
393
|
await HistoryModel.create({
|
|
205
394
|
op: context.op,
|
|
206
395
|
modelName: context.modelName,
|
|
@@ -211,12 +400,14 @@ async function updatePatch(opts, context, current, original) {
|
|
|
211
400
|
...user !== void 0 && { user },
|
|
212
401
|
...reason !== void 0 && { reason },
|
|
213
402
|
...metadata !== void 0 && { metadata }
|
|
403
|
+
}).catch((error) => {
|
|
404
|
+
onError(error);
|
|
214
405
|
});
|
|
215
406
|
}
|
|
216
|
-
}
|
|
217
|
-
async
|
|
407
|
+
};
|
|
408
|
+
const deletePatch = async (opts, context) => {
|
|
218
409
|
await bulkPatch(opts, context, "eventDeleted", "deletedDocs");
|
|
219
|
-
}
|
|
410
|
+
};
|
|
220
411
|
|
|
221
412
|
const deleteMethods = ["remove", "findOneAndDelete", "findOneAndRemove", "findByIdAndDelete", "findByIdAndRemove", "deleteOne", "deleteMany"];
|
|
222
413
|
const deleteHooksInitialize = (schema, opts) => {
|
|
@@ -227,8 +418,8 @@ const deleteHooksInitialize = (schema, opts) => {
|
|
|
227
418
|
const filter = this.getFilter();
|
|
228
419
|
this._context = {
|
|
229
420
|
op: this.op,
|
|
230
|
-
modelName: opts.modelName ??
|
|
231
|
-
collectionName: opts.collectionName ??
|
|
421
|
+
modelName: opts.modelName ?? model.modelName,
|
|
422
|
+
collectionName: opts.collectionName ?? model.collection.collectionName,
|
|
232
423
|
ignoreEvent: options.ignoreEvent,
|
|
233
424
|
ignorePatchHistory: options.ignorePatchHistory
|
|
234
425
|
};
|
|
@@ -244,12 +435,13 @@ const deleteHooksInitialize = (schema, opts) => {
|
|
|
244
435
|
}
|
|
245
436
|
}
|
|
246
437
|
if (opts.preDelete && isArray(this._context.deletedDocs) && !isEmpty(this._context.deletedDocs)) {
|
|
247
|
-
await opts.preDelete(this._context.deletedDocs);
|
|
438
|
+
await opts.preDelete(cloneDeep(this._context.deletedDocs));
|
|
248
439
|
}
|
|
249
440
|
});
|
|
250
441
|
schema.post(deleteMethods, { document: false, query: true }, async function() {
|
|
251
442
|
const options = this.getOptions();
|
|
252
443
|
if (isHookIgnored(options)) return;
|
|
444
|
+
if (!this._context) return;
|
|
253
445
|
await deletePatch(opts, this._context);
|
|
254
446
|
});
|
|
255
447
|
};
|
|
@@ -277,15 +469,42 @@ const saveHooksInitialize = (schema, opts) => {
|
|
|
277
469
|
};
|
|
278
470
|
|
|
279
471
|
const updateMethods = ["update", "updateOne", "replaceOne", "updateMany", "findOneAndUpdate", "findOneAndReplace", "findByIdAndUpdate"];
|
|
472
|
+
const trackChangedFields = (fields, updated, changed) => {
|
|
473
|
+
if (!fields) return;
|
|
474
|
+
for (const key of Object.keys(fields)) {
|
|
475
|
+
const root = key.split(".")[0];
|
|
476
|
+
changed.set(root, updated[root]);
|
|
477
|
+
}
|
|
478
|
+
};
|
|
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
|
+
};
|
|
280
489
|
const assignUpdate = (document, update, commands) => {
|
|
281
490
|
let updated = powerAssign.assign(document.toObject(toObjectOptions), update);
|
|
282
|
-
|
|
491
|
+
const changedByCommand = /* @__PURE__ */ new Map();
|
|
492
|
+
for (const command of commands) {
|
|
493
|
+
const op = Object.keys(command)[0];
|
|
494
|
+
const fields = command[op];
|
|
283
495
|
try {
|
|
284
496
|
updated = powerAssign.assign(updated, command);
|
|
497
|
+
trackChangedFields(fields, updated, changedByCommand);
|
|
285
498
|
} catch {
|
|
499
|
+
if (op === "$pullAll" && fields) {
|
|
500
|
+
applyPullAll(updated, fields, changedByCommand);
|
|
501
|
+
}
|
|
286
502
|
}
|
|
287
|
-
}
|
|
503
|
+
}
|
|
288
504
|
const doc = document.set(updated).toObject(toObjectOptions);
|
|
505
|
+
for (const [field, value] of changedByCommand) {
|
|
506
|
+
doc[field] = value;
|
|
507
|
+
}
|
|
289
508
|
if (update.createdAt) doc.createdAt = update.createdAt;
|
|
290
509
|
return doc;
|
|
291
510
|
};
|
|
@@ -294,28 +513,27 @@ const splitUpdateAndCommands = (updateQuery) => {
|
|
|
294
513
|
const commands = [];
|
|
295
514
|
if (!isEmpty(updateQuery) && !isArray(updateQuery) && isObjectLike(updateQuery)) {
|
|
296
515
|
update = cloneDeep(updateQuery);
|
|
297
|
-
const keysWithDollarSign = keys(update).filter((key) => key.startsWith("$"));
|
|
516
|
+
const keysWithDollarSign = Object.keys(update).filter((key) => key.startsWith("$"));
|
|
298
517
|
if (!isEmpty(keysWithDollarSign)) {
|
|
299
|
-
|
|
518
|
+
for (const key of keysWithDollarSign) {
|
|
300
519
|
commands.push({ [key]: update[key] });
|
|
301
520
|
delete update[key];
|
|
302
|
-
}
|
|
521
|
+
}
|
|
303
522
|
}
|
|
304
523
|
}
|
|
305
524
|
return { update, commands };
|
|
306
525
|
};
|
|
307
526
|
const updateHooksInitialize = (schema, opts) => {
|
|
308
|
-
schema.pre(updateMethods, async function() {
|
|
527
|
+
schema.pre(updateMethods, { document: false, query: true }, async function() {
|
|
309
528
|
const options = this.getOptions();
|
|
310
529
|
if (isHookIgnored(options)) return;
|
|
311
530
|
const model = this.model;
|
|
312
531
|
const filter = this.getFilter();
|
|
313
|
-
const count = await this.model.countDocuments(filter).exec();
|
|
314
532
|
this._context = {
|
|
315
533
|
op: this.op,
|
|
316
|
-
modelName: opts.modelName ??
|
|
317
|
-
collectionName: opts.collectionName ??
|
|
318
|
-
isNew: Boolean(options.upsert) &&
|
|
534
|
+
modelName: opts.modelName ?? model.modelName,
|
|
535
|
+
collectionName: opts.collectionName ?? model.collection.collectionName,
|
|
536
|
+
isNew: Boolean(options.upsert) && await model.countDocuments(filter).exec() === 0,
|
|
319
537
|
ignoreEvent: options.ignoreEvent,
|
|
320
538
|
ignorePatchHistory: options.ignorePatchHistory
|
|
321
539
|
};
|
|
@@ -327,25 +545,20 @@ const updateHooksInitialize = (schema, opts) => {
|
|
|
327
545
|
await updatePatch(opts, this._context, assignUpdate(doc, update, commands), origDoc);
|
|
328
546
|
});
|
|
329
547
|
});
|
|
330
|
-
schema.post(updateMethods, async function() {
|
|
548
|
+
schema.post(updateMethods, { document: false, query: true }, async function() {
|
|
331
549
|
const options = this.getOptions();
|
|
332
550
|
if (isHookIgnored(options)) return;
|
|
551
|
+
if (!this._context) return;
|
|
333
552
|
if (!this._context.isNew) return;
|
|
334
553
|
const model = this.model;
|
|
335
554
|
const updateQuery = this.getUpdate();
|
|
336
555
|
const { update, commands } = splitUpdateAndCommands(updateQuery);
|
|
337
|
-
let current = null;
|
|
338
556
|
const filter = this.getFilter();
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
current = await model.findOne(combined).sort("desc").lean().exec();
|
|
345
|
-
}
|
|
346
|
-
if (!isEmpty(filter) && !current) {
|
|
347
|
-
console.log("filter", filter);
|
|
348
|
-
current = await model.findOne(filter).sort("desc").lean().exec();
|
|
557
|
+
const candidates = [update, assignUpdate(model.hydrate({}), update, commands), filter];
|
|
558
|
+
let current = null;
|
|
559
|
+
for (const query of candidates) {
|
|
560
|
+
if (current || isEmpty(query)) continue;
|
|
561
|
+
current = await model.findOne(query).sort({ _id: -1 }).lean().exec();
|
|
349
562
|
}
|
|
350
563
|
if (current) {
|
|
351
564
|
this._context.createdDocs = [current];
|
|
@@ -354,15 +567,16 @@ const updateHooksInitialize = (schema, opts) => {
|
|
|
354
567
|
});
|
|
355
568
|
};
|
|
356
569
|
|
|
357
|
-
const
|
|
358
|
-
const
|
|
359
|
-
const
|
|
570
|
+
const major = Number.parseInt(mongoose.version, 10);
|
|
571
|
+
const isMongooseLessThan8 = major < 8;
|
|
572
|
+
const isMongooseLessThan7 = major < 7;
|
|
573
|
+
const isMongoose6 = major === 6;
|
|
360
574
|
if (isMongoose6) {
|
|
361
575
|
mongoose.set("strictQuery", false);
|
|
362
576
|
}
|
|
363
577
|
|
|
364
578
|
const remove = isMongooseLessThan7 ? "remove" : "deleteOne";
|
|
365
|
-
const patchHistoryPlugin =
|
|
579
|
+
const patchHistoryPlugin = (schema, opts) => {
|
|
366
580
|
saveHooksInitialize(schema, opts);
|
|
367
581
|
updateHooksInitialize(schema, opts);
|
|
368
582
|
deleteHooksInitialize(schema, opts);
|