Skip to content

Commit

Permalink
Add extendGraphQLSchema example (#5820)
Browse files Browse the repository at this point in the history
  • Loading branch information
timleslie authored May 28, 2021
1 parent 28ca49c commit 8598c83
Show file tree
Hide file tree
Showing 12 changed files with 805 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/shaggy-beers-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-next/example-extend-graphql-schema': major
---

Initial version of the `extend-graphql-schema` example.
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ jobs:
'basic.test.ts',
'blog.test.ts',
'default-values.test.ts',
'extend-graphql-schema.test.ts',
'json.test.ts',
'roles.test.ts',
'task-manager.test.ts',
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ You can use these projects to learn about, and experiment with specific features
- [`withAuth()`](./with-auth): Adding password based authentication to your Keystone application.
- [`JSON field`](./json): Using a JSON field in your Keystone application.
- [`defaultValue`](./default-values): Using `defaultValue` to set default values on fields in your data schema.
- [`extendGraphqlSchema](./extend-graphql-schema): Using `config.extendGraphqlSchema` to extend your GraphQL API.

## Running examples

Expand Down
1 change: 1 addition & 0 deletions examples/extend-graphql-schema/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @keystone-next/example-extend-graphql-schema
119 changes: 119 additions & 0 deletions examples/extend-graphql-schema/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
## Feature Example - Extend GraphQL Schema

This project demonstrates how to extend the GraphQL API provided by Keystone with custom queries and mutations.
It builds on the [Blog](../blog) starter project.

## Instructions

To run this project, clone the Keystone repository locally then navigate to this directory and run:

```shell
yarn dev
```

This will start the Admin UI at [localhost:3000](http://localhost:3000).
You can use the Admin UI to create items in your database.

You can also access a GraphQL Playground at [localhost:3000/api/graphql](http://localhost:3000/api/graphql), which allows you to directly run GraphQL queries and mutations.

## Features

This project demonstrates how to extend the GraphQL API provided by Keystone with custom queries and mutations.
Schema extensions are set using the [`extendGraphqlSchema`](https://next.keystonejs.com/apis/config#extend-graphql-schema) config option.

The function `graphQLSchemaExtension` accepts a `typeDefs` string, which lets you define your GraphQL types, and a `resolvers` object, which lets your define resolvers for your types.

The Apollo docs contain more information on GraphQL [types](https://www.apollographql.com/docs/apollo-server/schema/schema/) and [resolvers](https://www.apollographql.com/docs/apollo-server/data/resolvers/).

### Custom mutation

We add a custom mutation to our schema using `type Mutation` in the `typeDefs`, and defining `resolvers.Mutation`.

```typescript
extendGraphqlSchema: graphQLSchemaExtension({
typeDefs: `
type Mutation {
""" Publish a post """
publishPost(id: ID!): Post
}`,
resolvers: {
Mutation: {
publishPost: (root, { id }, context) => {
return context.db.lists.Post.updateOne({
id,
data: { status: 'published', publishDate: new Date().toUTCString() },
});
},
},
},
}),
```

### Custom query

We add a custom query to our schema using `type Query` in the `typeDefs`, and defining `resolvers.Query`.

```typescript
extendGraphqlSchema: graphQLSchemaExtension({
typeDefs: `
type Query {
""" Return all posts for a user from the last <days> days """
recentPosts(id: ID!, days: Int! = 7): [Post]
}`,
resolvers: {
Query: {
recentPosts: (root, { id, days }, context) => {
const cutoff = new Date(
new Date().setUTCDate(new Date().getUTCDate() - days)
).toUTCString();
return context.db.lists.Post.findMany({
where: { author: { id }, publishDate_gt: cutoff },
});
},
},
},
}),
```

### Custom type

We add a custom type to our schema using `type Statisics` in the `typeDefs`, and defining `resolvers.Statisics`.

```typescript
extendGraphqlSchema: graphQLSchemaExtension({
typeDefs: `
type Query {
""" Compute statistics for a user """
stats(id: ID!): Statistics
}
""" A custom type to represent statistics for a user """
type Statistics {
draft: Int
published: Int
latest: Post
}`,
resolvers: {
Query: {
stats: async (root, { id }, context) => {
const draft = await context.lists.Post.count({
where: { author: { id }, status: 'draft' },
});
const published = await context.lists.Post.count({
where: { author: { id }, status: 'published' },
});
const { posts } = await context.lists.Author.findOne({
where: { id },
query: 'posts(first: 1, orderBy: { publishDate: desc }) { id }',
});
return { draft, published, latestPostId: posts ? posts[0].id : null };
},
},
Statistics: {
latest: (root, args, context) =>
context.db.lists.Post.findOne({ where: { id: root.latestPostId } }),
},
},
}),
```
79 changes: 79 additions & 0 deletions examples/extend-graphql-schema/custom-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { graphQLSchemaExtension } from '@keystone-next/keystone/schema';

export const extendGraphqlSchema = graphQLSchemaExtension({
typeDefs: `
type Mutation {
""" Publish a post """
publishPost(id: ID!): Post
}
type Query {
""" Return all posts for a user from the last <days> days """
recentPosts(id: ID!, days: Int! = 7): [Post]
""" Compute statistics for a user """
stats(id: ID!): Statistics
}
""" A custom type to represent statistics for a user """
type Statistics {
draft: Int
published: Int
latest: Post
}`,
resolvers: {
Mutation: {
publishPost: (root, { id }, context) => {
// Note we use `context.db.lists.Post` here as we have a return type
// of Post, and this API provides results in the correct format.
// If you accidentally use `context.lists.Post` here you can expect problems
// when accessing the fields in your GraphQL client.
return context.db.lists.Post.updateOne({
id,
data: { status: 'published', publishDate: new Date().toUTCString() },
});
},
},
Query: {
recentPosts: (root, { id, days }, context) => {
// Create a date string <days> in the past from now()
const cutoff = new Date(
new Date().setUTCDate(new Date().getUTCDate() - days)
).toUTCString();

// Note we use `context.db.lists.Post` here as we have a return type
// of [Post], and this API provides results in the correct format.
// If you accidentally use `context.lists.Post` here you can expect problems
// when accessing the fields in your GraphQL client.
return context.db.lists.Post.findMany({
where: { author: { id }, publishDate_gt: cutoff },
});
},
stats: async (root, { id }, context) => {
const draft = await context.lists.Post.count({
where: { author: { id }, status: 'draft' },
});
const published = await context.lists.Post.count({
where: { author: { id }, status: 'published' },
});
const { posts } = await context.lists.Author.findOne({
where: { id },
query: 'posts(first: 1, orderBy: { publishDate: desc }) { id }',
});
return { draft, published, latestPostId: posts ? posts[0].id : null };
},
},
Statistics: {
// The stats resolver returns an object which is passed to this resolver as
// the root value. We use that object to further resolve ths specific fields.
// In this case we want to take root.latestPostId and resolve it as a Post object
//
// As above we use the context.db.lists.Post API to achieve this.
latest: (root, args, context) =>
context.db.lists.Post.findOne({ where: { id: root.latestPostId } }),
// We don't need to define resolvers for draft and published, as apollo will
// return root.draft and root.published respectively.
},
},
});
12 changes: 12 additions & 0 deletions examples/extend-graphql-schema/keystone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { config } from '@keystone-next/keystone/schema';
import { lists } from './schema';
import { extendGraphqlSchema } from './custom-schema';

export default config({
db: {
provider: 'sqlite',
url: process.env.DATABASE_URL || 'file:./keystone-example.db',
},
lists,
extendGraphqlSchema,
});
22 changes: 22 additions & 0 deletions examples/extend-graphql-schema/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@keystone-next/example-extend-graphql-schema",
"version": "0.0.0",
"private": true,
"license": "MIT",
"scripts": {
"dev": "keystone-next dev",
"start": "keystone-next start",
"build": "keystone-next build"
},
"dependencies": {
"@keystone-next/fields": "^9.0.0",
"@keystone-next/keystone": "^18.0.0"
},
"devDependencies": {
"typescript": "^4.2.4"
},
"engines": {
"node": "^12.20 || >= 14.13"
},
"repository": "https://github.com/keystonejs/keystone/tree/master/examples/extend-graphql-schema"
}
Loading

1 comment on commit 8598c83

@vercel
Copy link

@vercel vercel bot commented on 8598c83 May 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.