Files
ent/doc/md/tutorial-todo-gql-schema-generator.md
2023-06-27 07:56:04 +03:00

299 lines
7.8 KiB
Markdown

---
id: tutorial-todo-gql-schema-generator
title: Schema Generator
sidebar_label: Schema Generator
---
In this section, we will continue the [GraphQL example](tutorial-todo-gql.mdx) by explaining how to generate a
type-safe GraphQL schema from our `ent/schema`.
### Configure Ent
Go to your `ent/entc.go` file, and add the highlighted line (extension options):
```go {5} title="ent/entc.go"
func main() {
ex, err := entgql.NewExtension(
entgql.WithWhereInputs(true),
entgql.WithConfigPath("../gqlgen.yml"),
entgql.WithSchemaGenerator(),
entgql.WithSchemaPath("../ent.graphql"),
)
if err != nil {
log.Fatalf("creating entgql extension: %v", err)
}
opts := []entc.Option{
entc.Extensions(ex),
entc.TemplateDir("./template"),
}
if err := entc.Generate("./schema", &gen.Config{}, opts...); err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}
```
The `WithSchemaGenerator` option enables the GraphQL schema generation.
### Add Annotations To `Todo` Schema
The `entgql.RelayConnection()` annotation is used to generate the Relay `<T>Edge`, `<T>Connection`, and `PageInfo` types for the `Todo` type.
The `entgql.QueryField()` annotation is used to generate the `todos` field in the `Query` type.
```go {13,14} title="ent/schema/todo.go"
// Edges of the Todo.
func (Todo) Edges() []ent.Edge {
return []ent.Edge{
edge.To("parent", Todo.Type).
Unique().
From("children").
}
}
// Annotations of the Todo.
func (Todo) Annotations() []schema.Annotation {
return []schema.Annotation{
entgql.RelayConnection(),
entgql.QueryField(),
}
}
```
The `entgql.RelayConnection()` annotation can also be used on the edge fields, to generate first, last, after, before... arguments and change the field type to `<T>Connection!`. For example to change the `children` field from `children: [Todo!]!` to `children(first: Int, last: Int, after: Cursor, before: Cursor): TodoConnection!`. You can add the `entgql.RelayConnection()` annotation to the edge field:
```go {7} title="ent/schema/todo.go"
// Edges of the Todo.
func (Todo) Edges() []ent.Edge {
return []ent.Edge{
edge.To("parent", Todo.Type).
Unique().
From("children").
Annotations(entgql.RelayConnection()),
}
}
```
### Cleanup the handwritten schema
Please remove the types below from the `todo.graphql` to avoid conflict with the types that are generated by EntGQL in the `ent.graphql` file.
```diff title="todo.graphql"
-interface Node {
- id: ID!
-}
"""Maps a Time GraphQL scalar to a Go time.Time struct."""
scalar Time
-"""
-Define a Relay Cursor type:
-https://relay.dev/graphql/connections.htm#sec-Cursor
-"""
-scalar Cursor
-"""
-Define an enumeration type and map it later to Ent enum (Go type).
-https://graphql.org/learn/schema/#enumeration-types
-"""
-enum Status {
- IN_PROGRESS
- COMPLETED
-}
-
-type PageInfo {
- hasNextPage: Boolean!
- hasPreviousPage: Boolean!
- startCursor: Cursor
- endCursor: Cursor
-}
-type TodoConnection {
- totalCount: Int!
- pageInfo: PageInfo!
- edges: [TodoEdge]
-}
-type TodoEdge {
- node: Todo
- cursor: Cursor!
-}
-"""The following enums match the entgql annotations in the ent/schema."""
-enum TodoOrderField {
- CREATED_AT
- PRIORITY
- STATUS
- TEXT
-}
-enum OrderDirection {
- ASC
- DESC
-}
input TodoOrder {
direction: OrderDirection!
field: TodoOrderField
}
-"""
-Define an object type and map it later to the generated Ent model.
-https://graphql.org/learn/schema/#object-types-and-fields
-"""
-type Todo implements Node {
- id: ID!
- createdAt: Time
- status: Status!
- priority: Int!
- text: String!
- parent: Todo
- children: [Todo!]
-}
"""
Define an input type for the mutation below.
https://graphql.org/learn/schema/#input-types
Note that this type is mapped to the generated
input type in mutation_input.go.
"""
input CreateTodoInput {
status: Status! = IN_PROGRESS
priority: Int
text: String
parentID: ID
ChildIDs: [ID!]
}
"""
Define an input type for the mutation below.
https://graphql.org/learn/schema/#input-types
Note that this type is mapped to the generated
input type in mutation_input.go.
"""
input UpdateTodoInput {
status: Status
priority: Int
text: String
parentID: ID
clearParent: Boolean
addChildIDs: [ID!]
removeChildIDs: [ID!]
}
"""
Define a mutation for creating todos.
https://graphql.org/learn/queries/#mutations
"""
type Mutation {
createTodo(input: CreateTodoInput!): Todo!
updateTodo(id: ID!, input: UpdateTodoInput!): Todo!
updateTodos(ids: [ID!]!, input: UpdateTodoInput!): [Todo!]!
}
-"""Define a query for getting all todos and support the Node interface."""
-type Query {
- todos(after: Cursor, first: Int, before: Cursor, last: Int, orderBy: TodoOrder, where: TodoWhereInput): TodoConnection
- node(id: ID!): Node
- nodes(ids: [ID!]!): [Node]!
-}
```
### Ensure the execution order of Ent and GQLGen
We also need to do some changes to our `generate.go` files to ensure the execution order of Ent and GQLGen. The reason for this is to ensure that GQLGen sees the objects created by Ent and executes the code generator properly.
First, remove the `ent/generate.go` file. Then, update the `ent/entc.go` file with the correct path, because the Ent codegen will be run from the project root directory.
```diff title="ent/entc.go"
func main() {
ex, err := entgql.NewExtension(
entgql.WithWhereInputs(true),
- entgql.WithConfigPath("../gqlgen.yml"),
+ entgql.WithConfigPath("./gqlgen.yml"),
entgql.WithSchemaGenerator(),
- entgql.WithSchemaPath("../ent.graphql"),
+ entgql.WithSchemaPath("./ent.graphql"),
)
if err != nil {
log.Fatalf("creating entgql extension: %v", err)
}
opts := []entc.Option{
entc.Extensions(ex),
- entc.TemplateDir("./template"),
+ entc.TemplateDir("./ent/template"),
}
- if err := entc.Generate("./schema", &gen.Config{}, opts...); err != nil {
+ if err := entc.Generate("./ent/schema", &gen.Config{}, opts...); err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}
```
Update the `generate.go` to include the ent codegen.
```go {3} title="generate.go"
package todo
//go:generate go run -mod=mod ./ent/entc.go
//go:generate go run -mod=mod github.com/99designs/gqlgen
```
After changing the `generate.go` file, we're ready to execute the code generation as follows:
```console
go generate ./...
```
You will see that the `ent.graphql` file will be updated with the new content from EntGQL's Schema Generator.
### Extending the type that generated by Ent
You may note that the type generated will include the `Query` type object with some fields that are already defined:
```graphql
type Query {
"""Fetches an object given its ID."""
node(
"""ID of the object."""
id: ID!
): Node
"""Lookup nodes by a list of IDs."""
nodes(
"""The list of node IDs."""
ids: [ID!]!
): [Node]!
todos(
"""Returns the elements in the list that come after the specified cursor."""
after: Cursor
"""Returns the first _n_ elements from the list."""
first: Int
"""Returns the elements in the list that come before the specified cursor."""
before: Cursor
"""Returns the last _n_ elements from the list."""
last: Int
"""Ordering options for Todos returned from the connection."""
orderBy: TodoOrder
"""Filtering options for Todos returned from the connection."""
where: TodoWhereInput
): TodoConnection!
}
```
To add new fields to the `Query` type, you can do the following:
```graphql title="todo.graphql"
extend type Query {
"""Returns the literal string 'pong'."""
ping: String!
}
```
You can extend any type that is generated by Ent. To skip a field from the type, you can use the `entgql.Skip()` on that field or edge.
---
Well done! As you can see, after adapting the Schema Generator feature we don't have to write GQL schemas by hand anymore. Have questions? Need help with getting started? Feel free to join our [Discord server](https://discord.gg/qZmPgTE6RX) or [Slack channel](https://entgo.io/docs/slack).