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/helpers.ts DELETED
@@ -1,174 +0,0 @@
1
- import { HistoryModel } from './model'
2
- import { type Duration, ms } from './ms'
3
-
4
- import type { QueryOptions, ToObjectOptions } from 'mongoose'
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 Error]': {
48
- const err = value as unknown as Error
49
- const cloned = new (err.constructor as ErrorConstructor)(err.message)
50
- if (err.stack) cloned.stack = err.stack
51
- return cloned as T
52
- }
53
- case '[object ArrayBuffer]':
54
- return cloneArrayBuffer(value as unknown as ArrayBuffer) as T
55
- case '[object DataView]': {
56
- const dv = value as unknown as DataView
57
- const buffer = cloneArrayBuffer(dv.buffer as ArrayBuffer)
58
- return new DataView(buffer, dv.byteOffset, dv.byteLength) as T
59
- }
60
- }
61
-
62
- if (ArrayBuffer.isView(value)) {
63
- const ta = value as unknown as { buffer: ArrayBuffer; byteOffset: number; length: number }
64
- const buffer = cloneArrayBuffer(ta.buffer)
65
- return new (value.constructor as new (buffer: ArrayBuffer, byteOffset: number, length: number) => T)(buffer, ta.byteOffset, ta.length)
66
- }
67
-
68
- return undefined
69
- }
70
-
71
- const cloneCollection = <T extends object>(value: T, seen: WeakMap<object, unknown>): T => {
72
- if (value instanceof Map) {
73
- const map = new Map()
74
- seen.set(value, map)
75
- for (const [k, v] of value) map.set(k, cloneDeep(v, seen))
76
- return map as T
77
- }
78
-
79
- if (value instanceof Set) {
80
- const set = new Set()
81
- seen.set(value, set)
82
- for (const v of value) set.add(cloneDeep(v, seen))
83
- return set as T
84
- }
85
-
86
- if (Array.isArray(value)) {
87
- const arr = new Array(value.length) as unknown[]
88
- seen.set(value, arr)
89
- for (let i = 0; i < value.length; i++) {
90
- arr[i] = cloneDeep(value[i], seen)
91
- }
92
- return arr as T
93
- }
94
-
95
- const result = typeof value.constructor === 'function' ? (Object.create(Object.getPrototypeOf(value) as object) as T) : ({} as T)
96
- seen.set(value, result)
97
- for (const key of Object.keys(value)) {
98
- ;(result as Record<string, unknown>)[key] = cloneDeep((value as Record<string, unknown>)[key], seen)
99
- }
100
- return result
101
- }
102
-
103
- export const cloneDeep = <T>(value: T, seen = new WeakMap<object, unknown>()): T => {
104
- if (value === null || typeof value !== 'object') return value
105
- if (seen.has(value)) return seen.get(value) as T
106
-
107
- const immutable = cloneImmutable(value)
108
- if (immutable !== undefined) return immutable
109
-
110
- const record = value as Record<string, unknown>
111
-
112
- if (typeof record._bsontype === 'string' && typeof record.toHexString === 'function') {
113
- return new (value.constructor as new (hex: string) => T)((record.toHexString as () => string)())
114
- }
115
-
116
- if (typeof record.toJSON === 'function') {
117
- // NOSONAR — structuredClone cannot handle objects with non-cloneable methods (e.g. mongoose documents)
118
- return JSON.parse(JSON.stringify(value)) as T
119
- }
120
-
121
- return cloneCollection(value, seen)
122
- }
123
-
124
- export const chunk = <T>(array: T[], size: number): T[][] => {
125
- const result: T[][] = []
126
- for (let i = 0; i < array.length; i += size) {
127
- result.push(array.slice(i, i + size))
128
- }
129
- return result
130
- }
131
-
132
- export const isHookIgnored = <T>(options: QueryOptions<T>): boolean => {
133
- return options.ignoreHook === true || (options.ignoreEvent === true && options.ignorePatchHistory === true)
134
- }
135
-
136
- export const toObjectOptions: ToObjectOptions = {
137
- depopulate: true,
138
- virtuals: false,
139
- }
140
-
141
- export const setPatchHistoryTTL = async (ttl: Duration, onError?: (error: Error) => void): Promise<void> => {
142
- const name = 'createdAt_1_TTL'
143
- try {
144
- const indexes = await HistoryModel.collection.indexes()
145
- const existingIndex = indexes?.find((index) => index.name === name)
146
-
147
- if (!ttl && existingIndex) {
148
- await HistoryModel.collection.dropIndex(name)
149
- return
150
- }
151
-
152
- const milliseconds = ms(ttl)
153
-
154
- if (milliseconds < 1000 && existingIndex) {
155
- await HistoryModel.collection.dropIndex(name)
156
- return
157
- }
158
-
159
- const expireAfterSeconds = milliseconds / 1000
160
-
161
- if (existingIndex && existingIndex.expireAfterSeconds === expireAfterSeconds) {
162
- return
163
- }
164
-
165
- if (existingIndex) {
166
- await HistoryModel.collection.dropIndex(name)
167
- }
168
-
169
- await HistoryModel.collection.createIndex({ createdAt: 1 }, { expireAfterSeconds, name })
170
- } catch (err) {
171
- const handler = onError ?? console.error
172
- handler(err as Error)
173
- }
174
- }
@@ -1,49 +0,0 @@
1
- import { cloneDeep, isArray, isEmpty, isHookIgnored } from '../helpers'
2
- import { deletePatch } from '../patch'
3
-
4
- import type { HydratedDocument, Model, MongooseQueryMiddleware, Schema } from 'mongoose'
5
- import type { HookContext, PluginOptions } from '../types'
6
-
7
- const deleteMethods = ['remove', 'findOneAndDelete', 'findOneAndRemove', 'findByIdAndDelete', 'findByIdAndRemove', 'deleteOne', 'deleteMany']
8
-
9
- export const deleteHooksInitialize = <T>(schema: Schema<T>, opts: PluginOptions<T>): void => {
10
- schema.pre(deleteMethods as MongooseQueryMiddleware[], { document: false, query: true }, async function (this: HookContext<T>) {
11
- const options = this.getOptions()
12
- if (isHookIgnored(options)) return
13
-
14
- const model = this.model as Model<T>
15
- const filter = this.getFilter()
16
-
17
- this._context = {
18
- op: this.op,
19
- modelName: opts.modelName ?? model.modelName,
20
- collectionName: opts.collectionName ?? model.collection.collectionName,
21
- ignoreEvent: options.ignoreEvent as boolean,
22
- ignorePatchHistory: options.ignorePatchHistory as boolean,
23
- }
24
-
25
- if (['remove', 'deleteMany'].includes(this._context.op) && !options.single) {
26
- const docs = await model.find<T>(filter).lean().exec()
27
- if (!isEmpty(docs)) {
28
- this._context.deletedDocs = docs as HydratedDocument<T>[]
29
- }
30
- } else {
31
- const doc = await model.findOne<T>(filter).lean().exec()
32
- if (!isEmpty(doc)) {
33
- this._context.deletedDocs = [doc] as HydratedDocument<T>[]
34
- }
35
- }
36
-
37
- if (opts.preDelete && isArray(this._context.deletedDocs) && !isEmpty(this._context.deletedDocs)) {
38
- await opts.preDelete(cloneDeep(this._context.deletedDocs))
39
- }
40
- })
41
-
42
- schema.post(deleteMethods as MongooseQueryMiddleware[], { document: false, query: true }, async function (this: HookContext<T>) {
43
- const options = this.getOptions()
44
- if (isHookIgnored(options)) return
45
- if (!this._context) return
46
-
47
- await deletePatch(opts, this._context)
48
- })
49
- }
@@ -1,30 +0,0 @@
1
- import { toObjectOptions } from '../helpers'
2
- import { createPatch, updatePatch } from '../patch'
3
-
4
- import type { HydratedDocument, Model, Schema } from 'mongoose'
5
- import type { PatchContext, PluginOptions } from '../types'
6
-
7
- export const saveHooksInitialize = <T>(schema: Schema<T>, opts: PluginOptions<T>): void => {
8
- schema.pre('save', async function () {
9
- if (this.constructor.name !== 'model') return
10
-
11
- const current = this.toObject(toObjectOptions) as HydratedDocument<T>
12
- const model = this.constructor as Model<T>
13
-
14
- const context: PatchContext<T> = {
15
- op: this.isNew ? 'create' : 'update',
16
- modelName: opts.modelName ?? model.modelName,
17
- collectionName: opts.collectionName ?? model.collection.collectionName,
18
- createdDocs: [current],
19
- }
20
-
21
- if (this.isNew) {
22
- await createPatch(opts, context)
23
- } else {
24
- const original = await model.findById(current._id).lean().exec()
25
- if (original) {
26
- await updatePatch(opts, context, current, original as HydratedDocument<T>)
27
- }
28
- }
29
- })
30
- }
@@ -1,125 +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.split('.')[0] as string
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)[0] as string
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
- current = (await model.findOne(query).sort({ _id: -1 }).lean().exec()) as HydratedDocument<T>
117
- }
118
-
119
- if (current) {
120
- this._context.createdDocs = [current] as HydratedDocument<T>[]
121
-
122
- await createPatch(opts, this._context)
123
- }
124
- })
125
- }
package/src/index.ts DELETED
@@ -1,65 +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
- // @ts-expect-error - Mongoose 7 and below
40
- schema.pre(remove, { document: true, query: false }, async function () {
41
- // @ts-expect-error - Mongoose 7 and below
42
- const original = this.toObject(toObjectOptions) as HydratedDocument<T>
43
-
44
- if (opts.preDelete && !isEmpty(original)) {
45
- await opts.preDelete([original])
46
- }
47
- })
48
-
49
- // @ts-expect-error - Mongoose 7 and below
50
- schema.post(remove, { document: true, query: false }, async function (this: HydratedDocument<T>) {
51
- const original = this.toObject(toObjectOptions) as HydratedDocument<T>
52
- const model = this.constructor as Model<T>
53
-
54
- const context: PatchContext<T> = {
55
- op: 'delete',
56
- modelName: opts.modelName ?? model.modelName,
57
- collectionName: opts.collectionName ?? model.collection.collectionName,
58
- deletedDocs: [original],
59
- }
60
-
61
- await deletePatch(opts, context)
62
- })
63
- }
64
- /* v8 ignore end */
65
- }
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')
@@ -1,3 +0,0 @@
1
- declare module 'power-assign' {
2
- export function assign<T, U>(object1: T, object2: U): T & U
3
- }
package/src/ms.ts DELETED
@@ -1,66 +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 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 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
- }