Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
queryable.js288 linesDownload Raw Back to utils
1const util = require('node:util')
2const _delete = Symbol('delete')
3const _append = Symbol('append')
4
5const sqBracketsMatcher = str => str.match(/(.+)\[([^\]]+)\]\.?(.*)$/)
6
7// replaces any occurrence of an empty-brackets (e.g: []) with a special Symbol(append) to represent it
8// this is going to be useful for the setter method that will push values to the end of the array when finding these
9const replaceAppendSymbols = str => {
10  const matchEmptyBracket = str.match(/^(.*)\[\]\.?(.*)$/)
11
12  if (matchEmptyBracket) {
13    const [, pre, post] = matchEmptyBracket
14    return [...replaceAppendSymbols(pre), _append, post].filter(Boolean)
15  }
16
17  return [str]
18}
19
20const parseKeys = key => {
21  const sqBracketItems = new Set()
22  sqBracketItems.add(_append)
23  const parseSqBrackets = str => {
24    const index = sqBracketsMatcher(str)
25
26    // once we find square brackets, we recursively parse all these
27    if (index) {
28      const preSqBracketPortion = index[1]
29
30      // we want to have a `new String` wrapper here in order to differentiate between multiple occurrences of the same string,
31      // e.g: foo.bar[foo.bar] should split into { foo: { bar: { 'foo.bar': {} } }
32      /* eslint-disable-next-line no-new-wrappers */
33      const foundKey = new String(index[2])
34      const postSqBracketPortion = index[3]
35
36      // we keep track of items found during this step to make sure we don't try to split-separate keys that were defined within square brackets, since the key name itself might contain dots
37      sqBracketItems.add(foundKey)
38
39      // returns an array that contains either dot-separate items (that will be split apart during the next step OR the fully parsed keys read from square brackets
40      // e.g: foo.bar[1.0.0].a.b -> ['foo.bar', '1.0.0', 'a.b']
41      return [
42        ...parseSqBrackets(preSqBracketPortion),
43        foundKey,
44        ...(postSqBracketPortion ? parseSqBrackets(postSqBracketPortion) : []),
45      ]
46    }
47
48    // at the end of parsing, any usage of the special empty-bracket syntax (e.g: foo.array[]) has not yet been parsed
49    // here we'll take care of parsing it and adding a special symbol to represent it in the resulting list of keys
50    return replaceAppendSymbols(str)
51  }
52
53  const res = []
54  // starts by parsing items defined as square brackets
55  // those might be representing properties that have a dot in the name or just array indexes
56  // e.g: foo[1.0.0] or list[0]
57  const sqBracketKeys = parseSqBrackets(key.trim())
58
59  for (const k of sqBracketKeys) {
60    // keys parsed from square brackets should just be added to list of resulting keys as they might have dots as part of the key
61    if (sqBracketItems.has(k)) {
62      res.push(k)
63    } else {
64      // splits the dot-sep property names and add them to the list of keys
65      /* eslint-disable-next-line no-new-wrappers */
66      for (const splitKey of k.split('.')) {
67        res.push(String(splitKey))
68      }
69    }
70  }
71
72  // returns an ordered list of strings in which each entry represents a key in an object defined by the previous entry
73  return res
74}
75
76const getter = ({ data, key }, { unwrapSingleItemArrays = true } = {}) => {
77  // keys are a list in which each entry represents the name of a property that should be walked through the object in order to return the final found value
78  const keys = parseKeys(key)
79  let _data = data
80  let label = ''
81
82  for (const k of keys) {
83    // empty-bracket-shortcut-syntax is not supported on getter
84    if (k === _append) {
85      throw Object.assign(new Error('Empty brackets are not valid syntax for retrieving values.'), {
86        code: 'EINVALIDSYNTAX',
87      })
88    }
89
90    // extra logic to take into account printing array, along with its special syntax in which using a dot-sep property name after an array will expand it's results
91    // e.g: arr.name -> arr[0].name=value, arr[1].name=value, ...
92    const maybeIndex = Number(k)
93    if (Array.isArray(_data) && !Number.isInteger(maybeIndex)) {
94      _data = _data.reduce((acc, i, index) => {
95        acc[`${label}[${index}].${k}`] = i[k]
96        return acc
97      }, {})
98      return _data
99    } else {
100      if (!Object.hasOwn(_data, k)) {
101        return undefined
102      }
103      _data = _data[k]
104    }
105
106    label += k
107  }
108
109  // these are some legacy expectations from the old API consumed by lib/view.js
110  if (unwrapSingleItemArrays && Array.isArray(_data) && _data.length <= 1) {
111    _data = _data[0]
112  }
113
114  return {
115    [key]: _data,
116  }
117}
118
119const setter = ({ data, key, value, force }) => {
120  // setter goes to recursively transform the provided data obj
121  // setting properties from the list of parsed keys
122  // e.g: ['foo', 'bar', 'baz'] -> { foo: { bar: { baz:  {} } }
123  const keys = parseKeys(key)
124  const setKeys = (_data, _key) => {
125    // handles array indexes, converting valid integers to numbers
126    // note that occurrences of Symbol(append) will throw so we just ignore these for now
127    let maybeIndex = Number.NaN
128    try {
129      maybeIndex = Number(_key)
130    } catch {
131      // leave it NaN
132    }
133    if (!Number.isNaN(maybeIndex)) {
134      _key = maybeIndex
135    }
136
137    // creates new array in case key is an index and the array obj is not yet defined
138    const keyIsAnArrayIndex = _key === maybeIndex || _key === _append
139    const dataHasNoItems = !Object.keys(_data).length
140    if (keyIsAnArrayIndex && dataHasNoItems && !Array.isArray(_data)) {
141      _data = []
142    }
143
144    // converting from array to an object is also possible, in case the user is using force mode
145    // we should also convert existing arrays to an empty object if the current _data is an array
146    if (force && Array.isArray(_data) && !keyIsAnArrayIndex) {
147      _data = { ..._data }
148    }
149
150    // the _append key is a special key that is used to represent the empty-bracket notation
151    // e.g: arr[] -> arr[arr.length]
152    if (_key === _append) {
153      if (!Array.isArray(_data)) {
154        throw Object.assign(new Error(`Can't use append syntax in non-Array element`), {
155          code: 'ENOAPPEND',
156        })
157      }
158      _key = _data.length
159    }
160
161    // retrieves the next data object to recursively iterate on
162    // throws if trying to override a literal value or add props to an array
163    const next = () => {
164      const haveContents = !force && _data[_key] != null && value !== _delete
165      const shouldNotOverrideLiteralValue = !(typeof _data[_key] === 'object')
166      // if the next obj to recurse is an array and the next key to be appended to the resulting obj is not an array index, then it should throw since we can't append arbitrary props to arrays
167      const shouldNotAddPropsToArrays =
168        typeof keys[0] !== 'symbol' && Array.isArray(_data[_key]) && Number.isNaN(Number(keys[0]))
169
170      const overrideError = haveContents && shouldNotOverrideLiteralValue
171      if (overrideError) {
172        throw Object.assign(
173          new Error(`Property ${_key} already exists and is not an Array or Object.`),
174          { code: 'EOVERRIDEVALUE' }
175        )
176      }
177
178      const addPropsToArrayError = haveContents && shouldNotAddPropsToArrays
179      if (addPropsToArrayError) {
180        throw Object.assign(new Error(`Can't add property ${key} to an Array.`), {
181          code: 'ENOADDPROP',
182        })
183      }
184
185      return typeof _data[_key] === 'object' ? _data[_key] || {} : {}
186    }
187
188    // sets items from the parsed array of keys as objects, recurses to setKeys in case there are still items to be handled
189    // otherwise, it just sets the original value set by the user
190    if (keys.length) {
191      _data[_key] = setKeys(next(), keys.shift())
192    } else {
193      // handles special deletion cases for obj props / array items
194      if (value === _delete) {
195        if (Array.isArray(_data)) {
196          _data.splice(_key, 1)
197        } else {
198          delete _data[_key]
199        }
200      } else {
201        // finally, sets the value in its right place
202        _data[_key] = value
203      }
204    }
205
206    return _data
207  }
208
209  setKeys(data, keys.shift())
210}
211
212class Queryable {
213  static ALL = ''
214
215  #data = null
216
217  constructor (obj) {
218    if (!obj || typeof obj !== 'object') {
219      throw Object.assign(new Error('Queryable needs an object to query properties from.'), {
220        code: 'ENOQUERYABLEOBJ',
221      })
222    }
223
224    this.#data = obj
225  }
226
227  query (queries, opts) {
228    // this ugly interface here is meant to be a compatibility layer with the legacy API lib/view.js is consuming
229    // if at some point we refactor that command then we can revisit making this nicer
230    if (queries === Queryable.ALL) {
231      return { [Queryable.ALL]: this.#data }
232    }
233
234    const q = query =>
235      getter({
236        data: this.#data,
237        key: query,
238      }, opts)
239
240    if (Array.isArray(queries)) {
241      let res = {}
242      for (const query of queries) {
243        res = { ...res, ...q(query) }
244      }
245      return res
246    } else {
247      return q(queries)
248    }
249  }
250
251  // return the value for a single query if found; otherwise, returns undefined
252  get (query) {
253    const obj = this.query(query)
254    if (obj) {
255      return obj[query]
256    }
257  }
258
259  // creates objects along the way for the provided `query` parameter and assigns `value` to the last property of the query chain
260  set (query, value, { force } = {}) {
261    setter({
262      data: this.#data,
263      key: query,
264      value,
265      force,
266    })
267  }
268
269  // deletes the value of the property found at `query`
270  delete (query) {
271    setter({
272      data: this.#data,
273      key: query,
274      value: _delete,
275    })
276  }
277
278  toJSON () {
279    return this.#data
280  }
281
282  [util.inspect.custom] () {
283    return this.toJSON()
284  }
285}
286
287module.exports = Queryable
288 
codekingpro/portable-devtools · Team Ai