ts-patch-mongoose 2.7.1 → 2.8.1

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.
Files changed (156) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +8 -8
  3. package/biome.json +1 -1
  4. package/dist/index.cjs +395 -0
  5. package/dist/index.d.cts +75 -0
  6. package/dist/index.d.mts +75 -0
  7. package/dist/index.mjs +391 -0
  8. package/package.json +36 -32
  9. package/src/helpers.ts +6 -6
  10. package/src/hooks/delete-hooks.ts +8 -9
  11. package/src/hooks/save-hooks.ts +3 -4
  12. package/src/hooks/update-hooks.ts +14 -15
  13. package/src/{plugin.ts → index.ts} +31 -11
  14. package/src/{models/History.ts → model.ts} +3 -5
  15. package/src/patch.ts +27 -31
  16. package/src/types.ts +52 -0
  17. package/src/version.ts +2 -0
  18. package/tests/em.test.ts +1 -1
  19. package/tests/helpers.test.ts +6 -7
  20. package/tests/patch.test.ts +22 -27
  21. package/tests/plugin-event-created.test.ts +50 -51
  22. package/tests/plugin-event-deleted.test.ts +76 -77
  23. package/tests/plugin-event-updated.test.ts +65 -67
  24. package/tests/plugin-global.test.ts +25 -26
  25. package/tests/plugin-omit-all.test.ts +36 -37
  26. package/tests/plugin-patch-history-disabled.test.ts +36 -37
  27. package/tests/plugin-pre-delete.test.ts +17 -18
  28. package/tests/plugin-pre-save.test.ts +7 -8
  29. package/tests/plugin.test.ts +37 -39
  30. package/tests/schemas/{DescriptionSchema.ts → Description.ts} +4 -4
  31. package/tests/schemas/{ProductSchema.ts → Product.ts} +12 -5
  32. package/tests/schemas/{UserSchema.ts → User.ts} +7 -4
  33. package/tsconfig.json +5 -5
  34. package/vite.config.mts +1 -0
  35. package/.swcrc +0 -17
  36. package/dist/cjs/em.js +0 -9
  37. package/dist/cjs/em.js.map +0 -1
  38. package/dist/cjs/helpers.js +0 -43
  39. package/dist/cjs/helpers.js.map +0 -1
  40. package/dist/cjs/hooks/delete-hooks.js +0 -46
  41. package/dist/cjs/hooks/delete-hooks.js.map +0 -1
  42. package/dist/cjs/hooks/save-hooks.js +0 -30
  43. package/dist/cjs/hooks/save-hooks.js.map +0 -1
  44. package/dist/cjs/hooks/update-hooks.js +0 -93
  45. package/dist/cjs/hooks/update-hooks.js.map +0 -1
  46. package/dist/cjs/interfaces/IContext.js +0 -3
  47. package/dist/cjs/interfaces/IContext.js.map +0 -1
  48. package/dist/cjs/interfaces/IEvent.js +0 -3
  49. package/dist/cjs/interfaces/IEvent.js.map +0 -1
  50. package/dist/cjs/interfaces/IHistory.js +0 -3
  51. package/dist/cjs/interfaces/IHistory.js.map +0 -1
  52. package/dist/cjs/interfaces/IHookContext.js +0 -3
  53. package/dist/cjs/interfaces/IHookContext.js.map +0 -1
  54. package/dist/cjs/interfaces/IPluginOptions.js +0 -3
  55. package/dist/cjs/interfaces/IPluginOptions.js.map +0 -1
  56. package/dist/cjs/models/History.js +0 -46
  57. package/dist/cjs/models/History.js.map +0 -1
  58. package/dist/cjs/patch.js +0 -144
  59. package/dist/cjs/patch.js.map +0 -1
  60. package/dist/cjs/plugin.js +0 -51
  61. package/dist/cjs/plugin.js.map +0 -1
  62. package/dist/cjs/types/em.d.ts +0 -6
  63. package/dist/cjs/types/em.d.ts.map +0 -1
  64. package/dist/cjs/types/helpers.d.ts +0 -6
  65. package/dist/cjs/types/helpers.d.ts.map +0 -1
  66. package/dist/cjs/types/hooks/delete-hooks.d.ts +0 -4
  67. package/dist/cjs/types/hooks/delete-hooks.d.ts.map +0 -1
  68. package/dist/cjs/types/hooks/save-hooks.d.ts +0 -4
  69. package/dist/cjs/types/hooks/save-hooks.d.ts.map +0 -1
  70. package/dist/cjs/types/hooks/update-hooks.d.ts +0 -9
  71. package/dist/cjs/types/hooks/update-hooks.d.ts.map +0 -1
  72. package/dist/cjs/types/interfaces/IContext.d.ts +0 -13
  73. package/dist/cjs/types/interfaces/IContext.d.ts.map +0 -1
  74. package/dist/cjs/types/interfaces/IEvent.d.ts +0 -9
  75. package/dist/cjs/types/interfaces/IEvent.d.ts.map +0 -1
  76. package/dist/cjs/types/interfaces/IHistory.d.ts +0 -16
  77. package/dist/cjs/types/interfaces/IHistory.d.ts.map +0 -1
  78. package/dist/cjs/types/interfaces/IHookContext.d.ts +0 -8
  79. package/dist/cjs/types/interfaces/IHookContext.d.ts.map +0 -1
  80. package/dist/cjs/types/interfaces/IPluginOptions.d.ts +0 -18
  81. package/dist/cjs/types/interfaces/IPluginOptions.d.ts.map +0 -1
  82. package/dist/cjs/types/models/History.d.ts +0 -17
  83. package/dist/cjs/types/models/History.d.ts.map +0 -1
  84. package/dist/cjs/types/patch.d.ts +0 -18
  85. package/dist/cjs/types/patch.d.ts.map +0 -1
  86. package/dist/cjs/types/plugin.d.ts +0 -23
  87. package/dist/cjs/types/plugin.d.ts.map +0 -1
  88. package/dist/cjs/types/version.d.ts +0 -4
  89. package/dist/cjs/types/version.d.ts.map +0 -1
  90. package/dist/cjs/version.js +0 -13
  91. package/dist/cjs/version.js.map +0 -1
  92. package/dist/esm/em.js +0 -6
  93. package/dist/esm/em.js.map +0 -1
  94. package/dist/esm/helpers.js +0 -37
  95. package/dist/esm/helpers.js.map +0 -1
  96. package/dist/esm/hooks/delete-hooks.js +0 -42
  97. package/dist/esm/hooks/delete-hooks.js.map +0 -1
  98. package/dist/esm/hooks/save-hooks.js +0 -26
  99. package/dist/esm/hooks/save-hooks.js.map +0 -1
  100. package/dist/esm/hooks/update-hooks.js +0 -87
  101. package/dist/esm/hooks/update-hooks.js.map +0 -1
  102. package/dist/esm/interfaces/IContext.js +0 -2
  103. package/dist/esm/interfaces/IContext.js.map +0 -1
  104. package/dist/esm/interfaces/IEvent.js +0 -2
  105. package/dist/esm/interfaces/IEvent.js.map +0 -1
  106. package/dist/esm/interfaces/IHistory.js +0 -2
  107. package/dist/esm/interfaces/IHistory.js.map +0 -1
  108. package/dist/esm/interfaces/IHookContext.js +0 -2
  109. package/dist/esm/interfaces/IHookContext.js.map +0 -1
  110. package/dist/esm/interfaces/IPluginOptions.js +0 -2
  111. package/dist/esm/interfaces/IPluginOptions.js.map +0 -1
  112. package/dist/esm/models/History.js +0 -44
  113. package/dist/esm/models/History.js.map +0 -1
  114. package/dist/esm/package.json +0 -1
  115. package/dist/esm/patch.js +0 -129
  116. package/dist/esm/patch.js.map +0 -1
  117. package/dist/esm/plugin.js +0 -45
  118. package/dist/esm/plugin.js.map +0 -1
  119. package/dist/esm/types/em.d.ts +0 -6
  120. package/dist/esm/types/em.d.ts.map +0 -1
  121. package/dist/esm/types/helpers.d.ts +0 -6
  122. package/dist/esm/types/helpers.d.ts.map +0 -1
  123. package/dist/esm/types/hooks/delete-hooks.d.ts +0 -4
  124. package/dist/esm/types/hooks/delete-hooks.d.ts.map +0 -1
  125. package/dist/esm/types/hooks/save-hooks.d.ts +0 -4
  126. package/dist/esm/types/hooks/save-hooks.d.ts.map +0 -1
  127. package/dist/esm/types/hooks/update-hooks.d.ts +0 -9
  128. package/dist/esm/types/hooks/update-hooks.d.ts.map +0 -1
  129. package/dist/esm/types/interfaces/IContext.d.ts +0 -13
  130. package/dist/esm/types/interfaces/IContext.d.ts.map +0 -1
  131. package/dist/esm/types/interfaces/IEvent.d.ts +0 -9
  132. package/dist/esm/types/interfaces/IEvent.d.ts.map +0 -1
  133. package/dist/esm/types/interfaces/IHistory.d.ts +0 -16
  134. package/dist/esm/types/interfaces/IHistory.d.ts.map +0 -1
  135. package/dist/esm/types/interfaces/IHookContext.d.ts +0 -8
  136. package/dist/esm/types/interfaces/IHookContext.d.ts.map +0 -1
  137. package/dist/esm/types/interfaces/IPluginOptions.d.ts +0 -18
  138. package/dist/esm/types/interfaces/IPluginOptions.d.ts.map +0 -1
  139. package/dist/esm/types/models/History.d.ts +0 -17
  140. package/dist/esm/types/models/History.d.ts.map +0 -1
  141. package/dist/esm/types/patch.d.ts +0 -18
  142. package/dist/esm/types/patch.d.ts.map +0 -1
  143. package/dist/esm/types/plugin.d.ts +0 -23
  144. package/dist/esm/types/plugin.d.ts.map +0 -1
  145. package/dist/esm/types/version.d.ts +0 -4
  146. package/dist/esm/types/version.d.ts.map +0 -1
  147. package/dist/esm/version.js +0 -9
  148. package/dist/esm/version.js.map +0 -1
  149. package/src/interfaces/IContext.ts +0 -14
  150. package/src/interfaces/IEvent.ts +0 -10
  151. package/src/interfaces/IHistory.ts +0 -17
  152. package/src/interfaces/IHookContext.ts +0 -6
  153. package/src/interfaces/IPluginOptions.ts +0 -20
  154. package/tests/interfaces/IDescription.ts +0 -5
  155. package/tests/interfaces/IProduct.ts +0 -14
  156. package/tests/interfaces/IUser.ts +0 -8
