Files
30-seconds-of-code/node_modules/gatsby/dist/db/loki/nodes-query.js
2019-08-20 15:52:05 +02:00

282 lines
7.8 KiB
JavaScript

"use strict";
const _ = require(`lodash`);
const prepareRegex = require(`../../utils/prepare-regex`);
const {
getNodeTypeCollection
} = require(`./nodes`);
const {
emitter
} = require(`../../redux`); // Cleared on DELETE_CACHE
const fieldUsages = {};
const FIELD_INDEX_THRESHOLD = 5;
emitter.on(`DELETE_CACHE`, () => {
for (var field in fieldUsages) {
delete fieldUsages[field];
}
}); // Takes a raw graphql filter and converts it into a mongo-like args
// object that can be understood by loki. E.g `eq` becomes
// `$eq`. gqlFilter should be the raw graphql filter returned from
// graphql-js. e.g gqlFilter:
//
// {
// internal: {
// type: {
// eq: "TestNode"
// },
// content: {
// glob: "et"
// }
// },
// id: {
// glob: "12*"
// }
// }
//
// would return
//
// {
// internal: {
// type: {
// $eq: "TestNode" // append $ to eq
// },
// content: {
// $regex: new MiniMatch(v) // convert glob to regex
// }
// },
// id: {
// $regex: // as above
// }
// }
function toMongoArgs(gqlFilter, lastFieldType) {
const mongoArgs = {};
_.each(gqlFilter, (v, k) => {
if (_.isPlainObject(v)) {
if (k === `elemMatch`) {
const gqlFieldType = lastFieldType.ofType;
mongoArgs[`$elemMatch`] = toMongoArgs(v, gqlFieldType);
} else {
const gqlFieldType = lastFieldType.getFields()[k].type;
mongoArgs[k] = toMongoArgs(v, gqlFieldType);
}
} else {
if (k === `regex`) {
const re = prepareRegex(v); // To ensure that false is returned if a field doesn't
// exist. E.g `{nested.field: {$regex: /.*/}}`
mongoArgs[`$where`] = obj => !_.isUndefined(obj) && re.test(obj);
} else if (k === `glob`) {
const Minimatch = require(`minimatch`).Minimatch;
const mm = new Minimatch(v);
mongoArgs[`$regex`] = mm.makeRe();
} else if (k === `eq` && v === null) {
mongoArgs[`$in`] = [null, undefined];
} else if (k === `eq` && lastFieldType && lastFieldType.constructor.name === `GraphQLList`) {
mongoArgs[`$contains`] = v;
} else if (k === `ne` && lastFieldType && lastFieldType.constructor.name === `GraphQLList`) {
mongoArgs[`$containsNone`] = v;
} else if (k === `in` && lastFieldType && lastFieldType.constructor.name === `GraphQLList`) {
mongoArgs[`$containsAny`] = v;
} else if (k === `nin` && lastFieldType && lastFieldType.constructor.name === `GraphQLList`) {
mongoArgs[`$containsNone`] = v;
} else if (k === `ne` && v === null) {
mongoArgs[`$ne`] = undefined;
} else if (k === `nin` && lastFieldType.name === `Boolean`) {
mongoArgs[`$nin`] = v.concat([undefined]);
} else {
mongoArgs[`$${k}`] = v;
}
}
});
return mongoArgs;
} // Converts a nested mongo args object into a dotted notation. acc
// (accumulator) must be a reference to an empty object. The converted
// fields will be added to it. E.g
//
// {
// internal: {
// type: {
// $eq: "TestNode"
// },
// content: {
// $regex: new MiniMatch(v)
// }
// },
// id: {
// $regex: newMiniMatch(v)
// }
// }
//
// After execution, acc would be:
//
// {
// "internal.type": {
// $eq: "TestNode"
// },
// "internal.content": {
// $regex: new MiniMatch(v)
// },
// "id": {
// $regex: // as above
// }
// }
const toDottedFields = (filter, acc = {}, path = []) => {
Object.keys(filter).forEach(key => {
const value = filter[key];
const nextValue = _.isPlainObject(value) && value[Object.keys(value)[0]];
if (key === `$elemMatch`) {
acc[path.join(`.`)] = {
[`$elemMatch`]: toDottedFields(value)
};
} else if (_.isPlainObject(nextValue)) {
toDottedFields(value, acc, path.concat(key));
} else {
acc[path.concat(key).join(`.`)] = value;
}
});
return acc;
}; // The query language that Gatsby has used since day 1 is `sift`. Both
// sift and loki are mongo-like query languages, but they have some
// subtle differences. One is that in sift, a nested filter such as
// `{foo: {bar: {ne: true} } }` will return true if the foo field
// doesn't exist, is null, or bar is null. Whereas loki will return
// false if the foo field doesn't exist or is null. This ensures that
// loki queries behave like sift
const isNeTrue = (obj, path) => {
if (path.length) {
const [first, ...rest] = path;
return obj == null || obj[first] == null || isNeTrue(obj[first], rest);
} else {
return obj !== true;
}
};
const fixNeTrue = filter => Object.keys(filter).reduce((acc, key) => {
const value = filter[key];
if (value[`$ne`] === true) {
const [first, ...path] = key.split(`.`);
acc[first] = {
[`$where`]: obj => isNeTrue(obj, path)
};
} else {
acc[key] = value;
}
return acc;
}, {}); // Converts graphQL args to a loki filter
const convertArgs = (gqlArgs, gqlType) => fixNeTrue(toDottedFields(toMongoArgs(gqlArgs.filter, gqlType))); // Converts graphql Sort args into the form expected by loki, which is
// a vector where the first value is a field name, and the second is a
// boolean `isDesc`. E.g
//
// {
// fields: [ `frontmatter___date`, `id` ],
// order: [`desc`]
// }
//
// would return
//
// [ [ `frontmatter.date`, true ], [ `id`, false ] ]
//
function toSortFields(sortArgs) {
const {
fields,
order
} = sortArgs;
const lokiSortFields = [];
for (let i = 0; i < fields.length; i++) {
const dottedField = fields[i];
const isDesc = order[i] && order[i].toLowerCase() === `desc`;
lokiSortFields.push([dottedField, isDesc]);
}
return lokiSortFields;
} // Every time we run a query, we increment a counter for each of its
// fields, so that we can determine which fields are used the
// most. Any time a field is seen more than `FIELD_INDEX_THRESHOLD`
// times, we create a loki index so that future queries with that
// field will execute faster.
function ensureFieldIndexes(coll, lokiArgs) {
_.forEach(lokiArgs, (v, fieldName) => {
// Increment the usages of the field
_.update(fieldUsages, fieldName, n => n ? n + 1 : 1); // If we have crossed the threshold, then create the index
if (_.get(fieldUsages, fieldName) === FIELD_INDEX_THRESHOLD) {
// Loki ensures that this is a noop if index already exists. E.g
// if it was previously added via a sort field
coll.ensureIndex(fieldName);
}
});
}
/**
* Runs the graphql query over the loki nodes db.
*
* @param {Object} args. Object with:
*
* {Object} gqlType: A GraphQL type
*
* {Object} queryArgs: The raw graphql query as a js object. E.g `{
* filter: { fields { slug: { eq: "/somepath" } } } }`
*
* {Object} context: The context from the QueryJob
*
* {boolean} firstOnly: Whether to return the first found match, or
* all matching results
*
* @returns {promise} A promise that will eventually be resolved with
* a collection of matching objects (even if `firstOnly` is true)
*/
async function runQuery({
gqlType,
queryArgs,
firstOnly
}) {
// Clone args as for some reason graphql-js removes the constructor
// from nested objects which breaks a check in sift.js.
const gqlArgs = JSON.parse(JSON.stringify(queryArgs));
const lokiArgs = convertArgs(gqlArgs, gqlType);
const coll = getNodeTypeCollection(gqlType.name);
ensureFieldIndexes(coll, lokiArgs);
let chain = coll.chain().find(lokiArgs, firstOnly);
if (queryArgs.sort) {
const sortFields = toSortFields(queryArgs.sort); // Create an index for each sort field. Indexing requires sorting
// so we lose nothing by ensuring an index is added for each sort
// field. Loki ensures this is a noop if the index already exists
for (const sortField of sortFields) {
coll.ensureIndex(sortField[0]);
}
chain = chain.compoundsort(sortFields);
}
return chain.data();
}
module.exports = runQuery;
//# sourceMappingURL=nodes-query.js.map