@ministryofjustice/hmpps-digital-prison-reporting-frontend
Version:
The Digital Prison Reporting Frontend contains templates and code to help display data effectively in UI applications.
192 lines (188 loc) • 6.42 kB
JavaScript
;
var dateHelper = require('./dateHelper.js');
// ------------------------------------------
// Normalization
// ------------------------------------------
/**
* Normalizes a querystring parameter into a consistent string array.
*
* Needed because Express/qs may return repeated query params as strings, arrays,
* or numeric‑keyed objects depending on parser settings and arrayLimit behavior
*
* @param value The raw query parameter value from req.query.
* @returns An array of strings representing the normalized parameter.
*/
const normalizeQueryStringArray = (queryParamValue) => {
if (queryParamValue == null)
return undefined;
if (Array.isArray(queryParamValue))
return queryParamValue;
if (typeof queryParamValue === 'object') {
const obj = queryParamValue;
const numericKeys = Object.keys(obj)
.filter(k => /^\d+$/.test(k))
.sort((a, b) => Number(a) - Number(b));
return numericKeys.length > 0 ? numericKeys.map(k => obj[k]) : undefined;
}
return typeof queryParamValue === 'string' ? [queryParamValue] : undefined;
};
// ------------------------------------------
// Parsing
// ------------------------------------------
const DEFAULT_EXCLUDED_KEYS = new Set([
'_csrf',
'reportName',
'name',
'reportId',
'id',
'description',
'type',
'sections',
]);
/**
* Converts a querystring to a query object
*
* @param {string} qs
* @param {string} prefix
* @return {*} {(Record<string, string | string[]>)}
*/
const qsToQueryObject = (qs, prefix) => {
return Array.from(new URLSearchParams(qs).entries())
.filter(([key]) => (prefix ? key.startsWith(prefix) : true))
.reduce((acc, [key, value]) => {
const existing = acc[key];
if (existing === undefined) {
acc[key] = value;
}
else if (Array.isArray(existing)) {
acc[key] = [...existing, value];
}
else {
acc[key] = [existing, value];
}
return acc;
}, {});
};
/**
* Converts a query obect back to a querystring
*
* @param {(Record<string, string | string[]>)} query
* @return {*} {string}
*/
const queryObjectToQs = (source, exclude = DEFAULT_EXCLUDED_KEYS) => {
return Object.entries(source)
.filter(([key]) => !exclude.has(key))
.flatMap(([key, value]) => Array.isArray(value)
? value.map(v => [key, normaliseUiDateIfPresent(v)])
: [[key, normaliseUiDateIfPresent(value)]])
.reduce((params, [key, value]) => {
params.append(key, value);
return params;
}, new URLSearchParams())
.toString();
};
const UI_DATE_REGEX = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/;
const normaliseUiDateIfPresent = (value) => {
if (!UI_DATE_REGEX.test(value)) {
return value;
}
return dateHelper.uiDateToApi(value) ?? value;
};
/**
* Extracts `filters.*` entries from a query object.
*/
const extractFiltersFromQuery = (query) => {
return Object.fromEntries(Object.entries(query).filter(([key]) => key.startsWith('filters.')));
};
/**
* Given a form request body, will strip out the
* 'filters.' prefix to return the field name/id and value
*
* @param {Record<string, unknown>} body
* @return {*} {Record<string, unknown>}
*/
const extractFiltersFromBody = (body) => {
const result = {};
Object.entries(body).forEach(([key, value]) => {
if (!key.startsWith('filters.'))
return;
const path = key.replace('filters.', '').split('.');
let current = result;
path.forEach((segment, index) => {
const isLast = index === path.length - 1;
if (isLast) {
current[segment] = value;
return;
}
if (typeof current[segment] !== 'object' || current[segment] === null) {
current[segment] = {};
}
current = current[segment];
});
});
return result;
};
const getQueryParamAsString = (query, name) => query[name] ? query[name].toString() : null;
// ------------------------------------------
// UI to API adapters
// ------------------------------------------
/**
* Builds a query string from a form body
*
* @param {Record<string, unknown>} body
* @param {Set<string>} [exclude=new Set()]
* @return {*} {string}
*/
const formBodyToQs = (body, exclude = DEFAULT_EXCLUDED_KEYS) => {
const params = new URLSearchParams();
Object.entries(body).forEach(([key, value]) => {
if (exclude.has(key))
return;
if (value == null || value === '')
return;
if (Array.isArray(value)) {
const values = value.filter(v => v != null && v !== '').map(v => normaliseUiDateIfPresent(String(v)));
if (values.length > 0) {
params.set(key, values.join(','));
}
return;
}
params.set(key, normaliseUiDateIfPresent(String(value)));
});
return params.toString();
};
/**
* Converts a form body to an API query object
* - Excludes specified keys
* - Drops null / empty values
* - Collapses arrays to CSV (BE contract)
*
* @param {Record<string, unknown>} body
* @return {*} {(Record<string, string | string[]>)}
*/
const formBodyToQueryObject = (body, exclude = DEFAULT_EXCLUDED_KEYS) => {
return Object.entries(body).reduce((acc, [key, value]) => {
if (exclude.has(key))
return acc;
if (value === undefined || value === null || value === '')
return acc;
if (Array.isArray(value)) {
const values = value.filter(v => v != null && v !== '').map(v => normaliseUiDateIfPresent(String(v)));
if (values.length > 0) {
acc[key] = values.join(',');
}
return acc;
}
acc[key] = normaliseUiDateIfPresent(String(value));
return acc;
}, {});
};
exports.extractFiltersFromBody = extractFiltersFromBody;
exports.extractFiltersFromQuery = extractFiltersFromQuery;
exports.formBodyToQs = formBodyToQs;
exports.formBodyToQueryObject = formBodyToQueryObject;
exports.getQueryParamAsString = getQueryParamAsString;
exports.normalizeQueryStringArray = normalizeQueryStringArray;
exports.qsToQueryObject = qsToQueryObject;
exports.queryObjectToQs = queryObjectToQs;
//# sourceMappingURL=queryMappers.js.map