Skip to content

Commit

Permalink
docs: move source type section into its own guide
Browse files Browse the repository at this point in the history
  • Loading branch information
Weakky committed Dec 7, 2020
1 parent c16f6c6 commit 01f129a
Show file tree
Hide file tree
Showing 7 changed files with 455 additions and 268 deletions.
274 changes: 6 additions & 268 deletions docs/content/014-guides/020-schema.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -511,275 +511,13 @@ query {



## Backing types in principle
## Type-safety

As you begin to implement a schema for the first time you will notice something that may not have been obvious at first. The data that the client sees in the data graph is _not_ the same data flowing through the internal resolvers used to fulfill that graph. The client sees the API types but the API author deals with something else, _backing types_.
Nexus is designed to be nearly 100% type-safe by default. It can automatically type your resolvers args and context, but there's one thing it cannot do without your help: **knowing the types that come from your data-sources (such as your database).**

Here is an example of resolution for a query as it would be seen roughly from a GraphQL type _only_ point of view.
By default, Nexus will generate types based on your GraphQL schema.
However, the data that flows through your resolvers can be completely different than what your GraphQL Schema expresses.

![](/docs/assets/diagram-backing-types-1.png)
If that's the case, you will need to tell Nexus what the shape of this data is thanks to the ***Source Types***.

When a field's type is an object, then the field's resolver returns a backing type. Concretely this might for example be a plain JavaScript object containing node/row/document data from a database call. This backing type data is in turn passed down to all the object type's own field resolvers.

Here is the above diagram updated to include backing types now.

![](/docs/assets/diagram-backing-types-2.png)

Here is a step-by-step breakdown of what is going on (follow the diagram annotation numbers):

1. Client sends a query
2. The field resolver for `Query.user` runs. Remember `Query` fields (along with `Subscription`, `Mutation`) are _entrypoints_.
3. Within this resolver, the database client fetches a user from the database. The resolver returns this data. This data will now become **backing type** data...
4. Resolution continues since the type of `Query.user` field is an object, not a scalar. As such its own fields need resolving. The fields that get resolved are limited to those selected by the client, in this case: `fullName`, `age`, `comments`. Those three field resolvers run. Their `parent` argument is the user model data fetched in step 3. _This is the backing type data for the GraphQL `User` object_.

```ts
t.field('...', {
resolve(parent, args, ctx, info) {
// ^------------------------------- Here
},
}
```

5. The `comments` field is is an object type so just like with `Query.users` before, its own fields must be resolved. The `comments` resolver fetches comments data from the database. Like in step 3 this data becomes _backing type_ data.

6. Much like the GraphQL `Comment` object field were resolved, so is `Comment`. Resolution runs once for every comment retrieved from the database in the previous step. The `text` field is scalar so resolution of that path can terminate there. But the `likers` field is typed to an object and so once again goes through the object-field resolution pattern.

7. A request to the database for users who liked this comment is made.

8. A repeat of step 4. But this time from a different edge in the graph. Before it was the entrypoint field `Query.user`. Now we're resolving from relation with `Comment`. Note how the backing type requirements of `User`, regardless of which part of the graph is pointing at it, remain the same. One other difference from step 4 is that, like in step 6, we are dealing with a list of data. That is, this resolution is run every user returned in step 7.

Hopefully you can see how the GraphQL types seen by the client are distinct from the backing types flowing through the resolvers. Below, you can find a code sample of how the implementation of this schema might look like.

<details>
<summary>See code implementation</summary>

```ts
query({
definition(t) {
t.user({
args: {
id: nonNull(idArg()),
},
resolve(_, { id }, { db }) {
return db.fetchUser({ where: { id } })
},
})
},
})

object({
name: 'User',
rootTyping: 'Prisma.User',
definition(t) {
t.string('fullName', {
resolve(user) {
return [user.firstName, user.middleName, user.lastName].join(', ')
},
})
t.int('age', {
resolve(user) {
return yearsSinceUnixTimestamp(user.birthDate)
},
})
t.list.field('comments', {
type: 'Comment',
resolve(user, _args, { db }) {
return db.comment.fetchMany({ where: { author: user.id } })
},
})
},
})

object({
name: 'Comment',
rootTyping: 'Prisma.Comment',
definition(t) {
t.string('title', {
resolve(comment) {
return comment.title
},
})
t.field('body', {
resolve(comment) {
return comment.body
},
})
t.field('post', {
type: 'Post',
resolve(comment, _args, { db }) {
return db.post.fetchOne({ where: { id: comment.postId } })
},
})
t.field('author', {
type: 'User',
resolve(comment, _args, { db }) {
return db.user.fetchOne({ where: { id: comment.authorId } })
},
})
},
})

object({
name: 'Post',
rootTyping: 'Prisma.Post',
definition(t) {
t.string('title', {
resolve(post) {
return post.title
},
})
t.field('body', {
resolve(post) {
return post.body
},
})
t.list.field('comments', {
type: 'Comment',
resolve(post, _args, { db }) {
return db.comment.fetchMany({ where: { id: post.commentId } })
},
})
},
})
```

</details>

## Backing types in Nexus

### Inferred types

When you first begin creating your schema, you may have objects without backing types setup. In these cases Nexus infers that the backing type is an exact match of the GraphQL type. Take this schema for example:

<!-- prettier-ignore -->
```ts
// Nexus infers the backing type of:
//
// { fullName: string, age: number } ---> |
// |
object({ // |
name: 'User', // |
definition(t) { // |
t.string('fullName', { // |
resolve(user) { // |
// ^-------------------------- |
return user.fullName // |
}, // |
}) // |
t.int('age', { // |
resolve(user) { // |
// ^-------------------------- |
return user.age // |
}, // |
}) // |
}, // |
}) // |
// |
queryType({ // |
definition(t) { // |
t.list.field('users', { // |
type: 'User', // |
resolve() { // |
return [/**/] // |
// ^----------------------- |
},
})
},
})
```

This may suffice well enough for some time, but most apps will eventually see their GraphQL and backing types diverge. Once this happens, you can tell Nexus about it using the `rootTyping` object type config property.

### `rootTyping` property

```ts
export interface MyDBUser {
// | ^-------------------- Create your backing type
// ^-------------------------------- Export your backing type (required)
firstName: string
lastName: string
birthDate: number
}

object({
name: 'User',
rootTyping: 'MyDBUser',
// ^---------------------- Tell Nexus what the backing type is.
// Now, Nexus types...
definition(t) {
t.string('fullName', {
resolve(user) {
// ^----------------------- as: MyDBUser
return [user.firstName, user.lastName].join(', ')
},
})
t.int('age', {
resolve(user) {
// ^------------------------ as: MyDBUser
return yearsSinceUnixTimestamp(user.birthDate)
},
})
},
})

queryType({
definition(t) {
t.list.field('users', {
type: 'User',
resolve(_root, args, ctx) {
// ^------- return as: MyDBUser[]
return ctx.db.user.getMany()
},
})
},
})
```

Nexus does not care about where `MyDBUser` is defined. So long as it is defined and exported from a module within your app, it will be available for use in any `rootTyping` property.

The `rootTyping` property is statically typed as a union of all the valid possible types your app makes available. Thus, your IDE will/should give you autocompletion here.

### Third-party types

If you would like to use types from a third party package, you can just re-export them in your own app. Here's the above example re-visited using some third party typings:

```ts
export type * as Spotify from 'spotify-api'
// ^------ Export your third-party type(s)
// Can be anywhere within your app

object({
name: 'User',
rootTyping: 'Spotify.Foo',
// ^---------------------- Tell Nexus what the backing type is.
// Now, Nexus types...
definition(t) {
t.string('fullName', {
resolve(user) {
// ^----------------------- as: Spotify.Foo
return user.fullName
},
})
t.int('age', {
resolve(user) {
// ^------------------------ as: Spotify.Foo
return user.age
},
})
},
})

queryType({
definition(t) {
t.list.field('users', {
type: 'User',
resolve(_root, args, ctx) {
// ^------- return as: Spotify.Foo[]
return ctx.db.user.getMany()
},
})
},
})
```

> **Note**: The backing type configuration is co-located with the GraphQL object because Nexus takes the view that a GraphQL object owns its backing type requirements and all nodes in the graph pointing to it must then satisfy those requirements in their own resolvers. We saw a bit of this in the Backing Types Conepts section before, where `User` object was related to by multiple nodes in the graph, and those various nodes passed the same kinds of backing types during resolution.
[You can read more about them here](/guides/source-types).
Loading

0 comments on commit 01f129a

Please sign in to comment.