mongoose
Version:
Mongoose MongoDB ODM
168 lines (153 loc) • 4.75 kB
JavaScript
'use strict';
const wildcardSegment = '$*';
const numericSegmentRegex = /^\d+$/;
class PathTrieNode {
constructor() {
this.children = new Map();
this.terminal = false;
this.hasWildcard = false;
}
}
/**
* Trie of dotted paths for efficient path matching. Supports the map wildcard
* segment `$*`, which matches any single segment. For example, a trie
* containing `settings.$*` matches `settings.theme` but not `settings` or
* `profile.theme`. In array lookups, numeric segments like array indexes are
* ignorable: a trie containing `comments.text` matches `comments.0.text`.
* Single-segment string lookups skip the numeric check entirely.
*
* @api private
*/
class PathTrie {
/**
* @param {string[]} [paths] paths to add to the trie
*/
constructor(paths) {
this.root = new PathTrieNode();
if (paths != null) {
for (const path of paths) {
this.add(path);
}
}
}
/**
* Add a dotted path, like `settings.$*` or `profile.firstName`, to the trie.
*
* @param {string} path
*/
add(path) {
if (path.indexOf('.') === -1) {
this._addSegment(this.root, path).terminal = true;
return;
}
let node = this.root;
for (const segment of path.split('.')) {
node = this._addSegment(node, segment);
}
node.terminal = true;
}
/**
* @param {PathTrieNode} node
* @param {string} segment
*/
_addSegment(node, segment) {
let child = node.children.get(segment);
if (child == null) {
child = new PathTrieNode();
node.children.set(segment, child);
if (segment === wildcardSegment) {
node.hasWildcard = true;
}
}
return child;
}
/**
* Check if `path` or any of its ancestor paths exist in the trie. `path` is
* either a single segment string or a pre-split array of segments — lookups
* never split, callers are responsible for splitting dotted paths.
* For example, with a trie containing 'profile':
* matchesPathOrAncestor(['profile', 'firstName']) === true
* matchesPathOrAncestor('profile') === true
* But with a trie containing 'profile.firstName':
* matchesPathOrAncestor('profile') === false
*
* @param {string|string[]} path
*/
matchesPathOrAncestor(path) {
return this._walk(path) === true;
}
/**
* Check if `path`, any of its ancestor paths, or any of its descendant paths
* exist in the trie. `path` is either a single segment string or a pre-split
* array of segments — lookups never split, callers are responsible for
* splitting dotted paths. For example:
* new PathTrie(['profile']).overlapsPath(['profile', 'firstName']) === true
* new PathTrie(['profile.firstName']).overlapsPath('profile') === true
*
* @param {string|string[]} path
*/
overlapsPath(path) {
return this._walk(path) !== false;
}
/**
* Walk `path` through the trie. `path` is assumed to be pre-split - if the
* path has multiple segments, pass in an array of strings. Returns `true` if
* a terminal node was found at `path` or one of its ancestors, `false` if the
* walk dead-ended, and the array of reached nodes if `path` is a strict prefix
* of entries in the trie.
*
* @param {string|string[]} path
*/
_walk(path) {
if (typeof path === 'string') {
const next = [];
const literal = this.root.children.get(path);
if (literal != null) {
if (literal.terminal) {
return true;
}
next.push(literal);
}
if (this.root.hasWildcard) {
const wildcard = this.root.children.get(wildcardSegment);
if (wildcard.terminal) {
return true;
}
next.push(wildcard);
}
return next.length === 0 ? false : next;
}
let nodes = [this.root];
for (const segment of path) {
const next = [];
const isNumericSegment = numericSegmentRegex.test(segment);
for (const node of nodes) {
if (isNumericSegment) {
// Numeric segments like array indexes are ignorable: `comments.text`
// in the trie matches the modified path `comments.0.text`.
next.push(node);
}
const literal = node.children.get(segment);
if (literal != null) {
if (literal.terminal) {
return true;
}
next.push(literal);
}
if (node.hasWildcard) {
const wildcard = node.children.get(wildcardSegment);
if (wildcard.terminal) {
return true;
}
next.push(wildcard);
}
}
if (next.length === 0) {
return false;
}
nodes = next;
}
return nodes;
}
}
module.exports = PathTrie;