ts-patch-mongoose 2.9.5 → 3.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.
package/src/helpers.ts CHANGED
@@ -1,8 +1,122 @@
1
- import ms from 'ms'
2
1
  import { HistoryModel } from './model'
2
+ import { type Duration, ms } from './ms'
3
3
 
4
4
  import type { QueryOptions, ToObjectOptions } from 'mongoose'
5
5
 
6
+ export const isArray = Array.isArray
7
+
8
+ export const isEmpty = (value: unknown): boolean => {
9
+ if (value == null) return true
10
+ if (Array.isArray(value) || typeof value === 'string') return value.length === 0
11
+ if (value instanceof Map || value instanceof Set) return value.size === 0
12
+ if (typeof value === 'object') {
13
+ for (const key in value) {
14
+ if (Object.hasOwn(value, key)) return false
15
+ }
16
+ return true
17
+ }
18
+ return true
19
+ }
20
+
21
+ export const isFunction = (value: unknown): value is (...args: unknown[]) => unknown => {
22
+ return typeof value === 'function'
23
+ }
24
+
25
+ export const isObjectLike = (value: unknown): value is Record<string, unknown> => {
26
+ return typeof value === 'object' && value !== null
27
+ }
28
+
29
+ const cloneArrayBuffer = (arrayBuffer: ArrayBuffer): ArrayBuffer => {
30
+ const result = new ArrayBuffer(arrayBuffer.byteLength)
31
+ new Uint8Array(result).set(new Uint8Array(arrayBuffer))
32
+ return result
33
+ }
34
+
35
+ const cloneImmutable = <T>(value: T): T | undefined => {
36
+ const tag = Object.prototype.toString.call(value)
37
+
38
+ switch (tag) {
39
+ case '[object Date]':
40
+ return new Date(+(value as unknown as Date)) as T
41
+ case '[object RegExp]': {
42
+ const re = value as unknown as RegExp
43
+ const cloned = new RegExp(re.source, re.flags)
44
+ cloned.lastIndex = re.lastIndex
45
+ return cloned as T
46
+ }
47
+ case '[object ArrayBuffer]':
48
+ return cloneArrayBuffer(value as unknown as ArrayBuffer) as T
49
+ case '[object DataView]': {
50
+ const dv = value as unknown as DataView
51
+ const buffer = cloneArrayBuffer(dv.buffer as ArrayBuffer)
52
+ return new DataView(buffer, dv.byteOffset, dv.byteLength) as T
53
+ }
54
+ }
55
+
56
+ if (ArrayBuffer.isView(value)) {
57
+ const ta = value as unknown as { buffer: ArrayBuffer; byteOffset: number; length: number }
58
+ const buffer = cloneArrayBuffer(ta.buffer)
59
+ return new (value.constructor as new (buffer: ArrayBuffer, byteOffset: number, length: number) => T)(buffer, ta.byteOffset, ta.length)
60
+ }
61
+
62
+ return undefined
63
+ }
64
+
65
+ const cloneCollection = <T extends object>(value: T, seen: WeakMap<object, unknown>): T => {
66
+ if (value instanceof Map) {
67
+ const map = new Map()
68
+ seen.set(value, map)
69
+ for (const [k, v] of value) map.set(k, cloneDeep(v, seen))
70
+ return map as T
71
+ }
72
+
73
+ if (value instanceof Set) {
74
+ const set = new Set()
75
+ seen.set(value, set)
76
+ for (const v of value) set.add(cloneDeep(v, seen))
77
+ return set as T
78
+ }
79
+
80
+ if (Array.isArray(value)) {
81
+ const arr = new Array(value.length) as unknown[]
82
+ seen.set(value, arr)
83
+ for (let i = 0; i < value.length; i++) {
84
+ arr[i] = cloneDeep(value[i], seen)
85
+ }
86
+ return arr as T
87
+ }
88
+
89
+ const result = typeof value.constructor === 'function' ? (Object.create(Object.getPrototypeOf(value) as object) as T) : ({} as T)
90
+ seen.set(value, result)
91
+ for (const key of Object.keys(value)) {
92
+ ;(result as Record<string, unknown>)[key] = cloneDeep((value as Record<string, unknown>)[key], seen)
93
+ }
94
+ return result
95
+ }
96
+
97
+ export const cloneDeep = <T>(value: T, seen = new WeakMap<object, unknown>()): T => {
98
+ if (value === null || typeof value !== 'object') return value
99
+ if (seen.has(value)) return seen.get(value) as T
100
+
101
+ const immutable = cloneImmutable(value)
102
+ if (immutable !== undefined) return immutable
103
+
104
+ if ('toJSON' in value && typeof (value as Record<string, unknown>).toJSON === 'function') {
105
+ // NOSONAR — structuredClone cannot handle objects with non-cloneable methods (e.g. mongoose documents)
106
+ return JSON.parse(JSON.stringify(value)) as T
107
+ }
108
+
109
+ return cloneCollection(value, seen)
110
+ }
111
+
112
+ export const chunk = <T>(array: T[], size: number): T[][] => {
113
+ const result: T[][] = []
114
+ for (let i = 0; i < array.length; i += size) {
115
+ result.push(array.slice(i, i + size))
116
+ }
117
+ return result
118
+ }
119
+
6
120
  export const isHookIgnored = <T>(options: QueryOptions<T>): boolean => {
7
121
  return options.ignoreHook === true || (options.ignoreEvent === true && options.ignorePatchHistory === true)
8
122
  }
