ts-patch-mongoose 2.9.6 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -27
- package/biome.json +2 -5
- package/dist/index.cjs +307 -93
- package/dist/index.d.cts +42 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +42 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +307 -93
- package/package.json +13 -19
- package/src/helpers.ts +132 -10
- package/src/hooks/delete-hooks.ts +5 -7
- package/src/hooks/update-hooks.ts +48 -34
- package/src/index.ts +4 -32
- package/src/ms.ts +66 -0
- package/src/omit-deep.ts +56 -0
- package/src/patch.ts +42 -47
- package/src/types.ts +1 -0
- package/src/version.ts +5 -4
- package/tests/em.test.ts +26 -8
- package/tests/helpers.test.ts +291 -2
- package/tests/ms.test.ts +113 -0
- package/tests/omit-deep.test.ts +235 -0
- package/tests/patch.test.ts +6 -5
- package/tests/plugin-all-features.test.ts +844 -0
- package/tests/plugin-complex-data.test.ts +2647 -0
- package/tests/plugin-event-created.test.ts +10 -10
- package/tests/plugin-event-deleted.test.ts +10 -10
- package/tests/plugin-event-updated.test.ts +9 -9
- package/tests/plugin-global.test.ts +6 -6
- package/tests/plugin-omit-all.test.ts +1 -1
- package/tests/plugin-patch-history-disabled.test.ts +1 -1
- package/tests/plugin-pre-delete.test.ts +8 -8
- package/tests/plugin-pre-save.test.ts +2 -2
- package/tests/plugin.test.ts +3 -3
- package/tsconfig.json +2 -3
- package/vite.config.mts +2 -1
- package/src/modules/omit-deep.d.ts +0 -3
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { omitDeep } from '../src/omit-deep'
|
|
4
|
+
|
|
5
|
+
describe('omitDeep', () => {
|
|
6
|
+
it('should return empty object for undefined', () => {
|
|
7
|
+
expect(omitDeep(undefined, 'a')).toEqual({})
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('should return primitives as-is', () => {
|
|
11
|
+
expect(omitDeep(42 as unknown, 'a')).toBe(42)
|
|
12
|
+
expect(omitDeep('hello' as unknown, 'a')).toBe('hello')
|
|
13
|
+
expect(omitDeep(null as unknown, 'a')).toBe(null)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should omit top-level keys', () => {
|
|
17
|
+
const obj = { a: 1, b: 2, c: 3 }
|
|
18
|
+
expect(omitDeep(obj, ['b'])).toEqual({ a: 1, c: 3 })
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should omit with a single string key', () => {
|
|
22
|
+
const obj = { a: 1, b: 2 }
|
|
23
|
+
expect(omitDeep(obj, 'b')).toEqual({ a: 1 })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should omit nested keys recursively', () => {
|
|
27
|
+
const obj = { a: 1, nested: { a: 2, b: 3 } }
|
|
28
|
+
expect(omitDeep(obj, ['a'])).toEqual({ nested: { b: 3 } })
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should handle arrays of objects', () => {
|
|
32
|
+
const arr = [
|
|
33
|
+
{ a: 1, b: 2 },
|
|
34
|
+
{ a: 3, b: 4 },
|
|
35
|
+
]
|
|
36
|
+
expect(omitDeep(arr, ['a'])).toEqual([{ b: 2 }, { b: 4 }])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should handle deeply nested structures', () => {
|
|
40
|
+
const obj = { level1: { level2: { level3: { secret: 'hidden', keep: 'visible' } } } }
|
|
41
|
+
expect(omitDeep(obj, ['secret'])).toEqual({ level1: { level2: { level3: { keep: 'visible' } } } })
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should support dot-notation paths', () => {
|
|
45
|
+
const obj = { address: { street: '123 Main', city: 'Springfield' }, name: 'John' }
|
|
46
|
+
expect(omitDeep(obj, ['address.street'])).toEqual({ address: { city: 'Springfield' }, name: 'John' })
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should handle dot-notation with nested arrays', () => {
|
|
50
|
+
const obj = { config: { items: [{ secret: 1 }] }, meta: { secret: 2 } }
|
|
51
|
+
expect(omitDeep(obj, ['secret'])).toEqual({ config: { items: [{}] }, meta: {} })
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should block __proto__ in dot-notation paths', () => {
|
|
55
|
+
const obj = { a: { b: 1 } }
|
|
56
|
+
omitDeep(obj, ['__proto__.polluted'])
|
|
57
|
+
expect(({} as Record<string, unknown>).polluted).toBeUndefined()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should block constructor in dot-notation paths', () => {
|
|
61
|
+
const obj = { a: 1 }
|
|
62
|
+
omitDeep(obj, ['constructor.something'])
|
|
63
|
+
expect(obj).toEqual({ a: 1 })
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should block prototype in dot-notation paths', () => {
|
|
67
|
+
const obj = { a: 1 }
|
|
68
|
+
omitDeep(obj, ['prototype.something'])
|
|
69
|
+
expect(obj).toEqual({ a: 1 })
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should return value if keys is not an array or string', () => {
|
|
73
|
+
const obj = { a: 1 }
|
|
74
|
+
expect(omitDeep(obj, 123 as unknown as string[])).toEqual({ a: 1 })
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should handle non-plain objects', () => {
|
|
78
|
+
const date = new Date()
|
|
79
|
+
expect(omitDeep(date as unknown, 'a')).toBe(date)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should handle empty objects', () => {
|
|
83
|
+
expect(omitDeep({}, ['a'])).toEqual({})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should handle empty arrays', () => {
|
|
87
|
+
expect(omitDeep([], ['a'])).toEqual([])
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should handle dot-notation where intermediate path is missing', () => {
|
|
91
|
+
const obj = { a: 1 }
|
|
92
|
+
expect(omitDeep(obj, ['x.y.z'])).toEqual({ a: 1 })
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('should handle dot-notation where intermediate is not an object', () => {
|
|
96
|
+
const obj = { a: 'string' }
|
|
97
|
+
expect(omitDeep(obj, ['a.b'])).toEqual({ a: 'string' })
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should handle objects with null prototype', () => {
|
|
101
|
+
const obj = Object.create(null) as Record<string, unknown>
|
|
102
|
+
obj.a = 1
|
|
103
|
+
obj.b = 2
|
|
104
|
+
expect(omitDeep(obj, ['b'])).toEqual({ a: 1 })
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should handle multiple keys at once', () => {
|
|
108
|
+
const obj = { a: 1, b: 2, c: 3, d: 4 }
|
|
109
|
+
expect(omitDeep(obj, ['a', 'c'])).toEqual({ b: 2, d: 4 })
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('should omit from dot-notation even when value is empty', () => {
|
|
113
|
+
const obj = { a: { b: '' } }
|
|
114
|
+
expect(omitDeep(obj, ['a.b'])).toEqual({ a: {} })
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('should omit dot-notation with nested value present', () => {
|
|
118
|
+
const obj = { a: { b: { c: 1 } } }
|
|
119
|
+
expect(omitDeep(obj, ['a.b.c'])).toEqual({ a: { b: {} } })
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should handle object where nested values are all empty', () => {
|
|
123
|
+
const obj = { a: { b: {}, c: '' } }
|
|
124
|
+
expect(omitDeep(obj, ['x'])).toEqual({ a: { b: {}, c: '' } })
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('should handle object with RegExp values', () => {
|
|
128
|
+
const obj = { pattern: /test/i, name: 'hello' }
|
|
129
|
+
expect(omitDeep(obj, ['name'])).toEqual({ pattern: /test/i })
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should handle object with Error values', () => {
|
|
133
|
+
const err = new Error('test error')
|
|
134
|
+
const obj = { error: err, name: 'hello' }
|
|
135
|
+
const result = omitDeep(obj, ['name'])
|
|
136
|
+
expect(result).toEqual({ error: err })
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('should handle object with Map values', () => {
|
|
140
|
+
const map = new Map([['key', 'value']])
|
|
141
|
+
const obj = { data: map, name: 'hello' } as Record<string, unknown>
|
|
142
|
+
const result = omitDeep(obj, ['name'])
|
|
143
|
+
expect(result.data).toBe(map)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('should handle object with Set values', () => {
|
|
147
|
+
const set = new Set([1, 2, 3])
|
|
148
|
+
const obj = { data: set, name: 'hello' } as Record<string, unknown>
|
|
149
|
+
const result = omitDeep(obj, ['name'])
|
|
150
|
+
expect(result.data).toBe(set)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('should handle object with boolean and number values at dot path', () => {
|
|
154
|
+
const obj = { config: { enabled: true, count: 0 } }
|
|
155
|
+
expect(omitDeep(obj, ['config.enabled'])).toEqual({ config: { count: 0 } })
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('should handle object with function values at dot path', () => {
|
|
159
|
+
const fn = () => 42
|
|
160
|
+
const obj = { handler: fn, name: 'test' } as Record<string, unknown>
|
|
161
|
+
expect(omitDeep(obj, ['name'])).toEqual({ handler: fn })
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('should handle object with array values at dot path', () => {
|
|
165
|
+
const obj = { tags: ['a', 'b'], name: 'test' }
|
|
166
|
+
expect(omitDeep(obj, ['tags'])).toEqual({ name: 'test' })
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('should unset non-empty RegExp at dot path', () => {
|
|
170
|
+
const obj = { config: { pattern: /test/i } }
|
|
171
|
+
expect(omitDeep(obj, ['config.pattern'])).toEqual({ config: {} })
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('should unset Error with message at dot path', () => {
|
|
175
|
+
const obj = { config: { err: new Error('oops') } }
|
|
176
|
+
expect(omitDeep(obj, ['config.err'])).toEqual({ config: {} })
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('should unset non-empty Map at dot path', () => {
|
|
180
|
+
const obj = { config: { data: new Map([['a', 1]]) } } as Record<string, unknown>
|
|
181
|
+
expect(omitDeep(obj, ['config.data'])).toEqual({ config: {} })
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('should unset empty Map at dot path', () => {
|
|
185
|
+
const obj = { config: { data: new Map() } } as Record<string, unknown>
|
|
186
|
+
expect(omitDeep(obj, ['config.data'])).toEqual({ config: {} })
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('should unset non-empty Set at dot path', () => {
|
|
190
|
+
const obj = { config: { data: new Set([1]) } } as Record<string, unknown>
|
|
191
|
+
expect(omitDeep(obj, ['config.data'])).toEqual({ config: {} })
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('should unset empty Set at dot path', () => {
|
|
195
|
+
const obj = { config: { data: new Set() } } as Record<string, unknown>
|
|
196
|
+
expect(omitDeep(obj, ['config.data'])).toEqual({ config: {} })
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('should handle null value at dot-notation intermediate', () => {
|
|
200
|
+
const obj = { a: null } as Record<string, unknown>
|
|
201
|
+
expect(omitDeep(obj, ['a.b'])).toEqual({ a: null })
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('should handle mixed arrays with primitives and objects', () => {
|
|
205
|
+
const arr = [1, 'two', { a: 1, b: 2 }, null]
|
|
206
|
+
expect(omitDeep(arr, ['a'])).toEqual([1, 'two', { b: 2 }, null])
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('should unset non-empty array at dot path', () => {
|
|
210
|
+
const obj = { config: { items: [1, 2] } }
|
|
211
|
+
expect(omitDeep(obj, ['config.items'])).toEqual({ config: {} })
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('should unset empty array at dot path', () => {
|
|
215
|
+
const obj = { config: { items: [] } }
|
|
216
|
+
expect(omitDeep(obj, ['config.items'])).toEqual({ config: {} })
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('should not mutate the original object', () => {
|
|
220
|
+
const obj = { a: 1, b: 2, nested: { c: 3, d: 4 } }
|
|
221
|
+
const original = structuredClone(obj)
|
|
222
|
+
omitDeep(obj, ['b', 'nested.c'])
|
|
223
|
+
expect(obj).toEqual(original)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('should not mutate the original array', () => {
|
|
227
|
+
const arr = [
|
|
228
|
+
{ a: 1, b: 2 },
|
|
229
|
+
{ a: 3, b: 4 },
|
|
230
|
+
]
|
|
231
|
+
const original = structuredClone(arr)
|
|
232
|
+
omitDeep(arr, ['b'])
|
|
233
|
+
expect(arr).toEqual(original)
|
|
234
|
+
})
|
|
235
|
+
})
|
package/tests/patch.test.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
2
|
|
|
3
|
-
import { afterEach } from 'node:test'
|
|
4
3
|
import mongoose from 'mongoose'
|
|
5
4
|
import em from '../src/em'
|
|
6
5
|
import { patchHistoryPlugin } from '../src/index'
|
|
@@ -37,8 +36,8 @@ describe('patch tests', () => {
|
|
|
37
36
|
await mongoose.connection.collection('patches').deleteMany({})
|
|
38
37
|
})
|
|
39
38
|
|
|
40
|
-
afterEach(
|
|
41
|
-
vi.
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
vi.resetAllMocks()
|
|
42
41
|
})
|
|
43
42
|
|
|
44
43
|
describe('getObjects', () => {
|
|
@@ -125,6 +124,8 @@ describe('patch tests', () => {
|
|
|
125
124
|
it('should return if one object is empty', async () => {
|
|
126
125
|
const current = await UserModel.create({ name: 'John', role: 'user' })
|
|
127
126
|
|
|
127
|
+
vi.clearAllMocks()
|
|
128
|
+
|
|
128
129
|
const pluginOptions: PluginOptions<User> = {
|
|
129
130
|
eventDeleted: USER_DELETED,
|
|
130
131
|
patchHistoryDisabled: true,
|
|
@@ -137,7 +138,7 @@ describe('patch tests', () => {
|
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
await updatePatch(pluginOptions, context, current, {} as HydratedDocument<User>)
|
|
140
|
-
expect(em.emit).toHaveBeenCalled()
|
|
141
|
+
expect(em.emit).not.toHaveBeenCalled()
|
|
141
142
|
})
|
|
142
143
|
})
|
|
143
144
|
|