282 lines
7.8 KiB
JavaScript
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
|