@aedart/support
Version:
The Ion support package
887 lines (862 loc) • 32.1 kB
JavaScript
import { unset, get as get$1, hasIn, set as set$1 } from 'lodash-es';
import { hasMethod, isWeakKind, isKeyUnsafe, isKeySafe } from '@aedart/support/reflections';
import { isset as isset$1, descTag } from '@aedart/support/misc';
import { DEFAULT_MAX_MERGE_DEPTH } from '@aedart/contracts/support/objects';
import { configureCustomError, getErrorMessage } from '@aedart/support/exceptions';
import { isConcatSpreadable, isSafeArrayLike, merge as merge$1 } from '@aedart/support/arrays';
import { MergeError as MergeError$1, canCloneUsingStructuredClone as canCloneUsingStructuredClone$1, populate as populate$1, isCloneable as isCloneable$1 } from '@aedart/support/objects';
import { TYPED_ARRAY_PROTOTYPE } from '@aedart/contracts/support/reflections';
/**
* @aedart/support
*
* BSD-3-Clause, Copyright (c) 2023-present Alin Eugen Deac <aedart@gmail.com>.
*/
/**
* Remove value in object at given path
* (Alias for Lodash' [unset]{@link import('lodash').unset}) method
*
* @type {(object: any, path: import('@aedart/contracts/support').Key) => boolean}
*/
const forget = unset;
/**
* Remove all values in object that match given paths
*
* @template T
*
* @param {T} object Target object
* @param {...Key} paths Property path(s)
*/
function forgetAll(object, ...paths) {
for (const path of paths) {
forget(object, path);
}
}
/**
* @typedef {import('lodash').PropertyPath} PropertyPath
* @typedef {import('lodash').NumericDictionary} NumericDictionary
* @typedef {import('lodash').GetFieldType} GetFieldType
*/
/**
* Get value from object that matches given path
* (Alias for Lodash' [get]{@link import('lodash').get}) method
*
* @type {{<TObject extends object, TKey extends keyof TObject>(object: TObject, path: ([TKey] | TKey)): TObject[TKey], <TObject extends object, TKey extends keyof TObject>(object: (TObject | null | undefined), path: ([TKey] | TKey)): (TObject[TKey] | undefined), <TObject extends object, TKey extends keyof TObject, TDefault>(object: (TObject | null | undefined), path: ([TKey] | TKey), defaultValue: TDefault): (Exclude<TObject[TKey], undefined> | TDefault), <TObject extends object, TKey1 extends keyof TObject, TKey2 extends keyof TObject[TKey1]>(object: TObject, path: [TKey1, TKey2]): TObject[TKey1][TKey2], <TObject extends object, TKey1 extends keyof TObject, TKey2 extends keyof TObject[TKey1]>(object: (TObject | null | undefined), path: [TKey1, TKey2]): (TObject[TKey1][TKey2] | undefined), <TObject extends object, TKey1 extends keyof TObject, TKey2 extends keyof TObject[TKey1], TDefault>(object: (TObject | null | undefined), path: [TKey1, TKey2], defaultValue: TDefault): (Exclude<TObject[TKey1][TKey2], undefined> | TDefault), <TObject extends object, TKey1 extends keyof TObject, TKey2 extends keyof TObject[TKey1], TKey3 extends keyof TObject[TKey1][TKey2]>(object: TObject, path: [TKey1, TKey2, TKey3]): TObject[TKey1][TKey2][TKey3], <TObject extends object, TKey1 extends keyof TObject, TKey2 extends keyof TObject[TKey1], TKey3 extends keyof TObject[TKey1][TKey2]>(object: (TObject | null | undefined), path: [TKey1, TKey2, TKey3]): (TObject[TKey1][TKey2][TKey3] | undefined), <TObject extends object, TKey1 extends keyof TObject, TKey2 extends keyof TObject[TKey1], TKey3 extends keyof TObject[TKey1][TKey2], TDefault>(object: (TObject | null | undefined), path: [TKey1, TKey2, TKey3], defaultValue: TDefault): (Exclude<TObject[TKey1][TKey2][TKey3], undefined> | TDefault), <TObject extends object, TKey1 extends keyof TObject, TKey2 extends keyof TObject[TKey1], TKey3 extends keyof TObject[TKey1][TKey2], TKey4 extends keyof TObject[TKey1][TKey2][TKey3]>(object: TObject, path: [TKey1, TKey2, TKey3, TKey4]): TObject[TKey1][TKey2][TKey3][TKey4], <TObject extends object, TKey1 extends keyof TObject, TKey2 extends keyof TObject[TKey1], TKey3 extends keyof TObject[TKey1][TKey2], TKey4 extends keyof TObject[TKey1][TKey2][TKey3]>(object: (TObject | null | undefined), path: [TKey1, TKey2, TKey3, TKey4]): (TObject[TKey1][TKey2][TKey3][TKey4] | undefined), <TObject extends object, TKey1 extends keyof TObject, TKey2 extends keyof TObject[TKey1], TKey3 extends keyof TObject[TKey1][TKey2], TKey4 extends keyof TObject[TKey1][TKey2][TKey3], TDefault>(object: (TObject | null | undefined), path: [TKey1, TKey2, TKey3, TKey4], defaultValue: TDefault): (Exclude<TObject[TKey1][TKey2][TKey3][TKey4], undefined> | TDefault), <T>(object: NumericDictionary<T>, path: number): T, <T>(object: (NumericDictionary<T> | null | undefined), path: number): (T | undefined), <T, TDefault>(object: (NumericDictionary<T> | null | undefined), path: number, defaultValue: TDefault): (T | TDefault), <TDefault>(object: (null | undefined), path: PropertyPath, defaultValue: TDefault): TDefault, (object: (null | undefined), path: PropertyPath): undefined, <TObject, TPath extends string>(data: TObject, path: TPath): string extends TPath ? any : GetFieldType<TObject, TPath>, <TObject, TPath extends string, TDefault=GetFieldType<TObject, TPath>>(data: TObject, path: TPath, defaultValue: TDefault): (Exclude<GetFieldType<TObject, TPath>, null | undefined> | TDefault), (object: any, path: PropertyPath, defaultValue?: any): any}}
*/
const get = get$1;
/**
* Determine if path is a property of given object
*
* (Alias for Lodash' [hasIn]{@link import('lodash').hasIn}) method
*
* @type {<T>(object: T, path: import('@aedart/contracts/support').Key) => boolean}
*/
const has = hasIn;
/**
* Determine if all paths are properties of given object
* @template T
*
* @param {T} object Target object
* @param {...Key} paths Property path(s)
*
* @returns {boolean}
*/
function hasAll(object, ...paths) {
if (object === undefined || paths.length === 0) {
return false;
}
for (const path of paths) {
if (!has(object, path)) {
return false;
}
}
return true;
}
/**
* Determine if any paths are properties of given object
* @template T
*
* @param {T} object Target object
* @param {...Key} paths Property path(s)
*
* @returns {boolean}
*/
function hasAny(object, ...paths) {
for (const path of paths) {
if (has(object, path)) {
return true;
}
}
return false;
}
/**
* Object ID
*
* Utility that is able to return a numeric ID for objects.
*
* Source is heavily inspired by Nicolas Gehlert's blog post:
* "Get object reference IDs in JavaScript/TypeScript" (September 28, 2022)
* @see https://developapa.com/object-ids/
* @see https://github.com/ngehlert/developapa/blob/master/content/blog/object-ids/index.md
*/
class ObjectId {
/**
* Internal counter
*
* @type {number}
*
* @protected
* @static
*/
static _count = 0;
/**
* Weak Map of objects and their associated id
*
* @type {WeakMap<object, number>}
*
* @protected
* @readonly
*/
static _map = new WeakMap();
/**
* Returns a unique ID for target object.
*
* If no ID exists for the given object, then a new ID is
* generated and returned. Subsequent calls to this method using
* the same object will return the same ID.
*
* @param {object} target
* @returns {number}
*/
static get(target) {
const id = ObjectId._map.get(target);
if (id !== undefined) {
return id;
}
ObjectId._count += 1;
ObjectId._map.set(target, ObjectId._count);
return ObjectId._count;
}
/**
* Determine if a unique ID exists for target object
*
* @param {object} target
*
* @returns {boolean}
*/
static has(target) {
return ObjectId._map.has(target);
}
}
/**
* Alias for {@link ObjectId.has}
*/
const hasUniqueId = ObjectId.has;
/**
* Determine if target object is cloneable.
*
* **Note**: _Method assumes that target is cloneable if it implements the
* [Cloneable]{@link import('@aedart/constracts/support/objects').Cloneable} interface._
*
* @param {object} target
*
* @return {boolean}
*/
function isCloneable(target) {
return hasMethod(target, 'clone');
}
/**
* Determine if target is populatable
*
* **Note**: _Method assumes that target is populatable if it implements the
* [Populatable]{@link import('@aedart/constracts/support/objects').Populatable} interface._
*
* @param {object} target
*
* @return {boolean}
*/
function isPopulatable(target) {
return hasMethod(target, 'populate');
}
/**
* Determine if properties at given paths are declared, and their values are not undefined or null
*
* @template T
*
* @param {T} object
* @param {...Key} paths
*
* @returns {boolean}
*/
function isset(object, ...paths) {
if (object === undefined || paths.length === 0) {
return false;
}
for (const path of paths) {
if (!isset$1(get(object, path))) {
return false;
}
}
return true;
}
/**
* Merge Error
*
* @see MergeException
*/
class MergeError extends Error {
/**
* Create a new Merge Error instance
*
* @param {string} [message]
* @param {ErrorOptions} [options]
*/
constructor(message, options) {
super(message, options);
configureCustomError(this);
}
}
/**
* The default merge callback
*
* @type {MergeCallback}
*/
const defaultMergeCallback = function (target, next, options) {
const { result, key, value, source, sourceIndex, depth } = target;
// Determine if result contains key
const hasExisting = Reflect.has(result, key);
// The existing value, if any
// @ts-expect-error Existing value can be of any type here...
const existingValue /* eslint-disable-line @typescript-eslint/no-explicit-any */ = result[key];
// Determine the type and resolve value based on it...
const type = typeof value;
switch (type) {
// -------------------------------------------------------------------------------------------------------- //
// Primitives
// @see https://developer.mozilla.org/en-US/docs/Glossary/Primitive
case 'undefined':
// Do not overwrite existing value with `undefined`, if options do not allow it...
if (value === undefined
&& options.overwriteWithUndefined === false
&& hasExisting
&& existingValue !== undefined) {
return existingValue;
}
return value;
case 'string':
case 'number':
case 'bigint':
case 'boolean':
case 'symbol':
return value;
// -------------------------------------------------------------------------------------------------------- //
// Functions
case 'function':
return value;
// -------------------------------------------------------------------------------------------------------- //
// Null, Arrays and Objects
case 'object':
// Null (primitive) - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
if (value === null) {
return value;
}
// Arrays - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const isArray = Array.isArray(value); /* eslint-disable-line no-case-declarations */
if (isArray || isConcatSpreadable(value) || isSafeArrayLike(value)) {
// If required to merge with existing value, if one exists...
if (options.mergeArrays === true
&& hasExisting
&& (isArray || Array.isArray(existingValue))) {
// If either existing or new value is of the type array, merge values into
// a new array.
return merge$1()
.using(options.arrayMergeOptions)
.of(existingValue, value);
}
else if (isArray) {
// When not requested merged, just overwrite existing value with a new array,
// if new value is an array.
return merge$1()
.using(options.arrayMergeOptions)
.of(value);
}
// For concat spreadable objects or array-like objects, the "basic object" merge logic
// will deal with them.
}
// Objects (of "native" kind) - - - - - - - - - - - - - - - - - - - - - - - -
// Clone the object of a "native" kind value, if supported.
if (canCloneUsingStructuredClone$1(value)) {
return structuredClone(value);
}
// Objects (WeakRef, WeakMap and WeakSet) - - - - - - - - - - - - - - - - - -
// "Weak Reference" kind of objects cannot, nor should they, be cloned.
if (isWeakKind(value)) {
return value;
}
// Objects (basic)- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Merge with existing, if existing value is not null...
if (hasExisting
&& typeof existingValue == 'object'
&& existingValue !== null
&& !(Array.isArray(existingValue))) {
return next([existingValue, value], options, depth + 1);
}
// Otherwise, create a new object and merge it.
return next([Object.create(null), value], options, depth + 1);
// -------------------------------------------------------------------------------------------------------- //
// If for some reason this point is reached, it means that we are unable to merge "something".
default:
throw new MergeError$1(`Unable to merge value of type ${type} (${descTag(value)}) at source index ${sourceIndex}`, {
cause: {
key: key,
value: value,
source: source,
sourceIndex: sourceIndex,
depth: depth,
options: options
}
});
}
};
/**
* Returns a new skip callback for given property keys
*
* @param {PropertyKey[]} keys
*
* @return {SkipKeyCallback}
*/
function makeSkipCallback(keys) {
return (key, source, result /* eslint-disable-line @typescript-eslint/no-unused-vars */) => {
return keys.includes(key) && Reflect.has(source, key);
};
}
/**
* Default Merge Options
*
* @see MergeOptions
*/
class DefaultMergeOptions {
/**
* The maximum merge depth
*
* **Note**: _Value must be greater than or equal zero._
*
* **Note**: _Defaults to [DEFAULT_MAX_MERGE_DEPTH]{@link import('@aedart/contracts/support/objects').DEFAULT_MAX_MERGE_DEPTH}
* when not specified._
*
* @type {number}
*/
depth = DEFAULT_MAX_MERGE_DEPTH;
/**
* Property Keys that must not be merged.
*
* **Note**: [DANGEROUS_PROPERTIES]{@link import('@aedart/contracts/support/objects').DANGEROUS_PROPERTIES}
* are always skipped, regardless of specified keys._
*
* **Callback**: _A callback can be specified to determine if a given key,
* in a source object should be skipped._
*
* **Example:**
* ```js
* const a = { 'foo': true };
* const b = { 'bar': true, 'zar': true };
*
* merge().using({ skip: [ 'zar' ] }).of(a, b); // { 'foo': true, 'bar': true }
*
* merge().using({ skip: (key, source) => {
* return key === 'bar' && Reflect.has(source, key);
* } }).of(a, b); // { 'foo': true, 'zar': true }
* ```
*
* @type {PropertyKey[] | SkipKeyCallback}
*/
skip = [];
/**
* Flag, overwrite property values with `undefined`.
*
* **When `true` (_default behaviour_)**: _If an existing property value is not `undefined`, it will be overwritten
* with new value, even if the new value is `undefined`._
*
* **When `false`**: _If an existing property value is not `undefined`, it will NOT be overwritten
* with new value, if the new value is `undefined`._
*
* **Example:**
* ```js
* const a = { 'foo': true };
* const b = { 'foo': undefined };
*
* merge(a, b); // { 'foo': undefined }
*
* merge().using({ overwriteWithUndefined: false }).of(a, b) // { 'foo': true }
* ```
*
* @type {boolean}
*/
overwriteWithUndefined = true;
/**
* Flag, if source object is [`Cloneable`]{@link import('@aedart/contracts/support/objects').Cloneable}, then the
* resulting object from the `clone()` method is used.
*
* **When `true` (_default behaviour_)**: _If source object is cloneable then the resulting object from `clone()`
* method is used. Its properties are then iterated by the merge function._
*
* **When `false`**: _Cloneable objects are treated like any other objects, the `clone()` method is ignored._
*
* **Example:**
* ```js
* const a = { 'foo': { 'name': 'John Doe' } };
* const b = { 'foo': {
* 'name': 'Jane Doe',
* clone() {
* return {
* 'name': 'Rick Doe',
* 'age': 26
* }
* }
* } };
*
* merge(a, b); // { 'foo': { 'name': 'Rick Doe', 'age': 26 } }
*
* merge().using({ useCloneable: false }).of(a, b); // { 'foo': { 'name': 'Jane Doe', clone() {...} } }
* ```
*
* @see [`Cloneable`]{@link import('@aedart/contracts/support/objects').Cloneable}
*
* @type {boolean}
*/
useCloneable = true;
/**
* Flag, whether to merge array, array-like, and [concat spreadable]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable}
* properties or not.
*
* **When `true`**: _existing property is merged with new property value._
*
* **When `false` (_default behaviour_)**: _existing property is overwritten with new property value_
*
* **Example:**
* ```js
* const a = { 'foo': [ 1, 2, 3 ] };
* const b = { 'foo': [ 4, 5, 6 ] };
*
* merge([ a, b ]); // { 'foo': [ 4, 5, 6 ] }
* merge([ a, b ], { mergeArrays: true }); // { 'foo': [ 1, 2, 3, 4, 5, 6 ] }
* ```
*
* **Note**: _`String()` (object) and [Typed Arrays]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray}
* are not merged, even though they are considered to be "array-like" (they offer a `length` property).
* You need to manually handle these, via a custom [callback]{@link MergeCallback}, if such value types must be merged._
*
* @see [merge (array)]{@link import('@aedart/support/arrays').merge}
*
* @type {boolean}
*/
mergeArrays = false;
/**
* Merge Options for arrays
*
* @type {ArrayMergeOptions}
*/
arrayMergeOptions = {};
/**
* The merge callback that must be applied
*
* **Note**: _When no callback is provided, then the merge function's default
* callback is used._
*
* @type {MergeCallback}
*/
callback;
/**
* Creates a new Merge Options instance
*
* @param {MergeCallback | MergeOptions} [options]
*/
constructor(options) {
// Merge provided options, if any given
if (options && typeof options == 'object') {
populate$1(this, options);
}
// Abort in case of invalid maximum depth - other options can also be asserted, but they are less important.
// The Browser / Node.js Engine will throw an error in case that they maximum recursion level is reached!
if (this.depth < 0) {
throw new MergeError('Invalid maximum "depth" merge option value', {
cause: {
options: this
}
});
}
// Resolve merge callback
this.callback = (options && typeof options == 'function')
? options
: defaultMergeCallback;
// Resolve skip callback
if (typeof this.skip != 'function') {
this.skip = makeSkipCallback(this.skip);
}
}
/**
* Create new default merge options from given options
*
* @param {MergeCallback | MergeOptions} [options]
*
* @return {DefaultMergeOptions}
*
* @throws {MergeError}
*/
static from(options) {
const resoled = new this(options);
return Object.freeze(resoled);
}
}
/**
* Objects Merger
*
* @see ObjectsMerger
*/
class Merger {
/**
* The merge options to be applied
*
* @type {Readonly<DefaultMergeOptions | MergeOptions>}
*
* @protected
*/
_options;
/**
* Callback to perform the merging of nested objects.
*
* @type {NextCallback}
*
* @protected
* @readonly
*/
_next;
/**
* Create a new objects merger instance
*
* @param {MergeCallback | MergeOptions} [options]
*
* @throws {MergeError}
*/
constructor(options) {
// @ts-expect-error Need to init options, however they are resolved via "using".
this._options = null;
this._next = this.merge;
this.using(options);
}
/**
* Returns the merge options that are to be applied
*
* @return {Readonly<DefaultMergeOptions | MergeOptions>}
*/
get options() {
return this._options;
}
/**
* Returns the "next" callback that performs merging of nested objects.
*/
get nextCallback() {
return this._next;
}
/**
* Use the following merge options or merge callback
*
* @param {MergeCallback | MergeOptions} [options]
*
* @return {this}
*
* @throws {MergeError}
*/
using(options) {
this._options = this.resolveOptions(options);
return this;
}
of(...sources) {
try {
return this.nextCallback(sources, this.options, 0);
}
catch (error) {
if (error instanceof MergeError) {
// @ts-expect-error Error SHOULD have a cause object set - support by all browsers now!
error.cause.sources = sources;
// @ts-expect-error Error SHOULD have a cause object set - support by all browsers now!
error.cause.options = this.options;
throw error;
}
const reason = getErrorMessage(error);
throw new MergeError(`Unable to merge objects: ${reason}`, {
cause: {
previous: error,
sources: sources,
options: this.options
}
});
}
}
/**
* Merge given source objects into a single object
*
* @param {object[]} sources
* @param {Readonly<MergeOptions>} options
* @param {number} [depth] Current recursion depth
*
* @return {object}
*
* @throws {MergeError}
*/
merge(sources, options, depth = 0) {
// Abort if maximum depth has been reached
this.assertMaxDepthNotExceeded(depth, sources, options);
// Resolve callbacks to apply
const nextCallback = this.nextCallback.bind(this);
const skipCallback = options.skip.bind(this);
const mergeCallback = options.callback.bind(this);
// Loop through the sources and merge them into a single object
return sources.reduce((result, source, index) => {
// Abort if source is invalid...
this.assertSourceObject(source, index, depth);
// If allowed and source implements "Cloneable" interface, favour "clone()" method's resulting object.
const resolved = this.resolveSourceObject(source, options);
// Loop through all the source's properties, including symbols
const keys = Reflect.ownKeys(resolved);
for (const key of keys) {
// Skip key if needed ...
if (isKeyUnsafe(key) || skipCallback(key, resolved, result)) {
continue;
}
// Resolve the value via callback and set it in resulting object.
// @ts-expect-error Safe to set the value in result object!
result[key] = mergeCallback({
result,
key,
// @ts-expect-error Value can be of any type
value: resolved[key],
source: resolved,
sourceIndex: index,
depth: depth,
}, nextCallback, options);
}
return result;
}, Object.create(null));
}
/**
* Resolves the source object
*
* @param {object} source
* @param {MergeOptions} options
*
* @protected
*
* @return {object}
*/
resolveSourceObject(source, options) {
let output = source;
if (options.useCloneable && isCloneable$1(source)) {
output = this.cloneSource(source);
}
return output;
}
/**
* Invokes the "clone()" method on given cloneable object
*
* @param {Cloneable} source
*
* @protected
*
* @return {object}
*
* @return {MergeError} If unable to
*/
cloneSource(source) {
const clone = source.clone();
// Abort if resulting value from "clone()" is not a valid value...
if (!clone || typeof clone != 'object' || Array.isArray(clone)) {
throw new MergeError(`Expected clone() method to return object for source, ${descTag(clone)} was returned`, {
cause: {
source: source,
clone: clone,
}
});
}
return clone;
}
/**
* Resolves provided merge options
*
* @param {MergeCallback | MergeOptions} [options]
*
* @protected
*
* @return {Readonly<DefaultMergeOptions | MergeOptions>}
*
* @throws {MergeError}
*/
resolveOptions(options) {
return DefaultMergeOptions.from(options);
}
/**
* Assert that current recursion depth has now exceeded the maximum depth
*
* @param {number} currentDepth
* @param {object[]} sources
* @param {MergeOptions} [options] Defaults to this Merger's options when none given
*
* @protected
*
* @throws {MergeError}
*/
assertMaxDepthNotExceeded(currentDepth, sources, options) {
const max = options?.depth || this.options.depth;
if (max && currentDepth > max) {
throw new MergeError(`Maximum merge depth (${max}) has been exceeded`, {
cause: {
source: sources,
depth: currentDepth
}
});
}
}
/**
* Assert given source is a valid object
*
* @param {unknown} source
* @param {number} index
* @param {number} currentDepth
*
* @protected
*
* @throws {MergeError}
*/
assertSourceObject(source, index, currentDepth) {
if (!source || typeof source != 'object' || Array.isArray(source)) {
throw new MergeError(`Unable to merge source of invalid type "${descTag(source)}" (source index: ${index})`, {
cause: {
source: source,
index: index,
depth: currentDepth
}
});
}
}
}
/**
* Returns a merger of given source objects
*
* **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy}
* of all given sources._
*
* @param {....object} [sources]
*
* @return {ObjectsMerger|object}
*
* @throws {MergeError}
*/
function merge(...sources) {
const merger = new Merger();
if (sources.length == 0) {
return merger;
}
return merger.of(...sources);
}
/**
* Populate target object with the properties from source object
*
* **Warning**: _This method performs a shallow copy of properties in source object!_
*
* **Warning**: _`target` object is mutated!_
*
* **Note**: _Properties that are [unsafe]{@link import('@aedart/support/reflections').isKeyUnsafe} are always disregarded!_
*
* @template TargetObj extends object = object
* @template SourceObj extends object = object
*
* @param {object} target
* @param {object} source
* @param {PropertyKey | PropertyKey[] | SourceKeysCallback} [keys='*'] Keys to select and copy from `source` object.
* If wildcard (`*`) given, then all properties from the `source`
* are selected. If a callback is given, then that callback must return
* key or keys to select from `source`.
* @param {boolean} [safe=true] When `true`, properties must exist in target (_must be defined in target_),
* before they are shallow copied.
*
* @returns {object} The populated target
*
* @throws {TypeError} If a key does not exist in `target` (_when `safe = true`_).
* Or, if key does not exist in `source` (_regardless of `safe` flag_).
*/
function populate(target, source, keys = '*', safe = true) {
if (keys === '*') {
keys = Reflect.ownKeys(source);
}
else if (typeof keys == 'function') {
keys = keys(source, target);
}
if (!Array.isArray(keys)) {
keys = [keys];
}
// Always remove dangerous keys, regardless of "safe" flag.
keys = keys.filter((key) => isKeySafe(key));
// Populate...
for (const key of keys) {
// If "safe" is enabled, then only keys that are already defined in target are allowed.
if (safe && !Reflect.has(target, key)) {
throw new TypeError(`Key "${key.toString()}" does not exist in target object`);
}
// However, fail if property does not exist in source, regardless of "safe" flag.
if (!Reflect.has(source, key)) {
throw new TypeError(`Key "${key.toString()}" does not exist in source object`);
}
// @ts-expect-error At this point, all should be safe...
target[key] = source[key];
}
return target;
}
/**
* @typedef {import('@aedart/contracts/support').Key} Key
*/
/**
* Set value in object at given path
* (Alias for Lodash' [set]{@link import('lodash').set}) method
*
* @type {{<T extends object>(object: T, path: Key, value: any): T, <TResult>(object: object, path: Key, value: any): TResult}}
*/
const set = set$1;
/**
* Alias for {@link ObjectId.get}
*/
const uniqueId = ObjectId.get;
/**
* Determine if an object value can be cloned via `structuredClone()`
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
*
* @internal
*
* @param {object} value
*
* @return {boolean}
*/
function canCloneUsingStructuredClone(value) {
const supported = [
// Array, // Handled by array, with evt. array value merges
ArrayBuffer,
Boolean,
DataView,
Date,
Error,
Map,
Number,
// Object, // Handled by "basic" objects merging...
// (Primitive Types), // Also handled elsewhere...
RegExp,
Set,
String,
TYPED_ARRAY_PROTOTYPE
];
for (const constructor of supported) {
if (value instanceof constructor) {
return true;
}
}
return false;
}
export { DefaultMergeOptions, MergeError, Merger, ObjectId, canCloneUsingStructuredClone, defaultMergeCallback, forget, forgetAll, get, has, hasAll, hasAny, hasUniqueId, isCloneable, isPopulatable, isset, makeSkipCallback, merge, populate, set, uniqueId };