Skip to content

Commit

Permalink
fix(gatsby): Fix Loki query operators special casing (#11517)
Browse files Browse the repository at this point in the history
fix(gatsby): Fix Loki special-casing for $ne: true
  • Loading branch information
stefanprobst authored and freiksenet committed Feb 7, 2019
1 parent 55e3425 commit e61692d
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 50 deletions.
88 changes: 47 additions & 41 deletions packages/gatsby/src/db/loki/nodes-query.js
Expand Up @@ -116,6 +116,18 @@ function toMongoArgs(gqlFilter, lastFieldType) {
const Minimatch = require(`minimatch`).Minimatch
const mm = new Minimatch(v)
mongoArgs[`$regex`] = mm.makeRe()
} 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 &&
Expand All @@ -124,13 +136,14 @@ function toMongoArgs(gqlFilter, lastFieldType) {
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([false])
mongoArgs[`$nin`] = v.concat([undefined])
} else {
mongoArgs[`$${k}`] = v
}
Expand Down Expand Up @@ -170,16 +183,17 @@ function toMongoArgs(gqlFilter, lastFieldType) {
// $regex: // as above
// }
// }
function dotNestedFields(acc, o, path = ``) {
if (_.isPlainObject(o)) {
if (_.isPlainObject(_.sample(o))) {
_.forEach(o, (v, k) => {
dotNestedFields(acc, v, path + `.` + k)
})
const toDottedFields = (filter, acc = {}, path = []) => {
Object.keys(filter).forEach(key => {
const value = filter[key]
const nextValue = _.isPlainObject(value) && value[Object.keys(value)[0]]
if (_.isPlainObject(nextValue)) {
toDottedFields(value, acc, path.concat(key))
} else {
acc[_.trimStart(path, `.`)] = o
acc[path.concat(key).join(`.`)] = value
}
}
})
return acc
}

// The query language that Gatsby has used since day 1 is `sift`. Both
Expand All @@ -189,35 +203,30 @@ function dotNestedFields(acc, o, path = ``) {
// 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
function fixNeTrue(flattenedFields) {
return _.transform(flattenedFields, (result, v, k) => {
if (v[`$ne`] === true) {
const s = k.split(`.`)
if (s.length > 1) {
result[s[0]] = {
$or: [
{
$exists: false,
},
{
$where: obj => obj === null || obj[s[1]] !== true,
},
],
}
return result
}
}
result[k] = v
return result
})
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
function convertArgs(gqlArgs, gqlType) {
const dottedFields = {}
dotNestedFields(dottedFields, toMongoArgs(gqlArgs.filter, gqlType))
return fixNeTrue(dottedFields)
}
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
Expand Down Expand Up @@ -281,16 +290,13 @@ function ensureFieldIndexes(coll, lokiArgs) {
* a collection of matching objects (even if `firstOnly` is true)
*/
async function runQuery({ gqlType, queryArgs, context = {}, 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 lokiArgs = convertArgs(queryArgs, gqlType)
const coll = getNodeTypeCollection(gqlType.name)
ensureFieldIndexes(coll, lokiArgs)
let chain = coll.chain().find(lokiArgs, firstOnly)

if (gqlArgs.sort) {
const sortFields = toSortFields(gqlArgs.sort)
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
Expand Down
7 changes: 2 additions & 5 deletions packages/gatsby/src/redux/run-sift.js
Expand Up @@ -279,14 +279,11 @@ function handleMany(siftArgs, nodes, sort) {
*/
module.exports = (args: Object) => {
const { queryArgs, gqlType, firstOnly = false } = args
// Clone args as for some reason graphql-js removes the constructor
// from nested objects which breaks a check in sift.js.
const clonedArgs = JSON.parse(JSON.stringify(queryArgs))

// If nodes weren't provided, then load them from the DB
const nodes = args.nodes || getNodesByType(gqlType.name)

const { siftArgs, fieldsToSift } = parseFilter(clonedArgs.filter)
const { siftArgs, fieldsToSift } = parseFilter(queryArgs.filter)

// If the the query for single node only has a filter for an "id"
// using "eq" operator, then we'll just grab that ID and return it.
Expand All @@ -312,7 +309,7 @@ module.exports = (args: Object) => {
if (firstOnly) {
return handleFirst(siftArgs, resolvedNodes)
} else {
return handleMany(siftArgs, resolvedNodes, clonedArgs.sort)
return handleMany(siftArgs, resolvedNodes, queryArgs.sort)
}
})
}
34 changes: 30 additions & 4 deletions packages/gatsby/src/schema/__tests__/run-query.js
Expand Up @@ -51,6 +51,7 @@ const makeNodes = () => [
anArray: [1, 2, 5, 4],
waxOnly: {
foo: true,
bar: { baz: true },
},
anotherKey: {
withANested: {
Expand Down Expand Up @@ -199,12 +200,26 @@ describe(`Filter fields`, () => {
expect(result[0].hair).toEqual(1)
})

it(`handles ne: true operator`, async () => {
let result = await runFilter({ boolean: { ne: true } })

expect(result.length).toEqual(2)
})

it(`handles nested ne: true operator`, async () => {
let result = await runFilter({ waxOnly: { foo: { ne: true } } })

expect(result.length).toEqual(2)
})

it(`handles deeply nested ne: true operator`, async () => {
let result = await runFilter({
waxOnly: { bar: { baz: { ne: true } } },
})

expect(result.length).toEqual(2)
})

it(`handles lt operator`, async () => {
let result = await runFilter({ hair: { lt: 2 } })

Expand Down Expand Up @@ -400,10 +415,7 @@ describe(`Filter fields`, () => {
let result = await runFilter({ boolean: { nin: [true, null] } })

expect(result.length).toEqual(1)
result.forEach(edge => {
expect(edge.boolean).not.toEqual(null)
expect(edge.boolean).not.toEqual(true)
})
expect(result[0].boolean).toBe(false)
})

it(`handles the glob operator`, async () => {
Expand All @@ -420,6 +432,20 @@ describe(`Filter fields`, () => {
expect(result[0].index).toEqual(0)
expect(result[1].index).toEqual(2)
})

it(`handles the eq operator for array field values`, async () => {
const result = await runFilter({ anArray: { eq: 5 } })

expect(result.length).toBe(1)
expect(result[0].index).toBe(1)
})

it(`handles the ne operator for array field values`, async () => {
const result = await runFilter({ anArray: { ne: 1 } })

expect(result.length).toBe(1)
expect(result[0].index).toBe(2)
})
})

describe(`collection fields`, () => {
Expand Down

0 comments on commit e61692d

Please sign in to comment.