package/dist/index.mjs ADDED
@@ -0,0 +1,391 @@
1
+ import _ from 'lodash';
2
+ import EventEmitter from 'node:events';
3
+ import ms from 'ms';
4
+ import mongoose, { Schema, model } from 'mongoose';
5
+ import jsonpatch from 'fast-json-patch';
6
+ import omit from 'omit-deep';
7
+ import { satisfies } from 'semver';
8
+ import { assign } from 'power-assign';
9
+
10
+ class PatchEventEmitter extends EventEmitter {
11
+ }
12
+ const em = new PatchEventEmitter();
13
+
14
+ const HistorySchema = new Schema(
15
+ {
16
+ op: {
17
+ type: String,
18
+ required: true
19
+ },
20
+ modelName: {
21
+ type: String,
22
+ required: true
23
+ },
24
+ collectionName: {
25
+ type: String,
26
+ required: true
27
+ },
28
+ collectionId: {
29
+ type: Schema.Types.ObjectId,
30
+ required: true
31
+ },
32
+ doc: {
33
+ type: Object
34
+ },
35
+ patch: {
36
+ type: Array
37
+ },
38
+ user: {
39
+ type: Object
40
+ },
41
+ reason: {
42
+ type: String
43
+ },
44
+ metadata: {
45
+ type: Object
46
+ },
47
+ version: {
48
+ type: Number,
49
+ min: 0,
50
+ default: 0
51
+ }
52
+ },
53
+ { timestamps: true }
54
+ );
55
+ HistorySchema.index({ collectionId: 1, version: -1 });
56
+ HistorySchema.index({ op: 1, modelName: 1, collectionName: 1, collectionId: 1, reason: 1, version: 1 });
57
+ const HistoryModel = model("History", HistorySchema, "history");
58
+
59
+ const isHookIgnored = (options) => {
60
+ return options.ignoreHook === true || options.ignoreEvent === true && options.ignorePatchHistory === true;
61
+ };
62
+ const toObjectOptions = {
63
+ depopulate: true,
64
+ virtuals: false
65
+ };
66
+ const setPatchHistoryTTL = async (ttl) => {
67
+ const name = "createdAt_1_TTL";
68
+ try {
69
+ const indexes = await HistoryModel.collection.indexes();
70
+ const existingIndex = indexes?.find((index) => index.name === name);
71
+ if (!ttl && existingIndex) {
72
+ await HistoryModel.collection.dropIndex(name);
73
+ return;
74
+ }
75
+ const milliseconds = typeof ttl === "string" ? ms(ttl) : ttl;
76
+ if (milliseconds < 1e3 && existingIndex) {
77
+ await HistoryModel.collection.dropIndex(name);
78
+ return;
79
+ }
80
+ const expireAfterSeconds = milliseconds / 1e3;
81
+ if (existingIndex && existingIndex.expireAfterSeconds === expireAfterSeconds) {
82
+ return;
83
+ }
84
+ if (existingIndex) {
85
+ await HistoryModel.collection.dropIndex(name);
86
+ }
87
+ await HistoryModel.collection.createIndex({ createdAt: 1 }, { expireAfterSeconds, name });
88
+ } catch (err) {
89
+ console.error("Couldn't create or update index for history collection", err);
90
+ }
91
+ };
92
+
93
+ function isPatchHistoryEnabled(opts, context) {
94
+ return !opts.patchHistoryDisabled && !context.ignorePatchHistory;
95
+ }
96
+ function getJsonOmit(opts, doc) {
97
+ const object = JSON.parse(JSON.stringify(doc));
98
+ if (opts.omit) {
99
+ return omit(object, opts.omit);
100
+ }
101
+ return object;
102
+ }
103
+ function getObjectOmit(opts, doc) {
104
+ if (opts.omit) {
105
+ return omit(_.isFunction(doc?.toObject) ? doc.toObject() : doc, opts.omit);
106
+ }
107
+ return doc;
108
+ }
109
+ async function getUser(opts) {
110
+ if (_.isFunction(opts.getUser)) {
111
+ return await opts.getUser();
112
+ }
113
+ return void 0;
114
+ }
115
+ async function getReason(opts) {
116
+ if (_.isFunction(opts.getReason)) {
117
+ return await opts.getReason();
118
+ }
119
+ return void 0;
120
+ }
121
+ async function getMetadata(opts) {
122
+ if (_.isFunction(opts.getMetadata)) {
123
+ return await opts.getMetadata();
124
+ }
125
+ return void 0;
126
+ }
127
+ function getValue(item) {
128
+ return item.status === "fulfilled" ? item.value : void 0;
129
+ }
130
+ async function getData(opts) {
131
+ return Promise.allSettled([getUser(opts), getReason(opts), getMetadata(opts)]).then(([user, reason, metadata]) => {
132
+ return [getValue(user), getValue(reason), getValue(metadata)];
133
+ });
134
+ }
135
+ function emitEvent(context, event, data) {
136
+ if (event && !context.ignoreEvent) {
137
+ em.emit(event, data);
138
+ }
139
+ }
140
+ async function bulkPatch(opts, context, eventKey, docsKey) {
141
+ const history = isPatchHistoryEnabled(opts, context);
142
+ const event = opts[eventKey];
143
+ const docs = context[docsKey];
144
+ const key = eventKey === "eventCreated" ? "doc" : "oldDoc";
145
+ if (_.isEmpty(docs) || !event && !history) return;
146
+ const [user, reason, metadata] = await getData(opts);
147
+ const chunks = _.chunk(docs, 1e3);
148
+ for await (const chunk of chunks) {
149
+ const bulk = [];
150
+ for (const doc of chunk) {
151
+ emitEvent(context, event, { [key]: doc });
152
+ if (history) {
153
+ bulk.push({
154
+ insertOne: {
155
+ document: {
156
+ op: context.op,
157
+ modelName: context.modelName,
158
+ collectionName: context.collectionName,
159
+ collectionId: doc._id,
160
+ doc: getObjectOmit(opts, doc),
161
+ user,
162
+ reason,
163
+ metadata,
164
+ version: 0
165
+ }
166
+ }
167
+ });
168
+ }
169
+ }
170
+ if (history && !_.isEmpty(bulk)) {
171
+ await HistoryModel.bulkWrite(bulk, { ordered: false }).catch((error) => {
172
+ console.error(error.message);
173
+ });
174
+ }
175
+ }
176
+ }
177
+ async function createPatch(opts, context) {
178
+ await bulkPatch(opts, context, "eventCreated", "createdDocs");
179
+ }
180
+ async function updatePatch(opts, context, current, original) {
181
+ const history = isPatchHistoryEnabled(opts, context);
182
+ const currentObject = getJsonOmit(opts, current);
183
+ const originalObject = getJsonOmit(opts, original);
184
+ if (_.isEmpty(originalObject) || _.isEmpty(currentObject)) return;
185
+ const patch = jsonpatch.compare(originalObject, currentObject, true);
186
+ if (_.isEmpty(patch)) return;
187
+ emitEvent(context, opts.eventUpdated, { oldDoc: original, doc: current, patch });
188
+ if (history) {
189
+ let version = 0;
190
+ const lastHistory = await HistoryModel.findOne({ collectionId: original._id }).sort("-version").exec();
191
+ if (lastHistory && lastHistory.version >= 0) {
192
+ version = lastHistory.version + 1;
193
+ }
194
+ const [user, reason, metadata] = await getData(opts);
195
+ await HistoryModel.create({
196
+ op: context.op,
197
+ modelName: context.modelName,
198
+ collectionName: context.collectionName,
199
+ collectionId: original._id,
200
+ patch,
201
+ user,
202
+ reason,
203
+ metadata,
204
+ version
205
+ });
206
+ }
207
+ }
208
+ async function deletePatch(opts, context) {
209
+ await bulkPatch(opts, context, "eventDeleted", "deletedDocs");
210
+ }
211
+
212
+ const isMongooseLessThan8 = satisfies(mongoose.version, "<8");
213
+ const isMongooseLessThan7 = satisfies(mongoose.version, "<7");
214
+ const isMongoose6 = satisfies(mongoose.version, "6");
215
+ if (isMongoose6) {
216
+ mongoose.set("strictQuery", false);
217
+ }
218
+
219
+ const deleteMethods = ["remove", "findOneAndDelete", "findOneAndRemove", "findByIdAndDelete", "findByIdAndRemove", "deleteOne", "deleteMany"];
220
+ const deleteHooksInitialize = (schema, opts) => {
221
+ schema.pre(deleteMethods, { document: false, query: true }, async function() {
222
+ const options = this.getOptions();
223
+ if (isHookIgnored(options)) return;
224
+ const model = this.model;
225
+ const filter = this.getFilter();
226
+ this._context = {
227
+ op: this.op,
228
+ modelName: opts.modelName ?? this.model.modelName,
229
+ collectionName: opts.collectionName ?? this.model.collection.collectionName,
230
+ ignoreEvent: options.ignoreEvent,
231
+ ignorePatchHistory: options.ignorePatchHistory
232
+ };
233
+ if (["remove", "deleteMany"].includes(this._context.op) && !options.single) {
234
+ const docs = await model.find(filter).lean().exec();
235
+ if (!_.isEmpty(docs)) {
236
+ this._context.deletedDocs = docs;
237
+ }
238
+ } else {
239
+ const doc = await model.findOne(filter).lean().exec();
240
+ if (!_.isEmpty(doc)) {
241
+ this._context.deletedDocs = [doc];
242
+ }
243
+ }
244
+ if (opts.preDelete && _.isArray(this._context.deletedDocs) && !_.isEmpty(this._context.deletedDocs)) {
245
+ await opts.preDelete(this._context.deletedDocs);
246
+ }
247
+ });
248
+ schema.post(deleteMethods, { document: false, query: true }, async function() {
249
+ const options = this.getOptions();
250
+ if (isHookIgnored(options)) return;
251
+ await deletePatch(opts, this._context);
252
+ });
253
+ };
254
+
255
+ const saveHooksInitialize = (schema, opts) => {
256
+ schema.pre("save", async function() {
257
+ if (this.constructor.name !== "model") return;
258
+ const current = this.toObject(toObjectOptions);
259
+ const model = this.constructor;
260
+ const context = {
261
+ op: this.isNew ? "create" : "update",
262
+ modelName: opts.modelName ?? model.modelName,
263
+ collectionName: opts.collectionName ?? model.collection.collectionName,
264
+ createdDocs: [current]
265
+ };
266
+ if (this.isNew) {
267
+ await createPatch(opts, context);
268
+ } else {
269
+ const original = await model.findById(current._id).lean().exec();
270
+ if (original) {
271
+ await updatePatch(opts, context, current, original);
272
+ }
273
+ }
274
+ });
275
+ };
276
+
277
+ const updateMethods = ["update", "updateOne", "replaceOne", "updateMany", "findOneAndUpdate", "findOneAndReplace", "findByIdAndUpdate"];
278
+ const assignUpdate = (document, update, commands) => {
279
+ let updated = assign(document.toObject(toObjectOptions), update);
280
+ _.forEach(commands, (command) => {
281
+ try {
282
+ updated = assign(updated, command);
283
+ } catch {
284
+ }
285
+ });
286
+ const doc = document.set(updated).toObject(toObjectOptions);
287
+ if (update.createdAt) doc.createdAt = update.createdAt;
288
+ return doc;
289
+ };
290
+ const splitUpdateAndCommands = (updateQuery) => {
291
+ let update = {};
292
+ const commands = [];
293
+ if (!_.isEmpty(updateQuery) && !_.isArray(updateQuery) && _.isObjectLike(updateQuery)) {
294
+ update = _.cloneDeep(updateQuery);
295
+ const keysWithDollarSign = _.keys(update).filter((key) => key.startsWith("$"));
296
+ if (!_.isEmpty(keysWithDollarSign)) {
297
+ _.forEach(keysWithDollarSign, (key) => {
298
+ commands.push({ [key]: update[key] });
299
+ delete update[key];
300
+ });
301
+ }
302
+ }
303
+ return { update, commands };
304
+ };
305
+ const updateHooksInitialize = (schema, opts) => {
306
+ schema.pre(updateMethods, async function() {
307
+ const options = this.getOptions();
308
+ if (isHookIgnored(options)) return;
309
+ const model = this.model;
310
+ const filter = this.getFilter();
311
+ const count = await this.model.countDocuments(filter).exec();
312
+ this._context = {
313
+ op: this.op,
314
+ modelName: opts.modelName ?? this.model.modelName,
315
+ collectionName: opts.collectionName ?? this.model.collection.collectionName,
316
+ isNew: Boolean(options.upsert) && count === 0,
317
+ ignoreEvent: options.ignoreEvent,
318
+ ignorePatchHistory: options.ignorePatchHistory
319
+ };
320
+ const updateQuery = this.getUpdate();
321
+ const { update, commands } = splitUpdateAndCommands(updateQuery);
322
+ const cursor = model.find(filter).cursor();
323
+ await cursor.eachAsync(async (doc) => {
324
+ const origDoc = doc.toObject(toObjectOptions);
325
+ await updatePatch(opts, this._context, assignUpdate(doc, update, commands), origDoc);
326
+ });
327
+ });
328
+ schema.post(updateMethods, async function() {
329
+ const options = this.getOptions();
330
+ if (isHookIgnored(options)) return;
331
+ if (!this._context.isNew) return;
332
+ const model = this.model;
333
+ const updateQuery = this.getUpdate();
334
+ const { update, commands } = splitUpdateAndCommands(updateQuery);
335
+ let current = null;
336
+ const filter = this.getFilter();
337
+ const combined = assignUpdate(model.hydrate({}), update, commands);
338
+ if (!_.isEmpty(update) && !current) {
339
+ current = await model.findOne(update).sort("desc").lean().exec();
340
+ }
341
+ if (!_.isEmpty(combined) && !current) {
342
+ current = await model.findOne(combined).sort("desc").lean().exec();
343
+ }
344
+ if (!_.isEmpty(filter) && !current) {
345
+ console.log("filter", filter);
346
+ current = await model.findOne(filter).sort("desc").lean().exec();
347
+ }
348
+ if (current) {
349
+ this._context.createdDocs = [current];
350
+ await createPatch(opts, this._context);
351
+ }
352
+ });
353
+ };
354
+
355
+ const remove = isMongooseLessThan7 ? "remove" : "deleteOne";
356
+ const patchEventEmitter = em;
357
+ const patchHistoryPlugin = function plugin(schema, opts) {
358
+ saveHooksInitialize(schema, opts);
359
+ updateHooksInitialize(schema, opts);
360
+ deleteHooksInitialize(schema, opts);
361
+ schema.post("insertMany", async function(docs) {
362
+ const context = {
363
+ op: "create",
364
+ modelName: opts.modelName ?? this.modelName,
365
+ collectionName: opts.collectionName ?? this.collection.collectionName,
366
+ createdDocs: docs
367
+ };
368
+ await createPatch(opts, context);
369
+ });
370
+ if (isMongooseLessThan8) {
371
+ schema.pre(remove, { document: true, query: false }, async function() {
372
+ const original = this.toObject(toObjectOptions);
373
+ if (opts.preDelete && !_.isEmpty(original)) {
374
+ await opts.preDelete([original]);
375
+ }
376
+ });
377
+ schema.post(remove, { document: true, query: false }, async function() {
378
+ const original = this.toObject(toObjectOptions);
379
+ const model = this.constructor;
380
+ const context = {
381
+ op: "delete",
382
+ modelName: opts.modelName ?? model.modelName,
383
+ collectionName: opts.collectionName ?? model.collection.collectionName,
384
+ deletedDocs: [original]
385
+ };
386
+ await deletePatch(opts, context);
387
+ });
388
+ }
389
+ };
390
+
391
+ export { patchEventEmitter, patchHistoryPlugin, setPatchHistoryTTL };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "ts-patch-mongoose",
3
- "version": "2.7.1",
3
+ "version": "2.8.1",
4
4
  "description": "Patch history & events for mongoose models",
