ts-patch-mongoose 3.1.0 → 4.0.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.
Files changed (46) hide show
  1. package/README.md +39 -27
  2. package/dist/index.cjs +287 -32
  3. package/dist/index.d.cts +23 -1
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.mts +23 -1
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +287 -32
  8. package/package.json +19 -24
  9. package/biome.json +0 -47
  10. package/src/em.ts +0 -6
  11. package/src/helpers.ts +0 -174
  12. package/src/hooks/delete-hooks.ts +0 -49
  13. package/src/hooks/save-hooks.ts +0 -30
  14. package/src/hooks/update-hooks.ts +0 -125
  15. package/src/index.ts +0 -65
  16. package/src/model.ts +0 -50
  17. package/src/modules/power-assign.d.ts +0 -3
  18. package/src/ms.ts +0 -66
  19. package/src/omit-deep.ts +0 -56
  20. package/src/patch.ts +0 -154
  21. package/src/types.ts +0 -53
  22. package/src/version.ts +0 -13
  23. package/tests/constants/events.ts +0 -7
  24. package/tests/em.test.ts +0 -70
  25. package/tests/helpers.test.ts +0 -373
  26. package/tests/mongo/.gitignore +0 -3
  27. package/tests/mongo/server.ts +0 -31
  28. package/tests/ms.test.ts +0 -113
  29. package/tests/omit-deep.test.ts +0 -235
  30. package/tests/patch.test.ts +0 -200
  31. package/tests/plugin-all-features.test.ts +0 -844
  32. package/tests/plugin-complex-data.test.ts +0 -2647
  33. package/tests/plugin-event-created.test.ts +0 -371
  34. package/tests/plugin-event-deleted.test.ts +0 -400
  35. package/tests/plugin-event-updated.test.ts +0 -503
  36. package/tests/plugin-global.test.ts +0 -545
  37. package/tests/plugin-omit-all.test.ts +0 -349
  38. package/tests/plugin-patch-history-disabled.test.ts +0 -162
  39. package/tests/plugin-pre-delete.test.ts +0 -160
  40. package/tests/plugin-pre-save.test.ts +0 -54
  41. package/tests/plugin.test.ts +0 -576
  42. package/tests/schemas/Description.ts +0 -15
  43. package/tests/schemas/Product.ts +0 -38
  44. package/tests/schemas/User.ts +0 -22
  45. package/tsconfig.json +0 -32
  46. package/vite.config.mts +0 -24
