ts-patch-mongoose 3.1.2 → 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.
- package/README.md +9 -3
- package/dist/index.cjs +275 -23
- package/dist/index.d.cts +21 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +21 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +275 -23
- package/package.json +8 -12
- package/src/em.ts +0 -6
- package/src/helpers.ts +0 -174
- package/src/hooks/delete-hooks.ts +0 -49
- package/src/hooks/save-hooks.ts +0 -30
- package/src/hooks/update-hooks.ts +0 -130
- package/src/index.ts +0 -68
- package/src/model.ts +0 -50
- package/src/modules/power-assign.d.ts +0 -3
- package/src/ms.ts +0 -67
- package/src/omit-deep.ts +0 -56
- package/src/patch.ts +0 -154
- package/src/types.ts +0 -55
- package/src/version.ts +0 -13
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { assign } from 'power-assign'
|
|
2
|
-
import { cloneDeep, isArray, isEmpty, isHookIgnored, isObjectLike, toObjectOptions } from '../helpers'
|
|
3
|
-
import { createPatch, updatePatch } from '../patch'
|
|
4
|
-
|
|
5
|
-
import type { HydratedDocument, Model, MongooseQueryMiddleware, Schema, UpdateQuery, UpdateWithAggregationPipeline } from 'mongoose'
|
|
6
|
-
import type { HookContext, PluginOptions } from '../types'
|
|
7
|
-
|
|
8
|
-
const updateMethods = ['update', 'updateOne', 'replaceOne', 'updateMany', 'findOneAndUpdate', 'findOneAndReplace', 'findByIdAndUpdate']
|
|
9
|
-
|
|
10
|
-
const trackChangedFields = (fields: Record<string, unknown> | undefined, updated: Record<string, unknown>, changed: Map<string, unknown>): void => {
|
|
11
|
-
if (!fields) return
|
|
12
|
-
for (const key of Object.keys(fields)) {
|
|
13
|
-
const [root = key] = key.split('.')
|
|
14
|
-
changed.set(root, updated[root])
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const applyPullAll = (updated: Record<string, unknown>, fields: Record<string, unknown[]>, changed: Map<string, unknown>): void => {
|
|
19
|
-
for (const [field, values] of Object.entries(fields)) {
|
|
20
|
-
const arr = updated[field]
|
|
21
|
-
if (Array.isArray(arr)) {
|
|
22
|
-
const filtered = arr.filter((item: unknown) => !values.some((v) => JSON.stringify(v) === JSON.stringify(item)))
|
|
23
|
-
updated[field] = filtered
|
|
24
|
-
changed.set(field, filtered)
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export const assignUpdate = <T>(document: HydratedDocument<T>, update: UpdateQuery<T>, commands: Record<string, unknown>[]): HydratedDocument<T> => {
|
|
30
|
-
let updated = assign(document.toObject(toObjectOptions), update) as Record<string, unknown>
|
|
31
|
-
const changedByCommand = new Map<string, unknown>()
|
|
32
|
-
|
|
33
|
-
for (const command of commands) {
|
|
34
|
-
const [op = ''] = Object.keys(command)
|
|
35
|
-
const fields = command[op] as Record<string, unknown> | undefined
|
|
36
|
-
try {
|
|
37
|
-
updated = assign(updated, command)
|
|
38
|
-
trackChangedFields(fields, updated, changedByCommand)
|
|
39
|
-
} catch {
|
|
40
|
-
if (op === '$pullAll' && fields) {
|
|
41
|
-
applyPullAll(updated, fields as Record<string, unknown[]>, changedByCommand)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const doc = document.set(updated).toObject(toObjectOptions) as HydratedDocument<T> & { createdAt?: Date }
|
|
47
|
-
for (const [field, value] of changedByCommand) {
|
|
48
|
-
;(doc as unknown as Record<string, unknown>)[field] = value
|
|
49
|
-
}
|
|
50
|
-
if (update.createdAt) doc.createdAt = update.createdAt
|
|
51
|
-
return doc
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export const splitUpdateAndCommands = <T>(updateQuery: UpdateWithAggregationPipeline | UpdateQuery<T> | null): { update: UpdateQuery<T>; commands: Record<string, unknown>[] } => {
|
|
55
|
-
let update: UpdateQuery<T> = {}
|
|
56
|
-
const commands: Record<string, unknown>[] = []
|
|
57
|
-
|
|
58
|
-
if (!isEmpty(updateQuery) && !isArray(updateQuery) && isObjectLike(updateQuery)) {
|
|
59
|
-
update = cloneDeep(updateQuery)
|
|
60
|
-
const keysWithDollarSign = Object.keys(update).filter((key) => key.startsWith('$'))
|
|
61
|
-
if (!isEmpty(keysWithDollarSign)) {
|
|
62
|
-
for (const key of keysWithDollarSign) {
|
|
63
|
-
commands.push({ [key]: update[key] as unknown })
|
|
64
|
-
delete update[key]
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return { update, commands }
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export const updateHooksInitialize = <T>(schema: Schema<T>, opts: PluginOptions<T>): void => {
|
|
73
|
-
schema.pre(updateMethods as MongooseQueryMiddleware[], { document: false, query: true }, async function (this: HookContext<T>) {
|
|
74
|
-
const options = this.getOptions()
|
|
75
|
-
if (isHookIgnored(options)) return
|
|
76
|
-
|
|
77
|
-
const model = this.model as Model<T>
|
|
78
|
-
const filter = this.getFilter()
|
|
79
|
-
|
|
80
|
-
this._context = {
|
|
81
|
-
op: this.op,
|
|
82
|
-
modelName: opts.modelName ?? model.modelName,
|
|
83
|
-
collectionName: opts.collectionName ?? model.collection.collectionName,
|
|
84
|
-
isNew: Boolean(options.upsert) && (await model.countDocuments(filter).exec()) === 0,
|
|
85
|
-
ignoreEvent: options.ignoreEvent as boolean,
|
|
86
|
-
ignorePatchHistory: options.ignorePatchHistory as boolean,
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const updateQuery = this.getUpdate()
|
|
90
|
-
const { update, commands } = splitUpdateAndCommands(updateQuery)
|
|
91
|
-
|
|
92
|
-
const cursor = model.find(filter).cursor()
|
|
93
|
-
await cursor.eachAsync(async (doc: HydratedDocument<T>) => {
|
|
94
|
-
const origDoc = doc.toObject(toObjectOptions) as HydratedDocument<T>
|
|
95
|
-
await updatePatch(opts, this._context, assignUpdate(doc, update, commands), origDoc)
|
|
96
|
-
})
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
schema.post(updateMethods as MongooseQueryMiddleware[], { document: false, query: true }, async function (this: HookContext<T>) {
|
|
100
|
-
const options = this.getOptions()
|
|
101
|
-
if (isHookIgnored(options)) return
|
|
102
|
-
if (!this._context) return
|
|
103
|
-
|
|
104
|
-
if (!this._context.isNew) return
|
|
105
|
-
|
|
106
|
-
const model = this.model as Model<T>
|
|
107
|
-
const updateQuery = this.getUpdate()
|
|
108
|
-
const { update, commands } = splitUpdateAndCommands(updateQuery)
|
|
109
|
-
|
|
110
|
-
const filter = this.getFilter()
|
|
111
|
-
const candidates = [update, assignUpdate(model.hydrate({}), update, commands), filter]
|
|
112
|
-
|
|
113
|
-
let current: HydratedDocument<T> | null = null
|
|
114
|
-
for (const query of candidates) {
|
|
115
|
-
if (current || isEmpty(query)) continue
|
|
116
|
-
const found = await model
|
|
117
|
-
.findOne(query as never)
|
|
118
|
-
.sort({ _id: -1 })
|
|
119
|
-
.lean()
|
|
120
|
-
.exec()
|
|
121
|
-
current = found as HydratedDocument<T>
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (current) {
|
|
125
|
-
this._context.createdDocs = [current] as HydratedDocument<T>[]
|
|
126
|
-
|
|
127
|
-
await createPatch(opts, this._context)
|
|
128
|
-
}
|
|
129
|
-
})
|
|
130
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { isEmpty, toObjectOptions } from './helpers'
|
|
2
|
-
import { deleteHooksInitialize } from './hooks/delete-hooks'
|
|
3
|
-
import { saveHooksInitialize } from './hooks/save-hooks'
|
|
4
|
-
import { updateHooksInitialize } from './hooks/update-hooks'
|
|
5
|
-
import { createPatch, deletePatch } from './patch'
|
|
6
|
-
import { isMongooseLessThan7, isMongooseLessThan8 } from './version'
|
|
7
|
-
|
|
8
|
-
import type { HydratedDocument, Model, Schema } from 'mongoose'
|
|
9
|
-
import type { PatchContext, PluginOptions } from './types'
|
|
10
|
-
|
|
11
|
-
const remove = isMongooseLessThan7 ? 'remove' : 'deleteOne'
|
|
12
|
-
|
|
13
|
-
export { default as patchEventEmitter } from './em'
|
|
14
|
-
export { setPatchHistoryTTL } from './helpers'
|
|
15
|
-
export * from './types'
|
|
16
|
-
|
|
17
|
-
export type { Duration } from './ms'
|
|
18
|
-
|
|
19
|
-
export const patchHistoryPlugin = <T>(schema: Schema<T>, opts: PluginOptions<T>): void => {
|
|
20
|
-
saveHooksInitialize(schema, opts)
|
|
21
|
-
updateHooksInitialize(schema, opts)
|
|
22
|
-
deleteHooksInitialize(schema, opts)
|
|
23
|
-
|
|
24
|
-
schema.post('insertMany', async function (docs) {
|
|
25
|
-
const context = {
|
|
26
|
-
op: 'create',
|
|
27
|
-
modelName: opts.modelName ?? this.modelName,
|
|
28
|
-
collectionName: opts.collectionName ?? this.collection.collectionName,
|
|
29
|
-
createdDocs: docs as unknown as HydratedDocument<T>[],
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
await createPatch(opts, context)
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
/* v8 ignore start */
|
|
36
|
-
// In Mongoose 7, doc.deleteOne() returned a promise that resolved to doc.
|
|
37
|
-
// In Mongoose 8, doc.deleteOne() returns a query for easier chaining, as well as consistency with doc.updateOne().
|
|
38
|
-
if (isMongooseLessThan8) {
|
|
39
|
-
type LegacySchema = {
|
|
40
|
-
pre(name: string, options: { document: boolean; query: boolean }, fn: (this: HydratedDocument<T>) => Promise<void>): void
|
|
41
|
-
post(name: string, options: { document: boolean; query: boolean }, fn: (this: HydratedDocument<T>) => Promise<void>): void
|
|
42
|
-
}
|
|
43
|
-
const legacySchema = schema as unknown as LegacySchema
|
|
44
|
-
|
|
45
|
-
legacySchema.pre(remove, { document: true, query: false }, async function (this: HydratedDocument<T>) {
|
|
46
|
-
const original = this.toObject(toObjectOptions) as HydratedDocument<T>
|
|
47
|
-
|
|
48
|
-
if (opts.preDelete && !isEmpty(original)) {
|
|
49
|
-
await opts.preDelete([original])
|
|
50
|
-
}
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
legacySchema.post(remove, { document: true, query: false }, async function (this: HydratedDocument<T>) {
|
|
54
|
-
const original = this.toObject(toObjectOptions) as HydratedDocument<T>
|
|
55
|
-
const model = this.constructor as Model<T>
|
|
56
|
-
|
|
57
|
-
const context: PatchContext<T> = {
|
|
58
|
-
op: 'delete',
|
|
59
|
-
modelName: opts.modelName ?? model.modelName,
|
|
60
|
-
collectionName: opts.collectionName ?? model.collection.collectionName,
|
|
61
|
-
deletedDocs: [original],
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
await deletePatch(opts, context)
|
|
65
|
-
})
|
|
66
|
-
}
|
|
67
|
-
/* v8 ignore end */
|
|
68
|
-
}
|
package/src/model.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { model, Schema } from 'mongoose'
|
|
2
|
-
|
|
3
|
-
import type { History } from './types'
|
|
4
|
-
|
|
5
|
-
export const HistorySchema = new Schema<History>(
|
|
6
|
-
{
|
|
7
|
-
op: {
|
|
8
|
-
type: String,
|
|
9
|
-
required: true,
|
|
10
|
-
},
|
|
11
|
-
modelName: {
|
|
12
|
-
type: String,
|
|
13
|
-
required: true,
|
|
14
|
-
},
|
|
15
|
-
collectionName: {
|
|
16
|
-
type: String,
|
|
17
|
-
required: true,
|
|
18
|
-
},
|
|
19
|
-
collectionId: {
|
|
20
|
-
type: Schema.Types.ObjectId,
|
|
21
|
-
required: true,
|
|
22
|
-
},
|
|
23
|
-
doc: {
|
|
24
|
-
type: Object,
|
|
25
|
-
},
|
|
26
|
-
patch: {
|
|
27
|
-
type: Array,
|
|
28
|
-
},
|
|
29
|
-
user: {
|
|
30
|
-
type: Object,
|
|
31
|
-
},
|
|
32
|
-
reason: {
|
|
33
|
-
type: String,
|
|
34
|
-
},
|
|
35
|
-
metadata: {
|
|
36
|
-
type: Object,
|
|
37
|
-
},
|
|
38
|
-
version: {
|
|
39
|
-
type: Number,
|
|
40
|
-
min: 0,
|
|
41
|
-
default: 0,
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
{ timestamps: true },
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
HistorySchema.index({ collectionId: 1, version: -1 })
|
|
48
|
-
HistorySchema.index({ op: 1, modelName: 1, collectionName: 1, collectionId: 1, reason: 1, version: 1 })
|
|
49
|
-
|
|
50
|
-
export const HistoryModel = model<History>('History', HistorySchema, 'history')
|
package/src/ms.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
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 [, numStr, unitStr] = match
|
|
64
|
-
const n = Number.parseFloat(String(numStr))
|
|
65
|
-
const type = (unitStr ?? 'ms').toLowerCase() as Unit
|
|
66
|
-
return n * UNITS[type]
|
|
67
|
-
}
|
package/src/omit-deep.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
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 classifyKeys = (omitKeys: string[]): { topLevel: Set<string>; nested: Map<string, string[]> } => {
|
|
12
|
-
const topLevel = new Set<string>()
|
|
13
|
-
const nested = new Map<string, string[]>()
|
|
14
|
-
|
|
15
|
-
for (const key of omitKeys) {
|
|
16
|
-
const dotIdx = key.indexOf('.')
|
|
17
|
-
if (dotIdx === -1) {
|
|
18
|
-
topLevel.add(key)
|
|
19
|
-
} else {
|
|
20
|
-
const head = key.slice(0, dotIdx)
|
|
21
|
-
const tail = key.slice(dotIdx + 1)
|
|
22
|
-
if (!isUnsafeKey(head)) {
|
|
23
|
-
const existing = nested.get(head) ?? []
|
|
24
|
-
existing.push(tail)
|
|
25
|
-
nested.set(head, existing)
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return { topLevel, nested }
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export const omitDeep = <T>(value: T, keys: string | string[]): T => {
|
|
34
|
-
if (value === undefined) return {} as T
|
|
35
|
-
|
|
36
|
-
if (Array.isArray(value)) {
|
|
37
|
-
return value.map((item) => omitDeep(item, keys)) as T
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (!isPlainObject(value)) return value
|
|
41
|
-
|
|
42
|
-
const omitKeys = typeof keys === 'string' ? [keys] : keys
|
|
43
|
-
if (!Array.isArray(omitKeys)) return value
|
|
44
|
-
|
|
45
|
-
const { topLevel, nested } = classifyKeys(omitKeys)
|
|
46
|
-
const result = {} as Record<string, unknown>
|
|
47
|
-
|
|
48
|
-
for (const key of Object.keys(value)) {
|
|
49
|
-
if (topLevel.has(key)) continue
|
|
50
|
-
|
|
51
|
-
const nestedKeys = nested.get(key)
|
|
52
|
-
result[key] = omitDeep((value as Record<string, unknown>)[key], nestedKeys ?? omitKeys)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return result as T
|
|
56
|
-
}
|
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() as Partial<T>) : 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) {
|
|
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,55 +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
|
-
createdAt?: Date
|
|
16
|
-
updatedAt?: Date
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface PatchEvent<T> {
|
|
20
|
-
oldDoc?: HydratedDocument<T>
|
|
21
|
-
doc?: HydratedDocument<T>
|
|
22
|
-
patch?: Operation[]
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface PatchContext<T> {
|
|
26
|
-
op: string
|
|
27
|
-
modelName: string
|
|
28
|
-
collectionName: string
|
|
29
|
-
isNew?: boolean
|
|
30
|
-
createdDocs?: HydratedDocument<T>[]
|
|
31
|
-
deletedDocs?: HydratedDocument<T>[]
|
|
32
|
-
ignoreEvent?: boolean
|
|
33
|
-
ignorePatchHistory?: boolean
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export type HookContext<T> = Query<T, T> & { op: string; _context: PatchContext<T> }
|
|
37
|
-
|
|
38
|
-
export type User = Record<string, unknown>
|
|
39
|
-
|
|
40
|
-
export type Metadata = Record<string, unknown>
|
|
41
|
-
|
|
42
|
-
export interface PluginOptions<T> {
|
|
43
|
-
modelName?: string
|
|
44
|
-
collectionName?: string
|
|
45
|
-
eventUpdated?: string
|
|
46
|
-
eventCreated?: string
|
|
47
|
-
eventDeleted?: string
|
|
48
|
-
getUser?: (doc: HydratedDocument<T>) => Promise<User> | User
|
|
49
|
-
getReason?: (doc: HydratedDocument<T>) => Promise<string> | string
|
|
50
|
-
getMetadata?: (doc: HydratedDocument<T>) => Promise<Metadata> | Metadata
|
|
51
|
-
omit?: string[]
|
|
52
|
-
patchHistoryDisabled?: boolean
|
|
53
|
-
preDelete?: (docs: HydratedDocument<T>[]) => Promise<void>
|
|
54
|
-
onError?: (error: Error) => void
|
|
55
|
-
}
|
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 */
|