5
- "author": "Alex Eagle",
5
+ "author": "ilovepixelart",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
@@ -25,7 +25,6 @@
25
25
  "nosql",
26
26
  "ts",
27
27
  "typescript",
28
- "swc",
29
28
  "patch",
30
29
  "history",
31
30
  "event",
@@ -43,60 +42,65 @@
43
42
  "dist",
44
43
  "src",
45
44
  "tests",
46
- ".swcrc",
47
45
  "tsconfig.json",
48
46
  "vite.config.mts",
49
47
  "biome.json"
50
48
  ],
49
+ "type": "module",
51
50
  "exports": {
52
- ".": {
53
- "import": {
54
- "types": "./dist/esm/types/plugin.d.ts",
55
- "default": "./dist/esm/plugin.js"
56
- },
57
- "require": {
58
- "types": "./dist/cjs/types/plugin.d.ts",
59
- "default": "./dist/cjs/plugin.js"
60
- }
51
+ "require": {
52
+ "types": "./dist/index.d.cts",
53
+ "default": "./dist/index.cjs"
54
+ },
55
+ "import": {
56
+ "types": "./dist/index.d.mts",
57
+ "default": "./dist/index.mjs"
61
58
  }
62
59
  },
63
- "main": "dist/cjs/plugin.js",
64
- "module": "dist/esm/plugin.js",
65
- "types": "dist/cjs/types/plugin.d.ts",
60
+ "main": "./dist/index.cjs",
61
+ "module": "./dist/index.mjs",
62
+ "types": "./dist/index.d.cts",
66
63
  "scripts": {
64
+ "prepare": "simple-git-hooks",
67
65
  "biome": "npx @biomejs/biome check",
68
66
  "biome:fix": "npx @biomejs/biome check --write .",
69
67
  "test": "vitest run --coverage",
70
68
  "test:open": "vitest run --coverage && open-cli coverage/lcov-report/index.html",
71
- "clean": "rm -rf ./dist",
72
- "build": "npm run clean && npm run build:cjs && npm run build:esm",
73
- "build:cjs": "tsc -p config/tsconfig.cjs.json",
74
- "build:esm": "tsc -p config/tsconfig.esm.json && bash/esm.sh",
75
- "release": "npm install && npm run biome && npm run build && np"
69
+ "type:check": "tsc --noEmit",
70
+ "build": "pkgroll --clean-dist",
71
+ "release": "npm install && npm run biome && npm run type:check && npm run build && np"
76
72
  },
77
73
  "dependencies": {
74
+ "@types/lodash": "4.17.16",
75
+ "@types/ms": "2.1.0",
76
+ "@types/semver": "7.5.8",
78
77
  "fast-json-patch": "3.1.1",
79
78
  "lodash": "4.17.21",
80
79
  "ms": "2.1.3",
81
80
  "omit-deep": "0.3.0",
82
81
  "power-assign": "0.2.10",
83
- "semver": "7.6.3"
82
+ "semver": "7.7.1"
84
83
  },
85
84
  "devDependencies": {
86
85
  "@biomejs/biome": "1.9.4",
87
- "@types/lodash": "4.17.14",
88
- "@types/ms": "2.1.0",
89
- "@types/node": "22.10.7",
90
- "@types/semver": "7.5.8",
91
- "@vitest/coverage-v8": "3.0.2",
92
- "merge": "2.1.1",
93
- "mongodb-memory-server": "10.1.3",
94
- "mongoose": "8.9.5",
86
+ "@types/node": "22.13.8",
87
+ "@vitest/coverage-v8": "3.0.7",
88
+ "mongodb-memory-server": "10.1.4",
89
+ "mongoose": "8.11.0",
95
90
  "open-cli": "8.0.0",
96
- "typescript": "5.7.3",
97
- "vitest": "3.0.2"
91
+ "pkgroll": "2.11.2",
92
+ "simple-git-hooks": "2.11.1",
93
+ "typescript": "5.8.2",
94
+ "vitest": "3.0.7"
98
95
  },
99
96
  "peerDependencies": {
100
97
  "mongoose": ">=6.6.0 < 9"
98
+ },
99
+ "simple-git-hooks": {
100
+ "pre-commit": "npm run type:check",
101
+ "pre-push": "npm run biome:fix"
102
+ },
103
+ "overrides": {
104
+ "esbuild": "0.25.0"
101
105
  }
102
106
  }
