ts-patch-mongoose 2.9.6 → 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/README.md +42 -27
- package/biome.json +1 -1
- package/dist/index.cjs +273 -53
- package/dist/index.d.cts +41 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +41 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +273 -53
- package/package.json +12 -18
- package/src/helpers.ts +118 -9
- package/src/hooks/delete-hooks.ts +1 -4
- package/src/hooks/update-hooks.ts +6 -15
- package/src/index.ts +4 -32
- package/src/ms.ts +66 -0
- package/src/omit-deep.ts +95 -0
- package/src/patch.ts +19 -21
- package/src/version.ts +5 -4
- package/tests/em.test.ts +2 -0
- package/tests/helpers.test.ts +229 -2
- package/tests/ms.test.ts +113 -0
- package/tests/omit-deep.test.ts +220 -0
- package/tests/plugin-all-features.test.ts +741 -0
- package/tests/plugin-complex-data.test.ts +1332 -0
- package/tsconfig.json +2 -3
- package/src/modules/omit-deep.d.ts +0 -3
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
+
}
|
package/src/omit-deep.ts
ADDED
|
@@ -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
|
-
|
|
10
|
+
const isPatchHistoryEnabled = <T>(opts: PluginOptions<T>, context: PatchContext<T>): boolean => {
|
|
14
11
|
return !opts.patchHistoryDisabled && !context.ignorePatchHistory
|
|
15
12
|
}
|
|
16
13
|
|
|
17
|
-
export
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
79
|
+
for (const batch of chunks) {
|
|
82
80
|
const bulk = []
|
|
83
81
|
|
|
84
|
-
for (const doc of
|
|
82
|
+
for (const doc of batch) {
|
|
85
83
|
emitEvent(context, event, { [key]: doc })
|
|
86
84
|
|
|
87
85
|
if (history) {
|
|
@@ -112,11 +110,11 @@ export async function bulkPatch<T>(opts: PluginOptions<T>, context: PatchContext
|
|
|
112
110
|
}
|
|
113
111
|
}
|
|
114
112
|
|
|
115
|
-
export async
|
|
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
|
|
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)
|
|
@@ -154,6 +152,6 @@ export async function updatePatch<T>(opts: PluginOptions<T>, context: PatchConte
|
|
|
154
152
|
}
|
|
155
153
|
}
|
|
156
154
|
|
|
157
|
-
export async
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
export const
|
|
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
|
|
package/tests/helpers.test.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Mock, MockInstance } from 'vitest'
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
import { setPatchHistoryTTL } from '../src/helpers'
|
|
4
|
+
import { cloneDeep, isEmpty, setPatchHistoryTTL } from '../src/helpers'
|
|
6
5
|
import { HistoryModel } from '../src/model'
|
|
6
|
+
import { ms } from '../src/ms'
|
|
7
7
|
|
|
8
8
|
vi.mock('../src/model', () => ({
|
|
9
9
|
HistoryModel: {
|
|
@@ -82,3 +82,230 @@ describe('useTTL', () => {
|
|
|
82
82
|
expect(createIndexSpy).toHaveBeenCalledWith({ createdAt: 1 }, { expireAfterSeconds, name })
|
|
83
83
|
})
|
|
84
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
|
+
})
|