Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(schema): Inference controls and improvements #13028

Merged
merged 35 commits into from
May 16, 2019
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ff596b9
WIP inference changes
freiksenet Mar 29, 2019
a88165c
Fixed all inferrence issues
freiksenet Apr 2, 2019
494f2e5
Reorder adding fields so field name resolving happens first
freiksenet Apr 2, 2019
b1fff33
Warnings and addResolver directives
freiksenet Apr 2, 2019
0ded10d
Kitchen sink tests
freiksenet Apr 3, 2019
354f11e
More fixes
freiksenet Apr 4, 2019
774ada4
Mapping via extensions
freiksenet Apr 4, 2019
ffa3960
Remove unused code
freiksenet Apr 4, 2019
fd57bb5
Fix comment
freiksenet Apr 4, 2019
5a6320f
Simplify some code, make some other code very complicated
freiksenet Apr 5, 2019
d065cb6
v2.4.0-alpha.1
freiksenet Apr 5, 2019
978e22e
Fix review comments
freiksenet Apr 5, 2019
1c2a501
[schema] Simplify type merging (#13557)
stefanprobst Apr 23, 2019
ce07eb1
Merge remote-tracking branch 'origin/master' into infer-fixing
freiksenet Apr 23, 2019
9ca160e
v2.4.0-alpha.2
freiksenet Apr 23, 2019
17d7857
More consistent naming
freiksenet Apr 24, 2019
c18d1e9
Ensure link() default arg (#13591)
stefanprobst Apr 24, 2019
9b2be71
[schema] Clarify warning message (#13693)
stefanprobst Apr 29, 2019
e40f6d2
Allow registering field extensions (#13623)
stefanprobst Apr 30, 2019
0a53e22
Revert "Allow registering field extensions (#13623)" (#13735)
stefanprobst Apr 30, 2019
9bad5a4
v2.4.0-alpha.3
freiksenet Apr 29, 2019
06b612a
Add blog post
freiksenet May 2, 2019
446c60a
Update docs
freiksenet May 2, 2019
2bcdc40
Fix link
freiksenet May 2, 2019
f2e1986
Improve docs
freiksenet May 2, 2019
5bf9bb8
[schema] Allow registering custom field extensions (#13738)
stefanprobst May 14, 2019
b8b96f9
Merge remote-tracking branch 'origin/master' into infer-fixing
freiksenet May 14, 2019
aee5186
Fix all addResolver uses
freiksenet May 15, 2019
daa2af3
v2.5.0-rc.1
freiksenet May 15, 2019
65e7504
Fix blog post
freiksenet May 15, 2019
b6c61e5
Apply suggestions from code review
freiksenet May 15, 2019
e76784c
Update packages/gatsby/src/redux/actions.js
stefanprobst May 15, 2019
69f179a
Update packages/gatsby/src/redux/actions.js
stefanprobst May 15, 2019
f338bdd
More blog post improvements
freiksenet May 15, 2019
1af8600
Merge branch 'master' into infer-fixing
freiksenet May 16, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions docs/blog/2019-04-23-improvements-to-schema-customization/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
title: Improvements to Schema Customization API - Available in Gatsby 2.4.0
date: 2019-04-23
author: Mikhail Novikov
tags:
- schema
- graphql
---

Today we are releasing further improvements to the schema customization that [we've released in version 2.2.0](/blog/2019-03-18-releasing-new-schema-customization). You can use them with Gatsby 2.4.0.

It is now possible to indicate to Gatsby, that you want to add a resolver to an explicitly defined fields. Use `addResolver` to add default arguments or/and resolvers to fields. In addition, when `@dontInfer` is set, Gatsby will no longer run inference for marked type, allowing one to improve performance for large data sets.

## Summary

After about a month of testing schema customization both here and in pre-release we determined a couple of issues. We set up to do this because we wanted to remove uncertainty in people's schemas, when the data changes. However, the original design allowed some uncertainties anyway. In addition, we have made inferrence a more heavy process, trading performance for consistency and didn't really provide a way to opt out of it completely. To summarize:

- Resolvers and arguments of fields like Date and File was determined by inferred data
- There was no easy way to use arguments/resolvers to override the above
- Inferrence was run even when `@dontInfer` flag was on
- There was no way to control inference outside of SDL, eg in Type Builders

Therefore we have some changes to the way we do inferrence. In addition, we are deprecating some of the features introduced is 2.2.0 and will remove them in Gatsby 3.

## noDefaultResolvers and inferrence modes

First of all, we are deprecating `noDefaultResolvers`. It was an argument of `infer` and `dontInfer`. We feel it was confusing and in some cases it didn't even actually add resolvers :). We will support `noDefaultResolvers` until version 3, after which `@infer` behaviour (see below) will become a default and `noDefaultResolvers` will be removed.

We didn't want to break things, so we keep old default behaviour, even though we think it's not optimal. Add explicit `@infer` and `@addResolver` to fields to be future proof.

### Default (deprecated, removed in v3)

Applies with no `@infer` and no `@dontInfer` on a type. Equals to `@infer(noDefaultResolvers: false)`.

Type gets all inferred fields added. If type has defined fields of types `Date`, `File` and any other node, and we inferred that they should have resolver options, resolver options will be added to type with a warning.

### Strict inference (future default in v3)

Applies with `@infer` or `@infer(noDefaultResolvers: true)`.

Type gets all inferred fields added. Existing fields won't automatically get resolvers (use `@addResolver` directive).

### No inferrence

Applies with `@dontInfer` or `@dontInfer(noDefaultResolvers: true)`.

Inferrence won't run at all. Existing fields won't automatically get resolvers (use @addResolver directive).

### No new fields with default resolvers (deprecated, removed in v3)

Applies with `@dontInfer(noDefaultResolvers: false)`

Inferrence will run, but fields won't be added. If type has defined fields of types `Date`, `File` and any other node, and we inferred that they should have resolvers/args, resolvers/args will be added to type with a warning.

## `addResolver`

Add resolver and resolver options (such as arguments) to the given field.

```graphql
type MyType @infer {
date: Date @addResolver(type: "dateformat", options: { formatString: "DD MMM", locale: "fi" })
image: File @addResolver(type: "fileByRelativePath")
authorByEmail: Author @addResolver(type: "link", { by: "email" })
freiksenet marked this conversation as resolved.
Show resolved Hide resolved
}
```

### Type Builders and extensions

You can now apply configuration to type builder types through extension property on them.

```js
schema.createObjectType({
name: MyType,
extensions: {
infer: true,
},
fields: {
date: {
type: "Date",
extensions: {
addResolver: {
type: "dateformat",
options: { formatString: "DD MMM", locale: "fi" },
},
},
},
},
})
```

## Conclusions

With these improvements we hope we'll solve most of the issues that people are having with new schema customization. We are working on further improvements, like allowing users and plugins to define their own extensions (see [PR #13738](https://github.com/gatsbyjs/gatsby/pull/13738)).

Useful links:

- [createTypes Documentation](https://www.gatsbyjs.org/docs/actions/#createTypes)
- [Umbrella issue for schema customization bug reports](https://github.com/gatsbyjs/gatsby/issues/12272)
4 changes: 2 additions & 2 deletions packages/gatsby/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "gatsby",
"description": "Blazing fast modern site generator for React",
"version": "2.3.25",
"version": "2.4.0-alpha.3",
"author": "Kyle Mathews <mathews.kyle@gmail.com>",
"bin": {
"gatsby": "./dist/bin/gatsby.js"
Expand Down Expand Up @@ -73,7 +73,7 @@
"glob": "^7.1.1",
"got": "8.0.0",
"graphql": "^14.1.1",
"graphql-compose": "6.0.3",
"graphql-compose": "^6.3.2",
"graphql-playground-middleware-express": "^1.7.10",
"hash-mod": "^0.0.5",
"invariant": "^2.2.4",
Expand Down
76 changes: 65 additions & 11 deletions packages/gatsby/src/redux/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1245,8 +1245,27 @@ import type GatsbyGraphQLType from "../schema/types/type-builders"
* with inferred field types, and default field resolvers for `Date` (which
* adds formatting options) and `File` (which resolves the field value as
* a `relativePath` foreign-key field) are added. This behavior can be
* customised with `@infer` and `@dontInfer` directives, and their
* `noDefaultResolvers` argument.
* customised with `@infer`, `@dontInfer` and `@addResolvers` directives.
*
*
* Schema customization controls:
* * `@infer` - run inference on the type and add fields that don't exist on the
* defined type to it.
* * `@dontInfer` - don't run any inference on the type
* * `@addResolver` - add resolver options to a field. Args are `type` and
* `options`. Type can be `dateformat`, `link` and `fileByRelativePath`.
*
* Resolver types:
* * `dateformat` - add date formatting arguments. Accepts `formatString` and
* `locale` options that sets the defaults for this field
* * `link` - connect to a different Node. Arguments `by` and `from`, which
* define which field to compare to on a remote node and which field to use on
* the source node
* * `fileByRelativePath` - connect to a File node. Same arguments. The
* difference from link is that this normalizes the relative path to be
* relative from the path where source node is found.
*
*
*
* @example
* exports.sourceNodes = ({ actions }) => {
Expand All @@ -1255,17 +1274,17 @@ import type GatsbyGraphQLType from "../schema/types/type-builders"
* """
* Markdown Node
* """
* type MarkdownRemark implements Node {
* type MarkdownRemark implements Node @infer {
* frontmatter: Frontmatter!
* }
*
* """
* Markdown Frontmatter
* """
* type Frontmatter {
* type Frontmatter @infer {
* title: String!
* author: AuthorJson!
* date: Date!
* author: AuthorJson! @addResolver(type: "link")
* date: Date! @addResolver(type: "dateformat")
* published: Boolean!
* tags: [String!]!
* }
Expand All @@ -1274,9 +1293,9 @@ import type GatsbyGraphQLType from "../schema/types/type-builders"
* Author information
* """
* # Does not include automatically inferred fields
* type AuthorJson implements Node @dontInfer(noFieldResolvers: true) {
* type AuthorJson implements Node @dontInfer {
* name: String!
* birthday: Date! # no default resolvers for Date formatting added
* birthday: Date! @addResolver(type: "dateformat")
* }
* `
* createTypes(typeDefs)
Expand All @@ -1292,6 +1311,9 @@ import type GatsbyGraphQLType from "../schema/types/type-builders"
* frontmatter: 'Frontmatter!'
* },
* interfaces: ['Node'],
* extensions: {
* infer: true,
* },
* }),
* schema.buildObjectType({
* name: 'Frontmatter',
Expand All @@ -1302,12 +1324,44 @@ import type GatsbyGraphQLType from "../schema/types/type-builders"
* return parent.title || '(Untitled)'
* }
* },
* author: 'AuthorJson!',
* date: 'Date!',
* author: {
* type: 'AuthorJson'
* extensions: {
* addResolver: {
* type: 'link',
* },
* },
* }
* date: {
* type: 'Date!'
* extensions: {
* addResolver: {
* type: 'date',
* },
* },
* },
* published: 'Boolean!',
* tags: '[String!]!',
* }
* })
* }),
* schema.buildObjectType({
* name: 'AuthorJson',
* fields: {
* name: 'String!'
* birthday: {
* type: 'Date!'
* extensions: {
* addResolver: {
* type: 'date',
* },
* },
* },
* },
* interfaces: ['Node'],
* extensions: {
* infer: false,
* },
* }),
* ]
* createTypes(typeDefs)
* }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Kichen sink schema test passes kitchen sink query 1`] = `
exports[`Kitchen sink schema test passes kitchen sink query 1`] = `
Object {
"data": Object {
"addResolvers": Array [
Expand Down Expand Up @@ -43,6 +43,7 @@ Object {
"_3invalidKey": null,
"code": "BShF_8qhtEv",
"comment": 0,
"defaultTime": "05 huhtikuu",
"id": "1486495736706552111",
"idWithDecoration": "decoration-1486495736706552111",
"image": Object {
Expand All @@ -51,6 +52,8 @@ Object {
},
},
"likes": 8,
"localeFormat": "05 huhtikuu 2017",
"localeString": "05 апреля",
"time": "05.04.2017",
},
},
Expand All @@ -59,6 +62,7 @@ Object {
"_3invalidKey": null,
"code": "BY6B8z5lR1F",
"comment": 0,
"defaultTime": "11 syyskuu",
"id": "1601601194425654597",
"idWithDecoration": "decoration-1601601194425654597",
"image": Object {
Expand All @@ -67,6 +71,8 @@ Object {
},
},
"likes": 9,
"localeFormat": "11 syyskuu 2017",
"localeString": "11 сентября",
"time": "11.09.2017",
},
},
Expand Down
69 changes: 59 additions & 10 deletions packages/gatsby/src/schema/__tests__/kitchen-sink.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
// @flow

const { SchemaComposer } = require(`graphql-compose`)
const { graphql } = require(`graphql`)
const {
graphql,
GraphQLSchema,
GraphQLNonNull,
GraphQLList,
GraphQLObjectType,
getNamedType,
} = require(`graphql`)
const { store } = require(`../../redux`)
const { build } = require(`../index`)
const fs = require(`fs-extra`)
Expand All @@ -14,7 +21,7 @@ jest.mock(`../../utils/api-runner-node`)
const apiRunnerNode = require(`../../utils/api-runner-node`)

// XXX(freiksenet): Expand
describe(`Kichen sink schema test`, () => {
describe(`Kitchen sink schema test`, () => {
let schema

const runQuery = query =>
Expand Down Expand Up @@ -47,17 +54,20 @@ describe(`Kichen sink schema test`, () => {
store.dispatch({
type: `CREATE_TYPES`,
payload: `
type PostsJson implements Node {
type PostsJson implements Node @infer {
id: String!
time: Date
time: Date @addResolver(type: "dateformat", options: { locale: "fi", formatString: "DD MMMM"})
code: String
image: File @addResolver(type: "fileByRelativePath")
}
`,
})
store.dispatch({
type: `ADD_THIRD_PARTY_SCHEMA`,
payload: buildThirdPartySchema(),
})
buildThirdPartySchemas().forEach(schema =>
store.dispatch({
type: `ADD_THIRD_PARTY_SCHEMA`,
payload: schema,
})
)
await build({})
schema = store.getState().schema
})
Expand All @@ -72,6 +82,9 @@ describe(`Kichen sink schema test`, () => {
id
idWithDecoration
time(formatString: "DD.MM.YYYY")
localeString: time(locale: "ru")
localeFormat: time(formatString: "DD MMMM YYYY")
defaultTime: time
code
likes
comment
Expand Down Expand Up @@ -130,9 +143,21 @@ describe(`Kichen sink schema test`, () => {
`)
).toMatchSnapshot()
})

it(`correctly resolves nested Query types from third-party types`, () => {
const queryFields = schema.getQueryType().getFields()
;[`relay`, `relay2`, `query`, `manyQueries`].forEach(fieldName =>
expect(getNamedType(queryFields[fieldName].type)).toBe(
schema.getQueryType()
)
)
expect(schema.getType(`Nested`).getFields().query.type).toBe(
schema.getQueryType()
)
})
})

const buildThirdPartySchema = () => {
const buildThirdPartySchemas = () => {
const schemaComposer = new SchemaComposer()
schemaComposer.addTypeDefs(`
type ThirdPartyStuff {
Expand Down Expand Up @@ -205,7 +230,31 @@ const buildThirdPartySchema = () => {
schemaComposer.addSchemaMustHaveType(
schemaComposer.getOTC(`ThirdPartyStuff3`)
)
return schemaComposer.buildSchema()

// Query type with non-default name
const RootQueryType = new GraphQLObjectType({
name: `RootQueryType`,
fields: () => {
return {
query: { type: RootQueryType },
manyQueries: {
type: new GraphQLNonNull(new GraphQLList(RootQueryType)),
},
nested: { type: Nested },
}
},
})
const Nested = new GraphQLObjectType({
name: `Nested`,
fields: () => {
return {
query: { type: RootQueryType },
}
},
})
const schema = new GraphQLSchema({ query: RootQueryType })

return [schemaComposer.buildSchema(), schema]
}

const mockSetFieldsOnGraphQLNodeType = async ({ type: { name } }) => {
Expand Down
Loading