@@ -12,21 +126,19 @@ export const toObjectOptions: ToObjectOptions = {
12
126
  virtuals: false,
13
127
  }
14
128
 
15
- export const setPatchHistoryTTL = async (ttl: number | ms.StringValue): Promise<void> => {
16
- const name = 'createdAt_1_TTL' // To avoid collision with user defined index / manually created index
129
+ export const setPatchHistoryTTL = async (ttl: Duration): Promise<void> => {
130
+ const name = 'createdAt_1_TTL'
17
131
  try {
18
132
  const indexes = await HistoryModel.collection.indexes()
19
133
  const existingIndex = indexes?.find((index) => index.name === name)
20
134
 
21
- // Drop the index if historyTTL is not set and index exists
22
135
  if (!ttl && existingIndex) {
23
136
  await HistoryModel.collection.dropIndex(name)
24
137
  return
25
138
  }
26
139
 
27
- const milliseconds = typeof ttl === 'string' ? ms(ttl) : ttl
140
+ const milliseconds = ms(ttl)
28
141
 
29
- // Drop the index if historyTTL is less than 1 second and index exists
30
142
  if (milliseconds < 1000 && existingIndex) {
31
143
  await HistoryModel.collection.dropIndex(name)
32
144
  return
@@ -35,16 +147,13 @@ export const setPatchHistoryTTL = async (ttl: number | ms.StringValue): Promise<
35
147
  const expireAfterSeconds = milliseconds / 1000
36
148
 
37
149
  if (existingIndex && existingIndex.expireAfterSeconds === expireAfterSeconds) {
38
- // Index already exists with the correct TTL, no need to recreate
39
150
  return
40
151
  }
41
152
 
42
153
  if (existingIndex) {
43
- // Drop the existing index if it exists and TTL is different
44
154
  await HistoryModel.collection.dropIndex(name)
45
155
  }
46
156
 
47
- // Create a new index with the correct TTL if it doesn't exist or if the TTL is different
48
157
  await HistoryModel.collection.createIndex({ createdAt: 1 }, { expireAfterSeconds, name })
49
158
  } catch (err) {
50
159
  console.error("Couldn't create or update index for history collection", err)
@@ -1,7 +1,4 @@
1
- // Using CJS lodash with .js extensions for ESM compatibility
2
- import isArray from 'lodash/isArray.js'
3
- import isEmpty from 'lodash/isEmpty.js'
4
- import { isHookIgnored } from '../helpers'
1
+ import { isArray, isEmpty, isHookIgnored } from '../helpers'
5
2
  import { deletePatch } from '../patch'
6
3
 
7
4
  import type { HydratedDocument, Model, MongooseQueryMiddleware, Schema } from 'mongoose'
@@ -1,12 +1,5 @@
1
- // Using CJS lodash with .js extensions for ESM compatibility
2
- import cloneDeep from 'lodash/cloneDeep.js'
3
- import forEach from 'lodash/forEach.js'
4
- import isArray from 'lodash/isArray.js'
5
- import isEmpty from 'lodash/isEmpty.js'
6
- import isObjectLike from 'lodash/isObjectLike.js'
7
- import keys from 'lodash/keys.js'
8
1
  import { assign } from 'power-assign'
9
- import { isHookIgnored, toObjectOptions } from '../helpers'
2
+ import { cloneDeep, isArray, isEmpty, isHookIgnored, isObjectLike, toObjectOptions } from '../helpers'
10
3
  import { createPatch, updatePatch } from '../patch'
11
4
 
12
5
  import type { HydratedDocument, Model, MongooseQueryMiddleware, Schema, UpdateQuery, UpdateWithAggregationPipeline } from 'mongoose'
@@ -16,14 +9,13 @@ const updateMethods = ['update', 'updateOne', 'replaceOne', 'updateMany', 'findO
16
9
 
17
10
  export const assignUpdate = <T>(document: HydratedDocument<T>, update: UpdateQuery<T>, commands: Record<string, unknown>[]): HydratedDocument<T> => {
18
11
  let updated = assign(document.toObject(toObjectOptions), update)
19
- // Try catch not working for of loop, keep it as is
20
- forEach(commands, (command) => {
12
+ for (const command of commands) {
21
13
  try {
22
14
  updated = assign(updated, command)
23
15
  } catch {
24
16
  // we catch assign keys that are not implemented
25
17
  }
26
- })
18
+ }
27
19
 
28
20
  const doc = document.set(updated).toObject(toObjectOptions) as HydratedDocument<T> & { createdAt?: Date }
29
21
  if (update.createdAt) doc.createdAt = update.createdAt
@@ -36,12 +28,12 @@ export const splitUpdateAndCommands = <T>(updateQuery: UpdateWithAggregationPipe
36
28
 
37
29
  if (!isEmpty(updateQuery) && !isArray(updateQuery) && isObjectLike(updateQuery)) {
38
30
  update = cloneDeep(updateQuery)
39
- const keysWithDollarSign = keys(update).filter((key) => key.startsWith('$'))
31
+ const keysWithDollarSign = Object.keys(update).filter((key) => key.startsWith('$'))
40
32
  if (!isEmpty(keysWithDollarSign)) {
41
- forEach(keysWithDollarSign, (key) => {
33
+ for (const key of keysWithDollarSign) {
42
34
  commands.push({ [key]: update[key] as unknown })
43
35
  delete update[key]
44
- })
36
+ }
45
37
  }
46
38
  }
47
39
 
@@ -98,7 +90,6 @@ export const updateHooksInitialize = <T>(schema: Schema<T>, opts: PluginOptions<
98
90
  }
99
91
 
100
92
  if (!isEmpty(filter) && !current) {
101
- console.log('filter', filter)
102
93
  current = (await model.findOne(filter).sort('desc').lean().exec()) as HydratedDocument<T>
103
94
  }
104
95
 
package/src/index.ts CHANGED
@@ -1,6 +1,4 @@
1
- // Using CJS lodash with .js extension for ESM compatibility
2
- import isEmpty from 'lodash/isEmpty.js'
3
- import { toObjectOptions } from './helpers'
1
+ import { isEmpty, toObjectOptions } from './helpers'
4
2
  import { deleteHooksInitialize } from './hooks/delete-hooks'
5
3
  import { saveHooksInitialize } from './hooks/save-hooks'
6
4
  import { updateHooksInitialize } from './hooks/update-hooks'
@@ -12,34 +10,17 @@ import type { PatchContext, PluginOptions } from './types'
12
10
 
13
11
  const remove = isMongooseLessThan7 ? 'remove' : 'deleteOne'
14
12
 
15
- /**
16
- * @description Event emitter for patch operations.
17
- */
18
13
  export { default as patchEventEmitter } from './em'
19
14
  export { setPatchHistoryTTL } from './helpers'
20
15
  export * from './types'
21
16
 
22
- /**
23
- * @description Mongoose plugin to track and manage patch history for documents.
24
- * This plugin initializes hooks for save, update, and delete operations to create
25
- * and manage patches.
26
- *
27
- * @template T
28
- * @param {Schema<T>} schema - The Mongoose schema to apply the plugin to.
29
- * @param {PluginOptions<T>} opts - Options for configuring the plugin.
30
- * @returns {void}
31
- */
32
- export const patchHistoryPlugin = function plugin<T>(schema: Schema<T>, opts: PluginOptions<T>): void {
33
- // Initialize hooks
17
+ export type { Duration } from './ms'
18
+
19
+ export const patchHistoryPlugin = <T>(schema: Schema<T>, opts: PluginOptions<T>): void => {
34
20
  saveHooksInitialize(schema, opts)
35
21
  updateHooksInitialize(schema, opts)
36
22
  deleteHooksInitialize(schema, opts)
37
23
 
38
- /**
39
- * @description Corner case for insertMany()
40
- * @param {Array<HydratedDocument<T>>} docs - The documents being inserted.
41
- * @returns {Promise<void>}
42
- */
43
24
  schema.post('insertMany', async function (docs) {
44
25
  const context = {
45
26
  op: 'create',
@@ -55,10 +36,6 @@ export const patchHistoryPlugin = function plugin<T>(schema: Schema<T>, opts: Pl
55
36
  // In Mongoose 7, doc.deleteOne() returned a promise that resolved to doc.
56
37
  // In Mongoose 8, doc.deleteOne() returns a query for easier chaining, as well as consistency with doc.updateOne().
57
38
  if (isMongooseLessThan8) {
58
- /**
59
- * @description Pre-delete hook for Mongoose 7 and below
60
- * @returns {Promise<void>}
61
- */
62
39
  // @ts-expect-error - Mongoose 7 and below
63
40
  schema.pre(remove, { document: true, query: false }, async function () {
64
41
  // @ts-expect-error - Mongoose 7 and below
@@ -69,11 +46,6 @@ export const patchHistoryPlugin = function plugin<T>(schema: Schema<T>, opts: Pl
69
46
  }
70
47
  })
71
48
 
72
- /**
73
- * @description Post-delete hook for Mongoose 7 and below
74
- * @param {HydratedDocument<T>} this - The document being deleted.
75
- * @returns {Promise<void>}
76
- */
77
49
  // @ts-expect-error - Mongoose 7 and below
78
50
  schema.post(remove, { document: true, query: false }, async function (this: HydratedDocument<T>) {
79
51
  const original = this.toObject(toObjectOptions) as HydratedDocument<T>
package/src/ms.ts ADDED
@@ -0,0 +1,66 @@
1
+ const s = 1000
2
+ const m = s * 60
3
+ const h = m * 60
4
+ const d = h * 24
5
+ const w = d * 7
6
+ const y = d * 365.25
7
+ const mo = y / 12
8
+
9
+ export const UNITS = {
10
+ milliseconds: 1,
11
+ millisecond: 1,
12
+ msecs: 1,
13
+ msec: 1,
14
+ ms: 1,
15
+ seconds: s,
16
+ second: s,
17
+ secs: s,
18
+ sec: s,
19
+ s,
20
+ minutes: m,
21
+ minute: m,
22
+ mins: m,
23
+ min: m,
24
+ m,
25
+ hours: h,
26
+ hour: h,
27
+ hrs: h,
28
+ hr: h,
29
+ h,
30
+ days: d,
31
+ day: d,
32
+ d,
33
+ weeks: w,
34
+ week: w,
35
+ w,
36
+ months: mo,
37
+ month: mo,
38
+ mo,
39
+ years: y,
40
+ year: y,
41
+ yrs: y,
42
+ yr: y,
43
+ y,
44
+ } as const satisfies Record<string, number>
45
+
46
+ export type Unit = keyof typeof UNITS
47
+
48
+ export type Duration = number | `${number}` | `${number}${Unit}` | `${number} ${Unit}`
49
+
50
+ const unitPattern = Object.keys(UNITS)
51
+ .sort((a, b) => b.length - a.length)
52
+ .join('|')
53
+
54
+ const RE = new RegExp(String.raw`^(-?(?:\d+)?\.?\d+)\s*(${unitPattern})?$`, 'i')
55
+
56
+ export const ms = (val: Duration): number => {
57
+ const str = String(val)
58
+ if (str.length > 100) return Number.NaN
59
+
60
+ const match = RE.exec(str)
61
+ if (!match) return Number.NaN
62
+
63
+ const n = Number.parseFloat(match[1] ?? '')
64
+ const type = (match[2] ?? 'ms').toLowerCase()
65
+ return n * (UNITS[type as Unit] ?? 0)
66
+ }
@@ -0,0 +1,95 @@
1
+ const isPlainObject = (val: unknown): val is Record<string, unknown> => {
2
+ if (Object.prototype.toString.call(val) !== '[object Object]') return false
3
+ const prot = Object.getPrototypeOf(val) as object | null
4
+ return prot === null || prot === Object.prototype
5
+ }
6
+
7
+ const isUnsafeKey = (key: string): boolean => {
8
+ return key === '__proto__' || key === 'constructor' || key === 'prototype'
9
+ }
10
+
11
+ const getValue = (obj: Record<string, unknown>, path: string): unknown => {
12
+ const segs = path.split('.')
13
+ let current: unknown = obj
14
+ for (const seg of segs) {
15
+ if (current == null || typeof current !== 'object') return undefined
16
+ current = (current as Record<string, unknown>)[seg]
17
+ }
18
+ return current
19
+ }
20
+
21
+ const hasValue = (val: unknown): boolean => {
22
+ if (val == null) return false
23
+ if (typeof val === 'boolean' || typeof val === 'number' || typeof val === 'function') return true
24
+ if (typeof val === 'string') return val.length !== 0
25
+ if (Array.isArray(val)) return val.length !== 0
26
+ if (val instanceof RegExp) return val.source !== '(?:)' && val.source !== ''
27
+ if (val instanceof Error) return val.message !== ''
28
+ if (val instanceof Map || val instanceof Set) return val.size !== 0
29
+ if (typeof val === 'object') {
30
+ for (const key of Object.keys(val)) {
31
+ if (hasValue((val as Record<string, unknown>)[key])) return true
32
+ }
33
+ return false
34
+ }
35
+ return true
36
+ }
37
+
38
+ const has = (obj: unknown, path: string): boolean => {
39
+ if (obj != null && typeof obj === 'object' && typeof path === 'string') {
40
+ return hasValue(getValue(obj as Record<string, unknown>, path))
41
+ }
42
+ return false
43
+ }
44
+
45
+ const unset = (obj: Record<string, unknown>, prop: string): boolean => {
46
+ if (typeof obj !== 'object' || obj === null) return false
47
+
48
+ if (Object.hasOwn(obj, prop)) {
49
+ delete obj[prop]
50
+ return true
51
+ }
52
+
53
+ if (has(obj, prop)) {
54
+ const segs = prop.split('.')
55
+ let last = segs.pop()
56
+ while (segs.length && segs.at(-1)?.slice(-1) === '\\') {
57
+ last = `${(segs.pop() as string).slice(0, -1)}.${last}`
58
+ }
59
+ let target: unknown = obj
60
+ while (segs.length) {
61
+ const seg = segs.shift() as string
62
+ if (isUnsafeKey(seg)) return false
63
+ target = (target as Record<string, unknown>)[seg]
64
+ }
65
+ return delete (target as Record<string, unknown>)[last ?? '']
66
+ }
67
+
68
+ return true
69
+ }
70
+
71
+ export const omitDeep = <T>(value: T, keys: string | string[]): T => {
72
+ if (value === undefined) return {} as T
73
+
74
+ if (Array.isArray(value)) {
75
+ for (let i = 0; i < value.length; i++) {
76
+ value[i] = omitDeep(value[i], keys)
77
+ }
78
+ return value
79
+ }
80
+
81
+ if (!isPlainObject(value)) return value
82
+
83
+ const omitKeys = typeof keys === 'string' ? [keys] : keys
84
+ if (!Array.isArray(omitKeys)) return value
85
+
86
+ for (const key of omitKeys) {
87
+ unset(value, key)
88
+ }
89
+
90
+ for (const key of Object.keys(value)) {
91
+ ;(value as Record<string, unknown>)[key] = omitDeep((value as Record<string, unknown>)[key], omitKeys)
92
+ }
93
+
94
+ return value
95
+ }
package/src/patch.ts CHANGED
@@ -1,20 +1,18 @@
1
1
  import jsonpatch from 'fast-json-patch'
2
- // Using CJS lodash with .js extensions for ESM compatibility
3
- import chunk from 'lodash/chunk.js'
4
- import isEmpty from 'lodash/isEmpty.js'
5
- import isFunction from 'lodash/isFunction.js'
6
- import omit from 'omit-deep'
7
2
  import em from './em'
3
+ import { chunk, isEmpty, isFunction } from './helpers'
8
4
  import { HistoryModel } from './model'
5
+ import { omitDeep as omit } from './omit-deep'
9
6
 
10
7
  import type { HydratedDocument, MongooseError, Types } from 'mongoose'
11
8
  import type { Metadata, PatchContext, PatchEvent, PluginOptions, User } from './types'
12
9
 
13
- function isPatchHistoryEnabled<T>(opts: PluginOptions<T>, context: PatchContext<T>): boolean {
10
+ const isPatchHistoryEnabled = <T>(opts: PluginOptions<T>, context: PatchContext<T>): boolean => {
14
11
  return !opts.patchHistoryDisabled && !context.ignorePatchHistory
15
12
  }
16
13
 
17
- export function getJsonOmit<T>(opts: PluginOptions<T>, doc: HydratedDocument<T>): Partial<T> {
14
+ export const getJsonOmit = <T>(opts: PluginOptions<T>, doc: HydratedDocument<T>): Partial<T> => {
15
+ // NOSONAR — structuredClone cannot handle mongoose documents (they contain non-cloneable methods)
18
16
  const object = JSON.parse(JSON.stringify(doc)) as Partial<T>
19
17
 
20
18
  if (opts.omit) {
@@ -24,7 +22,7 @@ export function getJsonOmit<T>(opts: PluginOptions<T>, doc: HydratedDocument<T>)
24
22
  return object
25
23
  }
26
24
 
27
- export function getObjectOmit<T>(opts: PluginOptions<T>, doc: HydratedDocument<T>): Partial<T> {
25
+ export const getObjectOmit = <T>(opts: PluginOptions<T>, doc: HydratedDocument<T>): Partial<T> => {
28
26
  if (opts.omit) {
29
27
  return omit(isFunction(doc?.toObject) ? doc.toObject() : doc, opts.omit)
30
28
  }
@@ -32,56 +30,56 @@ export function getObjectOmit<T>(opts: PluginOptions<T>, doc: HydratedDocument<T
32
30
  return doc
33
31
  }
34
32
 
35
- export async function getUser<T>(opts: PluginOptions<T>, doc: HydratedDocument<T>): Promise<User | undefined> {
33
+ export const getUser = async <T>(opts: PluginOptions<T>, doc: HydratedDocument<T>): Promise<User | undefined> => {
36
34
  if (isFunction(opts.getUser)) {
37
35
  return await opts.getUser(doc)
38
36
  }
39
37
  return undefined
40
38
  }
41
39
 
42
- export async function getReason<T>(opts: PluginOptions<T>, doc: HydratedDocument<T>): Promise<string | undefined> {
40
+ export const getReason = async <T>(opts: PluginOptions<T>, doc: HydratedDocument<T>): Promise<string | undefined> => {
43
41
  if (isFunction(opts.getReason)) {
44
42
  return await opts.getReason(doc)
45
43
  }
46
44
  return undefined
47
45
  }
48
46
 
49
- export async function getMetadata<T>(opts: PluginOptions<T>, doc: HydratedDocument<T>): Promise<Metadata | undefined> {
47
+ export const getMetadata = async <T>(opts: PluginOptions<T>, doc: HydratedDocument<T>): Promise<Metadata | undefined> => {
50
48
  if (isFunction(opts.getMetadata)) {
51
49
  return await opts.getMetadata(doc)
52
50
  }
53
51
  return undefined
54
52
  }
55
53
 
56
- export function getValue<T>(item: PromiseSettledResult<T>): T | undefined {
54
+ export const getValue = <T>(item: PromiseSettledResult<T>): T | undefined => {
57
55
  return item.status === 'fulfilled' ? item.value : undefined
58
56
  }
59
57
 
60
- export async function getData<T>(opts: PluginOptions<T>, doc: HydratedDocument<T>): Promise<[User | undefined, string | undefined, Metadata | undefined]> {
58
+ export const getData = async <T>(opts: PluginOptions<T>, doc: HydratedDocument<T>): Promise<[User | undefined, string | undefined, Metadata | undefined]> => {
61
59
  return Promise.allSettled([getUser(opts, doc), getReason(opts, doc), getMetadata(opts, doc)]).then(([user, reason, metadata]) => {
62
60
  return [getValue(user), getValue(reason), getValue(metadata)]
63
61
  })
64
62
  }
65
63
 
66
- export function emitEvent<T>(context: PatchContext<T>, event: string | undefined, data: PatchEvent<T>): void {
64
+ export const emitEvent = <T>(context: PatchContext<T>, event: string | undefined, data: PatchEvent<T>): void => {
67
65
  if (event && !context.ignoreEvent) {
68
66
  em.emit(event, data)
69
67
  }
70
68
  }
71
69
 
72
- export async function bulkPatch<T>(opts: PluginOptions<T>, context: PatchContext<T>, eventKey: 'eventCreated' | 'eventDeleted', docsKey: 'createdDocs' | 'deletedDocs'): Promise<void> {
70
+ export const bulkPatch = async <T>(opts: PluginOptions<T>, context: PatchContext<T>, eventKey: 'eventCreated' | 'eventDeleted', docsKey: 'createdDocs' | 'deletedDocs'): Promise<void> => {
73
71
  const history = isPatchHistoryEnabled(opts, context)
74
72
  const event = opts[eventKey]
75
73
  const docs = context[docsKey]
76
74
  const key = eventKey === 'eventCreated' ? 'doc' : 'oldDoc'
77
75
 
78
- if (isEmpty(docs) || (!event && !history)) return
76
+ if (isEmpty(docs) || !docs || (!event && !history)) return
79
77
 
80
78
  const chunks = chunk(docs, 1000)
81
- for (const chunk of chunks) {
79
+ for (const batch of chunks) {
82
80
  const bulk = []
83
81
 
84
- for (const doc of chunk) {
82
+ for (const doc of batch) {
85
83
  emitEvent(context, event, { [key]: doc })
86
84
 
87
85
  if (history) {
@@ -94,10 +92,10 @@ export async function bulkPatch<T>(opts: PluginOptions<T>, context: PatchContext
94
92
  collectionName: context.collectionName,
95
93
  collectionId: doc._id as Types.ObjectId,
96
94
  doc: getObjectOmit(opts, doc),
97
- user,
98
- reason,
99
- metadata,
100
95
  version: 0,
96
+ ...(user !== undefined && { user }),
97
+ ...(reason !== undefined && { reason }),
98
+ ...(metadata !== undefined && { metadata }),
101
99
  },
102
100
  },
103
101
  })
@@ -112,11 +110,11 @@ export async function bulkPatch<T>(opts: PluginOptions<T>, context: PatchContext
112
110
  }
113
111
  }
114
112
 
115
- export async function createPatch<T>(opts: PluginOptions<T>, context: PatchContext<T>): Promise<void> {
113
+ export const createPatch = async <T>(opts: PluginOptions<T>, context: PatchContext<T>): Promise<void> => {
116
114
  await bulkPatch(opts, context, 'eventCreated', 'createdDocs')
117
115
  }
118
116
 
119
- export async function updatePatch<T>(opts: PluginOptions<T>, context: PatchContext<T>, current: HydratedDocument<T>, original: HydratedDocument<T>): Promise<void> {
117
+ export const updatePatch = async <T>(opts: PluginOptions<T>, context: PatchContext<T>, current: HydratedDocument<T>, original: HydratedDocument<T>): Promise<void> => {
120
118
  const history = isPatchHistoryEnabled(opts, context)
121
119
 
122
120
  const currentObject = getJsonOmit(opts, current)
@@ -146,14 +144,14 @@ export async function updatePatch<T>(opts: PluginOptions<T>, context: PatchConte
146
144
  collectionName: context.collectionName,
147
145
  collectionId: original._id as Types.ObjectId,
148
146
  patch,
149
- user,
150
- reason,
151
- metadata,
152
147
  version,
148
+ ...(user !== undefined && { user }),
149
+ ...(reason !== undefined && { reason }),
150
+ ...(metadata !== undefined && { metadata }),
153
151
  })
154
152
  }
155
153
  }
156
154
 
157
- export async function deletePatch<T>(opts: PluginOptions<T>, context: PatchContext<T>): Promise<void> {
155
+ export const deletePatch = async <T>(opts: PluginOptions<T>, context: PatchContext<T>): Promise<void> => {
158
156
  await bulkPatch(opts, context, 'eventDeleted', 'deletedDocs')
159
157
  }
package/src/version.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import mongoose from 'mongoose'
2
- import { satisfies } from 'semver'
3
2
 
4
- export const isMongooseLessThan8 = satisfies(mongoose.version, '<8')
5
- export const isMongooseLessThan7 = satisfies(mongoose.version, '<7')
6
- export const isMongoose6 = satisfies(mongoose.version, '6')
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
7
8
 
8
9
  /* v8 ignore start */
9
10
  if (isMongoose6) {
package/tests/em.test.ts CHANGED
@@ -27,6 +27,7 @@ describe('em', () => {
27
27
  collectionName: 'tests',
28
28
  }
29
29
 
30
+ // @ts-expect-error expected
30
31
  emitEvent(context, 'test', { doc: { name: 'test' } })
31
32
  expect(fn).toHaveBeenCalledTimes(1)
32
33
 
@@ -44,6 +45,7 @@ describe('em', () => {
44
45
  collectionName: 'tests',
45
46
  }
46
47
 
48
+ // @ts-expect-error expected
47
49
  emitEvent(context, 'test', { doc: { name: 'test' } })
48
50
  expect(fn).toHaveBeenCalledTimes(0)
49
51