2025-03-23 21:00:08 +01:00

222 lines
8.2 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.selectUnknownFields = exports.selectFields = void 0;
exports.reconstructFieldPath = reconstructFieldPath;
const _ = require("lodash");
const selectFields = (req, fields, locations) => _(fields)
.flatMap(field => _.flatMap(locations, location => {
return expandField(req, field, location);
}))
// Avoid duplicates if multiple field selections would return the same field twice.
// E.g. with fields = ['*.foo', 'bar.foo'] and req.body = { bar: { foo: 1 }, baz: { foo: 2 } },
// the instance bla.foo would appear twice, and baz.foo once.
.uniqWith(isSameFieldInstance)
.value();
exports.selectFields = selectFields;
function isSameFieldInstance(a, b) {
return a.path === b.path && a.location === b.location;
}
function expandField(req, field, location) {
const originalPath = field;
const pathToExpand = location === 'headers' ? field.toLowerCase() : field;
const paths = expandPath(req[location], pathToExpand, []);
return paths.map(({ path, values }) => {
const value = path === '' ? req[location] : _.get(req[location], path);
return {
location,
path,
originalPath,
pathValues: values,
value,
};
});
}
function expandPath(object, path, currPath, currValues = []) {
const segments = _.toPath(path);
if (!segments.length) {
// no more paths to traverse
return [
{
path: reconstructFieldPath(currPath),
values: currValues,
},
];
}
const key = segments[0];
const rest = segments.slice(1);
if (object != null && !_.isObjectLike(object)) {
if (key === '**') {
if (!rest.length) {
// globstar leaves are always selected
return [
{
path: reconstructFieldPath(currPath),
values: currValues,
},
];
}
return [];
}
if (key === '*') {
// wildcard position does not exist
return [];
}
// value is a primitive, paths being traversed from here might be in their prototype, return the entire path
return [
{
path: reconstructFieldPath([...currPath, ...segments]),
values: currValues,
},
];
}
// Use a non-null value so that inexistent fields are still selected
object = object || {};
if (key === '*') {
return Object.keys(object).flatMap(key => expandPath(object[key], rest, currPath.concat(key), currValues.concat(key)));
}
if (key === '**') {
return Object.keys(object).flatMap(key => {
const nextPath = currPath.concat(key);
const value = object[key];
// recursively find matching subpaths
const selectedPaths = expandPath(value, segments, nextPath, [key]).concat(
// skip the first remaining segment, if it matches the current key
rest[0] === key ? expandPath(value, rest.slice(1), nextPath, []) : []);
return _.uniqBy(selectedPaths, ({ path }) => path).map(({ path, values }) => ({
path,
values: values.length ? [...currValues, values.flat()] : currValues,
}));
});
}
return expandPath(object[key], rest, currPath.concat(key), currValues);
}
const selectUnknownFields = (req, knownFields, locations) => {
const tree = {};
knownFields.map(field => {
const segments = field === '' ? [''] : _.toPath(field);
pathToTree(segments, tree);
});
const instances = [];
for (const location of locations) {
if (req[location] != null) {
instances.push(...findUnknownFields(location, req[location], tree));
}
}
return instances;
};
exports.selectUnknownFields = selectUnknownFields;
function pathToTree(segments, tree) {
// Will either create or merge into existing branch for the current path segment
const branch = tree[segments[0]] || (tree[segments[0]] = {});
if (segments.length > 1) {
pathToTree(segments.slice(1), branch);
}
else {
// Leaf value.
branch[''] = {};
}
}
/**
* Performs a depth-first search for unknown fields in `value`.
* The path to the unknown fields will be pushed to the `unknownFields` argument.
*
* Known fields must be passed via `tree`. A field won't be considered unknown if:
* - its branch is validated as a whole; that is, it contains an empty string key (e.g `{ ['']: {} }`); OR
* - its path is individually validated; OR
* - it's covered by a wildcard (`*`).
*
* @returns the list of unknown fields
*/
function findUnknownFields(location, value, tree, treePath = [], unknownFields = []) {
const globstarBranch = tree['**'];
if (tree[''] || globstarBranch?.['']) {
// The rest of the tree from here is covered by some validation chain
// For example, when the current treePath is `['foo', 'bar']` but `foo` is known
return unknownFields;
}
if (typeof value !== 'object') {
if (!treePath.length || globstarBranch) {
// This is either
// a. a req.body that isn't an object (e.g. `req.body = 'bla'`), and wasn't validated either
// b. a leaf value which wasn't the target of a globstar path, e.g. `foo.**.bar`
unknownFields.push({
path: reconstructFieldPath(treePath),
value,
location,
});
}
return unknownFields;
}
const wildcardBranch = tree['*'];
for (const key of Object.keys(value)) {
const keyBranch = tree[key];
const path = treePath.concat([key]);
if (!keyBranch && !wildcardBranch && !globstarBranch) {
// No trees cover this path, so it's an unknown one.
unknownFields.push({
path: reconstructFieldPath(path),
value: value[key],
location,
});
continue;
}
const keyUnknowns = keyBranch ? findUnknownFields(location, value[key], keyBranch, path) : [];
const wildcardUnknowns = wildcardBranch
? findUnknownFields(location, value[key], wildcardBranch, path)
: [];
const globstarUnknowns = globstarBranch
? findUnknownFields(location, value[key], { ['**']: globstarBranch, ...globstarBranch }, path)
: [];
// If any of the tested branches contain only known fields, then don't mark the fields not covered
// by the other branches to the list of unknown ones.
// For example, `foo` is more comprehensive than `foo.*.bar`.
if ((!keyBranch || keyUnknowns.length) &&
(!wildcardBranch || wildcardUnknowns.length) &&
(!globstarBranch || globstarUnknowns.length)) {
unknownFields.push(...keyUnknowns, ...wildcardUnknowns, ...globstarUnknowns);
}
}
return unknownFields;
}
/**
* Reconstructs a field path from a list of path segments.
*
* Most segments will be concatenated by a dot, for example `['foo', 'bar']` becomes `foo.bar`.
* However, a numeric segment will be wrapped in brackets to match regular JS array syntax:
*
* ```
* reconstructFieldPath(['foo', 0, 'bar']) // foo[0].bar
* ```
*
* Segments which have a special character such as `.` will be wrapped in brackets and quotes,
* which also matches JS syntax for objects with such keys.
*
* ```
* reconstructFieldPath(['foo', 'bar.baz', 'qux']) // foo["bar.baz"].qux
* ```
*/
function reconstructFieldPath(segments) {
return segments.reduce((prev, segment) => {
let part = '';
segment = segment === '\\*' ? '*' : segment;
// TODO: Handle brackets?
if (segment.includes('.')) {
// Special char key access
part = `["${segment}"]`;
}
else if (/^\d+$/.test(segment)) {
// Index access
part = `[${segment}]`;
}
else if (prev) {
// Object key access
part = `.${segment}`;
}
else {
// Top level key
part = segment;
}
return prev + part;
}, '');
}