Skip to content

Commit 9a49888

Browse files
committed
fix: support variables within input objects (#13)
1 parent acd4e51 commit 9a49888

File tree

4 files changed

+184
-12
lines changed

4 files changed

+184
-12
lines changed

Diff for: src/__tests__/regressions/issue-13.test.ts

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { buildSchema } from "graphql"
2+
import { dedent, printQuery, gatsbyApi } from "../test-utils"
3+
import {
4+
buildNodeDefinitions,
5+
createSourcingContext,
6+
compileNodeQueries,
7+
fetchNodeList,
8+
fetchNodeById,
9+
LimitOffset,
10+
} from "../.."
11+
import { IRemoteNode } from "../../types"
12+
13+
// See https://github.com/vladar/gatsby-graphql-toolkit/issues/13
14+
15+
const schema = buildSchema(`
16+
type Foo {
17+
id: ID!
18+
foo: String
19+
}
20+
input FooInput {
21+
id: ID
22+
limit: Int
23+
offset: Int
24+
}
25+
type Query {
26+
foo(input: FooInput): Foo
27+
allFoo(input: FooInput): [Foo]
28+
}
29+
`)
30+
31+
const gatsbyNodeTypes = [
32+
{
33+
remoteTypeName: `Foo`,
34+
queries: `
35+
query LIST_FOO {
36+
allFoo(input: { limit: $limit offset: $offset }) { ..._FooId_ }
37+
}
38+
query NODE_FOO {
39+
foo(input: { id: $id }) { ..._FooId_ }
40+
}
41+
fragment _FooId_ on Foo { __typename id }
42+
`,
43+
},
44+
]
45+
46+
const documents = compileNodeQueries({
47+
schema,
48+
gatsbyNodeTypes,
49+
customFragments: [],
50+
})
51+
52+
const fooNode = {
53+
remoteTypeName: `Foo`,
54+
remoteId: `1`,
55+
foo: `fooString`,
56+
}
57+
58+
it(`adds variable declarations for variables within input objects`, async () => {
59+
expect(printQuery(documents, `Foo`)).toEqual(dedent`
60+
query LIST_FOO ($limit: Int $offset: Int) {
61+
allFoo(input: { limit: $limit, offset: $offset }) {
62+
remoteTypeName: __typename
63+
..._FooId_
64+
}
65+
}
66+
67+
query NODE_FOO ($id: ID) {
68+
foo(input: {id: $id}) {
69+
remoteTypeName: __typename
70+
..._FooId_
71+
}
72+
}
73+
74+
fragment _FooId_ on Foo {
75+
remoteTypeName: __typename
76+
remoteId: id
77+
}
78+
`)
79+
})
80+
81+
it(`correctly detects field to paginate in list query`, async () => {
82+
const context = createSourcingContext({
83+
schema,
84+
gatsbyNodeDefs: buildNodeDefinitions({ gatsbyNodeTypes, documents }),
85+
execute: async () => Promise.resolve({ data: { allFoo: [fooNode] } }),
86+
gatsbyApi,
87+
gatsbyTypePrefix: `Test`,
88+
paginationAdapters: [LimitOffset],
89+
})
90+
91+
const fetchNodes = async () => {
92+
const nodes: IRemoteNode[] = []
93+
for await (const node of fetchNodeList(context, `Foo`, `LIST_FOO`)) {
94+
nodes.push(node)
95+
}
96+
return nodes
97+
}
98+
99+
// Without the fix it throws:
100+
// Cannot find field to paginate in the query LIST_FOO. Make sure you spread IDFragment in your source query:
101+
// query LIST_FOO { field { ...IDFragment } }
102+
await expect(fetchNodes()).resolves.toEqual([fooNode])
103+
})
104+
105+
it(`correctly detects node field in node query`, async () => {
106+
const context = createSourcingContext({
107+
schema,
108+
gatsbyNodeDefs: buildNodeDefinitions({ gatsbyNodeTypes, documents }),
109+
execute: async () => Promise.resolve({ data: { foo: fooNode } }),
110+
gatsbyApi,
111+
gatsbyTypePrefix: `Test`,
112+
paginationAdapters: [LimitOffset],
113+
})
114+
115+
// Without the fix it throws:
116+
// Value of the ID field "remoteTypeName" can't be nullish. Got object with keys: foo
117+
const fooId = {
118+
remoteTypeName: fooNode.remoteTypeName,
119+
remoteId: fooNode.remoteId,
120+
}
121+
await expect(fetchNodeById(context, `Foo`, fooId)).resolves.toEqual(fooNode)
122+
})

Diff for: src/compile-node-queries/__tests__/compile-node-queries.test.ts

+33
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,39 @@ describe(`Happy path`, () => {
913913
`)
914914
})
915915

916+
it(`supports variables within complex inputs`, () => {
917+
const queries = compileNodeQueries({
918+
schema,
919+
gatsbyNodeTypes: [
920+
{
921+
remoteTypeName: `Bar`,
922+
queries: `
923+
query LIST_Bar { allBar(page: { pageNumber: $pageNumber }) { ...BarId } }
924+
fragment BarId on Bar { testId }
925+
`,
926+
},
927+
],
928+
customFragments: [`fragment Bar on Bar { createdAt }`],
929+
})
930+
931+
expect(queries.size).toEqual(1)
932+
expect(printQuery(queries, `Bar`)).toEqual(dedent`
933+
query LIST_Bar($pageNumber: Int) {
934+
allBar(page: { pageNumber: $pageNumber }) {
935+
remoteTypeName: __typename
936+
...BarId
937+
...Bar
938+
}
939+
}
940+
fragment BarId on Bar {
941+
testId
942+
}
943+
fragment Bar on Bar {
944+
createdAt
945+
}
946+
`)
947+
})
948+
916949
it.todo(`Supports deeply nested variables`)
917950
it.todo(`Supports variables within fragments`)
918951
it.todo(`Supports deeply nested variables within fragments`)

Diff for: src/compile-node-queries/ast-transformers/add-variable-definitions.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,11 @@ export function addVariableDefinitions({
6868
FragmentSpread: node => {
6969
currentDefinition.usedFragments.add(node.name.value)
7070
},
71-
Argument: node => {
71+
Variable: node => {
7272
const inputType = typeInfo.getInputType()
73-
if (node.value.kind === "Variable" && inputType) {
74-
currentDefinition.variables.set(node.value.name.value, inputType)
73+
// FIXME: throw if no inputType found?
74+
if (inputType) {
75+
currentDefinition.variables.set(node.name.value, inputType)
7576
}
7677
},
7778
}

Diff for: src/source-nodes/utils/field-path-utils.ts

+25-9
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
FieldNode,
66
visit,
77
BREAK,
8-
VariableNode,
98
} from "graphql"
109
import * as GraphQLAST from "../../utils/ast-nodes"
1110
import { IPaginationAdapter } from "../../config/pagination-adapters"
@@ -32,8 +31,12 @@ export function findPaginatedFieldPath(
3231
const expectedVars = paginationAdapter.expectedVariableNames
3332

3433
if (!expectedVars.length) {
35-
// FIXME: use first fragment spread instead of __typename
36-
// TODO: consider to always use this instead of isPaginatedField
34+
// FIXME: LIST_ queries without variables do not work for Relay connections:
35+
// { myConnection { edges { node { __typename ...IdFragment } } }
36+
// We want to return path to `myConnection` here but it will detect `node` field.
37+
// So maybe allow pagination adapter to decide? i.e. Relay could look into
38+
// type name and return first field with `MyConnection` type
39+
// TODO: use first fragment spread instead of __typename
3740
const hasTypeNameField = (field: FieldNode) =>
3841
field.selectionSet
3942
? field.selectionSet.selections.some(
@@ -44,11 +47,14 @@ export function findPaginatedFieldPath(
4447
}
4548

4649
const isPaginatedField = (node: FieldNode) => {
47-
const variables = (node.arguments ?? [])
48-
.map(arg => arg.value)
49-
.filter((value): value is VariableNode => value.kind === "Variable")
50-
.map(value => value.name.value)
51-
50+
const variables: Array<string> = []
51+
node.arguments?.forEach(arg => {
52+
visit(arg, {
53+
Variable: variableNode => {
54+
variables.push(variableNode.name.value)
55+
},
56+
})
57+
})
5258
return (
5359
variables.length > 0 &&
5460
expectedVars.every(name => variables.includes(name))
@@ -66,7 +72,17 @@ export function findNodeFieldPath(
6672
): string[] {
6773
// For now simply assuming the first field with a variable
6874
const hasVariableArgument = (node: FieldNode) =>
69-
(node.arguments ?? []).some(arg => arg.value.kind === "Variable")
75+
(node.arguments ?? []).some(arg => {
76+
let hasVariable = false
77+
visit(arg, {
78+
Variable: () => {
79+
hasVariable = true
80+
return BREAK
81+
},
82+
})
83+
return hasVariable
84+
})
85+
7086
return findFieldPath(document, operationName, hasVariableArgument)
7187
}
7288

0 commit comments

Comments
 (0)