Skip to content

Commit

Permalink
Infer input fields from setFieldsOnGraphQLNodeType (#2075)
Browse files Browse the repository at this point in the history
* Infer input fields from setFieldsOnGraphQLNodeType

Closes #1931

* Test infer-graphql-input-type-from-fields

* Add tests
* Strengthen error recovery for objects
  • Loading branch information
Zalastax authored and KyleAMathews committed Sep 15, 2017
1 parent 6f6d90b commit 7dd5f3b
Show file tree
Hide file tree
Showing 8 changed files with 648 additions and 55 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
const _ = require(`lodash`)
const {
graphql,
GraphQLBoolean,
GraphQLFloat,
GraphQLInt,
GraphQLNonNull,
GraphQLString,
GraphQLObjectType,
GraphQLScalarType,
GraphQLList,
GraphQLSchema,
GraphQLInputObjectType,
} = require(`graphql`)
const { connectionArgs, connectionDefinitions } = require(`graphql-skip-limit`)

const {
createSortField,
inferInputObjectStructureFromFields,
} = require(`../infer-graphql-input-fields-from-fields.js`)

function isIntInput(type) {
expect(type instanceof GraphQLInputObjectType).toBeTruthy()
expect(type.getFields()).toEqual({
eq: { name: `eq`, type: GraphQLInt },
ne: { name: `ne`, type: GraphQLInt },
})
}

function isStringInput(type) {
expect(type instanceof GraphQLInputObjectType).toBeTruthy()
expect(type.getFields()).toEqual({
eq: { name: `eq`, type: GraphQLString },
ne: { name: `ne`, type: GraphQLString },
regex: { name: `regex`, type: GraphQLString },
glob: { name: `glob`, type: GraphQLString },
})
}

function typeField(type) {
return {
type,
}
}

describe(`GraphQL Input args from fields, test-only`, () => {
function oddValue(value) {
return value % 2 === 1 ? value : null
}

const OddType = new GraphQLScalarType({
name: `Odd`,
serialize: oddValue,
parseValue: oddValue,
parseLiteral(ast) {
if (ast.kind === Kind.INT) {
return oddValue(parseInt(ast.value, 10))
}
return null
},
})

it(`handles all known scalars`, async () => {
const fields = {
scal_int: typeField(GraphQLInt),
scal_float: typeField(GraphQLFloat),
scal_string: typeField(GraphQLString),
scal_bool: typeField(GraphQLBoolean),
scal_odd_unknown: typeField(OddType),
}

const { inferredFields } = inferInputObjectStructureFromFields({
fields,
typeName: `AType`,
})

const int = inferredFields.scal_int.type
expect(int.name).toBe(`scalIntQueryInt`)
isIntInput(int)

const float = inferredFields.scal_float.type
expect(float.name).toBe(`scalFloatQueryFloat`)
expect(float instanceof GraphQLInputObjectType).toBeTruthy()
expect(float.getFields()).toEqual({
eq: { name: `eq`, type: GraphQLFloat },
ne: { name: `ne`, type: GraphQLFloat },
})

const string = inferredFields.scal_string.type
expect(string.name).toBe(`scalStringQueryString`)
isStringInput(string)

const bool = inferredFields.scal_bool.type
expect(bool.name).toBe(`scalBoolQueryBoolean`)
expect(bool instanceof GraphQLInputObjectType).toBeTruthy()
expect(bool.getFields()).toEqual({
eq: { name: `eq`, type: GraphQLBoolean },
ne: { name: `ne`, type: GraphQLBoolean },
})

expect(inferredFields).not.toHaveProperty(`scal_odd_unknown`)
})

it(`recursively converts object types`, async () => {
const fields = {
obj: typeField(
new GraphQLObjectType({
name: `Obj`,
fields: {
foo: typeField(GraphQLInt),
bar: typeField(
new GraphQLObjectType({
name: `Jbo`,
fields: {
foo: typeField(GraphQLString),
},
})
),
},
})
),
}

const { inferredFields } = inferInputObjectStructureFromFields({
fields,
typeName: `AType`,
})

const obj = inferredFields.obj.type
const objFields = obj.getFields()

expect(obj instanceof GraphQLInputObjectType).toBeTruthy()
isIntInput(objFields.foo.type)

const innerObj = objFields.bar.type
const innerObjFields = innerObj.getFields()
isStringInput(innerObjFields.foo.type)
})

it(`recovers from unknown output types`, async () => {
const fields = {
obj: {
type: new GraphQLObjectType({
name: `Obj`,
fields: {
aa: typeField(OddType),
foo: typeField(GraphQLInt),
bar: typeField(
new GraphQLObjectType({
name: `Jbo`,
fields: {
aa: typeField(OddType),
foo: typeField(GraphQLString),
ba: typeField(OddType),
bar: typeField(GraphQLInt),
},
})
),
},
}),
},
odd: typeField(OddType),
}

const { inferredFields } = inferInputObjectStructureFromFields({
fields,
typeName: `AType`,
})

expect(inferredFields.odd).toBeUndefined()

const obj = inferredFields.obj.type
const objFields = obj.getFields()

expect(obj instanceof GraphQLInputObjectType).toBeTruthy()
isIntInput(objFields.foo.type)
expect(objFields.aa).toBeUndefined()

const innerObj = objFields.bar.type
const innerObjFields = innerObj.getFields()
expect(innerObjFields.aa).toBeUndefined()
isStringInput(innerObjFields.foo.type)
expect(innerObjFields.ba).toBeUndefined()
isIntInput(innerObjFields.bar.type)
})

it(`includes the filters of list elements`, async () => {
const fields = {
list: typeField(new GraphQLList(GraphQLFloat)),
}

const { inferredFields } = inferInputObjectStructureFromFields({
fields,
typeName: `AType`,
})

const list = inferredFields.list.type

expect(list instanceof GraphQLInputObjectType).toBeTruthy()
expect(list.getFields()).toEqual({
eq: { name: `eq`, type: GraphQLFloat },
ne: { name: `ne`, type: GraphQLFloat },
in: { name: `in`, type: new GraphQLList(GraphQLFloat) },
})
})

it(`strips away NonNull`, async () => {
const fields = {
nonNull: typeField(new GraphQLNonNull(GraphQLInt)),
}

const { inferredFields } = inferInputObjectStructureFromFields({
fields,
typeName: `AType`,
})

isIntInput(inferredFields.nonNull.type)
})

it(`extracts the fields you can sort on`, async () => {
const fields = {
foo: typeField(GraphQLString),
bar: typeField(GraphQLFloat),
baz: typeField(
new GraphQLObjectType({
name: `Baz`,
fields: {
ka: typeField(GraphQLFloat),
ma: typeField(
new GraphQLList(
new GraphQLObjectType({
name: `Hol`,
fields: {
go: typeField(GraphQLFloat),
},
})
)
),
},
})
),
}

const { sort } = inferInputObjectStructureFromFields({
fields,
typeName: `AType`,
})

expect(sort.sort()).toEqual([`bar`, `baz___ka`, `baz___ma`, `foo`])
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const buildConnectionFields = require(`../build-connection-fields`)
const {
inferInputObjectStructureFromNodes,
} = require(`../infer-graphql-input-fields`)
const {
createSortField,
} = require(`../infer-graphql-input-fields-from-fields.js`)

function queryResult(nodes, query, { types = [] } = {}) {
const nodeType = new GraphQLObjectType({
Expand Down Expand Up @@ -48,7 +51,7 @@ function queryResult(nodes, query, { types = [] } = {}) {
type: nodeConnection,
args: {
...connectionArgs,
sort,
sort: createSortField(`RootQueryType`, sort),
filter: {
type: new GraphQLInputObjectType({
name: _.camelCase(`filter test`),
Expand All @@ -62,6 +65,7 @@ function queryResult(nodes, query, { types = [] } = {}) {
args,
nodes,
connection: true,
type: nodeType,
})
},
},
Expand Down
28 changes: 25 additions & 3 deletions packages/gatsby/src/schema/build-node-connections.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ const { GraphQLInputObjectType } = require(`graphql`)
const {
inferInputObjectStructureFromNodes,
} = require(`./infer-graphql-input-fields`)
const {
inferInputObjectStructureFromFields,
createSortField,
} = require(`./infer-graphql-input-fields-from-fields`)

const buildConnectionFields = require(`./build-connection-fields`)
const { getNodes } = require(`../redux`)

Expand All @@ -18,16 +23,32 @@ module.exports = (types: any) => {
return
}
const nodes = type.nodes
const typeName = `${type.name}Connection`
const { connectionType: typeConnection } = connectionDefinitions({
nodeType: type.nodeObjectType,
connectionFields: () => buildConnectionFields(type),
})

const { sort, inferredFields } = inferInputObjectStructureFromNodes({
const inferredInputFieldsFromNodes = inferInputObjectStructureFromNodes({
nodes,
typeName: `${type.name}Connection`,
typeName,
})

const inferredInputFieldsFromPlugins = inferInputObjectStructureFromFields({
fields: type.fieldsFromPlugins,
typeName,
})

const filterFields = _.merge(
{},
inferredInputFieldsFromNodes.inferredFields,
inferredInputFieldsFromPlugins.inferredFields
)
const sortNames = inferredInputFieldsFromNodes.sort.concat(
inferredInputFieldsFromPlugins.sort
)
const sort = createSortField(typeName, sortNames)

connections[_.camelCase(`all ${type.name}`)] = {
type: typeConnection,
description: `Connection to all ${type.name} nodes`,
Expand All @@ -38,7 +59,7 @@ module.exports = (types: any) => {
type: new GraphQLInputObjectType({
name: _.camelCase(`filter ${type.name}`),
description: `Filter connection on its fields`,
fields: () => inferredFields,
fields: () => filterFields,
}),
},
},
Expand Down Expand Up @@ -66,6 +87,7 @@ module.exports = (types: any) => {
nodes: latestNodes,
connection: true,
path,
type: type.node.type,
})
},
}
Expand Down
18 changes: 17 additions & 1 deletion packages/gatsby/src/schema/build-node-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ const {
GraphQLList,
GraphQLString,
} = require(`graphql`)
import { NAMED_TYPE } from "graphql/language/kinds"

const apiRunner = require(`../utils/api-runner-node`)
const { inferObjectStructureFromNodes } = require(`./infer-graphql-type`)
const {
inferInputObjectStructureFromFields,
} = require(`./infer-graphql-input-fields-from-fields`)
const {
inferInputObjectStructureFromNodes,
} = require(`./infer-graphql-input-fields`)
Expand Down Expand Up @@ -129,6 +133,11 @@ module.exports = async () => {
})

const mergedFieldsFromPlugins = _.merge(...fieldsFromPlugins)

const inferredInputFieldsFromPlugins = inferInputObjectStructureFromFields({
fields: mergedFieldsFromPlugins,
})

const gqlType = new GraphQLObjectType({
name: typeName,
description: `Node of type ${typeName}`,
Expand All @@ -142,14 +151,20 @@ module.exports = async () => {
typeName,
})

const filterFields = _.merge(
{},
inferedInputFields.inferredFields,
inferredInputFieldsFromPlugins.inferredFields
)

const proccesedType: ProcessedNodeType = {
...intermediateType,
fieldsFromPlugins: mergedFieldsFromPlugins,
nodeObjectType: gqlType,
node: {
name: typeName,
type: gqlType,
args: inferedInputFields.inferredFields,
args: filterFields,
resolve(a, args, context) {
const runSift = require(`./run-sift`)
const latestNodes = _.filter(
Expand All @@ -163,6 +178,7 @@ module.exports = async () => {
args: { filter: { ...args } },
nodes: latestNodes,
path: context.path ? context.path : `LAYOUT___${context.id}`,
type: gqlType,
})
},
},
Expand Down
Loading

0 comments on commit 7dd5f3b

Please sign in to comment.