packages/@aws-cdk/toolkit-lib/lib/util/objects.ts (155 lines of code) (raw):
import type { Obj } from './types';
import { isArray, isObject } from './types';
import { ToolkitError } from '../api/toolkit-error';
/**
* Return a new object by adding missing keys into another object
*/
export function applyDefaults(hash: any, defaults: any) {
const result: any = { };
Object.keys(hash).forEach(k => result[k] = hash[k]);
Object.keys(defaults)
.filter(k => !(k in result))
.forEach(k => result[k] = defaults[k]);
return result;
}
/**
* Return whether the given parameter is an empty object or empty list.
*/
export function isEmpty(x: any) {
if (x == null) {
return false;
}
if (isArray(x)) {
return x.length === 0;
}
return Object.keys(x).length === 0;
}
/**
* Deep clone a tree of objects, lists or scalars
*
* Does not support cycles.
*/
export function deepClone(x: any): any {
if (typeof x === 'undefined') {
return undefined;
}
if (x === null) {
return null;
}
if (isArray(x)) {
return x.map(deepClone);
}
if (isObject(x)) {
return makeObject(mapObject(x, (k, v) => [k, deepClone(v)] as [string, any]));
}
return x;
}
/**
* Map over an object, treating it as a dictionary
*/
export function mapObject<T, U>(x: Obj<T>, fn: (key: string, value: T) => U): U[] {
const ret: U[] = [];
Object.keys(x).forEach(key => {
ret.push(fn(key, x[key]));
});
return ret;
}
/**
* Construct an object from a list of (k, v) pairs
*/
export function makeObject<T>(pairs: Array<[string, T]>): Obj<T> {
const ret: Obj<T> = {};
for (const pair of pairs) {
ret[pair[0]] = pair[1];
}
return ret;
}
/**
* Deep get a value from a tree of nested objects
*
* Returns undefined if any part of the path was unset or
* not an object.
*/
export function deepGet(x: any, path: string[]): any {
path = path.slice();
while (path.length > 0 && isObject(x)) {
const key = path.shift()!;
x = x[key];
}
return path.length === 0 ? x : undefined;
}
/**
* Deep set a value in a tree of nested objects
*
* Throws an error if any part of the path is not an object.
*/
export function deepSet(x: any, path: string[], value: any) {
path = path.slice();
if (path.length === 0) {
throw new ToolkitError('Path may not be empty');
}
while (path.length > 1 && isObject(x)) {
const key = path.shift()!;
if (isPrototypePollutingKey(key)) {
continue;
}
if (!(key in x)) {
x[key] = {};
}
x = x[key];
}
if (!isObject(x)) {
throw new ToolkitError(`Expected an object, got '${x}'`);
}
const finalKey = path[0];
if (isPrototypePollutingKey(finalKey)) {
return;
}
if (value !== undefined) {
x[finalKey] = value;
} else {
delete x[finalKey];
}
}
/**
* Helper to detect prototype polluting keys
*
* A key matching this, MUST NOT be used in an assignment.
* Use this to check user-input.
*/
function isPrototypePollutingKey(key: string) {
return key === '__proto__' || key === 'constructor' || key === 'prototype';
}
/**
* Recursively merge objects together
*
* The leftmost object is mutated and returned. Arrays are not merged
* but overwritten just like scalars.
*
* If an object is merged into a non-object, the non-object is lost.
*/
export function deepMerge(...objects: Array<Obj<any> | undefined>) {
function mergeOne(target: Obj<any>, source: Obj<any>) {
for (const key of Object.keys(source)) {
if (isPrototypePollutingKey(key)) {
continue;
}
const value = source[key];
if (isObject(value)) {
if (!isObject(target[key])) {
target[key] = {};
} // Overwrite on purpose
mergeOne(target[key], value);
} else if (typeof value !== 'undefined') {
target[key] = value;
}
}
}
const others = objects.filter(x => x != null) as Array<Obj<any>>;
if (others.length === 0) {
return {};
}
const into = others.splice(0, 1)[0];
others.forEach(other => mergeOne(into, other));
return into;
}
/**
* Splits the given object into two, such that:
*
* 1. The size of the first object (after stringified in UTF-8) is less than or equal to the provided size limit.
* 2. Merging the two objects results in the original one.
*/
export function splitBySize(data: any, maxSizeBytes: number): [any, any] {
if (maxSizeBytes < 2) {
// It's impossible to fit anything in the first object
return [undefined, data];
}
const entries = Object.entries(data);
return recurse(0, 0);
function recurse(index: number, runningTotalSize: number): [any, any] {
if (index >= entries.length) {
// Everything fits in the first object
return [data, undefined];
}
const size = runningTotalSize + entrySize(entries[index]);
return (size > maxSizeBytes) ? cutAt(index) : recurse(index + 1, size);
}
function entrySize(entry: [string, unknown]) {
return Buffer.byteLength(JSON.stringify(Object.fromEntries([entry])));
}
function cutAt(index: number): [any, any] {
return [
Object.fromEntries(entries.slice(0, index)),
Object.fromEntries(entries.slice(index)),
];
}
}
type Exclude = { [key: string]: Exclude | true };
/**
* This function transforms all keys (recursively) in the provided `val` object.
*
* @param val The object whose keys need to be transformed.
* @param transform The function that will be applied to each key.
* @param exclude The keys that will not be transformed and copied to output directly
* @returns A new object with the same values as `val`, but with all keys transformed according to `transform`.
*/
export function transformObjectKeys(val: any, transform: (str: string) => string, exclude: Exclude = {}): any {
if (val == null || typeof val !== 'object') {
return val;
}
if (Array.isArray(val)) {
// For arrays we just pass parent's exclude object directly
// since it makes no sense to specify different exclude options for each array element
return val.map((input: any) => transformObjectKeys(input, transform, exclude));
}
const ret: { [k: string]: any } = {};
for (const [k, v] of Object.entries(val)) {
const childExclude = exclude[k];
if (childExclude === true) {
// we don't transform this object if the key is specified in exclude
ret[transform(k)] = v;
} else {
ret[transform(k)] = transformObjectKeys(v, transform, childExclude);
}
}
return ret;
}