模式生成器
在本部分我们继续 GraphQL 示例 来解释如何由 ent/schema 生成类型安全的 GraphQL 模式。
配置 Ent
在 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)
}
}
WithSchemaGenerator 选项会开启 GraphQL 模式生成。
添加注解到 Todo 模式
entgql.RelayConnection() 用来为 Todo 类型生成 Relay 的 <T>Edge、 <T>Connection 和 PageInfo 类型。
entgql.QueryField() 注解用来在 Query 类型中生成 todos 字段。
// 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(),
}
}
entgql.RelayConnection() 注解也可以用在边字段上,可以用来生成第一个、最后一个、后一个、前一个等参数并修改字段类型为 <T>Connection!。
例如将 children 字段由 children: [Todo!]! 修改为 children(first: Int, last: Int, after: Cursor, before: Cursor): TodoConnection!。
可以添加 entgql.RelayConnection() 注解到边字段:
// Edges of the Todo.
func (Todo) Edges() []ent.Edge {
return []ent.Edge{
edge.To("parent", Todo.Type).
Unique().
From("children").
Annotations(entgql.RelayConnection()),
}
}
清理手写的模式
请在 todo.graphql 中移除下面的类型来避免与 ent.graphql 文件中 EntGQL 生成的类型产生冲突。
-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]!
-}
确保 Ent 和 GQLGen 执行顺序
我们还需要修改 generate.go 文件以确保 Ent 和 GQLGen 执行顺序。
这样做的原因是需要确保 GQLGen 可以识别 Ent 创建的对象并正确执行代码生成器。
首先移除 ent/generate.go 文件。然后更新 ent/entc.go 文件中正确的路径,因为 Ent 代码生成是运行在项目根目录中的。
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)
}
}
更新 generate.go 以包含 ent 代码生成内容。
package todo
//go:generate go run -mod=mod ./ent/entc.go
//go:generate go run -mod=mod github.com/99designs/gqlgen
更新完 generate.go 文件后,可以像如下执行代码生成:
go generate ./...
你会看到 ent.graphql 被更新为 EntGQL 模式生成器提供的新的内容了。
扩展 Ent 生成的类型
你可能已经注意到生成的类型会包含已经定义了一些字段的 Query 类型对象:
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!
}
你可以像如下向 Query 类型中添加新的字段:
extend type Query {
"""Returns the literal string 'pong'."""
ping: String!
}
你可以扩展 Ent 生成的任何字段。若要跳过某个类型的字段,可以在这个字段或边上使用 entgql.Skip()。
做得很好!正如你所见,在采用模式生成器功能后我们无需手写 GQL 模式了。 有任何问题吗?需要帮助吗?加入我们的 Discord 服务器 或 Slack 频道。