Move graphql doc in plugins section and update examples to match FoodAdvisor

This commit is contained in:
Jim LAURIE 2019-10-07 17:03:45 +02:00
parent 9bfcad2a54
commit f02d4dfcb0
2 changed files with 153 additions and 157 deletions

View File

@ -110,7 +110,6 @@ module.exports = {
'/3.0.0-beta.x/guides/email',
'/3.0.0-beta.x/guides/upload',
'/3.0.0-beta.x/guides/parameters',
'/3.0.0-beta.x/guides/graphql',
'/3.0.0-beta.x/guides/i18n',
'/3.0.0-beta.x/guides/models',
'/3.0.0-beta.x/guides/policies',
@ -141,6 +140,7 @@ module.exports = {
'/3.0.0-beta.x/plugins/users-permissions',
'/3.0.0-beta.x/plugins/documentation',
'/3.0.0-beta.x/plugins/upload',
'/3.0.0-beta.x/plugins/graphql',
],
},
{

View File

@ -1,8 +1,6 @@
# GraphQL
::: note
This feature requires the GraphQL plugin (not installed by default).
:::
By default Strapi create [REST endpoints](../content-api/api-endpoints) for each of your content types. With the GraphQL plugin, you will be able to add a GraphQL endpoint to fetch and mutate your content.
## Usage
@ -36,7 +34,7 @@ strapi install graphql
::::
Then, start your app and open your browser at [http://localhost:1337/graphql](http://localhost:1337/graphql). You should see the interface (GraphQL Playground) that will help you to write GraphQL query to explore your data.
Then, start your app and open your browser at [http://localhost:1337/graphql](http://localhost:1337/graphql). You should see the interface (**GraphQL Playground**) that will help you to write GraphQL query to explore your data.
::: note
Install the [ModHeader](https://chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj/related) extension to set the `Authorization` header in your request
@ -54,7 +52,7 @@ You can edit these configurations by creating following file.
**Path —** `./extensions/graphql/config/settings.json`.
```
```json
{
"endpoint": "/graphql",
"tracing": false,
@ -65,11 +63,11 @@ You can edit these configurations by creating following file.
}
```
### Query API
## Query API
In the section, we assume that the [Shadow CRUD](#shadow-crud) feature is enabled. For each model, the plugin auto-generates queries and mutations which just fit to your needs.
#### Fetch a single entry
### Fetch a single entry
- `id`: String
@ -82,7 +80,7 @@ query {
}
```
#### Fetch multiple entries
### Fetch multiple entries
```
query {
@ -93,7 +91,7 @@ query {
}
```
#### Create a new entry
### Create a new entry
- `input`: Object
- `data`: Object — Values to insert
@ -114,7 +112,7 @@ mutation {
}
```
The implementation of the mutations also supports relational attributes. For example, you can create a new `User` and attach many `Post` to it by writing your query like this:
The implementation of the mutations also supports relational attributes. For example, you can create a new `User` and attach many `Restaurant` to it by writing your query like this:
```
mutation {
@ -122,23 +120,23 @@ mutation {
data: {
username: "John",
email: "john@doe.com",
posts: ["5b51e3949db573a586ad22de", "5b5b26619b0820c1c2fb79c9"]
restaurants: ["5b51e3949db573a586ad22de", "5b5b26619b0820c1c2fb79c9"]
}
}) {
user {
username
email
posts {
title
content
publishedAt
restaurant {
name
description
price
}
}
}
}
```
#### Update an existing entry
### Update an existing entry
- `input`: Object
- `where`: Object - Entry's ID to update
@ -167,16 +165,16 @@ You can also update relational attributes by passing an ID or an array of IDs (d
```
mutation {
updatePost(input: {
updateRestaurant(input: {
where: {
id: "5b5b27f8164f75c29c728110"
},
data: {
author: "5b51e3949db573a586ad22de" // User ID
chef: "5b51e3949db573a586ad22de" // User ID
}
}) {
post {
author {
restaurant {
chef {
username
email
}
@ -185,7 +183,7 @@ mutation {
}
```
#### Delete an entry
### Delete an entry
- `input`: Object
- `where`: Object - Entry's ID to delete
@ -235,9 +233,9 @@ query {
username
email
},
books(limit: 10, where: { _id_nin: ["5c4dad1a8f3845222ca88a56", "5c4dad1a8f3845222ca88a57"] }) {
restaurants(limit: 10, where: { _id_nin: ["5c4dad1a8f3845222ca88a56", "5c4dad1a8f3845222ca88a57"] }) {
_id,
title
name
}
}
```
@ -261,24 +259,24 @@ To simplify and automate the build of the GraphQL schema, we introduced the Shad
### Example
If you've generated an API called `Post` using the CLI `strapi generate:api post` or the administration panel, your model looks like this:
If you've generated an API called `Restaurant` using the CLI `strapi generate:api restaurant` or the administration panel, your model looks like this:
**Path —** `./api/post/models/Post.settings.json`.
**Path —** `./api/restaurant/models/Restaurant.settings.json`.
```
```json
{
"connection": "default",
"options": {
"timestamps": true
},
"attributes": {
"title": {
"name": {
"type": "string"
}
"content": {
},
"description": {
"type": "text"
},
"published": {
"open": {
"type": "boolean"
}
}
@ -288,31 +286,31 @@ If you've generated an API called `Post` using the CLI `strapi generate:api post
The generated GraphQL type and queries will be:
```
// Post's Type definition
type Post {
// Restaurant's Type definition
type Restaurant {
_id: String
created_at: String
updated_at: String
title: String
content: String
published: Boolean
name: String
description: String
open: Boolean
}
// Queries to retrieve one or multiple posts.
// Queries to retrieve one or multiple restaurants.
type Query {
posts(sort: String, limit: Int, start: Int, where: JSON): [Post]
post(id: String!): Post
restaurants(sort: String, limit: Int, start: Int, where: JSON): [Restaurant]
restaurant(id: String!): Restaurant
}
// Mutations to create, update or delete a post.
// Mutations to create, update or delete a restaurant.
type Mutation {
createPost(input: createPostInput): createPostPayload!
updatePost(input: updatePostInput): updatePostPayload!
deletePost(input: deletePostInput): deletePostPayload!
createRestaurant(input: createRestaurantInput): createRestaurantPayload!
updateRestaurant(input: updateRestaurantInput): updateRestaurantPayload!
deleteRestaurant(input: deleteRestaurantInput): deleteRestaurantPayload!
}
```
The queries and mutations will use the generated controller's actions as resolvers. It means that the `posts` query will execute the `Post.find` action, the `post` query will use the `Post.findOne` action and the `createProduct` mutation will use the `Post.create` action, etc.
The queries and mutations will use the generated controller's actions as resolvers. It means that the `restaurants` query will execute the `Restaurant.find` action, the `restaurant` query will use the `Restaurant.findOne` action and the `createRestaurant` mutation will use the `Restaurant.create` action, etc.
## Aggregation & Grouping
@ -324,14 +322,14 @@ Strapi now supports Aggregation & Grouping.
Let's consider again the model mentioned above:
```
type Post {
type Restaurant {
_id: ID
createdAt: String
updatedAt: String
title: String
content: String
name: String
description: String
nb_likes: Int,
published: Boolean
open: Boolean
}
```
@ -340,52 +338,52 @@ Strapi will generate automatically for you the following queries & types:
### Aggregation
```
type PostConnection {
values: [Post]
groupBy: PostGroupBy
aggregate: PostAggregator
type RestaurantConnection {
values: [Restaurant]
groupBy: RestaurantGroupBy
aggregate: RestaurantAggregator
}
type PostGroupBy {
_id: [PostConnection_id]
createdAt: [PostConnectionCreatedAt]
updatedAt: [PostConnectionUpdatedAt]
title: [PostConnectionTitle]
content: [PostConnectionContent]
nb_likes: [PostConnectionNbLikes],
published: [PostConnectionPublished]
type RestaurantGroupBy {
_id: [RestaurantConnection_id]
createdAt: [RestaurantConnectionCreatedAt]
updatedAt: [RestaurantConnectionUpdatedAt]
name: [RestaurantConnectionTitle]
description: [RestaurantConnectionContent]
nb_likes: [RestaurantConnectionNbLikes],
open: [RestaurantConnectionPublished]
}
type PostConnectionPublished {
type RestaurantConnectionPublished {
key: Boolean
connection: PostConnection
connection: RestaurantConnection
}
type PostAggregator {
type RestaurantAggregator {
count: Int
sum: PostAggregatorSum
avg: PostAggregatorAvg
min: PostAggregatorMin
max: PostAggregatorMax
sum: RestaurantAggregatorSum
avg: RestaurantAggregatorAvg
min: RestaurantAggregatorMin
max: RestaurantAggregatorMax
}
type PostAggregatorAvg {
type RestaurantAggregatorAvg {
nb_likes: Float
}
type PostAggregatorMin { // Same for max and sum
type RestaurantAggregatorMin { // Same for max and sum
nb_likes: Int
}
type Query {
postsConnection(sort: String, limit: Int, start: Int, where: JSON): PostConnection
restaurantsConnection(sort: String, limit: Int, start: Int, where: JSON): RestaurantConnection
}
```
Getting the total count and the average likes of posts:
Getting the total count and the average likes of restaurants:
```
postsConnection {
restaurantsConnection {
aggregate {
count
avg {
@ -396,10 +394,10 @@ postsConnection {
}
```
Let's say we want to do the same query but for only published posts
Let's say we want to do the same query but for only open restaurants
```
postsConnection(where: { published: true }) {
restaurantsConnection(where: { open: true }) {
aggregate {
count
avg {
@ -410,12 +408,12 @@ postsConnection(where: { published: true }) {
}
```
Getting the average likes of published and unpublished posts
Getting the average likes of open and non open restaurants
```
postsConnection {
restaurantsConnection {
groupBy {
published: {
open: {
key
connection {
aggregate {
@ -431,12 +429,12 @@ postsConnection {
Result
```JSON
```json
{
data: {
postsConnection: {
restaurantsConnection: {
groupBy: {
published: [
open: [
{
key: true,
connection: {
@ -491,57 +489,55 @@ module.exports = {
### Example
Let say we are using the same previous `Post` model.
Let say we are using the same previous `Restaurant` model.
**Path —** `./api/post/config/schema.graphql`.
**Path —** `./api/restaurant/config/schema.graphql`.
```js
module.exports = {
definition: `
enum PostStatusInput {
draft
reviewing
reviewed
published
deleted
enum RestaurantStatusInput {
work
open
closed
}
`,
query: `
postsByAuthor(id: ID, status: PostStatusInput, limit: Int): [Post]!
restaurantsByChef(id: ID, status: RestaurantStatusInput, limit: Int): [Restaurant]!
`,
mutation: `
attachPostToAuthor(id: ID, authorID: ID): Post!
attachRestaurantToChef(id: ID, chefID: ID): Restaurant!
`
resolver: {
Query: {
post: {
description: 'Return a single post',
restaurant: {
description: 'Return a single restaurant',
policies: ['plugins.users-permissions.isAuthenticated', 'isOwner'], // Apply the 'isAuthenticated' policy of the `Users & Permissions` plugin, then the 'isOwner' policy before executing the resolver.
},
posts: {
description: 'Return a list of posts', // Add a description to the query.
deprecated: 'This query should not be used anymore. Please consider using postsByAuthor instead.'
restaurants: {
description: 'Return a list of restaurants', // Add a description to the query.
deprecated: 'This query should not be used anymore. Please consider using restaurantsByChef instead.'
},
postsByAuthor: {
description: 'Return the posts published by the author',
resolver: 'Post.findByAuthor'
restaurantsByChef: {
description: 'Return the restaurants open by the chef',
resolver: 'Restaurant.findByChef'
},
postsByTags: {
description: 'Return the posts published by the author',
resolverOf: 'Post.findByTags', // Will apply the same policy on the custom resolver as the controller's action `findByTags`.
restaurantsByCategories: {
description: 'Return the restaurants open by the category',
resolverOf: 'Restaurant.findByCategories', // Will apply the same policy on the custom resolver as the controller's action `findByCategories`.
resolver: (obj, options, ctx) => {
// ctx is the context of the Koa request.
await strapi.controllers.posts.findByTags(ctx);
await strapi.controllers.restaurants.findByCategories(ctx);
return ctx.body.posts || `There is no post.`;
return ctx.body.restaurants || `There is no restaurant.`;
}
}
},
Mutation: {
attachPostToAuthor: {
description: 'Attach a post to an author',
attachRestaurantToChef: {
description: 'Attach a restaurant to an chef',
policies: ['plugins.users-permissions.isAuthenticated', 'isOwner'],
resolver: 'Post.attachToAuthor'
resolver: 'Restaurant.attachToChef'
}
}
}
@ -553,7 +549,7 @@ module.exports = {
Edit the `definition` attribute in one of the `schema.graphql` files of your project by using the GraphQL Type language string.
::: note
The easiest way is to create a new model using the CLI `strapi generate:model category --api post`, so you don't need to customise anything.
The easiest way is to create a new model using the CLI `strapi generate:model category --api restaurant`, so you don't need to customise anything.
:::
```js
@ -634,30 +630,30 @@ module.exports = {
One of the most powerful features of GraphQL is the auto-documentation of the schema. The GraphQL plugin allows you to add a description to a type, a field and a query. You can also deprecate a field or a query.
**Path —** `./api/post/models/Post.settings.json`.
**Path —** `./api/restaurant/models/Restaurant.settings.json`.
```
```json
{
"connection": "default",
"info": {
"description": "The Post type description"
"description": "The Restaurant type description"
},
"options": {
"timestamps": true
},
"attributes": {
"title": {
"name": {
"type": "string",
"description": "The title of the post",
"deprecated": "We are not using the title anymore, it is auto-generated thanks to our powerful AI"
"description": "The name of the restaurant",
"deprecated": "We are not using the name anymore, it is auto-generated thanks to our powerful AI"
},
"content": {
"description": {
"type": "text",
"description": "The content of the post."
"description": "The description of the restaurant."
},
"published": {
"open": {
"type": "boolean",
"description": "Is the post published or not. Yes = true."
"description": "Is the restaurant open or not. Yes = true."
}
}
}
@ -669,21 +665,21 @@ It might happen that you want to add a description to a query or deprecate it. T
The `schema.graphql` file has to be placed into the config folder of each API `./api/*/config/schema.graphql` or plugin `./extensions/*/config/schema.graphql`.
:::
**Path —** `./api/post/config/schema.graphql`.
**Path —** `./api/restaurant/config/schema.graphql`.
```js
module.exports = {
resolver: {
Query: {
posts: {
description: 'Return a list of posts', // Add a description to the query.
restaurants: {
description: 'Return a list of restaurants', // Add a description to the query.
deprecated:
'This query should not be used anymore. Please consider using postsByAuthor instead.', // Deprecate the query and explain the reason why.
'This query should not be used anymore. Please consider using restaurantsByChef instead.', // Deprecate the query and explain the reason why.
},
},
Mutation: {
createPost: {
description: 'Create a new post',
createRestaurant: {
description: 'Create a new restaurant',
deprecated: 'Please use the dashboard UI instead',
},
},
@ -699,8 +695,8 @@ Sometimes a query needs to be only accessible to authenticated user. To handle t
module.exports = {
resolver: {
Query: {
posts: {
description: 'Return a list of posts',
restaurants: {
description: 'Return a list of restaurants',
policies: [
'plugins.users-permissions.isAuthenticated',
'isOwner',
@ -709,8 +705,8 @@ module.exports = {
},
},
Mutation: {
createPost: {
description: 'Create a new post',
createRestaurant: {
description: 'Create a new restaurant',
policies: [
'plugins.users-permissions.isAuthenticated',
'global.logging',
@ -721,42 +717,42 @@ module.exports = {
};
```
In this example, the policy `isAuthenticated` located in the `users-permissions` plugin will be executed first. Then, the `isOwner` policy located in the `Post` API `./api/post/config/policies/isOwner.js`. Next, it will execute the `logging` policy located in `./config/policies/logging.js`. Finally, the resolver will be executed.
In this example, the policy `isAuthenticated` located in the `users-permissions` plugin will be executed first. Then, the `isOwner` policy located in the `Restaurant` API `./api/restaurant/config/policies/isOwner.js`. Next, it will execute the `logging` policy located in `./config/policies/logging.js`. Finally, the resolver will be executed.
::: note
There is no custom resolver in that case, so it will execute the default resolver (Post.find) provided by the Shadow CRUD feature.
There is no custom resolver in that case, so it will execute the default resolver (Restaurant.find) provided by the Shadow CRUD feature.
:::
### Link a query or mutation to a controller action
By default, the plugin will execute the actions located in the controllers that has been generated via the Content-Type Builder plugin or the CLI. For example, the query `posts` is going to execute the logic inside the `find` action in the `Post.js` controller. It might happen that you want to execute another action or a custom logic for one of your query.
By default, the plugin will execute the actions located in the controllers that has been generated via the Content-Type Builder plugin or the CLI. For example, the query `restaurants` is going to execute the logic inside the `find` action in the `Restaurant.js` controller. It might happen that you want to execute another action or a custom logic for one of your query.
```js
module.exports = {
resolver: {
Query: {
posts: {
description: 'Return a list of posts by author',
resolver: 'Post.findByAuthor',
restaurants: {
description: 'Return a list of restaurants by chef',
resolver: 'Restaurant.findByChef',
},
},
Mutation: {
createPost: {
description: 'Create a new post',
resolver: 'Post.customCreate',
createRestaurant: {
description: 'Create a new restaurant',
resolver: 'Restaurant.customCreate',
},
},
},
};
```
In this example, it will execute the `findByAuthor` action of the `Post` controller. It also means that the resolver will apply on the `posts` query the permissions defined on the `findByAuthor` action (through the administration panel).
In this example, it will execute the `findByChef` action of the `Restaurant` controller. It also means that the resolver will apply on the `restaurants` query the permissions defined on the `findByChef` action (through the administration panel).
::: note
The `obj` parameter is available via `ctx.params` and the `options` are available via `ctx.query` in the controller's action.
:::
The same process is also applied for the `createPost` mutation. It will execute the `customCreate` action of the `Post` controller.
The same process is also applied for the `createRestaurant` mutation. It will execute the `customCreate` action of the `Restaurant` controller.
::: note
The `where` parameter is available via `ctx.params` and the `data` are available via `ctx.request.body` in the controller's action.
@ -768,28 +764,28 @@ The `where` parameter is available via `ctx.params` and the `data` are available
module.exports = {
resolver: {
Query: {
posts: {
description: 'Return a list of posts by author',
restaurants: {
description: 'Return a list of restaurants by chef',
resolver: (obj, options, { context }) => {
// You can return a raw JSON object or a promise.
return [{
title: 'My first blog post',
content: 'Whatever you want...'
name: 'My first blog restaurant',
description: 'Whatever you want...'
}];
}
}
},
Mutation: {
updatePost: {
description: 'Update an existing post',
updateRestaurant: {
description: 'Update an existing restaurant',
resolver: (obj, options, { context }) => {
// The `where` and `data` parameters passed as arguments
// of the GraphQL mutation are available via the `context` object.
const where = context.params;
const data = context.request.body;
return await strapi.api.post.services.post.addPost(data, where);
return await strapi.api.restaurant.services.restaurant.addRestaurant(data, where);
}
}
}
@ -807,28 +803,28 @@ It might happen that you want to apply our permissions layer on a query. That's
module.exports = {
resolver: {
Query: {
posts: {
description: 'Return a list of posts by author',
resolverOf: 'Post.find', // Will apply the same policy on the custom resolver as the controller's action `find` located in `Post.js`.
restaurants: {
description: 'Return a list of restaurants by chef',
resolverOf: 'Restaurant.find', // Will apply the same policy on the custom resolver as the controller's action `find` located in `Restaurant.js`.
resolver: (obj, options, context) => {
// You can return a raw JSON object or a promise.
return [{
title: 'My first blog post',
content: 'Whatever you want...'
name: 'My first blog restaurant',
description: 'Whatever you want...'
}];
}
}
},
Mutation: {
updatePost: {
description: 'Update an existing post',
resolverOf: 'Post.update', // Will apply the same policy on the custom resolver than the controller's action `update` located in `Post.js`.
updateRestaurant: {
description: 'Update an existing restaurant',
resolverOf: 'Restaurant.update', // Will apply the same policy on the custom resolver than the controller's action `update` located in `Restaurant.js`.
resolver: (obj, options, { context }) => {
const where = context.params;
const data = context.request.body;
return await strapi.api.post.services.post.addPost(data, where);
return await strapi.api.restaurant.services.restaurant.addRestaurant(data, where);
}
}
}
@ -843,14 +839,14 @@ To do that, we need to use the `schema.graphql` like below:
```js
module.exports = {
type: {
Post: false // The Post type won't be "queriable" or "mutable".
Restaurant: false // The Restaurant type won't be "queriable" or "mutable".
}
resolver: {
Query: {
posts: false // The `posts` query will no longer be in the GraphQL schema.
restaurants: false // The `restaurants` query will no longer be in the GraphQL schema.
},
Mutation: {
createPost: false,
createRestaurant: false,
deletePOst: false
}
}