package/src/patch.ts DELETED
@@ -1,154 +0,0 @@
1
- import jsonpatch from 'fast-json-patch'
2
- import em from './em'
3
- import { chunk, isEmpty, isFunction } from './helpers'
4
- import { HistoryModel } from './model'
5
- import { omitDeep as omit } from './omit-deep'
6
-
7
- import type { HydratedDocument, MongooseError, Types } from 'mongoose'
8
- import type { Metadata, PatchContext, PatchEvent, PluginOptions, User } from './types'
9
-
10
- const isPatchHistoryEnabled = <T>(opts: PluginOptions<T>, context: PatchContext<T>): boolean => {
11
- return !opts.patchHistoryDisabled && !context.ignorePatchHistory
12
- }
13
-
14
- const applyOmit = <T>(object: Partial<T>, opts: PluginOptions<T>): Partial<T> => {
15
- return opts.omit ? omit(object, opts.omit) : object
16
- }
17
-
18
- const replacer = (_key: string, value: unknown): unknown => (typeof value === 'bigint' ? value.toString() : value)
19
-
20
- export const getJsonOmit = <T>(opts: PluginOptions<T>, doc: HydratedDocument<T>): Partial<T> => {
21
- // NOSONAR — structuredClone cannot handle mongoose documents (they contain non-cloneable methods)
22
- return applyOmit(JSON.parse(JSON.stringify(doc, replacer)) as Partial<T>, opts)
23
- }
24
-
25
- export const getObjectOmit = <T>(opts: PluginOptions<T>, doc: HydratedDocument<T>): Partial<T> => {
26
- return applyOmit(isFunction(doc?.toObject) ? doc.toObject() : doc, opts)
27
- }
28
-
29
- const getOptionalField = async <T, R>(fn: ((doc: HydratedDocument<T>) => Promise<R> | R) | undefined, doc?: HydratedDocument<T>): Promise<R | undefined> => {
30
- if (isFunction(fn)) {
31
- return await fn(doc as HydratedDocument<T>)
32
- }
33
- return undefined
34
- }
35
-
36
- export const getUser = async <T>(opts: PluginOptions<T>, doc?: HydratedDocument<T>): Promise<User | undefined> => getOptionalField(opts.getUser, doc)
37
-
38
- export const getReason = async <T>(opts: PluginOptions<T>, doc?: HydratedDocument<T>): Promise<string | undefined> => getOptionalField(opts.getReason, doc)
39
-
40
- export const getMetadata = async <T>(opts: PluginOptions<T>, doc?: HydratedDocument<T>): Promise<Metadata | undefined> => getOptionalField(opts.getMetadata, doc)
41
-
42
- export const getValue = <T>(item: PromiseSettledResult<T>): T | undefined => {
43
- return item.status === 'fulfilled' ? item.value : undefined
44
- }
45
-
46
- export const getData = async <T>(opts: PluginOptions<T>, doc: HydratedDocument<T>): Promise<[User | undefined, string | undefined, Metadata | undefined]> => {
47
- return Promise.allSettled([getUser(opts, doc), getReason(opts, doc), getMetadata(opts, doc)]).then(([user, reason, metadata]) => {
48
- return [getValue(user), getValue(reason), getValue(metadata)]
49
- })
50
- }
51
-
52
- export const emitEvent = <T>(context: PatchContext<T>, event: string | undefined, data: PatchEvent<T>): void => {
53
- if (event && !context.ignoreEvent) {
54
- try {
55
- em.emit(event, data)
56
- } catch {
57
- // Listener errors must not crash patch history recording
58
- }
59
- }
60
- }
61
-
62
- export const bulkPatch = async <T>(opts: PluginOptions<T>, context: PatchContext<T>, eventKey: 'eventCreated' | 'eventDeleted', docsKey: 'createdDocs' | 'deletedDocs'): Promise<void> => {
63
- const history = isPatchHistoryEnabled(opts, context)
64
- const event = opts[eventKey]
65
- const docs = context[docsKey]
66
- const key = eventKey === 'eventCreated' ? 'doc' : 'oldDoc'
67
-
68
- if (isEmpty(docs) || !docs || (!event && !history)) return
69
-
70
- const chunks = chunk(docs, 1000)
71
- for (const batch of chunks) {
72
- const bulk = []
73
-
74
- for (const doc of batch) {
75
- const omitted = getObjectOmit(opts, doc)
76
- emitEvent(context, event, { [key]: omitted })
77
-
78
- if (history) {
79
- const [user, reason, metadata] = await getData(opts, doc)
80
- bulk.push({
81
- insertOne: {
82
- document: {
83
- op: context.op,
84
- modelName: context.modelName,
85
- collectionName: context.collectionName,
86
- collectionId: doc._id as Types.ObjectId,
87
- doc: omitted,
88
- version: 0,
89
- ...(user !== undefined && { user }),
90
- ...(reason !== undefined && { reason }),
91
- ...(metadata !== undefined && { metadata }),
92
- },
93
- },
94
- })
95
- }
96
- }
97
-
98
- if (history && !isEmpty(bulk)) {
99
- const onError = opts.onError ?? console.error
100
- await HistoryModel.bulkWrite(bulk, { ordered: false }).catch((error: MongooseError) => {
101
- onError(error)
102
- })
103
- }
104
- }
105
- }
106
-
107
- export const createPatch = async <T>(opts: PluginOptions<T>, context: PatchContext<T>): Promise<void> => {
108
- await bulkPatch(opts, context, 'eventCreated', 'createdDocs')
109
- }
110
-
111
- export const updatePatch = async <T>(opts: PluginOptions<T>, context: PatchContext<T>, current: HydratedDocument<T>, original: HydratedDocument<T>): Promise<void> => {
112
- const history = isPatchHistoryEnabled(opts, context)
113
-
114
- const currentObject = getJsonOmit(opts, current)
115
- const originalObject = getJsonOmit(opts, original)
116
- if (isEmpty(originalObject) || isEmpty(currentObject)) return
117
-
118
- const patch = jsonpatch.compare(originalObject, currentObject, true)
119
- if (isEmpty(patch)) return
120
-
121
- emitEvent(context, opts.eventUpdated, { oldDoc: original, doc: current, patch })
122
-
123
- if (history) {
124
- let version = 0
125
-
126
- const lastHistory = await HistoryModel.findOne({ collectionId: original._id as Types.ObjectId })
127
- .sort('-version')
128
- .exec()
129
-
130
- if (lastHistory && lastHistory.version >= 0) {
131
- version = lastHistory.version + 1
132
- }
133
-
134
- const [user, reason, metadata] = await getData(opts, current)
135
- const onError = opts.onError ?? console.error
136
- await HistoryModel.create({
137
- op: context.op,
138
- modelName: context.modelName,
139
- collectionName: context.collectionName,
140
- collectionId: original._id as Types.ObjectId,
141
- patch,
142
- version,
143
- ...(user !== undefined && { user }),
144
- ...(reason !== undefined && { reason }),
145
- ...(metadata !== undefined && { metadata }),
146
- }).catch((error: MongooseError) => {
147
- onError(error)
148
- })
149
- }
150
- }
151
-
152
- export const deletePatch = async <T>(opts: PluginOptions<T>, context: PatchContext<T>): Promise<void> => {
153
- await bulkPatch(opts, context, 'eventDeleted', 'deletedDocs')
154
- }
package/src/types.ts DELETED
@@ -1,53 +0,0 @@
1
- import type { Operation } from 'fast-json-patch'
2
- import type { HydratedDocument, Query, Types } from 'mongoose'
3
-
4
- export interface History {
5
- op: string
6
- modelName: string
7
- collectionName: string
8
- collectionId: Types.ObjectId
9
- version: number
10
- doc?: object
11
- user?: object
12
- reason?: string
13
- metadata?: object
14
- patch?: Operation[]
15
- }
16
-
17
- export interface PatchEvent<T> {
18
- oldDoc?: HydratedDocument<T>
19
- doc?: HydratedDocument<T>
20
- patch?: Operation[]
21
- }
22
-
23
- export interface PatchContext<T> {
24
- op: string
25
- modelName: string
26
- collectionName: string
27
- isNew?: boolean
28
- createdDocs?: HydratedDocument<T>[]
29
- deletedDocs?: HydratedDocument<T>[]
30
- ignoreEvent?: boolean
31
- ignorePatchHistory?: boolean
32
- }
33
-
34
- export type HookContext<T> = Query<T, T> & { op: string; _context: PatchContext<T> }
35
-
36
- export type User = Record<string, unknown>
37
-
38
- export type Metadata = Record<string, unknown>
39
-
40
- export interface PluginOptions<T> {
41
- modelName?: string
42
- collectionName?: string
43
- eventUpdated?: string
44
- eventCreated?: string
45
- eventDeleted?: string
46
- getUser?: (doc: HydratedDocument<T>) => Promise<User> | User
47
- getReason?: (doc: HydratedDocument<T>) => Promise<string> | string
48
- getMetadata?: (doc: HydratedDocument<T>) => Promise<Metadata> | Metadata
49
- omit?: string[]
50
- patchHistoryDisabled?: boolean
51
- preDelete?: (docs: HydratedDocument<T>[]) => Promise<void>
52
- onError?: (error: Error) => void
53
- }
package/src/version.ts DELETED
@@ -1,13 +0,0 @@
1
- import mongoose from 'mongoose'
2
-
3
- const major = Number.parseInt(mongoose.version, 10)
4
-
5
- export const isMongooseLessThan8 = major < 8
6
- export const isMongooseLessThan7 = major < 7
7
- export const isMongoose6 = major === 6
8
-
9
- /* v8 ignore start */
10
- if (isMongoose6) {
11
- mongoose.set('strictQuery', false)
12
- }
13
- /* v8 ignore end */
@@ -1,7 +0,0 @@
1
- export const USER_CREATED = 'user-created'
2
- export const USER_UPDATED = 'user-updated'
3
- export const USER_DELETED = 'user-deleted'
4
-
5
- export const GLOBAL_CREATED = 'global-created'
6
- export const GLOBAL_UPDATED = 'global-updated'
7
- export const GLOBAL_DELETED = 'global-deleted'
package/tests/em.test.ts DELETED
@@ -1,70 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest'
2
-
3
- import { patchEventEmitter } from '../src/index'
4
- import { emitEvent } from '../src/patch'
5
-
6
- describe('em', () => {
7
- afterEach(() => {
8
- patchEventEmitter.removeAllListeners()
9
- })
10
-
11
- it('should subscribe and count', () => {
12
- let count = 0
13
- const fn = () => {
14
- count++
15
- }
16
- patchEventEmitter.on('test', fn)
17
- patchEventEmitter.emit('test')
18
- expect(count).toBe(1)
19
- patchEventEmitter.off('test', fn)
20
- patchEventEmitter.emit('test')
21
- expect(count).toBe(1)
22
- })
23
-
24
- it('emitEvent', () => {
25
- const fn = vi.fn()
26
- patchEventEmitter.on('test', fn)
27
-
28
- const context = {
29
- op: 'test',
30
- modelName: 'Test',
31
- collectionName: 'tests',
32
- }
33
-
34
- // @ts-expect-error expected
35
- emitEvent(context, 'test', { doc: { name: 'test' } })
36
- expect(fn).toHaveBeenCalledOnce()
37
- })
38
-
39
- it('emitEvent ignore', () => {
40
- const fn = vi.fn()
41
- patchEventEmitter.on('test', fn)
42
-
43
- const context = {
44
- ignoreEvent: true,
45
- op: 'test',
46
- modelName: 'Test',
47
- collectionName: 'tests',
48
- }
49
-
50
- // @ts-expect-error expected
51
- emitEvent(context, 'test', { doc: { name: 'test' } })
52
- expect(fn).toHaveBeenCalledTimes(0)
53
- })
54
-
55
- it('emitEvent should not throw when listener throws', () => {
56
- const fn = () => {
57
- throw new Error('listener error')
58
- }
59
- patchEventEmitter.on('throw-test', fn)
60
-
61
- const context = {
62
- op: 'test',
63
- modelName: 'Test',
64
- collectionName: 'tests',
65
- }
66
-
67
- // @ts-expect-error expected
68
- expect(() => emitEvent(context, 'throw-test', { doc: { name: 'test' } })).not.toThrow()
69
- })
70
- })
@@ -1,373 +0,0 @@
1
- import type { Mock, MockInstance } from 'vitest'
2
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3
-
4
- import { cloneDeep, isEmpty, setPatchHistoryTTL } from '../src/helpers'
5
- import { HistoryModel } from '../src/model'
6
- import { ms } from '../src/ms'
7
-
8
- vi.mock('../src/model', () => ({
9
- HistoryModel: {
10
- collection: {
11
- indexes: vi.fn(),
12
- dropIndex: vi.fn(),
13
- createIndex: vi.fn(),
14
- },
15
- },
16
- }))
17
-
18
- const name = 'createdAt_1_TTL'
19
-
20
- describe('useTTL', () => {
21
- let dropIndexSpy: MockInstance
22
- let createIndexSpy: MockInstance
23
- const indexes = HistoryModel.collection.indexes as Mock
24
-
25
- beforeEach(() => {
26
- vi.clearAllMocks()
27
- dropIndexSpy = vi.spyOn(HistoryModel.collection, 'dropIndex')
28
- createIndexSpy = vi.spyOn(HistoryModel.collection, 'createIndex')
29
- })
30
-
31
- afterEach(() => {
32
- vi.restoreAllMocks()
33
- })
34
-
35
- it('should drop the index if historyTTL is not set and index exists', async () => {
36
- indexes.mockResolvedValue([{ name }])
37
-
38
- // @ts-expect-error ttl can't be undefined in this case but we want to test it
39
- await setPatchHistoryTTL(undefined)
40
- expect(dropIndexSpy).toHaveBeenCalledWith(name)
41
- })
42
-
43
- it('should drop the index if historyTTL is less than 1 second and index exists', async () => {
44
- indexes.mockResolvedValue([{ name }])
45
-
46
- await setPatchHistoryTTL('500ms')
47
- expect(dropIndexSpy).toHaveBeenCalledWith(name)
48
- })
49
-
50
- it('should not recreate the index if it already exists with the correct TTL', async () => {
51
- const ttl = '1h'
52
- const expireAfterSeconds = ms(ttl) / 1000
53
-
54
- indexes.mockResolvedValue([{ name, expireAfterSeconds }])
55
-
56
- await setPatchHistoryTTL(ttl)
57
- expect(dropIndexSpy).not.toHaveBeenCalled()
58
- expect(createIndexSpy).not.toHaveBeenCalled()
59
- })
60
-
61
- it('should drop and recreate the index if TTL is different', async () => {
62
- const ttlBefore = '1h'
63
- const ttlAfter = '2h'
64
-
65
- const expireAfterSecondsBefore = ms(ttlBefore) / 1000
66
- const expireAfterSecondsAfter = ms(ttlAfter) / 1000
67
-
68
- indexes.mockResolvedValue([{ name, expireAfterSeconds: expireAfterSecondsBefore }])
69
-
70
- await setPatchHistoryTTL(ttlAfter)
71
- expect(dropIndexSpy).toHaveBeenCalledWith(name)
72
- expect(createIndexSpy).toHaveBeenCalledWith({ createdAt: 1 }, { expireAfterSeconds: expireAfterSecondsAfter, name })
73
- })
74
-
75
- it('should create the index if it does not exist', async () => {
76
- const ttl = '1h'
77
- const expireAfterSeconds = ms(ttl) / 1000
78
-
79
- indexes.mockResolvedValue([])
80
-
81
- await setPatchHistoryTTL(ttl)
82
- expect(createIndexSpy).toHaveBeenCalledWith({ createdAt: 1 }, { expireAfterSeconds, name })
83
- })
84
- })
85
-
86
- describe('isEmpty', () => {
87
- it('should return true for null and undefined', () => {
88
- expect(isEmpty(null)).toBe(true)
89
- expect(isEmpty(undefined)).toBe(true)
90
- })
91
-
92
- it('should return true for empty arrays and strings', () => {
93
- expect(isEmpty([])).toBe(true)
94
- expect(isEmpty('')).toBe(true)
95
- })
96
-
97
- it('should return false for non-empty arrays and strings', () => {
98
- expect(isEmpty([1, 2])).toBe(false)
99
- expect(isEmpty('hello')).toBe(false)
100
- })
101
-
102
- it('should return true for empty objects', () => {
103
- expect(isEmpty({})).toBe(true)
104
- })
105
-
106
- it('should return false for non-empty objects', () => {
107
- expect(isEmpty({ a: 1 })).toBe(false)
108
- })
109
-
110
- it('should return true for empty Map and Set', () => {
111
- expect(isEmpty(new Map())).toBe(true)
112
- expect(isEmpty(new Set())).toBe(true)
113
- })
114
-
115
- it('should return false for non-empty Map and Set', () => {
116
- expect(isEmpty(new Map([['a', 1]]))).toBe(false)
117
- expect(isEmpty(new Set([1]))).toBe(false)
118
- })
119
-
120
- it('should return true for numbers, booleans, and functions (matches lodash)', () => {
121
- expect(isEmpty(0)).toBe(true)
122
- expect(isEmpty(1)).toBe(true)
123
- expect(isEmpty(true)).toBe(true)
124
- expect(isEmpty(false)).toBe(true)
125
- expect(isEmpty(() => {})).toBe(true)
126
- })
127
-
128
- it('should return true for Date and RegExp (matches lodash)', () => {
129
- expect(isEmpty(new Date())).toBe(true)
130
- expect(isEmpty(/test/)).toBe(true)
131
- })
132
- })
133
-
134
- describe('cloneDeep', () => {
135
- it('should clone primitives', () => {
136
- expect(cloneDeep(42)).toBe(42)
137
- expect(cloneDeep('hello')).toBe('hello')
138
- expect(cloneDeep(null)).toBe(null)
139
- expect(cloneDeep(undefined)).toBe(undefined)
140
- expect(cloneDeep(true)).toBe(true)
141
- })
142
-
143
- it('should deep clone plain objects', () => {
144
- const original = { a: 1, b: { c: 2 } }
145
- const cloned = cloneDeep(original)
146
- expect(cloned).toEqual(original)
147
- expect(cloned).not.toBe(original)
148
- expect(cloned.b).not.toBe(original.b)
149
- })
150
-
151
- it('should deep clone arrays', () => {
152
- const original = [1, [2, 3], { a: 4 }]
153
- const cloned = cloneDeep(original)
154
- expect(cloned).toEqual(original)
155
- expect(cloned).not.toBe(original)
156
- expect(cloned[1]).not.toBe(original[1])
157
- expect(cloned[2]).not.toBe(original[2])
158
- })
159
-
160
- it('should clone Date instances', () => {
161
- const original = new Date('2026-01-01')
162
- const cloned = cloneDeep(original)
163
- expect(cloned).toEqual(original)
164
- expect(cloned).not.toBe(original)
165
- expect(cloned.getTime()).toBe(original.getTime())
166
- })
167
-
168
- it('should clone RegExp instances', () => {
169
- const original = /test/gi
170
- const cloned = cloneDeep(original)
171
- expect(cloned).not.toBe(original)
172
- expect(cloned.source).toBe(original.source)
173
- expect(cloned.flags).toBe(original.flags)
174
- })
175
-
176
- it('should clone Map instances', () => {
177
- const original = new Map([['a', { nested: 1 }]])
178
- const cloned = cloneDeep(original)
179
- expect(cloned).not.toBe(original)
180
- expect(cloned.get('a')).toEqual({ nested: 1 })
181
- expect(cloned.get('a')).not.toBe(original.get('a'))
182
- })
183
-
184
- it('should clone Set instances', () => {
185
- const obj = { a: 1 }
186
- const original = new Set([obj])
187
- const cloned = cloneDeep(original)
188
- expect(cloned).not.toBe(original)
189
- expect(cloned.size).toBe(1)
190
- const [clonedItem] = cloned
191
- expect(clonedItem).toEqual(obj)
192
- expect(clonedItem).not.toBe(obj)
193
- })
194
-
195
- it('should handle circular references in objects', () => {
196
- const original: Record<string, unknown> = { a: 1 }
197
- original.self = original
198
- const cloned = cloneDeep(original)
199
- expect(cloned.a).toBe(1)
200
- expect(cloned.self).toBe(cloned)
201
- expect(cloned).not.toBe(original)
202
- })
203
-
204
- it('should handle circular references in arrays', () => {
205
- const original: unknown[] = [1, 2]
206
- original.push(original)
207
- const cloned = cloneDeep(original)
208
- expect(cloned[0]).toBe(1)
209
- expect(cloned[1]).toBe(2)
210
- expect(cloned[2]).toBe(cloned)
211
- expect(cloned).not.toBe(original)
212
- })
213
-
214
- it('should handle circular references in nested objects', () => {
215
- const child: Record<string, unknown> = { value: 'child' }
216
- const parent: Record<string, unknown> = { child }
217
- child.parent = parent
218
- const cloned = cloneDeep(parent)
219
- expect(cloned).not.toBe(parent)
220
- expect(cloned.child).not.toBe(child)
221
- expect((cloned.child as Record<string, unknown>).value).toBe('child')
222
- expect((cloned.child as Record<string, unknown>).parent).toBe(cloned)
223
- })
224
-
225
- it('should handle circular references in Maps', () => {
226
- const original = new Map<string, unknown>()
227
- original.set('self', original)
228
- const cloned = cloneDeep(original)
229
- expect(cloned).not.toBe(original)
230
- expect(cloned.get('self')).toBe(cloned)
231
- })
232
-
233
- it('should handle circular references in Sets', () => {
234
- const original = new Set<unknown>()
235
- original.add(original)
236
- const cloned = cloneDeep(original)
237
- expect(cloned).not.toBe(original)
238
- expect(cloned.has(cloned)).toBe(true)
239
- expect(cloned.size).toBe(1)
240
- })
241
-
242
- it('should clone objects with toJSON method via JSON round-trip', () => {
243
- const original = { value: 42, toJSON: () => ({ value: 42 }) }
244
- const cloned = cloneDeep(original)
245
- expect(cloned).toEqual({ value: 42 })
246
- expect(cloned).not.toBe(original)
247
- })
248
-
249
- it('should clone ArrayBuffer', () => {
250
- const original = new ArrayBuffer(8)
251
- new Uint8Array(original).set([1, 2, 3, 4, 5, 6, 7, 8])
252
- const cloned = cloneDeep(original)
253
- expect(cloned).not.toBe(original)
254
- expect(cloned.byteLength).toBe(8)
255
- expect(new Uint8Array(cloned)).toEqual(new Uint8Array(original))
256
- })
257
-
258
- it('should clone DataView', () => {
259
- const buffer = new ArrayBuffer(16)
260
- const original = new DataView(buffer, 4, 8)
261
- original.setInt32(0, 42)
262
- const cloned = cloneDeep(original)
263
- expect(cloned).not.toBe(original)
264
- expect(cloned.buffer).not.toBe(original.buffer)
265
- expect(cloned.byteOffset).toBe(4)
266
- expect(cloned.byteLength).toBe(8)
267
- expect(cloned.getInt32(0)).toBe(42)
268
- })
269
-
270
- it('should clone TypedArrays with offset and length', () => {
271
- const buffer = new ArrayBuffer(16)
272
- const original = new Uint8Array(buffer, 4, 8)
273
- original.set([10, 20, 30, 40, 50, 60, 70, 80])
274
- const cloned = cloneDeep(original)
275
- expect(cloned).not.toBe(original)
276
- expect(cloned.buffer).not.toBe(original.buffer)
277
- expect(cloned.byteOffset).toBe(4)
278
- expect(cloned.length).toBe(8)
279
- expect(Array.from(cloned)).toEqual([10, 20, 30, 40, 50, 60, 70, 80])
280
- })
281
-
282
- it('should clone Float64Array', () => {
283
- const original = new Float64Array([1.1, 2.2, 3.3])
284
- const cloned = cloneDeep(original)
285
- expect(cloned).not.toBe(original)
286
- expect(cloned.buffer).not.toBe(original.buffer)
287
- expect(Array.from(cloned)).toEqual([1.1, 2.2, 3.3])
288
- })
289
-
290
- it('should clone RegExp with lastIndex', () => {
291
- const original = /foo/g
292
- original.exec('foobar')
293
- expect(original.lastIndex).toBe(3)
294
- const cloned = cloneDeep(original)
295
- expect(cloned).not.toBe(original)
296
- expect(cloned.lastIndex).toBe(3)
297
- expect(cloned.source).toBe('foo')
298
- expect(cloned.flags).toBe('g')
299
- })
300
-
301
- it('should clone object without constructor', () => {
302
- const original = Object.create(null) as Record<string, unknown>
303
- original.a = 1
304
- original.b = { c: 2 }
305
- const cloned = cloneDeep(original)
306
- expect(cloned).not.toBe(original)
307
- expect(cloned.a).toBe(1)
308
- expect(cloned.b).toEqual({ c: 2 })
309
- expect(cloned.b).not.toBe(original.b)
310
- })
311
-
312
- it('should clone Error instances preserving message and stack', () => {
313
- const original = new Error('test error')
314
- const cloned = cloneDeep(original)
315
- expect(cloned).not.toBe(original)
316
- expect(cloned).toBeInstanceOf(Error)
317
- expect(cloned.message).toBe('test error')
318
- expect(cloned.stack).toBe(original.stack)
319
- })
320
-
321
- it('should fall through to cloneCollection for partial BSON-like objects', () => {
322
- const original = { _bsontype: 'SomeType', value: 42 }
323
- const cloned = cloneDeep(original)
324
- expect(cloned).toEqual({ _bsontype: 'SomeType', value: 42 })
325
- expect(cloned).not.toBe(original)
326
- })
327
-
328
- it('should clone BSON-like objects via toHexString', () => {
329
- const hex = '507f1f77bcf86cd799439011'
330
- const original = {
331
- _bsontype: 'ObjectId',
332
- toHexString: () => hex,
333
- toJSON: () => hex,
334
- }
335
- Object.setPrototypeOf(original, {
336
- constructor: class ObjectId {
337
- hex: string
338
- constructor(h: string) {
339
- this.hex = h
340
- }
341
- toHexString() {
342
- return this.hex
343
- }
344
- },
345
- })
346
- const cloned = cloneDeep(original)
347
- expect(cloned).not.toBe(original)
348
- expect(cloned.toHexString()).toBe(hex)
349
- })
350
- })
351
-
352
- describe('setPatchHistoryTTL', () => {
353
- it('should call custom onError handler on failure', async () => {
354
- const indexes = HistoryModel.collection.indexes as Mock
355
- indexes.mockRejectedValue(new Error('connection failed'))
356
-
357
- const onError = vi.fn()
358
- await setPatchHistoryTTL('1h', onError)
359
-
360
- expect(onError).toHaveBeenCalledOnce()
361
- expect(onError).toHaveBeenCalledWith(expect.any(Error))
362
- })
363
-
364
- it('should fall back to console.error when no onError provided', async () => {
365
- const indexes = HistoryModel.collection.indexes as Mock
366
- indexes.mockRejectedValue(new Error('connection failed'))
367
-
368
- const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
369
- await setPatchHistoryTTL('1h')
370
-
371
- expect(spy).toHaveBeenCalledOnce()
372
- })
373
- })
@@ -1,3 +0,0 @@
1
- *
2
- !.gitignore
3
- !server.ts