@domonda/query-params
Version:
Useful but simple query params manipulator for React.
124 lines (123 loc) • 5.06 kB
JavaScript
/**
*
* queryParams
*
*/
import { stringify as qsStringify, parse as qsParse } from 'query-string';
import { parseISOToDate, stripTime } from './date';
const ARRAY_FORMAT = 'bracket';
// consists of numbers, optional floating point, does not begin with zero
// (valid, native, `Number`s in JS never start with zero)
const NUMBER_REGEX = /^(?!0)([-0-9]+)(\.[0-9]+)?$/;
function isNaN(num) {
// only NaN is not equal to itself
return num !== num;
}
export function strictUriEncode(str) {
return encodeURIComponent(str).replace(/[!'()*]/g, (x) => `%${x.charCodeAt(0).toString(16).toUpperCase()}`);
}
/** Stringify function which omits `undefined` values and persists empty arrays. */
export function stringify(value, props = {}) {
const { prependQuestionMark } = props;
const str = qsStringify(Object.entries(value).reduce((acc, [key, val]) => {
// omit `undefined`s
if (val === undefined) {
return acc;
}
// convert valid dates to iso strings, omit invalid dates
if (val instanceof Date) {
if (isNaN(val.getDate())) {
return acc;
}
return Object.assign(Object.assign({}, acc), { [key]: val.toISOString() });
}
// persist empty arrays
if (Array.isArray(val) && val.length === 0) {
return Object.assign(Object.assign({}, acc), { [key]: [null] });
}
// everything else is just passed through
return Object.assign(Object.assign({}, acc), { [key]: val });
}, {}), { arrayFormat: ARRAY_FORMAT });
if (prependQuestionMark && str.length > 0) {
return '?' + str;
}
return str;
}
/** Parses the URL query string following the `QueryModel` definitions. */
export function parseQueryParams(queryString, model) {
const parsedQuery = qsParse(queryString, {
arrayFormat: ARRAY_FORMAT,
});
const parsedValues = Object.keys(model).reduce((accumulator, key) => {
const { type, validate, defaultValue } = model[key];
const parsedAndValidatedQueryValue = (() => {
let parsedQueryValue = parsedQuery[key];
if (parsedQueryValue) {
switch (type) {
case 'boolean':
const maybeBool = String(parsedQueryValue).toLowerCase();
if (maybeBool === 't' || maybeBool === 'true' || maybeBool === '1') {
parsedQueryValue = true;
}
else {
parsedQueryValue = false;
}
break;
case 'number':
parsedQueryValue = parseFloat(parsedQueryValue);
break;
case 'array': {
if (parsedQueryValue.length === 1 && parsedQueryValue[0] == null) {
parsedQueryValue = [];
}
else {
// parse numbers, wherever possible
parsedQueryValue = parsedQueryValue.map((val) => {
if (NUMBER_REGEX.test(val)) {
const maybeNum = parseFloat(val);
if (!isNaN(maybeNum)) {
return maybeNum;
}
}
return val;
});
}
break;
}
case 'date':
if (parsedQueryValue) {
parsedQueryValue = stripTime(parseISOToDate(parsedQueryValue));
if (isNaN(parsedQueryValue.getTime())) {
return undefined;
}
}
break;
}
}
if (parsedQueryValue !== undefined && validate && !validate(parsedQueryValue)) {
return undefined;
}
return parsedQueryValue;
})();
return deepFreeze(Object.assign(Object.assign({}, accumulator), { [key]: parsedAndValidatedQueryValue === undefined
? typeof defaultValue === 'function'
? defaultValue()
: defaultValue
: parsedAndValidatedQueryValue }));
}, {});
return parsedValues;
}
/** Following the model, gives the query params populated with default values. */
export function defaultQueryParams(model) {
return parseQueryParams('', model);
}
function deepFreeze(object) {
Object.freeze(object);
Object.getOwnPropertyNames(object).forEach((name) => {
const property = object[name];
if (property && typeof property === 'object' && !Object.isFrozen(property)) {
deepFreeze(property);
}
});
return object;
}