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.
- package/LICENSE +1 -1
- package/README.md +8 -8
- package/biome.json +1 -1
- package/dist/index.cjs +395 -0
- package/dist/index.d.cts +75 -0
- package/dist/index.d.mts +75 -0
- package/dist/index.mjs +391 -0
- package/package.json +36 -32
- package/src/helpers.ts +6 -6
- package/src/hooks/delete-hooks.ts +8 -9
- package/src/hooks/save-hooks.ts +3 -4
- package/src/hooks/update-hooks.ts +14 -15
- package/src/{plugin.ts → index.ts} +31 -11
- package/src/{models/History.ts → model.ts} +3 -5
- package/src/patch.ts +27 -31
- package/src/types.ts +52 -0
- package/src/version.ts +2 -0
- package/tests/em.test.ts +1 -1
- package/tests/helpers.test.ts +6 -7
- package/tests/patch.test.ts +22 -27
- package/tests/plugin-event-created.test.ts +50 -51
- package/tests/plugin-event-deleted.test.ts +76 -77
- package/tests/plugin-event-updated.test.ts +65 -67
- package/tests/plugin-global.test.ts +25 -26
- package/tests/plugin-omit-all.test.ts +36 -37
- package/tests/plugin-patch-history-disabled.test.ts +36 -37
- package/tests/plugin-pre-delete.test.ts +17 -18
- package/tests/plugin-pre-save.test.ts +7 -8
- package/tests/plugin.test.ts +37 -39
- package/tests/schemas/{DescriptionSchema.ts → Description.ts} +4 -4
- package/tests/schemas/{ProductSchema.ts → Product.ts} +12 -5
- package/tests/schemas/{UserSchema.ts → User.ts} +7 -4
- package/tsconfig.json +5 -5
- package/vite.config.mts +1 -0
- package/.swcrc +0 -17
- package/dist/cjs/em.js +0 -9
- package/dist/cjs/em.js.map +0 -1
- package/dist/cjs/helpers.js +0 -43
- package/dist/cjs/helpers.js.map +0 -1
- package/dist/cjs/hooks/delete-hooks.js +0 -46
- package/dist/cjs/hooks/delete-hooks.js.map +0 -1
- package/dist/cjs/hooks/save-hooks.js +0 -30
- package/dist/cjs/hooks/save-hooks.js.map +0 -1
- package/dist/cjs/hooks/update-hooks.js +0 -93
- package/dist/cjs/hooks/update-hooks.js.map +0 -1
- package/dist/cjs/interfaces/IContext.js +0 -3
- package/dist/cjs/interfaces/IContext.js.map +0 -1
- package/dist/cjs/interfaces/IEvent.js +0 -3
- package/dist/cjs/interfaces/IEvent.js.map +0 -1
- package/dist/cjs/interfaces/IHistory.js +0 -3
- package/dist/cjs/interfaces/IHistory.js.map +0 -1
- package/dist/cjs/interfaces/IHookContext.js +0 -3
- package/dist/cjs/interfaces/IHookContext.js.map +0 -1
- package/dist/cjs/interfaces/IPluginOptions.js +0 -3
- package/dist/cjs/interfaces/IPluginOptions.js.map +0 -1
- package/dist/cjs/models/History.js +0 -46
- package/dist/cjs/models/History.js.map +0 -1
- package/dist/cjs/patch.js +0 -144
- package/dist/cjs/patch.js.map +0 -1
- package/dist/cjs/plugin.js +0 -51
- package/dist/cjs/plugin.js.map +0 -1
- package/dist/cjs/types/em.d.ts +0 -6
- package/dist/cjs/types/em.d.ts.map +0 -1
- package/dist/cjs/types/helpers.d.ts +0 -6
- package/dist/cjs/types/helpers.d.ts.map +0 -1
- package/dist/cjs/types/hooks/delete-hooks.d.ts +0 -4
- package/dist/cjs/types/hooks/delete-hooks.d.ts.map +0 -1
- package/dist/cjs/types/hooks/save-hooks.d.ts +0 -4
- package/dist/cjs/types/hooks/save-hooks.d.ts.map +0 -1
- package/dist/cjs/types/hooks/update-hooks.d.ts +0 -9
- package/dist/cjs/types/hooks/update-hooks.d.ts.map +0 -1
- package/dist/cjs/types/interfaces/IContext.d.ts +0 -13
- package/dist/cjs/types/interfaces/IContext.d.ts.map +0 -1
- package/dist/cjs/types/interfaces/IEvent.d.ts +0 -9
- package/dist/cjs/types/interfaces/IEvent.d.ts.map +0 -1
- package/dist/cjs/types/interfaces/IHistory.d.ts +0 -16
- package/dist/cjs/types/interfaces/IHistory.d.ts.map +0 -1
- package/dist/cjs/types/interfaces/IHookContext.d.ts +0 -8
- package/dist/cjs/types/interfaces/IHookContext.d.ts.map +0 -1
- package/dist/cjs/types/interfaces/IPluginOptions.d.ts +0 -18
- package/dist/cjs/types/interfaces/IPluginOptions.d.ts.map +0 -1
- package/dist/cjs/types/models/History.d.ts +0 -17
- package/dist/cjs/types/models/History.d.ts.map +0 -1
- package/dist/cjs/types/patch.d.ts +0 -18
- package/dist/cjs/types/patch.d.ts.map +0 -1
- package/dist/cjs/types/plugin.d.ts +0 -23
- package/dist/cjs/types/plugin.d.ts.map +0 -1
- package/dist/cjs/types/version.d.ts +0 -4
- package/dist/cjs/types/version.d.ts.map +0 -1
- package/dist/cjs/version.js +0 -13
- package/dist/cjs/version.js.map +0 -1
- package/dist/esm/em.js +0 -6
- package/dist/esm/em.js.map +0 -1
- package/dist/esm/helpers.js +0 -37
- package/dist/esm/helpers.js.map +0 -1
- package/dist/esm/hooks/delete-hooks.js +0 -42
- package/dist/esm/hooks/delete-hooks.js.map +0 -1
- package/dist/esm/hooks/save-hooks.js +0 -26
- package/dist/esm/hooks/save-hooks.js.map +0 -1
- package/dist/esm/hooks/update-hooks.js +0 -87
- package/dist/esm/hooks/update-hooks.js.map +0 -1
- package/dist/esm/interfaces/IContext.js +0 -2
- package/dist/esm/interfaces/IContext.js.map +0 -1
- package/dist/esm/interfaces/IEvent.js +0 -2
- package/dist/esm/interfaces/IEvent.js.map +0 -1
- package/dist/esm/interfaces/IHistory.js +0 -2
- package/dist/esm/interfaces/IHistory.js.map +0 -1
- package/dist/esm/interfaces/IHookContext.js +0 -2
- package/dist/esm/interfaces/IHookContext.js.map +0 -1
- package/dist/esm/interfaces/IPluginOptions.js +0 -2
- package/dist/esm/interfaces/IPluginOptions.js.map +0 -1
- package/dist/esm/models/History.js +0 -44
- package/dist/esm/models/History.js.map +0 -1
- package/dist/esm/package.json +0 -1
- package/dist/esm/patch.js +0 -129
- package/dist/esm/patch.js.map +0 -1
- package/dist/esm/plugin.js +0 -45
- package/dist/esm/plugin.js.map +0 -1
- package/dist/esm/types/em.d.ts +0 -6
- package/dist/esm/types/em.d.ts.map +0 -1
- package/dist/esm/types/helpers.d.ts +0 -6
- package/dist/esm/types/helpers.d.ts.map +0 -1
- package/dist/esm/types/hooks/delete-hooks.d.ts +0 -4
- package/dist/esm/types/hooks/delete-hooks.d.ts.map +0 -1
- package/dist/esm/types/hooks/save-hooks.d.ts +0 -4
- package/dist/esm/types/hooks/save-hooks.d.ts.map +0 -1
- package/dist/esm/types/hooks/update-hooks.d.ts +0 -9
- package/dist/esm/types/hooks/update-hooks.d.ts.map +0 -1
- package/dist/esm/types/interfaces/IContext.d.ts +0 -13
- package/dist/esm/types/interfaces/IContext.d.ts.map +0 -1
- package/dist/esm/types/interfaces/IEvent.d.ts +0 -9
- package/dist/esm/types/interfaces/IEvent.d.ts.map +0 -1
- package/dist/esm/types/interfaces/IHistory.d.ts +0 -16
- package/dist/esm/types/interfaces/IHistory.d.ts.map +0 -1
- package/dist/esm/types/interfaces/IHookContext.d.ts +0 -8
- package/dist/esm/types/interfaces/IHookContext.d.ts.map +0 -1
- package/dist/esm/types/interfaces/IPluginOptions.d.ts +0 -18
- package/dist/esm/types/interfaces/IPluginOptions.d.ts.map +0 -1
- package/dist/esm/types/models/History.d.ts +0 -17
- package/dist/esm/types/models/History.d.ts.map +0 -1
- package/dist/esm/types/patch.d.ts +0 -18
- package/dist/esm/types/patch.d.ts.map +0 -1
- package/dist/esm/types/plugin.d.ts +0 -23
- package/dist/esm/types/plugin.d.ts.map +0 -1
- package/dist/esm/types/version.d.ts +0 -4
- package/dist/esm/types/version.d.ts.map +0 -1
- package/dist/esm/version.js +0 -9
- package/dist/esm/version.js.map +0 -1
- package/src/interfaces/IContext.ts +0 -14
- package/src/interfaces/IEvent.ts +0 -10
- package/src/interfaces/IHistory.ts +0 -17
- package/src/interfaces/IHookContext.ts +0 -6
- package/src/interfaces/IPluginOptions.ts +0 -20
- package/tests/interfaces/IDescription.ts +0 -5
- package/tests/interfaces/IProduct.ts +0 -14
- 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.
|
|
3
|
+
"version": "2.8.1",
|
|
4
4
|
"description": "Patch history & events for mongoose models",
|
|
5
|
-
"author": "
|
|
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
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"
|
|
58
|
-
|
|
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
|
|
64
|
-
"module": "dist/
|
|
65
|
-
"types": "dist/
|
|
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
|
-
"
|
|
72
|
-
"build": "
|
|
73
|
-
"
|
|
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.
|
|
82
|
+
"semver": "7.7.1"
|
|
84
83
|
},
|
|
85
84
|
"devDependencies": {
|
|
86
85
|
"@biomejs/biome": "1.9.4",
|
|
87
|
-
"@types/
|
|
88
|
-
"@
|
|
89
|
-
"
|
|
90
|
-
"
|
|
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
|
-
"
|
|
97
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
13
|
-
schema.pre(deleteMethods as MongooseQueryMiddleware[], { document: false, query: true }, async function (this:
|
|
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:
|
|
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
|
|
package/src/hooks/save-hooks.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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:
|
|
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,
|