field-selection.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.selectUnknownFields = exports.selectFields = void 0;
  4. exports.reconstructFieldPath = reconstructFieldPath;
  5. const _ = require("lodash");
  6. const selectFields = (req, fields, locations) => _(fields)
  7. .flatMap(field => _.flatMap(locations, location => {
  8. return expandField(req, field, location);
  9. }))
  10. // Avoid duplicates if multiple field selections would return the same field twice.
  11. // E.g. with fields = ['*.foo', 'bar.foo'] and req.body = { bar: { foo: 1 }, baz: { foo: 2 } },
  12. // the instance bla.foo would appear twice, and baz.foo once.
  13. .uniqWith(isSameFieldInstance)
  14. .value();
  15. exports.selectFields = selectFields;
  16. function isSameFieldInstance(a, b) {
  17. return a.path === b.path && a.location === b.location;
  18. }
  19. function expandField(req, field, location) {
  20. const originalPath = field;
  21. const pathToExpand = location === 'headers' ? field.toLowerCase() : field;
  22. const paths = expandPath(req[location], pathToExpand, []);
  23. return paths.map(({ path, values }) => {
  24. const value = path === '' ? req[location] : _.get(req[location], path);
  25. return {
  26. location,
  27. path,
  28. originalPath,
  29. pathValues: values,
  30. value,
  31. };
  32. });
  33. }
  34. function expandPath(object, path, currPath, currValues = []) {
  35. const segments = _.toPath(path);
  36. if (!segments.length) {
  37. // no more paths to traverse
  38. return [
  39. {
  40. path: reconstructFieldPath(currPath),
  41. values: currValues,
  42. },
  43. ];
  44. }
  45. const key = segments[0];
  46. const rest = segments.slice(1);
  47. if (object != null && !_.isObjectLike(object)) {
  48. if (key === '**') {
  49. if (!rest.length) {
  50. // globstar leaves are always selected
  51. return [
  52. {
  53. path: reconstructFieldPath(currPath),
  54. values: currValues,
  55. },
  56. ];
  57. }
  58. return [];
  59. }
  60. if (key === '*') {
  61. // wildcard position does not exist
  62. return [];
  63. }
  64. // value is a primitive, paths being traversed from here might be in their prototype, return the entire path
  65. return [
  66. {
  67. path: reconstructFieldPath([...currPath, ...segments]),
  68. values: currValues,
  69. },
  70. ];
  71. }
  72. // Use a non-null value so that inexistent fields are still selected
  73. object = object || {};
  74. if (key === '*') {
  75. return Object.keys(object).flatMap(key => expandPath(object[key], rest, currPath.concat(key), currValues.concat(key)));
  76. }
  77. if (key === '**') {
  78. return Object.keys(object).flatMap(key => {
  79. const nextPath = currPath.concat(key);
  80. const value = object[key];
  81. // recursively find matching subpaths
  82. const selectedPaths = expandPath(value, segments, nextPath, [key]).concat(
  83. // skip the first remaining segment, if it matches the current key
  84. rest[0] === key ? expandPath(value, rest.slice(1), nextPath, []) : []);
  85. return _.uniqBy(selectedPaths, ({ path }) => path).map(({ path, values }) => ({
  86. path,
  87. values: values.length ? [...currValues, values.flat()] : currValues,
  88. }));
  89. });
  90. }
  91. return expandPath(object[key], rest, currPath.concat(key), currValues);
  92. }
  93. const selectUnknownFields = (req, knownFields, locations) => {
  94. const tree = {};
  95. knownFields.forEach(field => {
  96. const segments = field === '' ? [''] : _.toPath(field);
  97. pathToTree(segments, tree);
  98. });
  99. const instances = [];
  100. for (const location of locations) {
  101. if (req[location] != null) {
  102. instances.push(...findUnknownFields(location, req[location], tree));
  103. }
  104. }
  105. return instances;
  106. };
  107. exports.selectUnknownFields = selectUnknownFields;
  108. function pathToTree(segments, tree) {
  109. // Will either create or merge into existing branch for the current path segment
  110. const branch = tree[segments[0]] || (tree[segments[0]] = {});
  111. if (segments.length > 1) {
  112. pathToTree(segments.slice(1), branch);
  113. }
  114. else {
  115. // Leaf value.
  116. branch[''] = {};
  117. }
  118. }
  119. /**
  120. * Performs a depth-first search for unknown fields in `value`.
  121. * The path to the unknown fields will be pushed to the `unknownFields` argument.
  122. *
  123. * Known fields must be passed via `tree`. A field won't be considered unknown if:
  124. * - its branch is validated as a whole; that is, it contains an empty string key (e.g `{ ['']: {} }`); OR
  125. * - its path is individually validated; OR
  126. * - it's covered by a wildcard (`*`).
  127. *
  128. * @returns the list of unknown fields
  129. */
  130. function findUnknownFields(location, value, tree, treePath = [], unknownFields = []) {
  131. const globstarBranch = tree['**'];
  132. if (tree[''] || globstarBranch?.['']) {
  133. // The rest of the tree from here is covered by some validation chain
  134. // For example, when the current treePath is `['foo', 'bar']` but `foo` is known
  135. return unknownFields;
  136. }
  137. if (typeof value !== 'object') {
  138. if (!treePath.length || globstarBranch) {
  139. // This is either
  140. // a. a req.body that isn't an object (e.g. `req.body = 'bla'`), and wasn't validated either
  141. // b. a leaf value which wasn't the target of a globstar path, e.g. `foo.**.bar`
  142. unknownFields.push({
  143. path: reconstructFieldPath(treePath),
  144. value,
  145. location,
  146. });
  147. }
  148. return unknownFields;
  149. }
  150. const wildcardBranch = tree['*'];
  151. for (const key of Object.keys(value)) {
  152. const keyBranch = tree[key];
  153. const path = treePath.concat([key]);
  154. if (!keyBranch && !wildcardBranch && !globstarBranch) {
  155. // No trees cover this path, so it's an unknown one.
  156. unknownFields.push({
  157. path: reconstructFieldPath(path),
  158. value: value[key],
  159. location,
  160. });
  161. continue;
  162. }
  163. const keyUnknowns = keyBranch ? findUnknownFields(location, value[key], keyBranch, path) : [];
  164. const wildcardUnknowns = wildcardBranch
  165. ? findUnknownFields(location, value[key], wildcardBranch, path)
  166. : [];
  167. const globstarUnknowns = globstarBranch
  168. ? findUnknownFields(location, value[key], { ['**']: globstarBranch, ...globstarBranch }, path)
  169. : [];
  170. // If any of the tested branches contain only known fields, then don't mark the fields not covered
  171. // by the other branches to the list of unknown ones.
  172. // For example, `foo` is more comprehensive than `foo.*.bar`.
  173. if ((!keyBranch || keyUnknowns.length) &&
  174. (!wildcardBranch || wildcardUnknowns.length) &&
  175. (!globstarBranch || globstarUnknowns.length)) {
  176. unknownFields.push(...keyUnknowns, ...wildcardUnknowns, ...globstarUnknowns);
  177. }
  178. }
  179. return unknownFields;
  180. }
  181. /**
  182. * Reconstructs a field path from a list of path segments.
  183. *
  184. * Most segments will be concatenated by a dot, for example `['foo', 'bar']` becomes `foo.bar`.
  185. * However, a numeric segment will be wrapped in brackets to match regular JS array syntax:
  186. *
  187. * ```
  188. * reconstructFieldPath(['foo', 0, 'bar']) // foo[0].bar
  189. * ```
  190. *
  191. * Segments which have a special character such as `.` will be wrapped in brackets and quotes,
  192. * which also matches JS syntax for objects with such keys.
  193. *
  194. * ```
  195. * reconstructFieldPath(['foo', 'bar.baz', 'qux']) // foo["bar.baz"].qux
  196. * ```
  197. */
  198. function reconstructFieldPath(segments) {
  199. return segments.reduce((prev, segment) => {
  200. let part = '';
  201. segment = segment === '\\*' ? '*' : segment;
  202. // TODO: Handle brackets?
  203. if (segment.includes('.')) {
  204. // Special char key access
  205. part = `["${segment}"]`;
  206. }
  207. else if (/^\d+$/.test(segment)) {
  208. // Index access
  209. part = `[${segment}]`;
  210. }
  211. else if (prev) {
  212. // Object key access
  213. part = `.${segment}`;
  214. }
  215. else {
  216. // Top level key
  217. part = segment;
  218. }
  219. return prev + part;
  220. }, '');
  221. }