gatsby
Version:
Blazing fast modern site generator for React
276 lines (259 loc) • 10.4 kB
JavaScript
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
exports.__esModule = true;
exports.applyFastFilters = applyFastFilters;
exports.runFastFiltersAndSort = runFastFiltersAndSort;
var _orderBy2 = _interopRequireDefault(require("lodash/orderBy"));
var _query = require("../common/query");
var _indexing = require("./indexing");
var _iterable = require("../common/iterable");
var _2 = require("../");
/**
* Creates a key for one filterCache inside FiltersCache
*/
function createFilterCacheKey(typeNames, filter) {
// Note: while `elemMatch` is a special case, in the key it's just `elemMatch`
// (This function is future proof for elemMatch support, won't receive it yet)
let filterStep = filter;
let comparator = ``;
const paths = [];
while (filterStep) {
paths.push(...filterStep.path);
if (filterStep.type === `elemMatch`) {
const q = filterStep;
filterStep = q.nestedQuery;
// Make distinction between filtering `a.elemMatch.b.eq` and `a.b.eq`
// In practice this is unlikely to be an issue, but it might
paths.push(`elemMatch`);
} else {
const q = filterStep;
comparator = q.query.comparator;
break;
}
}
// Note: the separators (`,` and `/`) are arbitrary but must be different
return typeNames.join(`,`) + `/` + paths.join(`,`) + `/` + comparator;
}
/**
* Given the path of a set of filters, return the sets of nodes that pass the
* filter.
* Only nodes of given node types will be considered
* A fast index is created if one doesn't exist yet so cold call is slower.
*
* Note: Not a public API. Exported for tests.
*/
function applyFastFilters(filters, nodeTypeNames, filtersCache, sortFields, resolvedFields) {
if (!filtersCache) {
// If no filter cache is passed on, explicitly don't use one
return null;
}
const nodesPerValueArrs = getBucketsForFilters(filters, nodeTypeNames, filtersCache, sortFields, resolvedFields);
if (!nodesPerValueArrs) {
return null;
}
if (nodesPerValueArrs.length === 0) {
return [];
} else {
// Put smallest last (we'll pop it)
nodesPerValueArrs.sort((a, b) => b.length - a.length);
// All elements of nodesPerValueArrs should be sorted by counter and deduped
// So if there's only one bucket in this list the next loop is skipped
while (nodesPerValueArrs.length > 1) {
// TS limitation: cannot guard against .pop(), so we must double cast
const a = nodesPerValueArrs.pop();
const b = nodesPerValueArrs.pop();
nodesPerValueArrs.push((0, _indexing.intersectNodesByCounter)(a, b));
}
const result = nodesPerValueArrs[0];
if (result.length === 0) {
// Intersection came up empty. Not one node appeared in every bucket.
return null;
}
return result;
}
}
/**
* If this returns undefined it means at least one cache was not found
*/
function getBucketsForFilters(filters, nodeTypeNames, filtersCache, sortFields, resolvedFields) {
const nodesPerValueArrs = [];
// Fail fast while trying to create and get the value-cache for each path
const every = filters.every(filter => {
const filterCacheKey = createFilterCacheKey(nodeTypeNames, filter);
if (filter.type === `query`) {
// (Let TS warn us if a new query type gets added)
const q = filter;
return getBucketsForQueryFilter(filterCacheKey, q, nodeTypeNames, filtersCache, nodesPerValueArrs, sortFields, resolvedFields);
} else {
// (Let TS warn us if a new query type gets added)
const q = filter;
return collectBucketForElemMatch(filterCacheKey, q, nodeTypeNames, filtersCache, nodesPerValueArrs, sortFields, resolvedFields);
}
});
if (every) {
return nodesPerValueArrs;
}
// "failed at least one"
return undefined;
}
/**
* Fetch all buckets for given query filter. That means it's not elemMatch.
* Returns `false` if it found none.
*/
function getBucketsForQueryFilter(filterCacheKey, filter, nodeTypeNames, filtersCache, nodesPerValueArrs, sortFields, resolvedFields) {
const {
path: filterPath,
query: {
comparator,
value: filterValue
}
} = filter;
if (!filtersCache.has(filterCacheKey)) {
// indexFields = sortFields
(0, _indexing.ensureIndexByQuery)(comparator, filterCacheKey, filterPath, nodeTypeNames, filtersCache, sortFields, resolvedFields);
}
const nodesPerValue = (0, _indexing.getNodesFromCacheByValue)(filterCacheKey, filterValue, filtersCache, false);
if (!nodesPerValue) {
return false;
}
// In all other cases this must be a non-empty arr because the indexing
// mechanism does not create an array unless there's a IGatsbyNode for it
nodesPerValueArrs.push(nodesPerValue);
return true;
}
/**
* Matching node arrs are put in given array by reference
*/
function collectBucketForElemMatch(filterCacheKey, filter, nodeTypeNames, filtersCache, nodesPerValueArrs, sortFields, resolvedFields) {
// Get comparator and target value for this elemMatch
let comparator = `$eq`; // (Must be overridden but TS requires init)
let targetValue = null;
let f = filter;
while (f) {
if (f.type === `elemMatch`) {
const q = f;
f = q.nestedQuery;
} else {
const q = f;
comparator = q.query.comparator;
targetValue = q.query.value;
break;
}
}
if (!filtersCache.has(filterCacheKey)) {
(0, _indexing.ensureIndexByElemMatch)(comparator, filterCacheKey, filter, nodeTypeNames, filtersCache, sortFields, resolvedFields);
}
const nodesByValue = (0, _indexing.getNodesFromCacheByValue)(filterCacheKey, targetValue, filtersCache, true);
if (!nodesByValue) {
return false;
}
// In all other cases this must be a non-empty arr because the indexing
// mechanism does not create an array unless there's a IGatsbyNode for it
nodesPerValueArrs.push(nodesByValue);
return true;
}
/**
* Filters and sorts a list of nodes using mongodb-like syntax.
*
* @param args raw graphql query filter/sort as an object
* @property {{filter?: Object, sort?: Object, skip?: number, limit?: number} | undefined} args.queryArgs
* @property {FiltersCache} args.filtersCache A cache of indexes where you can
* look up Nodes grouped by a FilterCacheKey, which yields a Map which holds
* an arr of Nodes for the value that the filter is trying to query against.
* This object lives in query/query-runner.js and is passed down runQuery.
* @returns Collection of results. Collection will be sliced by `skip` and `limit`
*/
function runFastFiltersAndSort(args) {
const {
queryArgs: {
filter,
sort,
limit,
skip = 0
} = {},
resolvedFields = {},
nodeTypeNames,
filtersCache,
stats
} = args;
const result = convertAndApplyFastFilters(filter, nodeTypeNames, filtersCache, resolvedFields, stats, sort);
const sortedResult = sortNodes(result, sort, resolvedFields, stats);
const totalCount = async () => sortedResult.length;
const entries = skip || limit ? sortedResult.slice(skip, limit ? skip + (limit !== null && limit !== void 0 ? limit : 0) : undefined) : sortedResult;
const nodeObjects = entries.map(nodeIds => (0, _2.getNode)(nodeIds.id));
return {
entries: new _iterable.GatsbyIterable(nodeObjects),
totalCount
};
}
/**
* Return a collection of results.
*/
function convertAndApplyFastFilters(filterFields, nodeTypeNames, filtersCache, resolvedFields, stats, sort) {
const filters = filterFields ? (0, _query.prefixResolvedFields)((0, _query.createDbQueriesFromObject)((0, _query.prepareQueryArgs)(filterFields)), resolvedFields) : [];
if (stats) {
filters.forEach(filter => {
const filterStats = filterToStats(filter);
const comparatorPath = filterStats.comparatorPath.join(`.`);
stats.comparatorsUsed.set(comparatorPath, (stats.comparatorsUsed.get(comparatorPath) || 0) + 1);
stats.uniqueFilterPaths.add(filterStats.filterPath.join(`.`));
});
if (filters.length > 1) {
stats.totalNonSingleFilters++;
}
}
if (filters.length === 0) {
const filterCacheKey = createFilterCacheKey(nodeTypeNames, null);
if (!filtersCache.has(filterCacheKey)) {
(0, _indexing.ensureEmptyFilterCache)(filterCacheKey, nodeTypeNames, filtersCache, (sort === null || sort === void 0 ? void 0 : sort.fields) || [], resolvedFields);
}
// If there's a filter, there (now) must be an entry for this cache key
const filterCache = filtersCache.get(filterCacheKey);
// If there is no filter then the ensureCache step will populate this:
const cache = filterCache.meta.orderedByCounter;
return cache.slice(0);
}
const result = applyFastFilters(filters, nodeTypeNames, filtersCache, (sort === null || sort === void 0 ? void 0 : sort.fields) || [], resolvedFields);
if (result) {
if (stats) {
stats.totalIndexHits++;
}
return result;
}
if (stats) {
// to mean, "empty results"
stats.totalSiftHits++;
}
return [];
}
function filterToStats(filter, filterPath = [], comparatorPath = []) {
if (filter.type === `elemMatch`) {
return filterToStats(filter.nestedQuery, filterPath.concat(filter.path), comparatorPath.concat([`elemMatch`]));
} else {
return {
filterPath: filterPath.concat(filter.path),
comparatorPath: comparatorPath.concat(filter.query.comparator)
};
}
}
/**
* Given a list of filtered nodes and sorting parameters, sort the nodes
* Returns same reference as input, sorted inline
*/
function sortNodes(nodes, sort, resolvedFields, stats) {
var _sort$fields;
if (!sort || ((_sort$fields = sort.fields) === null || _sort$fields === void 0 ? void 0 : _sort$fields.length) === 0 || !nodes || nodes.length === 0) {
return nodes;
}
// create functions that return the item to compare on
const sortFields = (0, _indexing.getSortFieldIdentifierKeys)(sort.fields, resolvedFields);
const sortFns = sortFields.map(field => v => field in v ? v[field] : (0, _indexing.getGatsbyNodePartial)(v, sort.fields, resolvedFields)[field]);
const sortOrder = sort.order.map(order => typeof order === `boolean` ? order : order.toLowerCase());
if (stats) {
sortFields.forEach(sortField => {
stats.uniqueSorts.add(sortField);
});
}
return (0, _orderBy2.default)(nodes, sortFns, sortOrder);
}
//# sourceMappingURL=run-fast-filters.js.map
;