UNPKG

nested-search-params

Version:

Parse URL search prams into nested structures

106 lines 3.62 kB
/** * Matches the full key with optional bracketed subkeys (e.g., "user[address][city]") */ const fullKeyRegex = /^([^[\]]+)((?:\[[^[\]]*])*)$/; /** * Matches each bracketed section inside a key (e.g., "[address]", "[city]") */ const bracketsRegex = /\[([^[\]]*)]/g; /** * Matches numeric array indices (e.g., "0", "1") */ const indexRegex = /^\d+$/; /** * Parses a query string key into a path array */ const parseKey = (key) => { const match = fullKeyRegex.exec(key); if (!match) { return null; } const path = [match[1]]; const brackets = match[2]; path.push(...Array.from(brackets.matchAll(bracketsRegex), (match) => match[1])); return path; }; /** * Recursively removes undefined entries and empty holes from arrays */ const filterEmptyItems = (current) => { if (Array.isArray(current)) { return current .filter((value) => value !== undefined) .map((value) => { if (typeof value === "string") { return value; } return filterEmptyItems(value); }); } return Object.fromEntries(Object.entries(current).map(([key, value]) => { if (typeof value === "string") { return [key, value]; } return [key, filterEmptyItems(value)]; })); }; /** * Parses a URL query string or `URLSearchParams` input into a deeply nested object * * Supports bracket and array notation like `user[address][city]=NY`. * * Any key parts named `__proto__`, `constructor` or `prototype` are skipped to prevent prototype pollution. * * @example * ```ts * parseSearchParams("foo[0]=bar&foo[1]=baz"); * // => { foo: ["bar", "baz"] } * ``` */ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Kept within one function for performance reasons export const parseSearchParams = (input) => { const searchParams = new URLSearchParams(input); const result = Object.create(null); for (const [key, value] of searchParams) { const path = parseKey(key); if (!path) { continue; } let previous = null; let keyInPrevious = null; let current = result; for (let i = 0; i < path.length; ++i) { const part = path[i]; const isLast = i === path.length - 1; const isIndex = indexRegex.test(part); if (Array.isArray(current) && !isIndex && part !== "") { current = Object.fromEntries([...current.entries()].filter(([_, value]) => value !== undefined)); if (previous && keyInPrevious !== null) { previous[keyInPrevious] = current; } } const key = Array.isArray(current) && isIndex ? Number.parseInt(part, 10) : Array.isArray(current) && part === "" ? current.length : part; if (key === "__proto__" || key === "constructor" || key === "prototype") { // Skip unsafe keys to prevent prototype pollution continue; } if (isLast) { current[key] = value; continue; } previous = current; keyInPrevious = key; if (!current[key] || typeof current[key] === "string") { const next = path[i + 1]; current[key] = next === "" || indexRegex.test(next) ? [] : {}; } current = current[key]; } } return filterEmptyItems(result); }; //# sourceMappingURL=index.js.map