11cb0ef41Sopenharmony_ciconst util = require('util') 21cb0ef41Sopenharmony_ciconst _delete = Symbol('delete') 31cb0ef41Sopenharmony_ciconst _append = Symbol('append') 41cb0ef41Sopenharmony_ci 51cb0ef41Sopenharmony_ciconst sqBracketsMatcher = str => str.match(/(.+)\[([^\]]+)\]\.?(.*)$/) 61cb0ef41Sopenharmony_ci 71cb0ef41Sopenharmony_ci// replaces any occurrence of an empty-brackets (e.g: []) with a special 81cb0ef41Sopenharmony_ci// Symbol(append) to represent it, this is going to be useful for the setter 91cb0ef41Sopenharmony_ci// method that will push values to the end of the array when finding these 101cb0ef41Sopenharmony_ciconst replaceAppendSymbols = str => { 111cb0ef41Sopenharmony_ci const matchEmptyBracket = str.match(/^(.*)\[\]\.?(.*)$/) 121cb0ef41Sopenharmony_ci 131cb0ef41Sopenharmony_ci if (matchEmptyBracket) { 141cb0ef41Sopenharmony_ci const [, pre, post] = matchEmptyBracket 151cb0ef41Sopenharmony_ci return [...replaceAppendSymbols(pre), _append, post].filter(Boolean) 161cb0ef41Sopenharmony_ci } 171cb0ef41Sopenharmony_ci 181cb0ef41Sopenharmony_ci return [str] 191cb0ef41Sopenharmony_ci} 201cb0ef41Sopenharmony_ci 211cb0ef41Sopenharmony_ciconst parseKeys = key => { 221cb0ef41Sopenharmony_ci const sqBracketItems = new Set() 231cb0ef41Sopenharmony_ci sqBracketItems.add(_append) 241cb0ef41Sopenharmony_ci const parseSqBrackets = str => { 251cb0ef41Sopenharmony_ci const index = sqBracketsMatcher(str) 261cb0ef41Sopenharmony_ci 271cb0ef41Sopenharmony_ci // once we find square brackets, we recursively parse all these 281cb0ef41Sopenharmony_ci if (index) { 291cb0ef41Sopenharmony_ci const preSqBracketPortion = index[1] 301cb0ef41Sopenharmony_ci 311cb0ef41Sopenharmony_ci // we want to have a `new String` wrapper here in order to differentiate 321cb0ef41Sopenharmony_ci // between multiple occurrences of the same string, e.g: 331cb0ef41Sopenharmony_ci // foo.bar[foo.bar] should split into { foo: { bar: { 'foo.bar': {} } } 341cb0ef41Sopenharmony_ci /* eslint-disable-next-line no-new-wrappers */ 351cb0ef41Sopenharmony_ci const foundKey = new String(index[2]) 361cb0ef41Sopenharmony_ci const postSqBracketPortion = index[3] 371cb0ef41Sopenharmony_ci 381cb0ef41Sopenharmony_ci // we keep track of items found during this step to make sure 391cb0ef41Sopenharmony_ci // we don't try to split-separate keys that were defined within 401cb0ef41Sopenharmony_ci // square brackets, since the key name itself might contain dots 411cb0ef41Sopenharmony_ci sqBracketItems.add(foundKey) 421cb0ef41Sopenharmony_ci 431cb0ef41Sopenharmony_ci // returns an array that contains either dot-separate items (that will 441cb0ef41Sopenharmony_ci // be split apart during the next step OR the fully parsed keys 451cb0ef41Sopenharmony_ci // read from square brackets, e.g: 461cb0ef41Sopenharmony_ci // foo.bar[1.0.0].a.b -> ['foo.bar', '1.0.0', 'a.b'] 471cb0ef41Sopenharmony_ci return [ 481cb0ef41Sopenharmony_ci ...parseSqBrackets(preSqBracketPortion), 491cb0ef41Sopenharmony_ci foundKey, 501cb0ef41Sopenharmony_ci ...(postSqBracketPortion ? parseSqBrackets(postSqBracketPortion) : []), 511cb0ef41Sopenharmony_ci ] 521cb0ef41Sopenharmony_ci } 531cb0ef41Sopenharmony_ci 541cb0ef41Sopenharmony_ci // at the end of parsing, any usage of the special empty-bracket syntax 551cb0ef41Sopenharmony_ci // (e.g: foo.array[]) has not yet been parsed, here we'll take care 561cb0ef41Sopenharmony_ci // of parsing it and adding a special symbol to represent it in 571cb0ef41Sopenharmony_ci // the resulting list of keys 581cb0ef41Sopenharmony_ci return replaceAppendSymbols(str) 591cb0ef41Sopenharmony_ci } 601cb0ef41Sopenharmony_ci 611cb0ef41Sopenharmony_ci const res = [] 621cb0ef41Sopenharmony_ci // starts by parsing items defined as square brackets, those might be 631cb0ef41Sopenharmony_ci // representing properties that have a dot in the name or just array 641cb0ef41Sopenharmony_ci // indexes, e.g: foo[1.0.0] or list[0] 651cb0ef41Sopenharmony_ci const sqBracketKeys = parseSqBrackets(key.trim()) 661cb0ef41Sopenharmony_ci 671cb0ef41Sopenharmony_ci for (const k of sqBracketKeys) { 681cb0ef41Sopenharmony_ci // keys parsed from square brackets should just be added to list of 691cb0ef41Sopenharmony_ci // resulting keys as they might have dots as part of the key 701cb0ef41Sopenharmony_ci if (sqBracketItems.has(k)) { 711cb0ef41Sopenharmony_ci res.push(k) 721cb0ef41Sopenharmony_ci } else { 731cb0ef41Sopenharmony_ci // splits the dot-sep property names and add them to the list of keys 741cb0ef41Sopenharmony_ci /* eslint-disable-next-line no-new-wrappers */ 751cb0ef41Sopenharmony_ci for (const splitKey of k.split('.')) { 761cb0ef41Sopenharmony_ci res.push(String(splitKey)) 771cb0ef41Sopenharmony_ci } 781cb0ef41Sopenharmony_ci } 791cb0ef41Sopenharmony_ci } 801cb0ef41Sopenharmony_ci 811cb0ef41Sopenharmony_ci // returns an ordered list of strings in which each entry 821cb0ef41Sopenharmony_ci // represents a key in an object defined by the previous entry 831cb0ef41Sopenharmony_ci return res 841cb0ef41Sopenharmony_ci} 851cb0ef41Sopenharmony_ci 861cb0ef41Sopenharmony_ciconst getter = ({ data, key }) => { 871cb0ef41Sopenharmony_ci // keys are a list in which each entry represents the name of 881cb0ef41Sopenharmony_ci // a property that should be walked through the object in order to 891cb0ef41Sopenharmony_ci // return the final found value 901cb0ef41Sopenharmony_ci const keys = parseKeys(key) 911cb0ef41Sopenharmony_ci let _data = data 921cb0ef41Sopenharmony_ci let label = '' 931cb0ef41Sopenharmony_ci 941cb0ef41Sopenharmony_ci for (const k of keys) { 951cb0ef41Sopenharmony_ci // empty-bracket-shortcut-syntax is not supported on getter 961cb0ef41Sopenharmony_ci if (k === _append) { 971cb0ef41Sopenharmony_ci throw Object.assign(new Error('Empty brackets are not valid syntax for retrieving values.'), { 981cb0ef41Sopenharmony_ci code: 'EINVALIDSYNTAX', 991cb0ef41Sopenharmony_ci }) 1001cb0ef41Sopenharmony_ci } 1011cb0ef41Sopenharmony_ci 1021cb0ef41Sopenharmony_ci // extra logic to take into account printing array, along with its 1031cb0ef41Sopenharmony_ci // special syntax in which using a dot-sep property name after an 1041cb0ef41Sopenharmony_ci // arry will expand it's results, e.g: 1051cb0ef41Sopenharmony_ci // arr.name -> arr[0].name=value, arr[1].name=value, ... 1061cb0ef41Sopenharmony_ci const maybeIndex = Number(k) 1071cb0ef41Sopenharmony_ci if (Array.isArray(_data) && !Number.isInteger(maybeIndex)) { 1081cb0ef41Sopenharmony_ci _data = _data.reduce((acc, i, index) => { 1091cb0ef41Sopenharmony_ci acc[`${label}[${index}].${k}`] = i[k] 1101cb0ef41Sopenharmony_ci return acc 1111cb0ef41Sopenharmony_ci }, {}) 1121cb0ef41Sopenharmony_ci return _data 1131cb0ef41Sopenharmony_ci } else { 1141cb0ef41Sopenharmony_ci if (!Object.hasOwn(_data, k)) { 1151cb0ef41Sopenharmony_ci return undefined 1161cb0ef41Sopenharmony_ci } 1171cb0ef41Sopenharmony_ci _data = _data[k] 1181cb0ef41Sopenharmony_ci } 1191cb0ef41Sopenharmony_ci 1201cb0ef41Sopenharmony_ci label += k 1211cb0ef41Sopenharmony_ci } 1221cb0ef41Sopenharmony_ci 1231cb0ef41Sopenharmony_ci // these are some legacy expectations from 1241cb0ef41Sopenharmony_ci // the old API consumed by lib/view.js 1251cb0ef41Sopenharmony_ci if (Array.isArray(_data) && _data.length <= 1) { 1261cb0ef41Sopenharmony_ci _data = _data[0] 1271cb0ef41Sopenharmony_ci } 1281cb0ef41Sopenharmony_ci 1291cb0ef41Sopenharmony_ci return { 1301cb0ef41Sopenharmony_ci [key]: _data, 1311cb0ef41Sopenharmony_ci } 1321cb0ef41Sopenharmony_ci} 1331cb0ef41Sopenharmony_ci 1341cb0ef41Sopenharmony_ciconst setter = ({ data, key, value, force }) => { 1351cb0ef41Sopenharmony_ci // setter goes to recursively transform the provided data obj, 1361cb0ef41Sopenharmony_ci // setting properties from the list of parsed keys, e.g: 1371cb0ef41Sopenharmony_ci // ['foo', 'bar', 'baz'] -> { foo: { bar: { baz: {} } } 1381cb0ef41Sopenharmony_ci const keys = parseKeys(key) 1391cb0ef41Sopenharmony_ci const setKeys = (_data, _key) => { 1401cb0ef41Sopenharmony_ci // handles array indexes, converting valid integers to numbers, 1411cb0ef41Sopenharmony_ci // note that occurrences of Symbol(append) will throw, 1421cb0ef41Sopenharmony_ci // so we just ignore these for now 1431cb0ef41Sopenharmony_ci let maybeIndex = Number.NaN 1441cb0ef41Sopenharmony_ci try { 1451cb0ef41Sopenharmony_ci maybeIndex = Number(_key) 1461cb0ef41Sopenharmony_ci } catch { 1471cb0ef41Sopenharmony_ci // leave it NaN 1481cb0ef41Sopenharmony_ci } 1491cb0ef41Sopenharmony_ci if (!Number.isNaN(maybeIndex)) { 1501cb0ef41Sopenharmony_ci _key = maybeIndex 1511cb0ef41Sopenharmony_ci } 1521cb0ef41Sopenharmony_ci 1531cb0ef41Sopenharmony_ci // creates new array in case key is an index 1541cb0ef41Sopenharmony_ci // and the array obj is not yet defined 1551cb0ef41Sopenharmony_ci const keyIsAnArrayIndex = _key === maybeIndex || _key === _append 1561cb0ef41Sopenharmony_ci const dataHasNoItems = !Object.keys(_data).length 1571cb0ef41Sopenharmony_ci if (keyIsAnArrayIndex && dataHasNoItems && !Array.isArray(_data)) { 1581cb0ef41Sopenharmony_ci _data = [] 1591cb0ef41Sopenharmony_ci } 1601cb0ef41Sopenharmony_ci 1611cb0ef41Sopenharmony_ci // converting from array to an object is also possible, in case the 1621cb0ef41Sopenharmony_ci // user is using force mode, we should also convert existing arrays 1631cb0ef41Sopenharmony_ci // to an empty object if the current _data is an array 1641cb0ef41Sopenharmony_ci if (force && Array.isArray(_data) && !keyIsAnArrayIndex) { 1651cb0ef41Sopenharmony_ci _data = { ..._data } 1661cb0ef41Sopenharmony_ci } 1671cb0ef41Sopenharmony_ci 1681cb0ef41Sopenharmony_ci // the _append key is a special key that is used to represent 1691cb0ef41Sopenharmony_ci // the empty-bracket notation, e.g: arr[] -> arr[arr.length] 1701cb0ef41Sopenharmony_ci if (_key === _append) { 1711cb0ef41Sopenharmony_ci if (!Array.isArray(_data)) { 1721cb0ef41Sopenharmony_ci throw Object.assign(new Error(`Can't use append syntax in non-Array element`), { 1731cb0ef41Sopenharmony_ci code: 'ENOAPPEND', 1741cb0ef41Sopenharmony_ci }) 1751cb0ef41Sopenharmony_ci } 1761cb0ef41Sopenharmony_ci _key = _data.length 1771cb0ef41Sopenharmony_ci } 1781cb0ef41Sopenharmony_ci 1791cb0ef41Sopenharmony_ci // retrieves the next data object to recursively iterate on, 1801cb0ef41Sopenharmony_ci // throws if trying to override a literal value or add props to an array 1811cb0ef41Sopenharmony_ci const next = () => { 1821cb0ef41Sopenharmony_ci const haveContents = !force && _data[_key] != null && value !== _delete 1831cb0ef41Sopenharmony_ci const shouldNotOverrideLiteralValue = !(typeof _data[_key] === 'object') 1841cb0ef41Sopenharmony_ci // if the next obj to recurse is an array and the next key to be 1851cb0ef41Sopenharmony_ci // appended to the resulting obj is not an array index, then it 1861cb0ef41Sopenharmony_ci // should throw since we can't append arbitrary props to arrays 1871cb0ef41Sopenharmony_ci const shouldNotAddPropsToArrays = 1881cb0ef41Sopenharmony_ci typeof keys[0] !== 'symbol' && Array.isArray(_data[_key]) && Number.isNaN(Number(keys[0])) 1891cb0ef41Sopenharmony_ci 1901cb0ef41Sopenharmony_ci const overrideError = haveContents && shouldNotOverrideLiteralValue 1911cb0ef41Sopenharmony_ci if (overrideError) { 1921cb0ef41Sopenharmony_ci throw Object.assign( 1931cb0ef41Sopenharmony_ci new Error(`Property ${_key} already exists and is not an Array or Object.`), 1941cb0ef41Sopenharmony_ci { code: 'EOVERRIDEVALUE' } 1951cb0ef41Sopenharmony_ci ) 1961cb0ef41Sopenharmony_ci } 1971cb0ef41Sopenharmony_ci 1981cb0ef41Sopenharmony_ci const addPropsToArrayError = haveContents && shouldNotAddPropsToArrays 1991cb0ef41Sopenharmony_ci if (addPropsToArrayError) { 2001cb0ef41Sopenharmony_ci throw Object.assign(new Error(`Can't add property ${key} to an Array.`), { 2011cb0ef41Sopenharmony_ci code: 'ENOADDPROP', 2021cb0ef41Sopenharmony_ci }) 2031cb0ef41Sopenharmony_ci } 2041cb0ef41Sopenharmony_ci 2051cb0ef41Sopenharmony_ci return typeof _data[_key] === 'object' ? _data[_key] || {} : {} 2061cb0ef41Sopenharmony_ci } 2071cb0ef41Sopenharmony_ci 2081cb0ef41Sopenharmony_ci // sets items from the parsed array of keys as objects, recurses to 2091cb0ef41Sopenharmony_ci // setKeys in case there are still items to be handled, otherwise it 2101cb0ef41Sopenharmony_ci // just sets the original value set by the user 2111cb0ef41Sopenharmony_ci if (keys.length) { 2121cb0ef41Sopenharmony_ci _data[_key] = setKeys(next(), keys.shift()) 2131cb0ef41Sopenharmony_ci } else { 2141cb0ef41Sopenharmony_ci // handles special deletion cases for obj props / array items 2151cb0ef41Sopenharmony_ci if (value === _delete) { 2161cb0ef41Sopenharmony_ci if (Array.isArray(_data)) { 2171cb0ef41Sopenharmony_ci _data.splice(_key, 1) 2181cb0ef41Sopenharmony_ci } else { 2191cb0ef41Sopenharmony_ci delete _data[_key] 2201cb0ef41Sopenharmony_ci } 2211cb0ef41Sopenharmony_ci } else { 2221cb0ef41Sopenharmony_ci // finally, sets the value in its right place 2231cb0ef41Sopenharmony_ci _data[_key] = value 2241cb0ef41Sopenharmony_ci } 2251cb0ef41Sopenharmony_ci } 2261cb0ef41Sopenharmony_ci 2271cb0ef41Sopenharmony_ci return _data 2281cb0ef41Sopenharmony_ci } 2291cb0ef41Sopenharmony_ci 2301cb0ef41Sopenharmony_ci setKeys(data, keys.shift()) 2311cb0ef41Sopenharmony_ci} 2321cb0ef41Sopenharmony_ci 2331cb0ef41Sopenharmony_ciclass Queryable { 2341cb0ef41Sopenharmony_ci #data = null 2351cb0ef41Sopenharmony_ci 2361cb0ef41Sopenharmony_ci constructor (obj) { 2371cb0ef41Sopenharmony_ci if (!obj || typeof obj !== 'object') { 2381cb0ef41Sopenharmony_ci throw Object.assign(new Error('Queryable needs an object to query properties from.'), { 2391cb0ef41Sopenharmony_ci code: 'ENOQUERYABLEOBJ', 2401cb0ef41Sopenharmony_ci }) 2411cb0ef41Sopenharmony_ci } 2421cb0ef41Sopenharmony_ci 2431cb0ef41Sopenharmony_ci this.#data = obj 2441cb0ef41Sopenharmony_ci } 2451cb0ef41Sopenharmony_ci 2461cb0ef41Sopenharmony_ci query (queries) { 2471cb0ef41Sopenharmony_ci // this ugly interface here is meant to be a compatibility layer 2481cb0ef41Sopenharmony_ci // with the legacy API lib/view.js is consuming, if at some point 2491cb0ef41Sopenharmony_ci // we refactor that command then we can revisit making this nicer 2501cb0ef41Sopenharmony_ci if (queries === '') { 2511cb0ef41Sopenharmony_ci return { '': this.#data } 2521cb0ef41Sopenharmony_ci } 2531cb0ef41Sopenharmony_ci 2541cb0ef41Sopenharmony_ci const q = query => 2551cb0ef41Sopenharmony_ci getter({ 2561cb0ef41Sopenharmony_ci data: this.#data, 2571cb0ef41Sopenharmony_ci key: query, 2581cb0ef41Sopenharmony_ci }) 2591cb0ef41Sopenharmony_ci 2601cb0ef41Sopenharmony_ci if (Array.isArray(queries)) { 2611cb0ef41Sopenharmony_ci let res = {} 2621cb0ef41Sopenharmony_ci for (const query of queries) { 2631cb0ef41Sopenharmony_ci res = { ...res, ...q(query) } 2641cb0ef41Sopenharmony_ci } 2651cb0ef41Sopenharmony_ci return res 2661cb0ef41Sopenharmony_ci } else { 2671cb0ef41Sopenharmony_ci return q(queries) 2681cb0ef41Sopenharmony_ci } 2691cb0ef41Sopenharmony_ci } 2701cb0ef41Sopenharmony_ci 2711cb0ef41Sopenharmony_ci // return the value for a single query if found, otherwise returns undefined 2721cb0ef41Sopenharmony_ci get (query) { 2731cb0ef41Sopenharmony_ci const obj = this.query(query) 2741cb0ef41Sopenharmony_ci if (obj) { 2751cb0ef41Sopenharmony_ci return obj[query] 2761cb0ef41Sopenharmony_ci } 2771cb0ef41Sopenharmony_ci } 2781cb0ef41Sopenharmony_ci 2791cb0ef41Sopenharmony_ci // creates objects along the way for the provided `query` parameter 2801cb0ef41Sopenharmony_ci // and assigns `value` to the last property of the query chain 2811cb0ef41Sopenharmony_ci set (query, value, { force } = {}) { 2821cb0ef41Sopenharmony_ci setter({ 2831cb0ef41Sopenharmony_ci data: this.#data, 2841cb0ef41Sopenharmony_ci key: query, 2851cb0ef41Sopenharmony_ci value, 2861cb0ef41Sopenharmony_ci force, 2871cb0ef41Sopenharmony_ci }) 2881cb0ef41Sopenharmony_ci } 2891cb0ef41Sopenharmony_ci 2901cb0ef41Sopenharmony_ci // deletes the value of the property found at `query` 2911cb0ef41Sopenharmony_ci delete (query) { 2921cb0ef41Sopenharmony_ci setter({ 2931cb0ef41Sopenharmony_ci data: this.#data, 2941cb0ef41Sopenharmony_ci key: query, 2951cb0ef41Sopenharmony_ci value: _delete, 2961cb0ef41Sopenharmony_ci }) 2971cb0ef41Sopenharmony_ci } 2981cb0ef41Sopenharmony_ci 2991cb0ef41Sopenharmony_ci toJSON () { 3001cb0ef41Sopenharmony_ci return this.#data 3011cb0ef41Sopenharmony_ci } 3021cb0ef41Sopenharmony_ci 3031cb0ef41Sopenharmony_ci [util.inspect.custom] () { 3041cb0ef41Sopenharmony_ci return this.toJSON() 3051cb0ef41Sopenharmony_ci } 3061cb0ef41Sopenharmony_ci} 3071cb0ef41Sopenharmony_ci 3081cb0ef41Sopenharmony_cimodule.exports = Queryable 309