ts-patch-mongoose 1.0.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/.eslintignore +4 -0
- package/.eslintrc +91 -0
- package/.swcrc +21 -0
- package/LICENSE +21 -0
- package/README.md +66 -0
- package/dist/cjs/em.d.ts +7 -0
- package/dist/cjs/em.d.ts.map +1 -0
- package/dist/cjs/em.js +9 -0
- package/dist/cjs/em.js.map +1 -0
- package/dist/cjs/interfaces/IContext.d.ts +12 -0
- package/dist/cjs/interfaces/IContext.d.ts.map +1 -0
- package/dist/cjs/interfaces/IContext.js +3 -0
- package/dist/cjs/interfaces/IContext.js.map +1 -0
- package/dist/cjs/interfaces/IHistory.d.ts +13 -0
- package/dist/cjs/interfaces/IHistory.d.ts.map +1 -0
- package/dist/cjs/interfaces/IHistory.js +3 -0
- package/dist/cjs/interfaces/IHistory.js.map +1 -0
- package/dist/cjs/interfaces/IHookContext.d.ts +8 -0
- package/dist/cjs/interfaces/IHookContext.d.ts.map +1 -0
- package/dist/cjs/interfaces/IHookContext.js +3 -0
- package/dist/cjs/interfaces/IHookContext.js.map +1 -0
- package/dist/cjs/interfaces/IPluginOptions.d.ts +13 -0
- package/dist/cjs/interfaces/IPluginOptions.d.ts.map +1 -0
- package/dist/cjs/interfaces/IPluginOptions.js +3 -0
- package/dist/cjs/interfaces/IPluginOptions.js.map +1 -0
- package/dist/cjs/models/History.d.ts +29 -0
- package/dist/cjs/models/History.d.ts.map +1 -0
- package/dist/cjs/models/History.js +36 -0
- package/dist/cjs/models/History.js.map +1 -0
- package/dist/cjs/plugin.d.ts +45 -0
- package/dist/cjs/plugin.d.ts.map +1 -0
- package/dist/cjs/plugin.js +224 -0
- package/dist/cjs/plugin.js.map +1 -0
- package/dist/esm/em.d.ts +7 -0
- package/dist/esm/em.d.ts.map +1 -0
- package/dist/esm/em.js +6 -0
- package/dist/esm/em.js.map +1 -0
- package/dist/esm/interfaces/IContext.d.ts +12 -0
- package/dist/esm/interfaces/IContext.d.ts.map +1 -0
- package/dist/esm/interfaces/IContext.js +2 -0
- package/dist/esm/interfaces/IContext.js.map +1 -0
- package/dist/esm/interfaces/IHistory.d.ts +13 -0
- package/dist/esm/interfaces/IHistory.d.ts.map +1 -0
- package/dist/esm/interfaces/IHistory.js +2 -0
- package/dist/esm/interfaces/IHistory.js.map +1 -0
- package/dist/esm/interfaces/IHookContext.d.ts +8 -0
- package/dist/esm/interfaces/IHookContext.d.ts.map +1 -0
- package/dist/esm/interfaces/IHookContext.js +2 -0
- package/dist/esm/interfaces/IHookContext.js.map +1 -0
- package/dist/esm/interfaces/IPluginOptions.d.ts +13 -0
- package/dist/esm/interfaces/IPluginOptions.d.ts.map +1 -0
- package/dist/esm/interfaces/IPluginOptions.js +2 -0
- package/dist/esm/interfaces/IPluginOptions.js.map +1 -0
- package/dist/esm/models/History.d.ts +29 -0
- package/dist/esm/models/History.d.ts.map +1 -0
- package/dist/esm/models/History.js +34 -0
- package/dist/esm/models/History.js.map +1 -0
- package/dist/esm/plugin.d.ts +45 -0
- package/dist/esm/plugin.d.ts.map +1 -0
- package/dist/esm/plugin.js +219 -0
- package/dist/esm/plugin.js.map +1 -0
- package/dist/types/em.d.ts +7 -0
- package/dist/types/em.d.ts.map +1 -0
- package/dist/types/interfaces/IContext.d.ts +12 -0
- package/dist/types/interfaces/IContext.d.ts.map +1 -0
- package/dist/types/interfaces/IHistory.d.ts +13 -0
- package/dist/types/interfaces/IHistory.d.ts.map +1 -0
- package/dist/types/interfaces/IHookContext.d.ts +8 -0
- package/dist/types/interfaces/IHookContext.d.ts.map +1 -0
- package/dist/types/interfaces/IPluginOptions.d.ts +13 -0
- package/dist/types/interfaces/IPluginOptions.d.ts.map +1 -0
- package/dist/types/models/History.d.ts +29 -0
- package/dist/types/models/History.d.ts.map +1 -0
- package/dist/types/plugin.d.ts +45 -0
- package/dist/types/plugin.d.ts.map +1 -0
- package/jest-mongodb-config.ts +10 -0
- package/jest.config.ts +35 -0
- package/package.json +101 -0
- package/src/em.ts +6 -0
- package/src/interfaces/IContext.ts +13 -0
- package/src/interfaces/IHistory.ts +14 -0
- package/src/interfaces/IHookContext.ts +6 -0
- package/src/interfaces/IPluginOptions.ts +14 -0
- package/src/models/History.ts +39 -0
- package/src/modules/omit-deep.d.ts +3 -0
- package/src/modules/power-assign.d.ts +3 -0
- package/src/plugin.ts +267 -0
- package/tests/constants/events.ts +3 -0
- package/tests/em.test.ts +16 -0
- package/tests/interfaces/IUser.ts +8 -0
- package/tests/models/User.ts +29 -0
- package/tests/mongose.test.ts +28 -0
- package/tests/plugin.test.ts +243 -0
- package/tests/utils/filesystem.ts +13 -0
- package/tsconfig.json +44 -0
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import omit from 'omit-deep'
|
|
3
|
+
import jsonpatch from 'fast-json-patch'
|
|
4
|
+
import { assign } from 'power-assign'
|
|
5
|
+
|
|
6
|
+
import type { CallbackError, HydratedDocument, Model, MongooseError, Schema, Types } from 'mongoose'
|
|
7
|
+
|
|
8
|
+
import type IPluginOptions from './interfaces/IPluginOptions'
|
|
9
|
+
import type IContext from './interfaces/IContext'
|
|
10
|
+
import type IHookContext from './interfaces/IHookContext'
|
|
11
|
+
|
|
12
|
+
import em from './em'
|
|
13
|
+
import History from './models/History'
|
|
14
|
+
|
|
15
|
+
const options = {
|
|
16
|
+
document: false,
|
|
17
|
+
query: true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getObjects<T> (opts: IPluginOptions<T>, current: HydratedDocument<T>, original: HydratedDocument<T>): { currentObject: Partial<T>, originalObject: Partial<T> } {
|
|
21
|
+
let currentObject = JSON.parse(JSON.stringify(current)) as Partial<T>
|
|
22
|
+
let originalObject = JSON.parse(JSON.stringify(original)) as Partial<T>
|
|
23
|
+
|
|
24
|
+
if (opts.omit) {
|
|
25
|
+
currentObject = omit(currentObject, opts.omit)
|
|
26
|
+
originalObject = omit(originalObject, opts.omit)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { currentObject, originalObject }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function bulkPatch<T> (opts: IPluginOptions<T>, context: IContext<T>): Promise<void> {
|
|
33
|
+
const chunks = _.chunk(context.deletedDocs, 1000)
|
|
34
|
+
for await (const chunk of chunks) {
|
|
35
|
+
const bulk = []
|
|
36
|
+
for (const oldDoc of chunk) {
|
|
37
|
+
if (opts.eventDeleted) {
|
|
38
|
+
em.emit(opts.eventDeleted, { oldDoc })
|
|
39
|
+
}
|
|
40
|
+
if (!opts.patchHistoryDisabled) {
|
|
41
|
+
bulk.push({
|
|
42
|
+
insertOne: {
|
|
43
|
+
document: {
|
|
44
|
+
op: context.op,
|
|
45
|
+
modelName: context.modelName,
|
|
46
|
+
collectionName: context.collectionName,
|
|
47
|
+
collectionId: oldDoc._id as Types.ObjectId,
|
|
48
|
+
doc: oldDoc,
|
|
49
|
+
version: 0
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (opts.patchHistoryDisabled) continue
|
|
57
|
+
await History.bulkWrite(bulk, { ordered: false }).catch((err: MongooseError) => {
|
|
58
|
+
console.error(err)
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function updatePatch<T> (opts: IPluginOptions<T>, context: IContext<T>, current: HydratedDocument<T>, original: HydratedDocument<T>): Promise<void> {
|
|
64
|
+
const { currentObject, originalObject } = getObjects(opts, current, original)
|
|
65
|
+
|
|
66
|
+
if (_.isEmpty(originalObject) || _.isEmpty(currentObject)) return
|
|
67
|
+
|
|
68
|
+
const patch = jsonpatch.compare(originalObject, currentObject, true)
|
|
69
|
+
|
|
70
|
+
if (_.isEmpty(patch)) return
|
|
71
|
+
|
|
72
|
+
if (opts.eventUpdated) {
|
|
73
|
+
em.emit(opts.eventUpdated, { oldDoc: original, doc: current, patch })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (opts.patchHistoryDisabled) return
|
|
77
|
+
|
|
78
|
+
let version = 0
|
|
79
|
+
|
|
80
|
+
const lastHistory = await History.findOne({ collectionId: original._id as Types.ObjectId }).sort('-version').exec()
|
|
81
|
+
|
|
82
|
+
if (lastHistory && lastHistory.version >= 0) {
|
|
83
|
+
version = lastHistory.version + 1
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await History.create({
|
|
87
|
+
op: context.op,
|
|
88
|
+
modelName: context.modelName,
|
|
89
|
+
collectionName: context.collectionName,
|
|
90
|
+
collectionId: original._id as Types.ObjectId,
|
|
91
|
+
patch,
|
|
92
|
+
version
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function createPatch<T> (opts: IPluginOptions<T>, context: IContext<T>, current: HydratedDocument<T>): Promise<void> {
|
|
97
|
+
if (opts.patchHistoryDisabled) return
|
|
98
|
+
|
|
99
|
+
await History.create({
|
|
100
|
+
op: context.op,
|
|
101
|
+
modelName: context.modelName,
|
|
102
|
+
collectionName: context.collectionName,
|
|
103
|
+
collectionId: current._id as Types.ObjectId,
|
|
104
|
+
doc: current
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @description Patch patch event emitter
|
|
110
|
+
*/
|
|
111
|
+
export const patchEventEmitter = em
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @description Patch history plugin
|
|
115
|
+
* @param {Schema} schema
|
|
116
|
+
* @param {IPluginOptions} opts
|
|
117
|
+
* @returns {void}
|
|
118
|
+
*/
|
|
119
|
+
export const patchHistoryPlugin = function plugin<T> (schema: Schema<T>, opts: IPluginOptions<T>): void {
|
|
120
|
+
schema.pre('save', async function (next) {
|
|
121
|
+
const current = this.toObject({ depopulate: true }) as HydratedDocument<T>
|
|
122
|
+
const model = this.constructor as Model<T>
|
|
123
|
+
|
|
124
|
+
const context: IContext<T> = {
|
|
125
|
+
op: this.isNew ? 'create' : 'update',
|
|
126
|
+
modelName: opts.modelName ?? model.modelName,
|
|
127
|
+
collectionName: opts.collectionName ?? model.collection.collectionName
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
if (this.isNew) {
|
|
132
|
+
if (opts.eventCreated) {
|
|
133
|
+
em.emit(opts.eventCreated, { doc: current })
|
|
134
|
+
}
|
|
135
|
+
await createPatch(opts, context, current)
|
|
136
|
+
} else {
|
|
137
|
+
const original = await model.findById(current._id).exec()
|
|
138
|
+
if (original) {
|
|
139
|
+
await updatePatch(opts, context, current, original)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
next()
|
|
143
|
+
} catch (error) {
|
|
144
|
+
next(error as CallbackError)
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
schema.pre(['findOneAndUpdate', 'update', 'updateOne', 'updateMany'], async function (this: IHookContext<T>, next) {
|
|
149
|
+
const filter = this.getFilter()
|
|
150
|
+
const update = this.getUpdate() as Record<string, Partial<T>> | null
|
|
151
|
+
const options = this.getOptions()
|
|
152
|
+
|
|
153
|
+
const count = await this.model.count(filter).exec()
|
|
154
|
+
const commands: Record<string, Partial<T>>[] = []
|
|
155
|
+
|
|
156
|
+
const context: IContext<T> = {
|
|
157
|
+
op: this.op,
|
|
158
|
+
modelName: opts.modelName ?? this.model.modelName,
|
|
159
|
+
collectionName: opts.collectionName ?? this.model.collection.collectionName,
|
|
160
|
+
isNew: options.upsert && count === 0
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this._context = context
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const keys = _.keys(update).filter((key) => key.startsWith('$'))
|
|
167
|
+
if (update && !_.isEmpty(keys)) {
|
|
168
|
+
_.forEach(keys, (key) => {
|
|
169
|
+
commands.push({ [key]: update[key] })
|
|
170
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
171
|
+
delete update[key]
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
const cursor = this.model.find(filter).cursor()
|
|
175
|
+
await cursor.eachAsync(async (doc: HydratedDocument<T>) => {
|
|
176
|
+
let current = doc.toObject({ depopulate: true }) as HydratedDocument<T>
|
|
177
|
+
current = assign(current, update)
|
|
178
|
+
_.forEach(commands, (command) => {
|
|
179
|
+
try {
|
|
180
|
+
current = assign(current, command)
|
|
181
|
+
} catch (error) {
|
|
182
|
+
// we catch assign keys that are not implemented
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
await updatePatch(opts, context, current, doc.toObject({ depopulate: true }) as HydratedDocument<T>)
|
|
186
|
+
})
|
|
187
|
+
next()
|
|
188
|
+
} catch (error) {
|
|
189
|
+
next(error as CallbackError)
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
schema.post(['findOneAndUpdate', 'update', 'updateOne', 'updateMany'], async function (this: IHookContext<T>) {
|
|
194
|
+
const update = this.getUpdate()
|
|
195
|
+
|
|
196
|
+
if (update && this._context.isNew) {
|
|
197
|
+
const cursor = this.model.findOne(update).cursor()
|
|
198
|
+
await cursor.eachAsync(async (current: HydratedDocument<T>) => {
|
|
199
|
+
if (opts.eventCreated) {
|
|
200
|
+
em.emit(opts.eventCreated, { doc: current })
|
|
201
|
+
}
|
|
202
|
+
await createPatch(opts, this._context, current)
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
schema.pre('updateMany', options, async function (this: IHookContext<T>, next) {
|
|
208
|
+
const filter = this.getFilter()
|
|
209
|
+
const options = this.getOptions()
|
|
210
|
+
const ignore = options.__ignore as boolean
|
|
211
|
+
|
|
212
|
+
const context: IContext<T> = {
|
|
213
|
+
op: this.op,
|
|
214
|
+
modelName: opts.modelName ?? this.model.modelName,
|
|
215
|
+
collectionName: opts.collectionName ?? this.model.collection.collectionName,
|
|
216
|
+
isNew: options.upsert
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!ignore) {
|
|
220
|
+
const ids = await this.model.distinct<Types.ObjectId>('_id', filter).exec()
|
|
221
|
+
context.updatedIds = ids
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this._context = context
|
|
225
|
+
|
|
226
|
+
next()
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
schema.post('updateMany', options, async function (this: IHookContext<T>) {
|
|
230
|
+
if (this._context.updatedIds?.length) return
|
|
231
|
+
|
|
232
|
+
const cursor = this.model.find({ _id: { $in: this._context.updatedIds } }).cursor()
|
|
233
|
+
await cursor.eachAsync((current: HydratedDocument<T>) => {
|
|
234
|
+
if (opts.eventUpdated) {
|
|
235
|
+
em.emit(opts.eventUpdated, { doc: current })
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
schema.pre(['remove', 'findOneAndDelete', 'findOneAndRemove', 'deleteOne', 'deleteMany'], options, async function (this: IHookContext<T>, next) {
|
|
241
|
+
const filter = this.getFilter()
|
|
242
|
+
const options = this.getOptions()
|
|
243
|
+
const ignore = options.__ignore as boolean
|
|
244
|
+
|
|
245
|
+
const context: IContext<T> = {
|
|
246
|
+
op: this.op,
|
|
247
|
+
modelName: opts.modelName ?? this.model.modelName,
|
|
248
|
+
collectionName: opts.collectionName ?? this.model.collection.collectionName
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!ignore) {
|
|
252
|
+
context.deletedDocs = await this.model.find(filter).exec()
|
|
253
|
+
if (opts.preDeleteManyCallback) {
|
|
254
|
+
await opts.preDeleteManyCallback(context.deletedDocs)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
this._context = context
|
|
259
|
+
next()
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
schema.post(['remove', 'findOneAndDelete', 'findOneAndRemove', 'deleteOne', 'deleteMany'], options, async function (this: IHookContext<T>) {
|
|
263
|
+
if (_.isEmpty(this._context.deletedDocs)) return
|
|
264
|
+
|
|
265
|
+
await bulkPatch(opts, this._context)
|
|
266
|
+
})
|
|
267
|
+
}
|
package/tests/em.test.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import em from '../src/em'
|
|
2
|
+
|
|
3
|
+
describe('em', () => {
|
|
4
|
+
it('should subscribe and count', async () => {
|
|
5
|
+
let count = 0
|
|
6
|
+
const fn = () => {
|
|
7
|
+
count++
|
|
8
|
+
}
|
|
9
|
+
em.on('test', fn)
|
|
10
|
+
em.emit('test')
|
|
11
|
+
expect(count).toBe(1)
|
|
12
|
+
em.off('test', fn)
|
|
13
|
+
em.emit('test')
|
|
14
|
+
expect(count).toBe(1)
|
|
15
|
+
})
|
|
16
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Schema, model } from 'mongoose'
|
|
2
|
+
|
|
3
|
+
import type IUser from '../interfaces/IUser'
|
|
4
|
+
|
|
5
|
+
import { patchHistoryPlugin } from '../../src/plugin'
|
|
6
|
+
|
|
7
|
+
import { USER_CREATED_EVENT, USER_DELETED_EVENT, USER_UPDATED_EVENT } from '../constants/events'
|
|
8
|
+
|
|
9
|
+
const UserSchema = new Schema<IUser>({
|
|
10
|
+
name: {
|
|
11
|
+
type: String,
|
|
12
|
+
required: true
|
|
13
|
+
},
|
|
14
|
+
role: {
|
|
15
|
+
type: String,
|
|
16
|
+
required: true
|
|
17
|
+
}
|
|
18
|
+
}, { timestamps: true })
|
|
19
|
+
|
|
20
|
+
UserSchema.plugin(patchHistoryPlugin, {
|
|
21
|
+
eventCreated: USER_CREATED_EVENT,
|
|
22
|
+
eventUpdated: USER_UPDATED_EVENT,
|
|
23
|
+
eventDeleted: USER_DELETED_EVENT,
|
|
24
|
+
omit: ['__v', 'role', 'createdAt', 'updatedAt']
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const User = model('User', UserSchema)
|
|
28
|
+
|
|
29
|
+
export default User
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import mongoose from 'mongoose'
|
|
2
|
+
|
|
3
|
+
describe('mongoose', () => {
|
|
4
|
+
const uri = `${globalThis.__MONGO_URI__}${globalThis.__MONGO_DB_NAME__}`
|
|
5
|
+
|
|
6
|
+
beforeAll(async () => {
|
|
7
|
+
await mongoose.connect(uri)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
afterAll(async () => {
|
|
11
|
+
await mongoose.connection.close()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
await mongoose.connection.collection('tests').deleteMany({})
|
|
16
|
+
await mongoose.connection.collection('patches').deleteMany({})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should insert a doc into collection', async () => {
|
|
20
|
+
const users = mongoose.connection.db.collection('users')
|
|
21
|
+
|
|
22
|
+
const mockUser = { name: 'John' }
|
|
23
|
+
const user = await users.insertOne(mockUser)
|
|
24
|
+
|
|
25
|
+
const insertedUser = await users.findOne({ _id: user.insertedId })
|
|
26
|
+
expect(insertedUser).toEqual(mockUser)
|
|
27
|
+
})
|
|
28
|
+
})
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import mongoose from 'mongoose'
|
|
2
|
+
|
|
3
|
+
import User from './models/User'
|
|
4
|
+
import History from '../src/models/History'
|
|
5
|
+
|
|
6
|
+
import em from '../src/em'
|
|
7
|
+
|
|
8
|
+
import { USER_CREATED_EVENT, USER_UPDATED_EVENT, USER_DELETED_EVENT } from './constants/events'
|
|
9
|
+
|
|
10
|
+
jest.mock('../src/em', () => {
|
|
11
|
+
return { emit: jest.fn() }
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe('plugin', () => {
|
|
15
|
+
const uri = `${globalThis.__MONGO_URI__}${globalThis.__MONGO_DB_NAME__}`
|
|
16
|
+
|
|
17
|
+
beforeAll(async () => {
|
|
18
|
+
await mongoose.connect(uri)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
afterAll(async () => {
|
|
22
|
+
await mongoose.connection.close()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
await mongoose.connection.collection('users').deleteMany({})
|
|
27
|
+
await mongoose.connection.collection('history').deleteMany({})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should createHistory', async () => {
|
|
31
|
+
const user = await User.create({ name: 'John', role: 'user' })
|
|
32
|
+
expect(user.name).toBe('John')
|
|
33
|
+
|
|
34
|
+
user.name = 'Alice'
|
|
35
|
+
await user.save()
|
|
36
|
+
|
|
37
|
+
user.name = 'Bob'
|
|
38
|
+
await user.save()
|
|
39
|
+
|
|
40
|
+
const history = await History.find({})
|
|
41
|
+
expect(history).toHaveLength(3)
|
|
42
|
+
|
|
43
|
+
const [first, second, third] = history
|
|
44
|
+
|
|
45
|
+
expect(first.op).toBe('create')
|
|
46
|
+
expect(first.patch).toHaveLength(0)
|
|
47
|
+
expect(first.doc.name).toBe('John')
|
|
48
|
+
expect(first.doc.role).toBe('user')
|
|
49
|
+
expect(first.version).toBe(0)
|
|
50
|
+
|
|
51
|
+
expect(second.op).toBe('update')
|
|
52
|
+
expect(second.patch).toHaveLength(2)
|
|
53
|
+
expect(second.patch[1].value).toBe('Alice')
|
|
54
|
+
expect(second.version).toBe(1)
|
|
55
|
+
|
|
56
|
+
expect(third.op).toBe('update')
|
|
57
|
+
expect(third.patch).toHaveLength(2)
|
|
58
|
+
expect(third.patch[1].value).toBe('Bob')
|
|
59
|
+
expect(third.version).toBe(2)
|
|
60
|
+
|
|
61
|
+
await User.deleteMany({ role: 'user' }).exec()
|
|
62
|
+
|
|
63
|
+
expect(em.emit).toHaveBeenCalledTimes(4)
|
|
64
|
+
expect(em.emit).toHaveBeenCalledWith(USER_CREATED_EVENT, { doc: first.doc })
|
|
65
|
+
expect(em.emit).toHaveBeenCalledWith(USER_UPDATED_EVENT, {
|
|
66
|
+
oldDoc: expect.objectContaining({ _id: user._id, name: 'John', role: 'user' }),
|
|
67
|
+
doc: expect.objectContaining({ _id: user._id, name: 'Alice', role: 'user' }),
|
|
68
|
+
patch: second.patch
|
|
69
|
+
})
|
|
70
|
+
expect(em.emit).toHaveBeenCalledWith(USER_UPDATED_EVENT, {
|
|
71
|
+
oldDoc: expect.objectContaining({ _id: user._id, name: 'Alice', role: 'user' }),
|
|
72
|
+
doc: expect.objectContaining({ _id: user._id, name: 'Bob', role: 'user' }),
|
|
73
|
+
patch: third.patch
|
|
74
|
+
})
|
|
75
|
+
expect(em.emit).toHaveBeenCalledWith(USER_DELETED_EVENT, {
|
|
76
|
+
oldDoc: expect.objectContaining({ _id: user._id, name: 'Bob', role: 'user' })
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should omit update of role', async () => {
|
|
81
|
+
const user = await User.create({ name: 'John', role: 'user' })
|
|
82
|
+
expect(user.name).toBe('John')
|
|
83
|
+
|
|
84
|
+
user.role = 'manager'
|
|
85
|
+
await user.save()
|
|
86
|
+
|
|
87
|
+
const history = await History.find({})
|
|
88
|
+
expect(history).toHaveLength(1)
|
|
89
|
+
|
|
90
|
+
const [first] = history
|
|
91
|
+
|
|
92
|
+
expect(first.op).toBe('create')
|
|
93
|
+
expect(first.patch).toHaveLength(0)
|
|
94
|
+
expect(first.doc.name).toBe('John')
|
|
95
|
+
expect(first.doc.role).toBe('user')
|
|
96
|
+
expect(first.version).toBe(0)
|
|
97
|
+
|
|
98
|
+
expect(user.role).toBe('manager')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should updateOne', async () => {
|
|
102
|
+
const user = await User.create({ name: 'John', role: 'user' })
|
|
103
|
+
expect(user.name).toBe('John')
|
|
104
|
+
|
|
105
|
+
await User.updateOne({ _id: user._id }, { name: 'Alice' }).exec()
|
|
106
|
+
|
|
107
|
+
const history = await History.find({})
|
|
108
|
+
expect(history).toHaveLength(2)
|
|
109
|
+
|
|
110
|
+
const [first, second] = history
|
|
111
|
+
|
|
112
|
+
expect(first.op).toBe('create')
|
|
113
|
+
expect(first.patch).toHaveLength(0)
|
|
114
|
+
expect(first.doc.name).toBe('John')
|
|
115
|
+
expect(first.doc.role).toBe('user')
|
|
116
|
+
expect(first.version).toBe(0)
|
|
117
|
+
|
|
118
|
+
expect(second.op).toBe('updateOne')
|
|
119
|
+
expect(second.patch).toHaveLength(2)
|
|
120
|
+
expect(second.patch[1].value).toBe('Alice')
|
|
121
|
+
expect(second.version).toBe(1)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should findOneAndUpdate', async () => {
|
|
125
|
+
const user = await User.create({ name: 'John', role: 'user' })
|
|
126
|
+
expect(user.name).toBe('John')
|
|
127
|
+
|
|
128
|
+
await User.findOneAndUpdate({ _id: user._id }, { name: 'Alice' }).exec()
|
|
129
|
+
|
|
130
|
+
const history = await History.find({})
|
|
131
|
+
expect(history).toHaveLength(2)
|
|
132
|
+
|
|
133
|
+
const [first, second] = history
|
|
134
|
+
|
|
135
|
+
expect(first.op).toBe('create')
|
|
136
|
+
expect(second.patch).toHaveLength(2)
|
|
137
|
+
expect(first.doc.name).toBe('John')
|
|
138
|
+
expect(first.version).toBe(0)
|
|
139
|
+
|
|
140
|
+
expect(second.op).toBe('findOneAndUpdate')
|
|
141
|
+
expect(second.patch).toHaveLength(2)
|
|
142
|
+
expect(second.patch[1].value).toBe('Alice')
|
|
143
|
+
expect(second.version).toBe(1)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('should update deprecated', async () => {
|
|
147
|
+
const user = await User.create({ name: 'John', role: 'user' })
|
|
148
|
+
expect(user.name).toBe('John')
|
|
149
|
+
|
|
150
|
+
await User.update({ _id: user._id }, { $set: { name: 'Alice' } }).exec()
|
|
151
|
+
|
|
152
|
+
const history = await History.find({})
|
|
153
|
+
expect(history).toHaveLength(2)
|
|
154
|
+
|
|
155
|
+
const [first, second] = history
|
|
156
|
+
|
|
157
|
+
expect(first.op).toBe('create')
|
|
158
|
+
expect(second.patch).toHaveLength(2)
|
|
159
|
+
expect(first.doc.name).toBe('John')
|
|
160
|
+
expect(first.version).toBe(0)
|
|
161
|
+
|
|
162
|
+
expect(second.op).toBe('update')
|
|
163
|
+
expect(second.patch).toHaveLength(2)
|
|
164
|
+
expect(second.patch[1].value).toBe('Alice')
|
|
165
|
+
expect(second.version).toBe(1)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('should updated deprecated with multi flag', async () => {
|
|
169
|
+
const john = await User.create({ name: 'John', role: 'user' })
|
|
170
|
+
expect(john.name).toBe('John')
|
|
171
|
+
const alice = await User.create({ name: 'Alice', role: 'user' })
|
|
172
|
+
expect(alice.name).toBe('Alice')
|
|
173
|
+
|
|
174
|
+
await User.update({ role: 'user' }, { $set: { name: 'Bob' } }, { multi: true }).exec()
|
|
175
|
+
|
|
176
|
+
const history = await History.find({})
|
|
177
|
+
expect(history).toHaveLength(4)
|
|
178
|
+
|
|
179
|
+
const [first, second, third, fourth] = history
|
|
180
|
+
|
|
181
|
+
expect(first.op).toBe('create')
|
|
182
|
+
expect(second.patch).toHaveLength(0)
|
|
183
|
+
expect(first.doc.name).toBe('John')
|
|
184
|
+
expect(first.doc.role).toBe('user')
|
|
185
|
+
expect(first.version).toBe(0)
|
|
186
|
+
|
|
187
|
+
expect(second.op).toBe('create')
|
|
188
|
+
expect(second.patch).toHaveLength(0)
|
|
189
|
+
expect(second.doc.name).toBe('Alice')
|
|
190
|
+
expect(first.doc.role).toBe('user')
|
|
191
|
+
expect(second.version).toBe(0)
|
|
192
|
+
|
|
193
|
+
expect(third.op).toBe('update')
|
|
194
|
+
expect(third.patch).toHaveLength(2)
|
|
195
|
+
expect(third.patch[1].value).toBe('Bob')
|
|
196
|
+
expect(third.version).toBe(1)
|
|
197
|
+
|
|
198
|
+
expect(fourth.op).toBe('update')
|
|
199
|
+
expect(fourth.patch).toHaveLength(2)
|
|
200
|
+
expect(fourth.patch[1].value).toBe('Bob')
|
|
201
|
+
expect(fourth.version).toBe(1)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('should create many', async () => {
|
|
205
|
+
await User.create([
|
|
206
|
+
{ name: 'John', role: 'user' },
|
|
207
|
+
{ name: 'Alice', role: 'user' }
|
|
208
|
+
])
|
|
209
|
+
|
|
210
|
+
const history = await History.find({})
|
|
211
|
+
expect(history).toHaveLength(2)
|
|
212
|
+
|
|
213
|
+
const [first, second] = history
|
|
214
|
+
|
|
215
|
+
expect(first.op).toBe('create')
|
|
216
|
+
expect(first.patch).toHaveLength(0)
|
|
217
|
+
expect(first.doc.name).toBe('John')
|
|
218
|
+
expect(first.doc.role).toBe('user')
|
|
219
|
+
expect(first.version).toBe(0)
|
|
220
|
+
|
|
221
|
+
expect(second.op).toBe('create')
|
|
222
|
+
expect(second.patch).toHaveLength(0)
|
|
223
|
+
expect(second.doc.name).toBe('Alice')
|
|
224
|
+
expect(second.doc.role).toBe('user')
|
|
225
|
+
expect(second.version).toBe(0)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('should findOneAndUpdate upsert', async () => {
|
|
229
|
+
await User.findOneAndUpdate({ name: 'John', role: 'user' }, { name: 'Bob', role: 'user' }, { upsert: true, runValidators: true }).exec()
|
|
230
|
+
const documents = await User.find({})
|
|
231
|
+
expect(documents).toHaveLength(1)
|
|
232
|
+
|
|
233
|
+
const history = await History.find({})
|
|
234
|
+
expect(history).toHaveLength(1)
|
|
235
|
+
|
|
236
|
+
const [first] = history
|
|
237
|
+
|
|
238
|
+
expect(first.op).toBe('findOneAndUpdate')
|
|
239
|
+
expect(first.patch).toHaveLength(0)
|
|
240
|
+
expect(first.doc.name).toBe('Bob')
|
|
241
|
+
expect(first.version).toBe(0)
|
|
242
|
+
})
|
|
243
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
export const clearDirectory = (dir: string) => {
|
|
5
|
+
if (!fs.existsSync(dir)) return
|
|
6
|
+
const files = fs.readdirSync(dir)
|
|
7
|
+
for (const file of files) {
|
|
8
|
+
const filePath = path.join(dir, file)
|
|
9
|
+
if (fs.statSync(filePath).isFile()) {
|
|
10
|
+
fs.unlinkSync(filePath)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2021",
|
|
4
|
+
"lib": [
|
|
5
|
+
"es2021"
|
|
6
|
+
],
|
|
7
|
+
"module": "commonjs",
|
|
8
|
+
"moduleResolution": "node",
|
|
9
|
+
"outDir": "dist",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"strictBindCallApply": true,
|
|
12
|
+
"strictFunctionTypes": true,
|
|
13
|
+
"strictNullChecks": true,
|
|
14
|
+
"strictPropertyInitialization": true,
|
|
15
|
+
"noEmitOnError": true,
|
|
16
|
+
"noFallthroughCasesInSwitch": true,
|
|
17
|
+
"noImplicitAny": true,
|
|
18
|
+
"noImplicitOverride": true,
|
|
19
|
+
"noImplicitUseStrict": false,
|
|
20
|
+
"noImplicitReturns": true,
|
|
21
|
+
"noImplicitThis": true,
|
|
22
|
+
"noUnusedLocals": true,
|
|
23
|
+
"noUnusedParameters": true,
|
|
24
|
+
"allowSyntheticDefaultImports": true,
|
|
25
|
+
"declaration": true,
|
|
26
|
+
"declarationMap": true,
|
|
27
|
+
"sourceMap": true,
|
|
28
|
+
"forceConsistentCasingInFileNames": true,
|
|
29
|
+
"esModuleInterop": true,
|
|
30
|
+
"importHelpers": true,
|
|
31
|
+
"removeComments": true
|
|
32
|
+
},
|
|
33
|
+
"include": [
|
|
34
|
+
"src/**/*"
|
|
35
|
+
],
|
|
36
|
+
"exclude": [
|
|
37
|
+
"dist",
|
|
38
|
+
"tools",
|
|
39
|
+
"node_modules"
|
|
40
|
+
],
|
|
41
|
+
"ts-node": {
|
|
42
|
+
"swc": true
|
|
43
|
+
}
|
|
44
|
+
}
|