|
| 1 | +/* |
| 2 | + * @nevware21/ts-utils |
| 3 | + * https://github.com/nevware21/ts-utils |
| 4 | + * |
| 5 | + * Copyright (c) 2026 NevWare21 Solutions LLC |
| 6 | + * Licensed under the MIT license. |
| 7 | + */ |
| 8 | + |
| 9 | +import { arrForEach } from "../array/forEach"; |
| 10 | +import { isStrictUndefined } from "../helpers/base"; |
| 11 | +import { forEachOwnKeySafe } from "./forEachOwnKey"; |
| 12 | +import { isUnsafeTarget } from "./isUnsafeTarget"; |
| 13 | + |
| 14 | +/** |
| 15 | + * Copies own enumerable properties from `source` to `target` **only** when the predicate |
| 16 | + * returns truthy for that key/value pair. All other own properties on `target` are left |
| 17 | + * unchanged. |
| 18 | + * |
| 19 | + * Security behavior: this helper iterates source keys via {@link forEachOwnKeySafe}, so unsafe |
| 20 | + * keys (`__proto__`, `constructor`, `prototype`) are ignored even if the predicate returns true. |
| 21 | + * It also skips all writes when `target` is a guarded built-in prototype object per |
| 22 | + * {@link isUnsafeTarget}. |
| 23 | + * @since 0.14.0 |
| 24 | + * @group Object |
| 25 | + * @typeParam T - The type of the target object |
| 26 | + * @param target - The target object to merge into |
| 27 | + * @param source - The source object to merge from. Null or undefined source is safely ignored. |
| 28 | + * @param predicate - A function `(key, srcValue, tgtValue) => boolean` called for each own |
| 29 | + * enumerable safe property key of `source` (string and symbol). The property is merged only |
| 30 | + * when the predicate returns truthy. |
| 31 | + * @returns The `target` object (mutated in place). |
| 32 | + * @example |
| 33 | + * ```ts |
| 34 | + * const target = { a: 1, b: 2 }; |
| 35 | + * const source = { b: 99, c: 3 }; |
| 36 | + * |
| 37 | + * // Only merge keys whose source value is greater than 2 |
| 38 | + * objMergeIf(target, source, (key, srcVal) => srcVal > 2); |
| 39 | + * // target => { a: 1, b: 99, c: 3 } |
| 40 | + * |
| 41 | + * // Only merge when the key does not already exist in target |
| 42 | + * const t2 = { x: 10 }; |
| 43 | + * objMergeIf(t2, { x: 99, y: 5 }, (key, _sv, tgtVal) => tgtVal === undefined); |
| 44 | + * // t2 => { x: 10, y: 5 } |
| 45 | + * |
| 46 | + * // Unsafe keys are filtered even if predicate returns true |
| 47 | + * const t3: any = {}; |
| 48 | + * objMergeIf(t3, { constructor: "ignored", safe: 1 } as any, () => true); |
| 49 | + * // t3 => { safe: 1 } |
| 50 | + * ``` |
| 51 | + */ |
| 52 | +export function objMergeIf<T>( |
| 53 | + target: T, |
| 54 | + source: Record<PropertyKey, any> | null | undefined, |
| 55 | + predicate: (key: PropertyKey, srcValue: any, tgtValue: any) => boolean |
| 56 | +): T { |
| 57 | + if (target && source && !isUnsafeTarget(target)) { |
| 58 | + forEachOwnKeySafe(source, (key, value) => { |
| 59 | + if (predicate(key, value, (target as any)[key])) { |
| 60 | + (target as any)[key] = value; |
| 61 | + } |
| 62 | + }); |
| 63 | + } |
| 64 | + return target; |
| 65 | +} |
| 66 | + |
| 67 | +/** |
| 68 | + * Assigns own enumerable properties from one or more `sources` onto `target` **only** for |
| 69 | + * properties that are currently `undefined` on `target` — it never overwrites an already-defined |
| 70 | + * value (including `null`). Sources are processed left-to-right; the first defined value wins. |
| 71 | + * |
| 72 | + * This is similar to Lodash `_.defaults()`, but it only considers each source object's own |
| 73 | + * enumerable properties and does not copy inherited source properties. |
| 74 | + * |
| 75 | + * **Security filtering:** to guard against prototype-pollution attacks this function applies two |
| 76 | + * layers of protection: |
| 77 | + * - **Unsafe source keys** (`__proto__`, `constructor`, `prototype`) are silently skipped and |
| 78 | + * never written to `target`, even when those keys exist as own enumerable properties of a source. |
| 79 | + * - **Guarded targets** (built-in prototype objects such as `Object.prototype`, |
| 80 | + * `Array.prototype`, etc.) are rejected entirely — `target` is returned unchanged. |
| 81 | + * |
| 82 | + * This means that calls like `objDefaults(obj, { constructor: fn })` or |
| 83 | + * `objDefaults(Object.prototype, ...)` are silently no-ops for the filtered keys / guarded target. |
| 84 | + * @since 0.14.0 |
| 85 | + * @group Object |
| 86 | + * @typeParam T - The type of the target object |
| 87 | + * @param target - The destination object. Modified in place. |
| 88 | + * @param sources - One or more source objects. Null / undefined sources are skipped. |
| 89 | + * @returns The `target` object with all defaults applied. |
| 90 | + * @example |
| 91 | + * ```ts |
| 92 | + * const options = { timeout: 5000 }; |
| 93 | + * const defaults = { timeout: 3000, retries: 3, verbose: false }; |
| 94 | + * |
| 95 | + * objDefaults(options, defaults); |
| 96 | + * // => { timeout: 5000, retries: 3, verbose: false } |
| 97 | + * // `timeout` was kept because it was already defined. |
| 98 | + * |
| 99 | + * // Multiple sources — first defined value wins |
| 100 | + * objDefaults({}, { a: 1 }, { a: 99, b: 2 }); |
| 101 | + * // => { a: 1, b: 2 } |
| 102 | + * |
| 103 | + * // Unsafe source keys are silently skipped (prototype-pollution guard) |
| 104 | + * const cfg: any = { host: "localhost" }; |
| 105 | + * const src: any = { host: "evil.com", __proto__: { admin: true }, constructor: String }; |
| 106 | + * objDefaults(cfg, src); |
| 107 | + * // => { host: "localhost" } — __proto__ and constructor were never written |
| 108 | + * ``` |
| 109 | + */ |
| 110 | +export function objDefaults<T>(target: T, ...sources: Array<Partial<T> | null | undefined>): T { |
| 111 | + if (target && !isUnsafeTarget(target)) { |
| 112 | + arrForEach(sources, (source) => { |
| 113 | + if (source) { |
| 114 | + forEachOwnKeySafe(source, (key, value) => { |
| 115 | + if (isStrictUndefined((target as any)[key])) { |
| 116 | + (target as any)[key] = value; |
| 117 | + } |
| 118 | + }); |
| 119 | + } |
| 120 | + }); |
| 121 | + } |
| 122 | + return target; |
| 123 | +} |
0 commit comments