ts-patch-mongoose 3.0.0 → 3.1.2
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 +31 -25
- package/dist/index.cjs +112 -115
- package/dist/index.d.cts +4 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +4 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +112 -115
- package/package.json +14 -15
- package/src/helpers.ts +16 -3
- package/src/hooks/delete-hooks.ts +5 -4
- package/src/hooks/update-hooks.ts +47 -19
- package/src/index.ts +8 -5
- package/src/ms.ts +4 -3
- package/src/omit-deep.ts +24 -63
- package/src/patch.ts +30 -33
- package/src/types.ts +3 -0
- package/biome.json +0 -50
- package/tests/constants/events.ts +0 -7
- package/tests/em.test.ts +0 -54
- package/tests/helpers.test.ts +0 -311
- 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 -220
- package/tests/patch.test.ts +0 -199
- package/tests/plugin-all-features.test.ts +0 -741
- package/tests/plugin-complex-data.test.ts +0 -1332
- 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 -23
package/README.md
CHANGED
|
@@ -15,16 +15,16 @@ Patch history (audit log) & events plugin for mongoose
|
|
|
15
15
|
|
|
16
16
|
## Motivation
|
|
17
17
|
|
|
18
|
-
ts-patch-mongoose is a plugin for mongoose
|
|
18
|
+
ts-patch-mongoose is a plugin for mongoose.
|
|
19
19
|
\
|
|
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.
|
|
20
|
+
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
21
|
|
|
22
22
|
## Supports and tested with
|
|
23
23
|
|
|
24
24
|
```json
|
|
25
25
|
{
|
|
26
26
|
"node": "20.x || 22.x || 24.x",
|
|
27
|
-
"mongoose": ">=6.6.
|
|
27
|
+
"mongoose": ">=6.6.0 || 7.x || 8.x || 9.x",
|
|
28
28
|
}
|
|
29
29
|
```
|
|
30
30
|
|
|
@@ -53,9 +53,9 @@ bun add ts-patch-mongoose mongoose
|
|
|
53
53
|
|
|
54
54
|
Works with any Node.js framework — Express, Fastify, Koa, Hono, Nest, etc.
|
|
55
55
|
\
|
|
56
|
-
How to use it with
|
|
56
|
+
How to use it with Express: [ts-express-tsx](https://github.com/ilovepixelart/ts-express-tsx)
|
|
57
57
|
|
|
58
|
-
Create your event constants `events.ts`
|
|
58
|
+
Create your event constants in `events.ts`
|
|
59
59
|
|
|
60
60
|
```typescript
|
|
61
61
|
export const BOOK_CREATED = 'book-created'
|
|
@@ -77,33 +77,34 @@ export type Book = {
|
|
|
77
77
|
}
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
Set up your mongoose model in `Book.ts`
|
|
81
81
|
|
|
82
82
|
```typescript
|
|
83
83
|
import { Schema, model } from 'mongoose'
|
|
84
84
|
|
|
85
|
-
import type { HydratedDocument
|
|
85
|
+
import type { HydratedDocument } from 'mongoose'
|
|
86
86
|
import type { Book } from '../types'
|
|
87
87
|
|
|
88
88
|
import { patchHistoryPlugin, setPatchHistoryTTL } from 'ts-patch-mongoose'
|
|
89
89
|
import { BOOK_CREATED, BOOK_UPDATED, BOOK_DELETED } from '../constants/events'
|
|
90
90
|
|
|
91
|
-
// You can set patch history TTL in plain
|
|
91
|
+
// You can set patch history TTL in plain English or in milliseconds as you wish.
|
|
92
92
|
// This will determine how long you want to keep patch history.
|
|
93
93
|
// 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
|
-
|
|
94
|
+
// Execute this method after you connected to your database somewhere in your application.
|
|
95
|
+
// Optional second argument for custom error handling
|
|
96
|
+
setPatchHistoryTTL('1 month', (error) => console.error('TTL setup failed:', error))
|
|
96
97
|
|
|
97
98
|
const BookSchema = new Schema<Book>({
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
title: {
|
|
100
|
+
type: String,
|
|
100
101
|
required: true
|
|
101
102
|
},
|
|
102
103
|
description: {
|
|
103
104
|
type: String,
|
|
104
105
|
},
|
|
105
106
|
authorId: {
|
|
106
|
-
type: Types.ObjectId,
|
|
107
|
+
type: Schema.Types.ObjectId,
|
|
107
108
|
required: true
|
|
108
109
|
}
|
|
109
110
|
}, { timestamps: true })
|
|
@@ -117,25 +118,25 @@ BookSchema.plugin(patchHistoryPlugin, {
|
|
|
117
118
|
// You can omit some properties in case you don't want to save them to patch history
|
|
118
119
|
omit: ['__v', 'createdAt', 'updatedAt'],
|
|
119
120
|
|
|
120
|
-
//
|
|
121
|
-
// Everything
|
|
121
|
+
// Additional options for patchHistoryPlugin
|
|
122
|
+
// Everything below is optional and just shows you what you can do:
|
|
122
123
|
|
|
123
|
-
// Code
|
|
124
|
-
// These three properties will be added to patch history document automatically and
|
|
124
|
+
// Code below is an abstract example, you can use any other way to get user, reason, metadata
|
|
125
|
+
// 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
126
|
getUser: async (doc: HydratedDocument<Book>) => {
|
|
126
127
|
// For example: get user from http context
|
|
127
128
|
// You should return an object, in case you want to save user to patch history
|
|
128
129
|
return httpContext.get('user') as Record<string, unknown>
|
|
129
130
|
},
|
|
130
131
|
|
|
131
|
-
// Reason
|
|
132
|
+
// Reason for the document change (create/update/delete) like: 'Excel upload', 'Manual update', 'API call', etc.
|
|
132
133
|
getReason: async (doc: HydratedDocument<Book>) => {
|
|
133
134
|
// For example: get reason from http context, or any other place of your application
|
|
134
|
-
// You
|
|
135
|
+
// You should return a string, in case you want to save reason to patch history
|
|
135
136
|
return httpContext.get('reason') as string
|
|
136
137
|
},
|
|
137
138
|
|
|
138
|
-
// You can provide any information you want to save
|
|
139
|
+
// You can provide any information you want to save along with patch history
|
|
139
140
|
getMetadata: async (doc: HydratedDocument<Book>) => {
|
|
140
141
|
// For example: get metadata from http context, or any other place of your application
|
|
141
142
|
// You should return an object, in case you want to save metadata to patch history
|
|
@@ -143,15 +144,20 @@ BookSchema.plugin(patchHistoryPlugin, {
|
|
|
143
144
|
},
|
|
144
145
|
|
|
145
146
|
// Do something before deleting documents
|
|
146
|
-
// This method will be executed before deleting document or documents and always returns a
|
|
147
|
+
// This method will be executed before deleting document or documents and always returns a non-empty array of documents
|
|
147
148
|
preDelete: async (docs) => {
|
|
148
149
|
const bookIds = docs.map((doc) => doc._id)
|
|
149
150
|
await SomeOtherModel.deleteMany({ bookId: { $in: bookIds } })
|
|
150
151
|
},
|
|
151
152
|
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
// Custom error handler for history write failures (defaults to console.error)
|
|
154
|
+
onError: (error) => {
|
|
155
|
+
console.error('Patch history error:', error)
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
// In case you just want to track changes in your models using events
|
|
159
|
+
// and don't want to save changes to patch history collection
|
|
160
|
+
// patchHistoryDisabled: true,
|
|
155
161
|
})
|
|
156
162
|
|
|
157
163
|
const Book = model('Book', BookSchema)
|
|
@@ -188,7 +194,7 @@ patchEventEmitter.on(BOOK_UPDATED, ({ doc, oldDoc, patch }) => {
|
|
|
188
194
|
patchEventEmitter.on(BOOK_DELETED, ({ oldDoc }) => {
|
|
189
195
|
try {
|
|
190
196
|
console.log('Event - book deleted', oldDoc)
|
|
191
|
-
// Do something with
|
|
197
|
+
// Do something with oldDoc here
|
|
192
198
|
} catch (error) {
|
|
193
199
|
console.error(error)
|
|
194
200
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -100,9 +100,10 @@ const ms = (val) => {
|
|
|
100
100
|
if (str.length > 100) return Number.NaN;
|
|
101
101
|
const match = RE.exec(str);
|
|
102
102
|
if (!match) return Number.NaN;
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
103
|
+
const [, numStr, unitStr] = match;
|
|
104
|
+
const n = Number.parseFloat(String(numStr));
|
|
105
|
+
const type = (unitStr ?? "ms").toLowerCase();
|
|
106
|
+
return n * UNITS[type];
|
|
106
107
|
};
|
|
107
108
|
|
|
108
109
|
const isArray = Array.isArray;
|
|
@@ -140,6 +141,12 @@ const cloneImmutable = (value) => {
|
|
|
140
141
|
cloned.lastIndex = re.lastIndex;
|
|
141
142
|
return cloned;
|
|
142
143
|
}
|
|
144
|
+
case "[object Error]": {
|
|
145
|
+
const err = value;
|
|
146
|
+
const cloned = new err.constructor(err.message);
|
|
147
|
+
if (err.stack) cloned.stack = err.stack;
|
|
148
|
+
return cloned;
|
|
149
|
+
}
|
|
143
150
|
case "[object ArrayBuffer]":
|
|
144
151
|
return cloneArrayBuffer(value);
|
|
145
152
|
case "[object DataView]": {
|
|
@@ -188,7 +195,11 @@ const cloneDeep = (value, seen = /* @__PURE__ */ new WeakMap()) => {
|
|
|
188
195
|
if (seen.has(value)) return seen.get(value);
|
|
189
196
|
const immutable = cloneImmutable(value);
|
|
190
197
|
if (immutable !== void 0) return immutable;
|
|
191
|
-
|
|
198
|
+
const record = value;
|
|
199
|
+
if (typeof record._bsontype === "string" && typeof record.toHexString === "function") {
|
|
200
|
+
return new value.constructor(record.toHexString());
|
|
201
|
+
}
|
|
202
|
+
if (typeof record.toJSON === "function") {
|
|
192
203
|
return JSON.parse(JSON.stringify(value));
|
|
193
204
|
}
|
|
194
205
|
return cloneCollection(value, seen);
|
|
@@ -207,7 +218,7 @@ const toObjectOptions = {
|
|
|
207
218
|
depopulate: true,
|
|
208
219
|
virtuals: false
|
|
209
220
|
};
|
|
210
|
-
const setPatchHistoryTTL = async (ttl) => {
|
|
221
|
+
const setPatchHistoryTTL = async (ttl, onError) => {
|
|
211
222
|
const name = "createdAt_1_TTL";
|
|
212
223
|
try {
|
|
213
224
|
const indexes = await HistoryModel.collection.indexes();
|
|
@@ -230,7 +241,8 @@ const setPatchHistoryTTL = async (ttl) => {
|
|
|
230
241
|
}
|
|
231
242
|
await HistoryModel.collection.createIndex({ createdAt: 1 }, { expireAfterSeconds, name });
|
|
232
243
|
} catch (err) {
|
|
233
|
-
|
|
244
|
+
const handler = onError ?? console.error;
|
|
245
|
+
handler(err);
|
|
234
246
|
}
|
|
235
247
|
};
|
|
236
248
|
|
|
@@ -246,113 +258,65 @@ const isPlainObject = (val) => {
|
|
|
246
258
|
const isUnsafeKey = (key) => {
|
|
247
259
|
return key === "__proto__" || key === "constructor" || key === "prototype";
|
|
248
260
|
};
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
for (const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (val instanceof Error) return val.message !== "";
|
|
265
|
-
if (val instanceof Map || val instanceof Set) return val.size !== 0;
|
|
266
|
-
if (typeof val === "object") {
|
|
267
|
-
for (const key of Object.keys(val)) {
|
|
268
|
-
if (hasValue(val[key])) return true;
|
|
269
|
-
}
|
|
270
|
-
return false;
|
|
271
|
-
}
|
|
272
|
-
return true;
|
|
273
|
-
};
|
|
274
|
-
const has = (obj, path) => {
|
|
275
|
-
if (obj != null && typeof obj === "object" && typeof path === "string") {
|
|
276
|
-
return hasValue(getValue$1(obj, path));
|
|
277
|
-
}
|
|
278
|
-
return false;
|
|
279
|
-
};
|
|
280
|
-
const unset = (obj, prop) => {
|
|
281
|
-
if (typeof obj !== "object" || obj === null) return false;
|
|
282
|
-
if (Object.hasOwn(obj, prop)) {
|
|
283
|
-
delete obj[prop];
|
|
284
|
-
return true;
|
|
285
|
-
}
|
|
286
|
-
if (has(obj, prop)) {
|
|
287
|
-
const segs = prop.split(".");
|
|
288
|
-
let last = segs.pop();
|
|
289
|
-
while (segs.length && segs.at(-1)?.slice(-1) === "\\") {
|
|
290
|
-
last = `${segs.pop().slice(0, -1)}.${last}`;
|
|
291
|
-
}
|
|
292
|
-
let target = obj;
|
|
293
|
-
while (segs.length) {
|
|
294
|
-
const seg = segs.shift();
|
|
295
|
-
if (isUnsafeKey(seg)) return false;
|
|
296
|
-
target = target[seg];
|
|
261
|
+
const classifyKeys = (omitKeys) => {
|
|
262
|
+
const topLevel = /* @__PURE__ */ new Set();
|
|
263
|
+
const nested = /* @__PURE__ */ new Map();
|
|
264
|
+
for (const key of omitKeys) {
|
|
265
|
+
const dotIdx = key.indexOf(".");
|
|
266
|
+
if (dotIdx === -1) {
|
|
267
|
+
topLevel.add(key);
|
|
268
|
+
} else {
|
|
269
|
+
const head = key.slice(0, dotIdx);
|
|
270
|
+
const tail = key.slice(dotIdx + 1);
|
|
271
|
+
if (!isUnsafeKey(head)) {
|
|
272
|
+
const existing = nested.get(head) ?? [];
|
|
273
|
+
existing.push(tail);
|
|
274
|
+
nested.set(head, existing);
|
|
275
|
+
}
|
|
297
276
|
}
|
|
298
|
-
return delete target[last ?? ""];
|
|
299
277
|
}
|
|
300
|
-
return
|
|
278
|
+
return { topLevel, nested };
|
|
301
279
|
};
|
|
302
280
|
const omitDeep = (value, keys) => {
|
|
303
281
|
if (value === void 0) return {};
|
|
304
282
|
if (Array.isArray(value)) {
|
|
305
|
-
|
|
306
|
-
value[i] = omitDeep(value[i], keys);
|
|
307
|
-
}
|
|
308
|
-
return value;
|
|
283
|
+
return value.map((item) => omitDeep(item, keys));
|
|
309
284
|
}
|
|
310
285
|
if (!isPlainObject(value)) return value;
|
|
311
286
|
const omitKeys = typeof keys === "string" ? [keys] : keys;
|
|
312
287
|
if (!Array.isArray(omitKeys)) return value;
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
288
|
+
const { topLevel, nested } = classifyKeys(omitKeys);
|
|
289
|
+
const result = {};
|
|
316
290
|
for (const key of Object.keys(value)) {
|
|
317
|
-
|
|
291
|
+
if (topLevel.has(key)) continue;
|
|
292
|
+
const nestedKeys = nested.get(key);
|
|
293
|
+
result[key] = omitDeep(value[key], nestedKeys ?? omitKeys);
|
|
318
294
|
}
|
|
319
|
-
return
|
|
295
|
+
return result;
|
|
320
296
|
};
|
|
321
297
|
|
|
322
298
|
const isPatchHistoryEnabled = (opts, context) => {
|
|
323
299
|
return !opts.patchHistoryDisabled && !context.ignorePatchHistory;
|
|
324
300
|
};
|
|
301
|
+
const applyOmit = (object, opts) => {
|
|
302
|
+
return opts.omit ? omitDeep(object, opts.omit) : object;
|
|
303
|
+
};
|
|
304
|
+
const replacer = (_key, value) => typeof value === "bigint" ? value.toString() : value;
|
|
325
305
|
const getJsonOmit = (opts, doc) => {
|
|
326
|
-
|
|
327
|
-
if (opts.omit) {
|
|
328
|
-
return omitDeep(object, opts.omit);
|
|
329
|
-
}
|
|
330
|
-
return object;
|
|
306
|
+
return applyOmit(JSON.parse(JSON.stringify(doc, replacer)), opts);
|
|
331
307
|
};
|
|
332
308
|
const getObjectOmit = (opts, doc) => {
|
|
333
|
-
|
|
334
|
-
return omitDeep(isFunction(doc?.toObject) ? doc.toObject() : doc, opts.omit);
|
|
335
|
-
}
|
|
336
|
-
return doc;
|
|
337
|
-
};
|
|
338
|
-
const getUser = async (opts, doc) => {
|
|
339
|
-
if (isFunction(opts.getUser)) {
|
|
340
|
-
return await opts.getUser(doc);
|
|
341
|
-
}
|
|
342
|
-
return void 0;
|
|
343
|
-
};
|
|
344
|
-
const getReason = async (opts, doc) => {
|
|
345
|
-
if (isFunction(opts.getReason)) {
|
|
346
|
-
return await opts.getReason(doc);
|
|
347
|
-
}
|
|
348
|
-
return void 0;
|
|
309
|
+
return applyOmit(isFunction(doc?.toObject) ? doc.toObject() : doc, opts);
|
|
349
310
|
};
|
|
350
|
-
const
|
|
351
|
-
if (isFunction(
|
|
352
|
-
return await
|
|
311
|
+
const getOptionalField = async (fn, doc) => {
|
|
312
|
+
if (isFunction(fn)) {
|
|
313
|
+
return await fn(doc);
|
|
353
314
|
}
|
|
354
315
|
return void 0;
|
|
355
316
|
};
|
|
317
|
+
const getUser = async (opts, doc) => getOptionalField(opts.getUser, doc);
|
|
318
|
+
const getReason = async (opts, doc) => getOptionalField(opts.getReason, doc);
|
|
319
|
+
const getMetadata = async (opts, doc) => getOptionalField(opts.getMetadata, doc);
|
|
356
320
|
const getValue = (item) => {
|
|
357
321
|
return item.status === "fulfilled" ? item.value : void 0;
|
|
358
322
|
};
|
|
@@ -363,7 +327,10 @@ const getData = async (opts, doc) => {
|
|
|
363
327
|
};
|
|
364
328
|
const emitEvent = (context, event, data) => {
|
|
365
329
|
if (event && !context.ignoreEvent) {
|
|
366
|
-
|
|
330
|
+
try {
|
|
331
|
+
em.emit(event, data);
|
|
332
|
+
} catch {
|
|
333
|
+
}
|
|
367
334
|
}
|
|
368
335
|
};
|
|
369
336
|
const bulkPatch = async (opts, context, eventKey, docsKey) => {
|
|
@@ -376,7 +343,8 @@ const bulkPatch = async (opts, context, eventKey, docsKey) => {
|
|
|
376
343
|
for (const batch of chunks) {
|
|
377
344
|
const bulk = [];
|
|
378
345
|
for (const doc of batch) {
|
|
379
|
-
|
|
346
|
+
const omitted = getObjectOmit(opts, doc);
|
|
347
|
+
emitEvent(context, event, { [key]: omitted });
|
|
380
348
|
if (history) {
|
|
381
349
|
const [user, reason, metadata] = await getData(opts, doc);
|
|
382
350
|
bulk.push({
|
|
@@ -386,7 +354,7 @@ const bulkPatch = async (opts, context, eventKey, docsKey) => {
|
|
|
386
354
|
modelName: context.modelName,
|
|
387
355
|
collectionName: context.collectionName,
|
|
388
356
|
collectionId: doc._id,
|
|
389
|
-
doc:
|
|
357
|
+
doc: omitted,
|
|
390
358
|
version: 0,
|
|
391
359
|
...user !== void 0 && { user },
|
|
392
360
|
...reason !== void 0 && { reason },
|
|
@@ -397,8 +365,9 @@ const bulkPatch = async (opts, context, eventKey, docsKey) => {
|
|
|
397
365
|
}
|
|
398
366
|
}
|
|
399
367
|
if (history && !isEmpty(bulk)) {
|
|
368
|
+
const onError = opts.onError ?? console.error;
|
|
400
369
|
await HistoryModel.bulkWrite(bulk, { ordered: false }).catch((error) => {
|
|
401
|
-
|
|
370
|
+
onError(error);
|
|
402
371
|
});
|
|
403
372
|
}
|
|
404
373
|
}
|
|
@@ -417,10 +386,11 @@ const updatePatch = async (opts, context, current, original) => {
|
|
|
417
386
|
if (history) {
|
|
418
387
|
let version = 0;
|
|
419
388
|
const lastHistory = await HistoryModel.findOne({ collectionId: original._id }).sort("-version").exec();
|
|
420
|
-
if (lastHistory
|
|
389
|
+
if (lastHistory) {
|
|
421
390
|
version = lastHistory.version + 1;
|
|
422
391
|
}
|
|
423
392
|
const [user, reason, metadata] = await getData(opts, current);
|
|
393
|
+
const onError = opts.onError ?? console.error;
|
|
424
394
|
await HistoryModel.create({
|
|
425
395
|
op: context.op,
|
|
426
396
|
modelName: context.modelName,
|
|
@@ -431,6 +401,8 @@ const updatePatch = async (opts, context, current, original) => {
|
|
|
431
401
|
...user !== void 0 && { user },
|
|
432
402
|
...reason !== void 0 && { reason },
|
|
433
403
|
...metadata !== void 0 && { metadata }
|
|
404
|
+
}).catch((error) => {
|
|
405
|
+
onError(error);
|
|
434
406
|
});
|
|
435
407
|
}
|
|
436
408
|
};
|
|
@@ -447,8 +419,8 @@ const deleteHooksInitialize = (schema, opts) => {
|
|
|
447
419
|
const filter = this.getFilter();
|
|
448
420
|
this._context = {
|
|
449
421
|
op: this.op,
|
|
450
|
-
modelName: opts.modelName ??
|
|
451
|
-
collectionName: opts.collectionName ??
|
|
422
|
+
modelName: opts.modelName ?? model.modelName,
|
|
423
|
+
collectionName: opts.collectionName ?? model.collection.collectionName,
|
|
452
424
|
ignoreEvent: options.ignoreEvent,
|
|
453
425
|
ignorePatchHistory: options.ignorePatchHistory
|
|
454
426
|
};
|
|
@@ -464,12 +436,13 @@ const deleteHooksInitialize = (schema, opts) => {
|
|
|
464
436
|
}
|
|
465
437
|
}
|
|
466
438
|
if (opts.preDelete && isArray(this._context.deletedDocs) && !isEmpty(this._context.deletedDocs)) {
|
|
467
|
-
await opts.preDelete(this._context.deletedDocs);
|
|
439
|
+
await opts.preDelete(cloneDeep(this._context.deletedDocs));
|
|
468
440
|
}
|
|
469
441
|
});
|
|
470
442
|
schema.post(deleteMethods, { document: false, query: true }, async function() {
|
|
471
443
|
const options = this.getOptions();
|
|
472
444
|
if (isHookIgnored(options)) return;
|
|
445
|
+
if (!this._context) return;
|
|
473
446
|
await deletePatch(opts, this._context);
|
|
474
447
|
});
|
|
475
448
|
};
|
|
@@ -497,15 +470,42 @@ const saveHooksInitialize = (schema, opts) => {
|
|
|
497
470
|
};
|
|
498
471
|
|
|
499
472
|
const updateMethods = ["update", "updateOne", "replaceOne", "updateMany", "findOneAndUpdate", "findOneAndReplace", "findByIdAndUpdate"];
|
|
473
|
+
const trackChangedFields = (fields, updated, changed) => {
|
|
474
|
+
if (!fields) return;
|
|
475
|
+
for (const key of Object.keys(fields)) {
|
|
476
|
+
const [root = key] = key.split(".");
|
|
477
|
+
changed.set(root, updated[root]);
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
const applyPullAll = (updated, fields, changed) => {
|
|
481
|
+
for (const [field, values] of Object.entries(fields)) {
|
|
482
|
+
const arr = updated[field];
|
|
483
|
+
if (Array.isArray(arr)) {
|
|
484
|
+
const filtered = arr.filter((item) => !values.some((v) => JSON.stringify(v) === JSON.stringify(item)));
|
|
485
|
+
updated[field] = filtered;
|
|
486
|
+
changed.set(field, filtered);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
};
|
|
500
490
|
const assignUpdate = (document, update, commands) => {
|
|
501
491
|
let updated = powerAssign.assign(document.toObject(toObjectOptions), update);
|
|
492
|
+
const changedByCommand = /* @__PURE__ */ new Map();
|
|
502
493
|
for (const command of commands) {
|
|
494
|
+
const [op = ""] = Object.keys(command);
|
|
495
|
+
const fields = command[op];
|
|
503
496
|
try {
|
|
504
497
|
updated = powerAssign.assign(updated, command);
|
|
498
|
+
trackChangedFields(fields, updated, changedByCommand);
|
|
505
499
|
} catch {
|
|
500
|
+
if (op === "$pullAll" && fields) {
|
|
501
|
+
applyPullAll(updated, fields, changedByCommand);
|
|
502
|
+
}
|
|
506
503
|
}
|
|
507
504
|
}
|
|
508
505
|
const doc = document.set(updated).toObject(toObjectOptions);
|
|
506
|
+
for (const [field, value] of changedByCommand) {
|
|
507
|
+
doc[field] = value;
|
|
508
|
+
}
|
|
509
509
|
if (update.createdAt) doc.createdAt = update.createdAt;
|
|
510
510
|
return doc;
|
|
511
511
|
};
|
|
@@ -525,17 +525,16 @@ const splitUpdateAndCommands = (updateQuery) => {
|
|
|
525
525
|
return { update, commands };
|
|
526
526
|
};
|
|
527
527
|
const updateHooksInitialize = (schema, opts) => {
|
|
528
|
-
schema.pre(updateMethods, async function() {
|
|
528
|
+
schema.pre(updateMethods, { document: false, query: true }, async function() {
|
|
529
529
|
const options = this.getOptions();
|
|
530
530
|
if (isHookIgnored(options)) return;
|
|
531
531
|
const model = this.model;
|
|
532
532
|
const filter = this.getFilter();
|
|
533
|
-
const count = await this.model.countDocuments(filter).exec();
|
|
534
533
|
this._context = {
|
|
535
534
|
op: this.op,
|
|
536
|
-
modelName: opts.modelName ??
|
|
537
|
-
collectionName: opts.collectionName ??
|
|
538
|
-
isNew: Boolean(options.upsert) &&
|
|
535
|
+
modelName: opts.modelName ?? model.modelName,
|
|
536
|
+
collectionName: opts.collectionName ?? model.collection.collectionName,
|
|
537
|
+
isNew: Boolean(options.upsert) && await model.countDocuments(filter).exec() === 0,
|
|
539
538
|
ignoreEvent: options.ignoreEvent,
|
|
540
539
|
ignorePatchHistory: options.ignorePatchHistory
|
|
541
540
|
};
|
|
@@ -547,24 +546,21 @@ const updateHooksInitialize = (schema, opts) => {
|
|
|
547
546
|
await updatePatch(opts, this._context, assignUpdate(doc, update, commands), origDoc);
|
|
548
547
|
});
|
|
549
548
|
});
|
|
550
|
-
schema.post(updateMethods, async function() {
|
|
549
|
+
schema.post(updateMethods, { document: false, query: true }, async function() {
|
|
551
550
|
const options = this.getOptions();
|
|
552
551
|
if (isHookIgnored(options)) return;
|
|
552
|
+
if (!this._context) return;
|
|
553
553
|
if (!this._context.isNew) return;
|
|
554
554
|
const model = this.model;
|
|
555
555
|
const updateQuery = this.getUpdate();
|
|
556
556
|
const { update, commands } = splitUpdateAndCommands(updateQuery);
|
|
557
|
-
let current = null;
|
|
558
557
|
const filter = this.getFilter();
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
current =
|
|
565
|
-
}
|
|
566
|
-
if (!isEmpty(filter) && !current) {
|
|
567
|
-
current = await model.findOne(filter).sort("desc").lean().exec();
|
|
558
|
+
const candidates = [update, assignUpdate(model.hydrate({}), update, commands), filter];
|
|
559
|
+
let current = null;
|
|
560
|
+
for (const query of candidates) {
|
|
561
|
+
if (current || isEmpty(query)) continue;
|
|
562
|
+
const found = await model.findOne(query).sort({ _id: -1 }).lean().exec();
|
|
563
|
+
current = found;
|
|
568
564
|
}
|
|
569
565
|
if (current) {
|
|
570
566
|
this._context.createdDocs = [current];
|
|
@@ -596,13 +592,14 @@ const patchHistoryPlugin = (schema, opts) => {
|
|
|
596
592
|
await createPatch(opts, context);
|
|
597
593
|
});
|
|
598
594
|
if (isMongooseLessThan8) {
|
|
599
|
-
|
|
595
|
+
const legacySchema = schema;
|
|
596
|
+
legacySchema.pre(remove, { document: true, query: false }, async function() {
|
|
600
597
|
const original = this.toObject(toObjectOptions);
|
|
601
598
|
if (opts.preDelete && !isEmpty(original)) {
|
|
602
599
|
await opts.preDelete([original]);
|
|
603
600
|
}
|
|
604
601
|
});
|
|
605
|
-
|
|
602
|
+
legacySchema.post(remove, { document: true, query: false }, async function() {
|
|
606
603
|
const original = this.toObject(toObjectOptions);
|
|
607
604
|
const model = this.constructor;
|
|
608
605
|
const context = {
|
package/dist/index.d.cts
CHANGED
|
@@ -13,6 +13,8 @@ interface History {
|
|
|
13
13
|
reason?: string;
|
|
14
14
|
metadata?: object;
|
|
15
15
|
patch?: Operation[];
|
|
16
|
+
createdAt?: Date;
|
|
17
|
+
updatedAt?: Date;
|
|
16
18
|
}
|
|
17
19
|
interface PatchEvent<T> {
|
|
18
20
|
oldDoc?: HydratedDocument<T>;
|
|
@@ -47,6 +49,7 @@ interface PluginOptions<T> {
|
|
|
47
49
|
omit?: string[];
|
|
48
50
|
patchHistoryDisabled?: boolean;
|
|
49
51
|
preDelete?: (docs: HydratedDocument<T>[]) => Promise<void>;
|
|
52
|
+
onError?: (error: Error) => void;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
declare class PatchEventEmitter extends EventEmitter {
|
|
@@ -92,7 +95,7 @@ declare const UNITS: {
|
|
|
92
95
|
type Unit = keyof typeof UNITS;
|
|
93
96
|
type Duration = number | `${number}` | `${number}${Unit}` | `${number} ${Unit}`;
|
|
94
97
|
|
|
95
|
-
declare const setPatchHistoryTTL: (ttl: Duration) => Promise<void>;
|
|
98
|
+
declare const setPatchHistoryTTL: (ttl: Duration, onError?: (error: Error) => void) => Promise<void>;
|
|
96
99
|
|
|
97
100
|
declare const patchHistoryPlugin: <T>(schema: Schema<T>, opts: PluginOptions<T>) => void;
|
|
98
101
|
|
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":";;;;AAGM,UAAW,OAAO;;;;kBAIR,KAAK,CAAC,QAAQ;;;;;;YAMpB,SAAS;;
|
|
1
|
+
{"version":3,"file":"index.d.cts","sources":["../src/types.ts","../src/em.ts","../src/ms.ts","../src/helpers.ts","../src/index.ts"],"mappings":";;;;AAGM,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
|
@@ -13,6 +13,8 @@ interface History {
|
|
|
13
13
|
reason?: string;
|
|
14
14
|
metadata?: object;
|
|
15
15
|
patch?: Operation[];
|
|
16
|
+
createdAt?: Date;
|
|
17
|
+
updatedAt?: Date;
|
|
16
18
|
}
|
|
17
19
|
interface PatchEvent<T> {
|
|
18
20
|
oldDoc?: HydratedDocument<T>;
|
|
@@ -47,6 +49,7 @@ interface PluginOptions<T> {
|
|
|
47
49
|
omit?: string[];
|
|
48
50
|
patchHistoryDisabled?: boolean;
|
|
49
51
|
preDelete?: (docs: HydratedDocument<T>[]) => Promise<void>;
|
|
52
|
+
onError?: (error: Error) => void;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
declare class PatchEventEmitter extends EventEmitter {
|
|
@@ -92,7 +95,7 @@ declare const UNITS: {
|
|
|
92
95
|
type Unit = keyof typeof UNITS;
|
|
93
96
|
type Duration = number | `${number}` | `${number}${Unit}` | `${number} ${Unit}`;
|
|
94
97
|
|
|
95
|
-
declare const setPatchHistoryTTL: (ttl: Duration) => Promise<void>;
|
|
98
|
+
declare const setPatchHistoryTTL: (ttl: Duration, onError?: (error: Error) => void) => Promise<void>;
|
|
96
99
|
|
|
97
100
|
declare const patchHistoryPlugin: <T>(schema: Schema<T>, opts: PluginOptions<T>) => void;
|
|
98
101
|
|
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":";;;;AAGM,UAAW,OAAO;;;;kBAIR,KAAK,CAAC,QAAQ;;;;;;YAMpB,SAAS;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","sources":["../src/types.ts","../src/em.ts","../src/ms.ts","../src/helpers.ts","../src/index.ts"],"mappings":";;;;AAGM,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":[]}
|