UNPKG

@feugene/mu

Version:

Helpful TS utilities without dependencies

108 lines 4.06 kB
import isNil from '../is/isNil.mjs'; import isNumeric from '../is/isNumeric.mjs'; const queryRe = /^\?/; const keyRe = /(\[):?([^\]]*)]/g; const nameRe = /^([^[]+)/; // eslint-disable-line no-useless-escape /** * Converts a query string back into an object. * * Implementation notes (v5): * - Uses native `URLSearchParams` for decoding; input may start with leading `?`. * - When `recursive=false` (default), repeated keys become arrays, otherwise the Rails-style * bracket syntax is parsed to objects/arrays (e.g., `a[b]=1&a[c]=2`, `arr[0]=x`). * - Security: forbidden keys (`"__proto__"`, `"prototype"`, `"constructor"`) are ignored at all nesting levels * to prevent prototype pollution. * * @example * fromQueryString("foo=1&bar=2"); // returns {foo: '1', bar: '2'} * fromQueryString("foo=&bar=2"); // returns {foo: '', bar: '2'} * fromQueryString("some%20price=%24300"); // returns {'some price': '$300'} * fromQueryString("colors=red&colors=green&colors=blue"); // returns {colors: ['red', 'green', 'blue']} * * * @param queryString The query string to decode. * @param recursive True to interpret bracket syntax and build nested structures. * @param options Options bag: `{ decodeName: boolean }` (names are already decoded by URLSearchParams). * @returns A plain object constructed from the query string. */ export default function fromQueryString(queryString, recursive = false, options = { decodeName: true }) { if (isNil(queryString)) { return {}; } const qs = String(queryString).replace(queryRe, ''); const params = new URLSearchParams(qs); const object = Object.create(null); let temporary; let matchedKeys; let matchedName; let keys; let key; let nextKey; const FORBIDDEN = new Set(['__proto__', 'prototype', 'constructor']); function isForbiddenKey(key) { return FORBIDDEN.has(key); } for (const [rawName, rawValue] of params) { const name = options.decodeName ? rawName : rawName; // already decoded by URLSearchParams const value = rawValue; // already decoded if (!recursive) { if (Object.hasOwn(object, name)) { if (!Array.isArray(object[name])) { object[name] = [object[name]]; } object[name].push(value); } else { if (!isForbiddenKey(name)) { object[name] = value; } } continue; } matchedKeys = name.match(keyRe); matchedName = name.match(nameRe); if (!matchedName) { // skip malformed entries instead of throwing to be more forgiving continue; } const top = matchedName[0]; if (isForbiddenKey(top)) { continue; } keys = []; if (matchedKeys === null) { object[top] = value; continue; } for (let j = 0, subLn = matchedKeys.length; j < subLn; j++) { key = matchedKeys[j]; key = key.length === 2 ? '' : key.substring(1, key.length - 1); keys.push(key); } keys.unshift(top); temporary = object; for (let j = 0, subLn = keys.length; j < subLn; j++) { key = keys[j]; if (isForbiddenKey(key)) { break; } if (j === subLn - 1) { if (Array.isArray(temporary) && key === '') { temporary.push(value); } else { temporary[key] = value; } } else { if (temporary[key] === undefined || typeof temporary[key] === 'string') { nextKey = keys[j + 1]; temporary[key] = isNumeric(nextKey) || nextKey === '' ? [] : {}; } temporary = temporary[key]; } } } return object; } //# sourceMappingURL=fromQueryString.mjs.map