package/src/helpers.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import ms from 'ms'
2
2
 
3
- import History from './models/History'
3
+ import { HistoryModel } from './model'
4
4
 
5
5
  import type { QueryOptions, ToObjectOptions } from 'mongoose'
6
6
 
@@ -16,12 +16,12 @@ export const toObjectOptions: ToObjectOptions = {
16
16
  export const setPatchHistoryTTL = async (ttl: number | ms.StringValue): Promise<void> => {
17
17
  const name = 'createdAt_1_TTL' // To avoid collision with user defined index / manually created index
18
18
  try {
19
- const indexes = await History.collection.indexes()
19
+ const indexes = await HistoryModel.collection.indexes()
20
20
  const existingIndex = indexes?.find((index) => index.name === name)
21
21
 
22
22
  // Drop the index if historyTTL is not set and index exists
23
23
  if (!ttl && existingIndex) {
24
- await History.collection.dropIndex(name)
24
+ await HistoryModel.collection.dropIndex(name)
25
25
  return
26
26
  }
27
27
 
@@ -29,7 +29,7 @@ export const setPatchHistoryTTL = async (ttl: number | ms.StringValue): Promise<
29
29
 
30
30
  // Drop the index if historyTTL is less than 1 second and index exists
31
31
  if (milliseconds < 1000 && existingIndex) {
32
- await History.collection.dropIndex(name)
32
+ await HistoryModel.collection.dropIndex(name)
33
33
  return
34
34
  }
35
35
 
@@ -42,11 +42,11 @@ export const setPatchHistoryTTL = async (ttl: number | ms.StringValue): Promise<
42
42
 
43
43
  if (existingIndex) {
44
44
  // Drop the existing index if it exists and TTL is different
45
- await History.collection.dropIndex(name)
45
+ await HistoryModel.collection.dropIndex(name)
46
46
  }
47
47
 
48
48
  // Create a new index with the correct TTL if it doesn't exist or if the TTL is different
49
- await History.collection.createIndex({ createdAt: 1 }, { expireAfterSeconds, name })
49
+ await HistoryModel.collection.createIndex({ createdAt: 1 }, { expireAfterSeconds, name })
50
50
  } catch (err) {
51
51
  console.error("Couldn't create or update index for history collection", err)
52
52
  }
@@ -1,16 +1,15 @@
1
- import { isArray, isEmpty } from 'lodash'
1
+ import _ from 'lodash'
2
2
 
3
3
  import { isHookIgnored } from '../helpers'
4
4
  import { deletePatch } from '../patch'
5
5
 
6
6
  import type { HydratedDocument, Model, MongooseQueryMiddleware, Schema } from 'mongoose'
7
- import type IHookContext from '../interfaces/IHookContext'
8
- import type IPluginOptions from '../interfaces/IPluginOptions'
7
+ import type { HookContext, PluginOptions } from '../types'
9
8
 
10
9
  const deleteMethods = ['remove', 'findOneAndDelete', 'findOneAndRemove', 'findByIdAndDelete', 'findByIdAndRemove', 'deleteOne', 'deleteMany']
11
10
 
12
- export const deleteHooksInitialize = <T>(schema: Schema<T>, opts: IPluginOptions<T>): void => {
13
- schema.pre(deleteMethods as MongooseQueryMiddleware[], { document: false, query: true }, async function (this: IHookContext<T>) {
11
+ export const deleteHooksInitialize = <T>(schema: Schema<T>, opts: PluginOptions<T>): void => {
12
+ schema.pre(deleteMethods as MongooseQueryMiddleware[], { document: false, query: true }, async function (this: HookContext<T>) {
14
13
  const options = this.getOptions()
15
14
  if (isHookIgnored(options)) return
16
15
 
@@ -27,22 +26,22 @@ export const deleteHooksInitialize = <T>(schema: Schema<T>, opts: IPluginOptions
27
26
 
28
27
  if (['remove', 'deleteMany'].includes(this._context.op) && !options.single) {
29
28
  const docs = await model.find<T>(filter).lean().exec()
30
- if (!isEmpty(docs)) {
29
+ if (!_.isEmpty(docs)) {
31
30
  this._context.deletedDocs = docs as HydratedDocument<T>[]
32
31
  }
33
32
  } else {
34
33
  const doc = await model.findOne<T>(filter).lean().exec()
35
- if (!isEmpty(doc)) {
34
+ if (!_.isEmpty(doc)) {
36
35
  this._context.deletedDocs = [doc] as HydratedDocument<T>[]
37
36
  }
38
37
  }
39
38
 
40
- if (opts.preDelete && isArray(this._context.deletedDocs) && !isEmpty(this._context.deletedDocs)) {
39
+ if (opts.preDelete && _.isArray(this._context.deletedDocs) && !_.isEmpty(this._context.deletedDocs)) {
41
40
  await opts.preDelete(this._context.deletedDocs)
42
41
  }
43
42
  })
44
43
 
45
- schema.post(deleteMethods as MongooseQueryMiddleware[], { document: false, query: true }, async function (this: IHookContext<T>) {
44
+ schema.post(deleteMethods as MongooseQueryMiddleware[], { document: false, query: true }, async function (this: HookContext<T>) {
46
45
  const options = this.getOptions()
47
46
  if (isHookIgnored(options)) return
48
47
 
@@ -2,17 +2,16 @@ import { toObjectOptions } from '../helpers'
2
2
  import { createPatch, updatePatch } from '../patch'
3
3
 
4
4
  import type { HydratedDocument, Model, Schema } from 'mongoose'
5
- import type IContext from '../interfaces/IContext'
6
- import type IPluginOptions from '../interfaces/IPluginOptions'
5
+ import type { PatchContext, PluginOptions } from '../types'
7
6
 
8
- export const saveHooksInitialize = <T>(schema: Schema<T>, opts: IPluginOptions<T>): void => {
7
+ export const saveHooksInitialize = <T>(schema: Schema<T>, opts: PluginOptions<T>): void => {
9
8
  schema.pre('save', async function () {
10
9
  if (this.constructor.name !== 'model') return
11
10
 
12
11
  const current = this.toObject(toObjectOptions) as HydratedDocument<T>
13
12
  const model = this.constructor as Model<T>
14
13
 
15
- const context: IContext<T> = {
14
+ const context: PatchContext<T> = {
16
15
  op: this.isNew ? 'create' : 'update',
17
16
  modelName: opts.modelName ?? model.modelName,
18
17
  collectionName: opts.collectionName ?? model.collection.collectionName,