ts-patch-mongoose 3.1.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 +12 -9
- package/dist/index.d.cts +2 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +2 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +12 -9
- package/package.json +13 -14
- package/src/hooks/update-hooks.ts +8 -3
- package/src/index.ts +8 -5
- package/src/ms.ts +4 -3
- package/src/patch.ts +3 -3
- package/src/types.ts +2 -0
- package/biome.json +0 -47
- 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
|
@@ -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;
|
|
@@ -385,7 +386,7 @@ const updatePatch = async (opts, context, current, original) => {
|
|
|
385
386
|
if (history) {
|
|
386
387
|
let version = 0;
|
|
387
388
|
const lastHistory = await HistoryModel.findOne({ collectionId: original._id }).sort("-version").exec();
|
|
388
|
-
if (lastHistory
|
|
389
|
+
if (lastHistory) {
|
|
389
390
|
version = lastHistory.version + 1;
|
|
390
391
|
}
|
|
391
392
|
const [user, reason, metadata] = await getData(opts, current);
|
|
@@ -472,7 +473,7 @@ const updateMethods = ["update", "updateOne", "replaceOne", "updateMany", "findO
|
|
|
472
473
|
const trackChangedFields = (fields, updated, changed) => {
|
|
473
474
|
if (!fields) return;
|
|
474
475
|
for (const key of Object.keys(fields)) {
|
|
475
|
-
const root = key.split(".")
|
|
476
|
+
const [root = key] = key.split(".");
|
|
476
477
|
changed.set(root, updated[root]);
|
|
477
478
|
}
|
|
478
479
|
};
|
|
@@ -490,7 +491,7 @@ const assignUpdate = (document, update, commands) => {
|
|
|
490
491
|
let updated = powerAssign.assign(document.toObject(toObjectOptions), update);
|
|
491
492
|
const changedByCommand = /* @__PURE__ */ new Map();
|
|
492
493
|
for (const command of commands) {
|
|
493
|
-
const op = Object.keys(command)
|
|
494
|
+
const [op = ""] = Object.keys(command);
|
|
494
495
|
const fields = command[op];
|
|
495
496
|
try {
|
|
496
497
|
updated = powerAssign.assign(updated, command);
|
|
@@ -558,7 +559,8 @@ const updateHooksInitialize = (schema, opts) => {
|
|
|
558
559
|
let current = null;
|
|
559
560
|
for (const query of candidates) {
|
|
560
561
|
if (current || isEmpty(query)) continue;
|
|
561
|
-
|
|
562
|
+
const found = await model.findOne(query).sort({ _id: -1 }).lean().exec();
|
|
563
|
+
current = found;
|
|
562
564
|
}
|
|
563
565
|
if (current) {
|
|
564
566
|
this._context.createdDocs = [current];
|
|
@@ -590,13 +592,14 @@ const patchHistoryPlugin = (schema, opts) => {
|
|
|
590
592
|
await createPatch(opts, context);
|
|
591
593
|
});
|
|
592
594
|
if (isMongooseLessThan8) {
|
|
593
|
-
|
|
595
|
+
const legacySchema = schema;
|
|
596
|
+
legacySchema.pre(remove, { document: true, query: false }, async function() {
|
|
594
597
|
const original = this.toObject(toObjectOptions);
|
|
595
598
|
if (opts.preDelete && !isEmpty(original)) {
|
|
596
599
|
await opts.preDelete([original]);
|
|
597
600
|
}
|
|
598
601
|
});
|
|
599
|
-
|
|
602
|
+
legacySchema.post(remove, { document: true, query: false }, async function() {
|
|
600
603
|
const original = this.toObject(toObjectOptions);
|
|
601
604
|
const model = this.constructor;
|
|
602
605
|
const context = {
|
package/dist/index.d.cts
CHANGED
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
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":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -98,9 +98,10 @@ const ms = (val) => {
|
|
|
98
98
|
if (str.length > 100) return Number.NaN;
|
|
99
99
|
const match = RE.exec(str);
|
|
100
100
|
if (!match) return Number.NaN;
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
|
|
101
|
+
const [, numStr, unitStr] = match;
|
|
102
|
+
const n = Number.parseFloat(String(numStr));
|
|
103
|
+
const type = (unitStr ?? "ms").toLowerCase();
|
|
104
|
+
return n * UNITS[type];
|
|
104
105
|
};
|
|
105
106
|
|
|
106
107
|
const isArray = Array.isArray;
|
|
@@ -383,7 +384,7 @@ const updatePatch = async (opts, context, current, original) => {
|
|
|
383
384
|
if (history) {
|
|
384
385
|
let version = 0;
|
|
385
386
|
const lastHistory = await HistoryModel.findOne({ collectionId: original._id }).sort("-version").exec();
|
|
386
|
-
if (lastHistory
|
|
387
|
+
if (lastHistory) {
|
|
387
388
|
version = lastHistory.version + 1;
|
|
388
389
|
}
|
|
389
390
|
const [user, reason, metadata] = await getData(opts, current);
|
|
@@ -470,7 +471,7 @@ const updateMethods = ["update", "updateOne", "replaceOne", "updateMany", "findO
|
|
|
470
471
|
const trackChangedFields = (fields, updated, changed) => {
|
|
471
472
|
if (!fields) return;
|
|
472
473
|
for (const key of Object.keys(fields)) {
|
|
473
|
-
const root = key.split(".")
|
|
474
|
+
const [root = key] = key.split(".");
|
|
474
475
|
changed.set(root, updated[root]);
|
|
475
476
|
}
|
|
476
477
|
};
|
|
@@ -488,7 +489,7 @@ const assignUpdate = (document, update, commands) => {
|
|
|
488
489
|
let updated = assign(document.toObject(toObjectOptions), update);
|
|
489
490
|
const changedByCommand = /* @__PURE__ */ new Map();
|
|
490
491
|
for (const command of commands) {
|
|
491
|
-
const op = Object.keys(command)
|
|
492
|
+
const [op = ""] = Object.keys(command);
|
|
492
493
|
const fields = command[op];
|
|
493
494
|
try {
|
|
494
495
|
updated = assign(updated, command);
|
|
@@ -556,7 +557,8 @@ const updateHooksInitialize = (schema, opts) => {
|
|
|
556
557
|
let current = null;
|
|
557
558
|
for (const query of candidates) {
|
|
558
559
|
if (current || isEmpty(query)) continue;
|
|
559
|
-
|
|
560
|
+
const found = await model.findOne(query).sort({ _id: -1 }).lean().exec();
|
|
561
|
+
current = found;
|
|
560
562
|
}
|
|
561
563
|
if (current) {
|
|
562
564
|
this._context.createdDocs = [current];
|
|
@@ -588,13 +590,14 @@ const patchHistoryPlugin = (schema, opts) => {
|
|
|
588
590
|
await createPatch(opts, context);
|
|
589
591
|
});
|
|
590
592
|
if (isMongooseLessThan8) {
|
|
591
|
-
|
|
593
|
+
const legacySchema = schema;
|
|
594
|
+
legacySchema.pre(remove, { document: true, query: false }, async function() {
|
|
592
595
|
const original = this.toObject(toObjectOptions);
|
|
593
596
|
if (opts.preDelete && !isEmpty(original)) {
|
|
594
597
|
await opts.preDelete([original]);
|
|
595
598
|
}
|
|
596
599
|
});
|
|
597
|
-
|
|
600
|
+
legacySchema.post(remove, { document: true, query: false }, async function() {
|
|
598
601
|
const original = this.toObject(toObjectOptions);
|
|
599
602
|
const model = this.constructor;
|
|
600
603
|
const context = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-patch-mongoose",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"description": "Patch history & events for mongoose models",
|
|
5
5
|
"author": "ilovepixelart",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,11 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"files": [
|
|
42
42
|
"dist",
|
|
43
|
-
"src"
|
|
44
|
-
"tests",
|
|
45
|
-
"tsconfig.json",
|
|
46
|
-
"vite.config.mts",
|
|
47
|
-
"biome.json"
|
|
43
|
+
"src"
|
|
48
44
|
],
|
|
49
45
|
"type": "module",
|
|
50
46
|
"exports": {
|
|
@@ -67,25 +63,26 @@
|
|
|
67
63
|
"test": "vitest run --coverage",
|
|
68
64
|
"test:open": "vitest run --coverage && open-cli coverage/lcov-report/index.html",
|
|
69
65
|
"type:check": "tsc --noEmit",
|
|
66
|
+
"type:check:tests": "tsc --noEmit -p tests/tsconfig.json",
|
|
70
67
|
"build": "pkgroll --clean-dist",
|
|
71
|
-
"release": "npm install && npm run biome && npm run type:check && npm run build && np --no-publish"
|
|
68
|
+
"release": "npm install && npm run biome && npm run type:check && npm run type:check:tests && npm run build && np --no-publish"
|
|
72
69
|
},
|
|
73
70
|
"dependencies": {
|
|
74
71
|
"fast-json-patch": "3.1.1",
|
|
75
72
|
"power-assign": "0.2.10"
|
|
76
73
|
},
|
|
77
74
|
"devDependencies": {
|
|
78
|
-
"@biomejs/biome": "2.4.
|
|
79
|
-
"@types/node": "25.
|
|
80
|
-
"@vitest/coverage-v8": "4.1.
|
|
75
|
+
"@biomejs/biome": "2.4.11",
|
|
76
|
+
"@types/node": "25.6.0",
|
|
77
|
+
"@vitest/coverage-v8": "4.1.4",
|
|
81
78
|
"mongodb-memory-server": "11.0.1",
|
|
82
|
-
"mongoose": "9.
|
|
83
|
-
"np": "11.0.
|
|
79
|
+
"mongoose": "9.4.1",
|
|
80
|
+
"np": "11.0.3",
|
|
84
81
|
"open-cli": "9.0.0",
|
|
85
82
|
"pkgroll": "2.27.0",
|
|
86
83
|
"simple-git-hooks": "2.13.1",
|
|
87
84
|
"typescript": "5.9.3",
|
|
88
|
-
"vitest": "4.1.
|
|
85
|
+
"vitest": "4.1.4"
|
|
89
86
|
},
|
|
90
87
|
"peerDependencies": {
|
|
91
88
|
"mongoose": ">=6.6.0 < 10"
|
|
@@ -99,6 +96,8 @@
|
|
|
99
96
|
},
|
|
100
97
|
"overrides": {
|
|
101
98
|
"tmp": "0.2.5",
|
|
102
|
-
"file-type": "21.3.2"
|
|
99
|
+
"file-type": "21.3.2",
|
|
100
|
+
"lodash": "4.18.1",
|
|
101
|
+
"vite": "8.0.8"
|
|
103
102
|
}
|
|
104
103
|
}
|
|
@@ -10,7 +10,7 @@ const updateMethods = ['update', 'updateOne', 'replaceOne', 'updateMany', 'findO
|
|
|
10
10
|
const trackChangedFields = (fields: Record<string, unknown> | undefined, updated: Record<string, unknown>, changed: Map<string, unknown>): void => {
|
|
11
11
|
if (!fields) return
|
|
12
12
|
for (const key of Object.keys(fields)) {
|
|
13
|
-
const root = key.split('.')
|
|
13
|
+
const [root = key] = key.split('.')
|
|
14
14
|
changed.set(root, updated[root])
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -31,7 +31,7 @@ export const assignUpdate = <T>(document: HydratedDocument<T>, update: UpdateQue
|
|
|
31
31
|
const changedByCommand = new Map<string, unknown>()
|
|
32
32
|
|
|
33
33
|
for (const command of commands) {
|
|
34
|
-
const op = Object.keys(command)
|
|
34
|
+
const [op = ''] = Object.keys(command)
|
|
35
35
|
const fields = command[op] as Record<string, unknown> | undefined
|
|
36
36
|
try {
|
|
37
37
|
updated = assign(updated, command)
|
|
@@ -113,7 +113,12 @@ export const updateHooksInitialize = <T>(schema: Schema<T>, opts: PluginOptions<
|
|
|
113
113
|
let current: HydratedDocument<T> | null = null
|
|
114
114
|
for (const query of candidates) {
|
|
115
115
|
if (current || isEmpty(query)) continue
|
|
116
|
-
|
|
116
|
+
const found = await model
|
|
117
|
+
.findOne(query as never)
|
|
118
|
+
.sort({ _id: -1 })
|
|
119
|
+
.lean()
|
|
120
|
+
.exec()
|
|
121
|
+
current = found as HydratedDocument<T>
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
if (current) {
|
package/src/index.ts
CHANGED
|
@@ -36,9 +36,13 @@ export const patchHistoryPlugin = <T>(schema: Schema<T>, opts: PluginOptions<T>)
|
|
|
36
36
|
// In Mongoose 7, doc.deleteOne() returned a promise that resolved to doc.
|
|
37
37
|
// In Mongoose 8, doc.deleteOne() returns a query for easier chaining, as well as consistency with doc.updateOne().
|
|
38
38
|
if (isMongooseLessThan8) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
type LegacySchema = {
|
|
40
|
+
pre(name: string, options: { document: boolean; query: boolean }, fn: (this: HydratedDocument<T>) => Promise<void>): void
|
|
41
|
+
post(name: string, options: { document: boolean; query: boolean }, fn: (this: HydratedDocument<T>) => Promise<void>): void
|
|
42
|
+
}
|
|
43
|
+
const legacySchema = schema as unknown as LegacySchema
|
|
44
|
+
|
|
45
|
+
legacySchema.pre(remove, { document: true, query: false }, async function (this: HydratedDocument<T>) {
|
|
42
46
|
const original = this.toObject(toObjectOptions) as HydratedDocument<T>
|
|
43
47
|
|
|
44
48
|
if (opts.preDelete && !isEmpty(original)) {
|
|
@@ -46,8 +50,7 @@ export const patchHistoryPlugin = <T>(schema: Schema<T>, opts: PluginOptions<T>)
|
|
|
46
50
|
}
|
|
47
51
|
})
|
|
48
52
|
|
|
49
|
-
|
|
50
|
-
schema.post(remove, { document: true, query: false }, async function (this: HydratedDocument<T>) {
|
|
53
|
+
legacySchema.post(remove, { document: true, query: false }, async function (this: HydratedDocument<T>) {
|
|
51
54
|
const original = this.toObject(toObjectOptions) as HydratedDocument<T>
|
|
52
55
|
const model = this.constructor as Model<T>
|
|
53
56
|
|
package/src/ms.ts
CHANGED
|
@@ -60,7 +60,8 @@ export const ms = (val: Duration): number => {
|
|
|
60
60
|
const match = RE.exec(str)
|
|
61
61
|
if (!match) return Number.NaN
|
|
62
62
|
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
63
|
+
const [, numStr, unitStr] = match
|
|
64
|
+
const n = Number.parseFloat(String(numStr))
|
|
65
|
+
const type = (unitStr ?? 'ms').toLowerCase() as Unit
|
|
66
|
+
return n * UNITS[type]
|
|
66
67
|
}
|
package/src/patch.ts
CHANGED
|
@@ -23,7 +23,7 @@ export const getJsonOmit = <T>(opts: PluginOptions<T>, doc: HydratedDocument<T>)
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export const getObjectOmit = <T>(opts: PluginOptions<T>, doc: HydratedDocument<T>): Partial<T> => {
|
|
26
|
-
return applyOmit(isFunction(doc?.toObject) ? doc.toObject() : doc, opts)
|
|
26
|
+
return applyOmit(isFunction(doc?.toObject) ? (doc.toObject() as Partial<T>) : doc, opts)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const getOptionalField = async <T, R>(fn: ((doc: HydratedDocument<T>) => Promise<R> | R) | undefined, doc?: HydratedDocument<T>): Promise<R | undefined> => {
|
|
@@ -43,7 +43,7 @@ export const getValue = <T>(item: PromiseSettledResult<T>): T | undefined => {
|
|
|
43
43
|
return item.status === 'fulfilled' ? item.value : undefined
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export const getData = async <T>(opts: PluginOptions<T>, doc
|
|
46
|
+
export const getData = async <T>(opts: PluginOptions<T>, doc?: HydratedDocument<T>): Promise<[User | undefined, string | undefined, Metadata | undefined]> => {
|
|
47
47
|
return Promise.allSettled([getUser(opts, doc), getReason(opts, doc), getMetadata(opts, doc)]).then(([user, reason, metadata]) => {
|
|
48
48
|
return [getValue(user), getValue(reason), getValue(metadata)]
|
|
49
49
|
})
|
|
@@ -127,7 +127,7 @@ export const updatePatch = async <T>(opts: PluginOptions<T>, context: PatchConte
|
|
|
127
127
|
.sort('-version')
|
|
128
128
|
.exec()
|
|
129
129
|
|
|
130
|
-
if (lastHistory
|
|
130
|
+
if (lastHistory) {
|
|
131
131
|
version = lastHistory.version + 1
|
|
132
132
|
}
|
|
133
133
|
|
package/src/types.ts
CHANGED
package/biome.json
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://biomejs.dev/schemas/2.4.9/schema.json",
|
|
3
|
-
"vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false },
|
|
4
|
-
"files": {
|
|
5
|
-
"ignoreUnknown": false,
|
|
6
|
-
"includes": ["src/**/*.ts", "tests/**/*.ts"]
|
|
7
|
-
},
|
|
8
|
-
"formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2 },
|
|
9
|
-
"assist": {
|
|
10
|
-
"actions": {
|
|
11
|
-
"source": {
|
|
12
|
-
"organizeImports": {
|
|
13
|
-
"level": "on",
|
|
14
|
-
"options": {
|
|
15
|
-
"groups": [
|
|
16
|
-
"vitest",
|
|
17
|
-
":BLANK_LINE:",
|
|
18
|
-
":NODE:",
|
|
19
|
-
{ "type": false },
|
|
20
|
-
":BLANK_LINE:"
|
|
21
|
-
]
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
"linter": {
|
|
28
|
-
"enabled": true,
|
|
29
|
-
"rules": {
|
|
30
|
-
"recommended": true
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
"javascript": {
|
|
34
|
-
"formatter": {
|
|
35
|
-
"trailingCommas": "all",
|
|
36
|
-
"quoteStyle": "single",
|
|
37
|
-
"semicolons": "asNeeded",
|
|
38
|
-
"lineWidth": 320
|
|
39
|
-
},
|
|
40
|
-
"globals": ["Atomics", "SharedArrayBuffer"]
|
|
41
|
-
},
|
|
42
|
-
"json": {
|
|
43
|
-
"formatter": {
|
|
44
|
-
"trailingCommas": "none"
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export const USER_CREATED = 'user-created'
|
|
2
|
-
export const USER_UPDATED = 'user-updated'
|
|
3
|
-
export const USER_DELETED = 'user-deleted'
|
|
4
|
-
|
|
5
|
-
export const GLOBAL_CREATED = 'global-created'
|
|
6
|
-
export const GLOBAL_UPDATED = 'global-updated'
|
|
7
|
-
export const GLOBAL_DELETED = 'global-deleted'
|
package/tests/em.test.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { patchEventEmitter } from '../src/index'
|
|
4
|
-
import { emitEvent } from '../src/patch'
|
|
5
|
-
|
|
6
|
-
describe('em', () => {
|
|
7
|
-
afterEach(() => {
|
|
8
|
-
patchEventEmitter.removeAllListeners()
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
it('should subscribe and count', () => {
|
|
12
|
-
let count = 0
|
|
13
|
-
const fn = () => {
|
|
14
|
-
count++
|
|
15
|
-
}
|
|
16
|
-
patchEventEmitter.on('test', fn)
|
|
17
|
-
patchEventEmitter.emit('test')
|
|
18
|
-
expect(count).toBe(1)
|
|
19
|
-
patchEventEmitter.off('test', fn)
|
|
20
|
-
patchEventEmitter.emit('test')
|
|
21
|
-
expect(count).toBe(1)
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('emitEvent', () => {
|
|
25
|
-
const fn = vi.fn()
|
|
26
|
-
patchEventEmitter.on('test', fn)
|
|
27
|
-
|
|
28
|
-
const context = {
|
|
29
|
-
op: 'test',
|
|
30
|
-
modelName: 'Test',
|
|
31
|
-
collectionName: 'tests',
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// @ts-expect-error expected
|
|
35
|
-
emitEvent(context, 'test', { doc: { name: 'test' } })
|
|
36
|
-
expect(fn).toHaveBeenCalledOnce()
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('emitEvent ignore', () => {
|
|
40
|
-
const fn = vi.fn()
|
|
41
|
-
patchEventEmitter.on('test', fn)
|
|
42
|
-
|
|
43
|
-
const context = {
|
|
44
|
-
ignoreEvent: true,
|
|
45
|
-
op: 'test',
|
|
46
|
-
modelName: 'Test',
|
|
47
|
-
collectionName: 'tests',
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// @ts-expect-error expected
|
|
51
|
-
emitEvent(context, 'test', { doc: { name: 'test' } })
|
|
52
|
-
expect(fn).toHaveBeenCalledTimes(0)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('emitEvent should not throw when listener throws', () => {
|
|
56
|
-
const fn = () => {
|
|
57
|
-
throw new Error('listener error')
|
|
58
|
-
}
|
|
59
|
-
patchEventEmitter.on('throw-test', fn)
|
|
60
|
-
|
|
61
|
-
const context = {
|
|
62
|
-
op: 'test',
|
|
63
|
-
modelName: 'Test',
|
|
64
|
-
collectionName: 'tests',
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// @ts-expect-error expected
|
|
68
|
-
expect(() => emitEvent(context, 'throw-test', { doc: { name: 'test' } })).not.toThrow()
|
|
69
|
-
})
|
|
70
|
-
})
|