nested-query-params
Version:
Rack like parsing of nested query parameters
139 lines • 4.65 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseQueryParamKey = exports.parseQuery = void 0;
const util_1 = require("./util");
const types_1 = require("./types");
const logger = prepareConsole(console, "nested-query-params");
/**
* Expands a search params into structural types. Supported types are Arrays,
* Objects and basic value types. Heavily inspired by
* {@link https://github.com/rack/rack/blob/bad8fe37c8867596855dcd0b3fe3030acc6b8621/lib/rack/query_parser.rb#L63-L68|Rack's nested query parser}.
*
* @example
* // returns { one: { two: "3" } };
* parseQuery("?one[two]=3")
*/
function parseQuery(query) {
const pairs = (0, util_1.splitQuery)(query);
return pairs.reduce((acc, [key, value]) => {
const comps = parseQueryParamKey(key);
if (Array.isArray(value)) {
return value.reduce((acc, value) => applyNestedParam(acc, comps, value), acc);
}
return applyNestedParam(acc, comps, value);
}, {});
}
exports.parseQuery = parseQuery;
/**
* Parses nested search parameters keys into their individual path components.
*/
function parseQueryParamKey(key) {
if (key.trim() === "") {
return [];
}
let start;
if ((start = key.indexOf("[", 1)) === -1) {
return [[types_1.QueryParamPathComponentKind.Map, key]];
}
const head = key.slice(0, start);
const rest = key.slice(start);
return [
[types_1.QueryParamPathComponentKind.Map, head],
...parseQueryParamKeyComponents(rest),
];
}
exports.parseQueryParamKey = parseQueryParamKey;
/**
* Applies given key value pair to search parameters.
*/
function applyNestedParam(params, comps, value) {
const [head] = comps;
if (!head) {
return params;
}
if (head[0] !== types_1.QueryParamPathComponentKind.Map) {
logger.warn("#applyNestedParam", "cannot apply list op to root");
return params;
}
return applyComponent(params, comps, value);
}
function applyComponent(params, comps, value) {
const log = prepareConsole(logger, "#applyComponent");
const [head, ...rest] = comps;
if (!head) {
return params;
}
if (head[0] === types_1.QueryParamPathComponentKind.Map) {
if (typeof params !== "object" || Array.isArray(params)) {
params = {};
}
if (rest.length === 0) {
return {
...params,
[head[1]]: Array.isArray(value) ? value[value.length - 1] : value,
};
}
const key = head[1];
return {
...params,
[head[1]]: applyComponent(params[key], rest, value),
};
}
if (head[0] === types_1.QueryParamPathComponentKind.List) {
if (!Array.isArray(params)) {
params = [];
}
if (rest.length === 0) {
return [
...params,
...(Array.isArray(value) ? value : [value]),
];
}
const [peek] = rest;
if (peek[0] !== types_1.QueryParamPathComponentKind.Map) {
log.warn("expected map");
return params;
}
const last = params[params.length - 1] || undefined;
if (typeof last === "object" && !Array.isArray(last) && !last[peek[1]]) {
const next = applyNestedParam(last, rest, value);
return [...params.slice(0, -1), next];
}
else if (Array.isArray(last)) {
log.warn("nested arrays are not supported");
}
else if (last && typeof last !== "object") {
log.warn("expected map");
}
// add value to list if list was previously empty or the given key is
// already in the map.
const next = applyNestedParam({}, rest, value);
return [...params, next];
}
return params;
}
function parseQueryParamKeyComponents(key) {
// list
if (key.startsWith("[]")) {
return [
[types_1.QueryParamPathComponentKind.List],
...parseQueryParamKeyComponents(key.slice(2)),
];
}
// map
let start;
if (key.startsWith("[") && (start = key.indexOf("]", 1)) !== -1) {
return [
[types_1.QueryParamPathComponentKind.Map, key.slice(1, start)],
...parseQueryParamKeyComponents(key.slice(start + 1)),
];
}
return [];
}
function prepareConsole(parent, ...init) {
return ["info", "error", "warn", "debug"].reduce((acc, method) => ({
[method]: (...msgs) => parent[method](...init, ...msgs),
...acc,
}), {});
}
//# sourceMappingURL=parse.js.map