Schema Generator

In this section, we will continue the GraphQL example 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):

ent/entc.go

  1. func main() {
  2. ex, err := entgql.NewExtension(
  3. entgql.WithWhereInputs(true),
  4. entgql.WithConfigPath("../gqlgen.yml"),
  5. entgql.WithSchemaGenerator(),
  6. entgql.WithSchemaPath("../ent.graphql"),
  7. )
  8. if err != nil {
  9. log.Fatalf("creating entgql extension: %v", err)
  10. }
  11. opts := []entc.Option{
  12. entc.Extensions(ex),
  13. entc.TemplateDir("./template"),
  14. }
  15. if err := entc.Generate("./schema", &gen.Config{}, opts...); err != nil {
  16. log.Fatalf("running ent codegen: %v", err)
  17. }
  18. }

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.

ent/schema/todo.go

  1. // Edges of the Todo.
  2. func (Todo) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.To("parent", Todo.Type).
  5. Unique().
  6. From("children").
  7. }
  8. }
  9. // Annotations of the Todo.
  10. func (Todo) Annotations() []schema.Annotation {
  11. return []schema.Annotation{
  12. entgql.RelayConnection(),
  13. entgql.QueryField(),
  14. }
  15. }

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:

ent/schema/todo.go

  1. // Edges of the Todo.
  2. func (Todo) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.To("parent", Todo.Type).
  5. Unique().
  6. From("children").
  7. Annotation(entgql.RelayConnection()),
  8. }
  9. }

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.

todo.graphql

  1. -interface Node {
  2. - id: ID!
  3. -}
  4. """Maps a Time GraphQL scalar to a Go time.Time struct."""
  5. scalar Time
  6. -"""
  7. -Define a Relay Cursor type:
  8. -https://relay.dev/graphql/connections.htm#sec-Cursor
  9. -"""
  10. -scalar Cursor
  11. -"""
  12. -Define an enumeration type and map it later to Ent enum (Go type).
  13. -https://graphql.org/learn/schema/#enumeration-types
  14. -"""
  15. -enum Status {
  16. - IN_PROGRESS
  17. - COMPLETED
  18. -}
  19. -
  20. -type PageInfo {
  21. - hasNextPage: Boolean!
  22. - hasPreviousPage: Boolean!
  23. - startCursor: Cursor
  24. - endCursor: Cursor
  25. -}
  26. -type TodoConnection {
  27. - totalCount: Int!
  28. - pageInfo: PageInfo!
  29. - edges: [TodoEdge]
  30. -}
  31. -type TodoEdge {
  32. - node: Todo
  33. - cursor: Cursor!
  34. -}
  35. -"""The following enums match the entgql annotations in the ent/schema."""
  36. -enum TodoOrderField {
  37. - CREATED_AT
  38. - PRIORITY
  39. - STATUS
  40. - TEXT
  41. -}
  42. -enum OrderDirection {
  43. - ASC
  44. - DESC
  45. -}
  46. input TodoOrder {
  47. direction: OrderDirection!
  48. field: TodoOrderField
  49. }
  50. -"""
  51. -Define an object type and map it later to the generated Ent model.
  52. -https://graphql.org/learn/schema/#object-types-and-fields
  53. -"""
  54. -type Todo implements Node {
  55. - id: ID!
  56. - createdAt: Time
  57. - status: Status!
  58. - priority: Int!
  59. - text: String!
  60. - parent: Todo
  61. - children: [Todo!]
  62. -}
  63. """
  64. Define an input type for the mutation below.
  65. https://graphql.org/learn/schema/#input-types
  66. Note that this type is mapped to the generated
  67. input type in mutation_input.go.
  68. """
  69. input CreateTodoInput {
  70. status: Status! = IN_PROGRESS
  71. priority: Int
  72. text: String
  73. parentID: ID
  74. ChildIDs: [ID!]
  75. }
  76. """
  77. Define an input type for the mutation below.
  78. https://graphql.org/learn/schema/#input-types
  79. Note that this type is mapped to the generated
  80. input type in mutation_input.go.
  81. """
  82. input UpdateTodoInput {
  83. status: Status
  84. priority: Int
  85. text: String
  86. parentID: ID
  87. clearParent: Boolean
  88. addChildIDs: [ID!]
  89. removeChildIDs: [ID!]
  90. }
  91. """
  92. Define a mutation for creating todos.
  93. https://graphql.org/learn/queries/#mutations
  94. """
  95. type Mutation {
  96. createTodo(input: CreateTodoInput!): Todo!
  97. updateTodo(id: ID!, input: UpdateTodoInput!): Todo!
  98. updateTodos(ids: [ID!]!, input: UpdateTodoInput!): [Todo!]!
  99. }
  100. -"""Define a query for getting all todos and support the Node interface."""
  101. -type Query {
  102. - todos(after: Cursor, first: Int, before: Cursor, last: Int, orderBy: TodoOrder, where: TodoWhereInput): TodoConnection
  103. - node(id: ID!): Node
  104. - nodes(ids: [ID!]!): [Node]!
  105. -}

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.

ent/entc.go

  1. func main() {
  2. ex, err := entgql.NewExtension(
  3. entgql.WithWhereInputs(true),
  4. - entgql.WithConfigPath("../gqlgen.yml"),
  5. + entgql.WithConfigPath("./gqlgen.yml"),
  6. entgql.WithSchemaGenerator(),
  7. - entgql.WithSchemaPath("../ent.graphql"),
  8. + entgql.WithSchemaPath("./ent.graphql"),
  9. )
  10. if err != nil {
  11. log.Fatalf("creating entgql extension: %v", err)
  12. }
  13. opts := []entc.Option{
  14. entc.Extensions(ex),
  15. - entc.TemplateDir("./template"),
  16. + entc.TemplateDir("./ent/template"),
  17. }
  18. - if err := entc.Generate("./schema", &gen.Config{}, opts...); err != nil {
  19. + if err := entc.Generate("./ent/schema", &gen.Config{}, opts...); err != nil {
  20. log.Fatalf("running ent codegen: %v", err)
  21. }
  22. }

Update the generate.go to include the ent codegen.

generate.go

  1. package todo
  2. //go:generate go run -mod=mod ./ent/entc.go
  3. //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:

  1. 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:

  1. type Query {
  2. """Fetches an object given its ID."""
  3. node(
  4. """ID of the object."""
  5. id: ID!
  6. ): Node
  7. """Lookup nodes by a list of IDs."""
  8. nodes(
  9. """The list of node IDs."""
  10. ids: [ID!]!
  11. ): [Node]!
  12. todos(
  13. """Returns the elements in the list that come after the specified cursor."""
  14. after: Cursor
  15. """Returns the first _n_ elements from the list."""
  16. first: Int
  17. """Returns the elements in the list that come before the specified cursor."""
  18. before: Cursor
  19. """Returns the last _n_ elements from the list."""
  20. last: Int
  21. """Ordering options for Todos returned from the connection."""
  22. orderBy: TodoOrder
  23. """Filtering options for Todos returned from the connection."""
  24. where: TodoWhereInput
  25. ): TodoConnection!
  26. }

To add new fields to the Query type, you can do the following:

todo.graphql

  1. extend type Query {
  2. """Returns the literal string 'pong'."""
  3. ping: String!
  4. }

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 or Slack channel.