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 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.x || 7.x || 8.x || 9.x",
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 express [ts-express-tsx](https://github.com/ilovepixelart/ts-express-tsx)
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
- Setup your mongoose model `Book.ts`
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, Types } from 'mongoose'
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 english or in milliseconds as you wish.
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 you database somewhere in your application.
95
- setPatchHistoryTTL('1 month')
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
- name: {
99
- title: String,
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
- // Addition options for patchHistoryPlugin plugin
121
- // Everything bellow is optional and just shows you what you can do:
121
+ // Additional options for patchHistoryPlugin
122
+ // Everything below is optional and just shows you what you can do:
122
123
 
123
- // Code bellow is abstract example, you can use any other way to get user, reason, metadata
124
- // These three properties will be added to patch history document automatically and give you flexibility to track who, why and when made changes to your documents
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 of document (create/update/delete) like: 'Excel upload', 'Manual update', 'API call', etc.
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 shout return a string, in case you want to save reason to patch history
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 in along with patch history
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 nonempty array of documents
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
- // In case you just want to track changes in your models using events below.
153
- // And don't want to save changes to patch history collection
154
- patchHistoryDisabled: true,
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 doc here
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 n = Number.parseFloat(match[1] ?? "");
104
- const type = (match[2] ?? "ms").toLowerCase();
105
- return n * (UNITS[type] ?? 0);
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 && lastHistory.version >= 0) {
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(".")[0];
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)[0];
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
- current = await model.findOne(query).sort({ _id: -1 }).lean().exec();
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
- schema.pre(remove, { document: true, query: false }, async function() {
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
- schema.post(remove, { document: true, query: false }, async function() {
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
@@ -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>;
@@ -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;;AAGb,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;;;ACjDzB,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":[]}
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>;
@@ -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;;AAGb,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;;;ACjDzB,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":[]}
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 n = Number.parseFloat(match[1] ?? "");
102
- const type = (match[2] ?? "ms").toLowerCase();
103
- return n * (UNITS[type] ?? 0);
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 && lastHistory.version >= 0) {
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(".")[0];
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)[0];
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
- current = await model.findOne(query).sort({ _id: -1 }).lean().exec();
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
- schema.pre(remove, { document: true, query: false }, async function() {
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
- schema.post(remove, { document: true, query: false }, async function() {
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.0",
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.9",
79
- "@types/node": "25.5.0",
80
- "@vitest/coverage-v8": "4.1.2",
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.3.3",
83
- "np": "11.0.2",
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.2"
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('.')[0] as string
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)[0] as string
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
- current = (await model.findOne(query).sort({ _id: -1 }).lean().exec()) as HydratedDocument<T>
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
- // @ts-expect-error - Mongoose 7 and below
40
- schema.pre(remove, { document: true, query: false }, async function () {
41
- // @ts-expect-error - Mongoose 7 and below
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
- // @ts-expect-error - Mongoose 7 and below
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 n = Number.parseFloat(match[1] ?? '')
64
- const type = (match[2] ?? 'ms').toLowerCase()
65
- return n * (UNITS[type as Unit] ?? 0)
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: HydratedDocument<T>): Promise<[User | undefined, string | undefined, Metadata | undefined]> => {
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 && lastHistory.version >= 0) {
130
+ if (lastHistory) {
131
131
  version = lastHistory.version + 1
132
132
  }
133
133
 
package/src/types.ts CHANGED
@@ -12,6 +12,8 @@ export interface History {
12
12
  reason?: string
13
13
  metadata?: object
14
14
  patch?: Operation[]
15
+ createdAt?: Date
16
+ updatedAt?: Date
15
17
  }
16
18
 
17
19
  export interface PatchEvent<T> {
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
- })