Skip to content

Commit

Permalink
✨ Support filtering on linked nodes (#3691)
Browse files Browse the repository at this point in the history
* ✨ Support filtering on linked nodes

The filtering is done by extracting all the related nodes using `extractFieldExample`. The example values are then cached to save some time. (As of this patch, caching is not tested automatically)

Further linking are disabled to avoid cyclic dependencies (input fields only).

TODO: Filtering on nodes linked by mappings and File

Note: If linking is done via array of IDs, only the structure of the first item is extracted. I'll be happy to add union support if someone can show me how to `$in` filter on an array of object.

Related #3613 #3190

* ✅ Test filtering on linked nodes
  • Loading branch information
alvinthen authored and KyleAMathews committed Feb 8, 2018
1 parent 7d64a7c commit c4082e9
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -483,3 +483,81 @@ describe(`GraphQL Input args`, () => {
expect(result).toMatchSnapshot()
})
})

describe(`filtering on linked nodes`, () => {
let types
beforeEach(() => {
const { store } = require(`../../redux`)
types = [
{
name: `Child`,
nodeObjectType: new GraphQLObjectType({
name: `Child`,
fields: inferObjectStructureFromNodes({
nodes: [{ id: `child_1`, hair: `brown`, height: 101 }],
types: [{ name: `Child` }],
}),
}),
},
{
name: `Pet`,
nodeObjectType: new GraphQLObjectType({
name: `Pet`,
fields: inferObjectStructureFromNodes({
nodes: [{ id: `pet_1`, species: `dog` }],
types: [{ name: `Pet` }],
}),
}),
},
]

store.dispatch({
type: `CREATE_NODE`,
payload: { id: `child_1`, internal: { type: `Child` }, hair: `brown` },
})
store.dispatch({
type: `CREATE_NODE`,
payload: { id: `child_2`, internal: { type: `Child` }, hair: `blonde`, height: 101 },
})
store.dispatch({
type: `CREATE_NODE`,
payload: { id: `pet_1`, internal: { type: `Pet` }, species: `dog` },
})
})

it(`filters on linked nodes via id`, async () => {
let result = await queryResult(
[{ linked___NODE: `child_2`, foo: `bar` }, { linked___NODE: `child_1`, foo: `baz` }],
`
{
allNode(filter: { linked: { hair: { eq: "blonde" } } }) {
edges { node { linked { hair, height }, foo } }
}
}
`,
{ types }
)
expect(result.data.allNode.edges.length).toEqual(1)
expect(result.data.allNode.edges[0].node.linked.hair).toEqual(`blonde`)
expect(result.data.allNode.edges[0].node.linked.height).toEqual(101)
expect(result.data.allNode.edges[0].node.foo).toEqual(`bar`)
})

it(`returns all matching linked nodes`, async () => {
let result = await queryResult(
[{ linked___NODE: `child_2`, foo: `bar` }, { linked___NODE: `child_2`, foo: `baz` }],
`
{
allNode(filter: { linked: { hair: { eq: "blonde" } } }) {
edges { node { linked { hair, height }, foo } }
}
}
`,
{ types }
)
expect(result.data.allNode.edges[0].node.linked.hair).toEqual(`blonde`)
expect(result.data.allNode.edges[0].node.linked.height).toEqual(101)
expect(result.data.allNode.edges[0].node.foo).toEqual(`bar`)
expect(result.data.allNode.edges[1].node.foo).toEqual(`baz`)
})
})
32 changes: 29 additions & 3 deletions packages/gatsby/src/schema/infer-graphql-input-fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ const {
isEmptyObjectOrArray,
} = require(`./data-tree-utils`)

const { findLinkedNode } = require(`./infer-graphql-type`)
const { getNodes } = require(`../redux`)

import type {
GraphQLInputFieldConfig,
GraphQLInputFieldConfigMap,
Expand Down Expand Up @@ -185,6 +188,8 @@ type InferInputOptions = {
exampleValue?: Object,
}

const linkedNodeCache = {}

export function inferInputObjectStructureFromNodes({
nodes,
typeName = ``,
Expand All @@ -196,13 +201,34 @@ export function inferInputObjectStructureFromNodes({

prefix = isRoot ? typeName : prefix

_.each(exampleValue, (value, key) => {
_.each(exampleValue, (v, k) => {
let value = v
let key = k
// Remove fields for traversing through nodes as we want to control
// setting traversing up not try to automatically infer them.
if (isRoot && EXCLUDE_KEYS[key]) return

// Input arguments on linked fields aren't currently supported
if (_.includes(key, `___NODE`)) return
if (_.includes(key, `___NODE`)) {
// TODO: Union the objects in array
const nodeToFind = _.isArray(value) ? value[0] : value
const linkedNode = findLinkedNode(nodeToFind)

// Get from cache if found, else store into it
if (linkedNodeCache[linkedNode.internal.type]) {
value = linkedNodeCache[linkedNode.internal.type]
} else {
const relatedNodes = getNodes().filter(node => node.internal.type === linkedNode.internal.type)
value = extractFieldExamples(relatedNodes)
value = _.omitBy(value, (_v, _k) => _.includes(_k, `___NODE`))
linkedNodeCache[linkedNode.internal.type] = value
}

if (_.isArray(value)) {
value = [value]
}

;[key] = key.split(`___`)
}

let field = inferGraphQLInputFields({
nodes,
Expand Down
2 changes: 1 addition & 1 deletion packages/gatsby/src/schema/infer-graphql-type.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ function inferFromMapping(
}
}

function findLinkedNode(value, linkedField, path) {
export function findLinkedNode(value, linkedField, path) {
let linkedNode
// If the field doesn't link to the id, use that for searching.
if (linkedField) {
Expand Down

0 comments on commit c4082e9

Please sign in to comment.