UNPKG

@exadel/esl

Version:

Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components

108 lines (107 loc) 4.46 kB
import { isObjectLike } from './types'; /** @returns PathKeyDef from the PathDef */ const toKeyDef = (key) => typeof key === 'object' ? key : { key }; /** Parses path to full {@link PathKeyDef} array */ export const parseKeys = (path) => { if (Array.isArray(path)) return path.map(toKeyDef); return parseKeysPath(path || '.'); }; /** Parses string path to full {@link PathKeyDef} array */ const parseKeysPath = (path) => { let start = 0; const parts = []; while (start < path.length) { let end = start = start + +(path[start] === '.'); // skip initial '.' if (path[start] === '[') { // handle index syntax end = ++start; // start bracket ignored while (end < path.length && path[end] !== ']') ++end; const key = path.substring(start, end); const isIndex = !key || Math.floor(+key) === +key; parts.push({ key, isIndex }); start = end + 1; // skip end bracket } else { // handle simple key while (end < path.length && path[end] !== '[' && path[end] !== '.') ++end; const key = path.substring(start, end); parts.push({ key }); start = end; } } return parts; }; // Keys that are blocked to prevent CWE-1321 (prototype pollution) const DEPRECATED_KEYS = new Set(['__proto__', 'constructor', 'prototype']); /** * Gets object property using "path" key * * Supports three types of key definition * - full array of {@link PathKeyDef} * - array of keys (string or number), can be mixed with a full definitions {@link PathKeyDef} * - string path mode (supports index syntax): * - `a.b` - simple key access (`{a : {b: val}}`) * - `a[0]` - index access, creates collection if it's not exists (`{a : [val]}`) * - `a[]` - pushes to the end of collection (`{a : [..., val]}`) * - `a[a.b.c]` - escaping: non-numeric indexes uses as a simple keys, delimiters inside square brackets are ignored (`{a : {'a.b.c': val}}`) * * @param data - object * @param path - key path. string or {@link PathKey} array * @param defaultValue - default * @returns specified object property */ export const getByPath = (data, path, defaultValue) => { const keys = parseKeys(path); const result = keys.reduce((curr, { key }) => { if (isObjectLike(curr)) return curr[key]; return undefined; }, data); return typeof result === 'undefined' ? defaultValue : result; }; /** * Gets object property using "path" with a keys separated by `.` * @see getByPath */ export const get = (data, path, defaultValue) => getByPath(data, (path || '').split('.'), defaultValue); /** * Sets object property using "path" key * Creates empty object if sub-key value is not presented. * * Supports three types of key definition * - full array of {@link PathKeyDef} * - array of keys (string or number), can be mixed with a full definitions {@link PathKeyDef} * - string path mode (supports index syntax and collection creation): * - `a.b` - simple key access (`{a : {b: val}}`) * - `a[0]` - index access, creates collection if it's not exists (`{a : [val]}`) * - `a[]` - pushes to the end of collection (`{a : [..., val]}`) * - `a[a.b.c]` - escaping: non-numeric indexes uses as a simple keys, delimiters inside square brackets are ignored (`{a : {'a.b.c': val}}`) * * @param target - object * @param path - key path. string or {@link PathKey} array * @param value - value of property * @returns original object */ export const setByPath = (target, path, value) => { const keys = parseKeys(path); const depth = keys.length - 1; // prevent prototype pollution if (keys.some(({ key }) => DEPRECATED_KEYS.has(key))) return target; keys.reduce((cur, { key, isIndex, isIndexed }, pos) => { if (isIndex && !key) key = cur.length || 0; // a[] only if (pos !== depth && isObjectLike(cur[key])) return cur[key]; // key already presented if (isIndexed === undefined && pos !== depth) isIndexed = keys[pos + 1].isIndex; return cur[key] = (pos === depth) ? value : (isIndexed ? [] : {}); }, target); return target; }; /** * Sets object property using "path" with a keys separated by `.` * @see setByPath */ export const set = (target, path, value) => setByPath(target, (path || '').split('.'), value);