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.
Files changed (95) hide show
  1. package/.eslintignore +4 -0
  2. package/.eslintrc +91 -0
  3. package/.swcrc +21 -0
  4. package/LICENSE +21 -0
  5. package/README.md +66 -0
  6. package/dist/cjs/em.d.ts +7 -0
  7. package/dist/cjs/em.d.ts.map +1 -0
  8. package/dist/cjs/em.js +9 -0
  9. package/dist/cjs/em.js.map +1 -0
  10. package/dist/cjs/interfaces/IContext.d.ts +12 -0
  11. package/dist/cjs/interfaces/IContext.d.ts.map +1 -0
  12. package/dist/cjs/interfaces/IContext.js +3 -0
  13. package/dist/cjs/interfaces/IContext.js.map +1 -0
  14. package/dist/cjs/interfaces/IHistory.d.ts +13 -0
  15. package/dist/cjs/interfaces/IHistory.d.ts.map +1 -0
  16. package/dist/cjs/interfaces/IHistory.js +3 -0
  17. package/dist/cjs/interfaces/IHistory.js.map +1 -0
  18. package/dist/cjs/interfaces/IHookContext.d.ts +8 -0
  19. package/dist/cjs/interfaces/IHookContext.d.ts.map +1 -0
  20. package/dist/cjs/interfaces/IHookContext.js +3 -0
  21. package/dist/cjs/interfaces/IHookContext.js.map +1 -0
  22. package/dist/cjs/interfaces/IPluginOptions.d.ts +13 -0
  23. package/dist/cjs/interfaces/IPluginOptions.d.ts.map +1 -0
  24. package/dist/cjs/interfaces/IPluginOptions.js +3 -0
  25. package/dist/cjs/interfaces/IPluginOptions.js.map +1 -0
  26. package/dist/cjs/models/History.d.ts +29 -0
  27. package/dist/cjs/models/History.d.ts.map +1 -0
  28. package/dist/cjs/models/History.js +36 -0
  29. package/dist/cjs/models/History.js.map +1 -0
  30. package/dist/cjs/plugin.d.ts +45 -0
  31. package/dist/cjs/plugin.d.ts.map +1 -0
  32. package/dist/cjs/plugin.js +224 -0
  33. package/dist/cjs/plugin.js.map +1 -0
  34. package/dist/esm/em.d.ts +7 -0
  35. package/dist/esm/em.d.ts.map +1 -0
  36. package/dist/esm/em.js +6 -0
  37. package/dist/esm/em.js.map +1 -0
  38. package/dist/esm/interfaces/IContext.d.ts +12 -0
  39. package/dist/esm/interfaces/IContext.d.ts.map +1 -0
  40. package/dist/esm/interfaces/IContext.js +2 -0
  41. package/dist/esm/interfaces/IContext.js.map +1 -0
  42. package/dist/esm/interfaces/IHistory.d.ts +13 -0
  43. package/dist/esm/interfaces/IHistory.d.ts.map +1 -0
  44. package/dist/esm/interfaces/IHistory.js +2 -0
  45. package/dist/esm/interfaces/IHistory.js.map +1 -0
  46. package/dist/esm/interfaces/IHookContext.d.ts +8 -0
  47. package/dist/esm/interfaces/IHookContext.d.ts.map +1 -0
  48. package/dist/esm/interfaces/IHookContext.js +2 -0
  49. package/dist/esm/interfaces/IHookContext.js.map +1 -0
  50. package/dist/esm/interfaces/IPluginOptions.d.ts +13 -0
  51. package/dist/esm/interfaces/IPluginOptions.d.ts.map +1 -0
  52. package/dist/esm/interfaces/IPluginOptions.js +2 -0
  53. package/dist/esm/interfaces/IPluginOptions.js.map +1 -0
  54. package/dist/esm/models/History.d.ts +29 -0
  55. package/dist/esm/models/History.d.ts.map +1 -0
  56. package/dist/esm/models/History.js +34 -0
  57. package/dist/esm/models/History.js.map +1 -0
  58. package/dist/esm/plugin.d.ts +45 -0
  59. package/dist/esm/plugin.d.ts.map +1 -0
  60. package/dist/esm/plugin.js +219 -0
  61. package/dist/esm/plugin.js.map +1 -0
  62. package/dist/types/em.d.ts +7 -0
  63. package/dist/types/em.d.ts.map +1 -0
  64. package/dist/types/interfaces/IContext.d.ts +12 -0
  65. package/dist/types/interfaces/IContext.d.ts.map +1 -0
  66. package/dist/types/interfaces/IHistory.d.ts +13 -0
  67. package/dist/types/interfaces/IHistory.d.ts.map +1 -0
  68. package/dist/types/interfaces/IHookContext.d.ts +8 -0
  69. package/dist/types/interfaces/IHookContext.d.ts.map +1 -0
  70. package/dist/types/interfaces/IPluginOptions.d.ts +13 -0
  71. package/dist/types/interfaces/IPluginOptions.d.ts.map +1 -0
  72. package/dist/types/models/History.d.ts +29 -0
  73. package/dist/types/models/History.d.ts.map +1 -0
  74. package/dist/types/plugin.d.ts +45 -0
  75. package/dist/types/plugin.d.ts.map +1 -0
  76. package/jest-mongodb-config.ts +10 -0
  77. package/jest.config.ts +35 -0
  78. package/package.json +101 -0
  79. package/src/em.ts +6 -0
  80. package/src/interfaces/IContext.ts +13 -0
  81. package/src/interfaces/IHistory.ts +14 -0
  82. package/src/interfaces/IHookContext.ts +6 -0
  83. package/src/interfaces/IPluginOptions.ts +14 -0
  84. package/src/models/History.ts +39 -0
  85. package/src/modules/omit-deep.d.ts +3 -0
  86. package/src/modules/power-assign.d.ts +3 -0
  87. package/src/plugin.ts +267 -0
  88. package/tests/constants/events.ts +3 -0
  89. package/tests/em.test.ts +16 -0
  90. package/tests/interfaces/IUser.ts +8 -0
  91. package/tests/models/User.ts +29 -0
  92. package/tests/mongose.test.ts +28 -0
  93. package/tests/plugin.test.ts +243 -0
  94. package/tests/utils/filesystem.ts +13 -0
  95. package/tsconfig.json +44 -0
@@ -0,0 +1,3 @@
1
+ declare module 'power-assign' {
2
+ export function assign<T, U>(object1: T, object2: U): T & U
3
+ }
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
+ }
@@ -0,0 +1,3 @@
1
+ export const USER_CREATED_EVENT = 'testCreated'
2
+ export const USER_UPDATED_EVENT = 'testUpdated'
3
+ export const USER_DELETED_EVENT = 'testDeleted'
@@ -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,8 @@
1
+ interface IUser {
2
+ name: string
3
+ role: string
4
+ createdAt?: Date
5
+ updatedAt?: Date
6
+ }
7
+
8
+ export default IUser
@@ -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
+ }