| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221 |
- "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.forEach(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;
- }, '');
- }
|