GraphQL:快速入门
环境
- https://github.com/notiz-dev/nestjs-prisma-starter 是一个基于 NestJS 和 Prisma 的后端项目模板,可以用来快速搭建后端项目。
Schema
在 GraphQL 中,Schema 描述数据类型和操作。Schema 由自定义类型(User Type)、查询(Query)、变更(Mutation)和订阅(Subscription)组成。
下面是一个示例 Schema:
1type Query {
2 posts: [Post]
3 post(id: ID!): Post
4}
5
6type Mutation {
7 createPost(title: String!, content: String!): Post!
8 updatePost(id: ID!, title: String!, content: String!): Post!
9 deletePost(id: ID!): Boolean
10}
11
12type Subscription {
13 postAdded: Post
14}
15
16type Post {
17 id: ID!
18 title: String!
19 content: String!
20 author: User
21}
22
23type User {
24 id: ID!
25 name: String!
26 email: String!
27}
这里,Post 和 User 是自定义类型,Query、Mutation 和 Subscription 是操作。
1type Query {
2 posts: [Post]
3 post(id: ID!): Post
4}
表示 Query 操作有两个字段,分别是 posts 和 post。posts 返回一个 Post 类型的数组,post(id: ID!): Post
这是一个参数为 id 的函数,返回一个 Post 类型的对象。!
表示这个字段是必须的。
一个真实的 Schema 例子
1type Auth {
2 # JWT access token
3 accessToken: JWT!
4
5 # JWT refresh token
6 refreshToken: JWT!
7 user: User!
8}
9
10input ChangePasswordInput {
11 newPassword: String!
12 oldPassword: String!
13}
14
15input CreatePostInput {
16 content: String!
17 title: String!
18}
19
20# A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
21scalar DateTime
22
23# A field whose value is a JSON Web Token (JWT): https://jwt.io/introduction.
24scalar JWT
25
26input LoginInput {
27 email: String!
28 password: String!
29}
30
31type Mutation {
32 changePassword(data: ChangePasswordInput!): User!
33 createPost(data: CreatePostInput!): Post!
34 login(data: LoginInput!): Auth!
35 refreshToken(token: JWT!): Token!
36 signup(data: SignupInput!): Auth!
37 updateUser(data: UpdateUserInput!): User!
38}
39
40# Possible directions in which to order a list of items when provided an `orderBy` argument.
41enum OrderDirection {
42 asc
43 desc
44}
45
46type PageInfo {
47 endCursor: String
48 hasNextPage: Boolean!
49 hasPreviousPage: Boolean!
50 startCursor: String
51}
52
53type Post {
54 author: User
55 content: String
56
57 # Identifies the date and time when the object was created.
58 createdAt: DateTime!
59 id: ID!
60 published: Boolean!
61 title: String!
62
63 # Identifies the date and time when the object was last updated.
64 updatedAt: DateTime!
65}
66
67type PostConnection {
68 edges: [PostEdge!]
69 pageInfo: PageInfo!
70 totalCount: Int!
71}
72
73type PostEdge {
74 cursor: String!
75 node: Post!
76}
77
78input PostOrder {
79 direction: OrderDirection!
80 field: PostOrderField!
81}
82
83# Properties by which post connections can be ordered.
84enum PostOrderField {
85 content
86 createdAt
87 id
88 published
89 title
90 updatedAt
91}
92
93type Query {
94 hello(name: String!): String!
95 helloWorld: String!
96 me: User!
97 post(postId: String!): Post!
98 publishedPosts(
99 after: String
100 before: String
101 first: Int
102 last: Int
103 orderBy: PostOrder
104 query: String
105 skip: Int
106 ): PostConnection!
107 userPosts(userId: String!): [Post!]!
108}
109
110# User role
111enum Role {
112 ADMIN
113 USER
114}
115
116input SignupInput {
117 email: String!
118 firstname: String
119 lastname: String
120 password: String!
121}
122
123type Subscription {
124 postCreated: Post!
125}
126
127type Token {
128 # JWT access token
129 accessToken: JWT!
130
131 # JWT refresh token
132 refreshToken: JWT!
133}
134
135input UpdateUserInput {
136 firstname: String
137 lastname: String
138}
139
140type User {
141 # Identifies the date and time when the object was created.
142 createdAt: DateTime!
143 email: String!
144 firstname: String
145 id: ID!
146 lastname: String
147 posts: [Post!]
148 role: Role!
149
150 # Identifies the date and time when the object was last updated.
151 updatedAt: DateTime!
152}
上面的 Schema 描述了一个博客系统的数据类型和操作。大多数内容都很好理解。除了下面这个可能需要解释一下:
1type PostConnection {
2 edges: [PostEdge!]
3 pageInfo: PageInfo!
4 totalCount: Int!
5}
6
7type PostEdge {
8 cursor: String!
9 node: Post!
10}
在 GraphQL 中,连接(Connection)和边缘(Edge)是分页数据的常用表示形式。
-
连接:表示分页数据的列表。
-
边缘:则表示连接中的单个元素,包括游标和节点。
Connection 和 Edge 这两个术语最初是由 Facebook 在 GraphQL 规范中引入的,用于描述分页数据的传输和表示形式。对于信息流的内容,一个连续的信息流就是一个连接,而每条信息就是一个边缘。这样,还可以通过边缘的游标来定位信息流中的某一条信息。
在这个示例 Schema 中,PostConnection 表示帖子列表的连接。它包括一个 edges 数组,每个元素都是一个 PostEdge 对象,表示一个帖子的边缘。
PostEdge 包括两个字段:cursor 和 node。cursor 是一个字符串类型的字段,它表示帖子边缘在连接中的位置。node 是一个 Post 类型的字段,它表示帖子节点本身。当你查询 PostConnection 时,你将获得一个包含多个 PostEdge 对象的 edges 数组。
读者可能疑问为什么使用 cursor
而非 id
。这是因为 id
是唯一的,而 cursor
不必要唯一。游标通常是由服务器随机生成的,不透明的字符串。这意味着,它的值只能由服务器生成和解析。客户端不需要了解游标的具体含义,只需要在后续查询中将游标值作为参数传递给服务器即可。
作为客户端使用 GraphQL
我们以一个完整的博客使用流程来说明如何使用 GraphQL。经历如下步骤:
-
注册用户
-
登录
-
创建帖子
-
查询帖子列表
-
查询帖子详情
-
更新帖子
-
删除帖子
-
注销
1. 注册用户
们使用 signup
变更来创建新用户。signup
变更需要一个包含 email
、password
、firstname
和 lastname
的 SignupInput
输入对象,并返回一个包含访问令牌、刷新令牌和用户信息的 Auth
对象。
可以使用以下查询:
1mutation {
2 signup(data: {
3 email: "test@example.com",
4 password: "password",
5 firstname: "John",
6 lastname: "Doe"
7 }) {
8 accessToken
9 refreshToken
10 user {
11 id
12 email
13 firstname
14 lastname
15 role
16 }
17 }
18}
执行上述查询后,返回:
1{
2 "data": {
3 "signup": {
4 "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbGc3YXBheTMwMDAwa2NvMnQ5NnUzaXVvIiwiaWF0IjoxNjgwOTE3MjAzLCJleHAiOjE2ODA5MTczMjN9.5B5E5bm5a-8o505HMzi82NTtc06v7gWy9-1c1L7p354",
5 "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbGc3YXBheTMwMDAwa2NvMnQ5NnUzaXVvIiwiaWF0IjoxNjgwOTE3MjAzLCJleHAiOjE2ODE1MjIwMDN9.8UaJU_YJW7rUq9-eRZjef9EqVumhQBMXr_1Ib9yb1Iw",
6 "user": {
7 "id": "clg7apay30000kco2t96u3iuo",
8 "email": "test@example.com",
9 "firstname": "John",
10 "lastname": "Doe",
11 "role": "USER"
12 }
13 }
14 }
15}
现在,我们已经注册了一个新用户,并收到了访问令牌和刷新令牌。
访问令牌用于访问需要身份验证的资源,刷新令牌用于获取新的访问令牌。
2. 登录
使用 login
变更来进行登录。login
变更需要一个包含 email
和 password
的 LoginInput
输入对象,并返回一个包含访问令牌、刷新令牌和用户信息的 Auth
对象。
使用以下查询:
1mutation {
2 login(data: {
3 email: "test@example.com",
4 password: "password"
5 }) {
6 accessToken
7 refreshToken
8 user {
9 id
10 email
11 firstname
12 lastname
13 role
14 }
15 }
16}
执行上述查询后,返回:
1{
2 "data": {
3 "login": {
4 "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbGc3YXBheTMwMDAwa2NvMnQ5NnUzaXVvIiwiaWF0IjoxNjgwOTIxNzcyLCJleHAiOjE2ODA5MjE4OTJ9.1e7Dk5wibQ4UCCcvzs_nyBOYkiB7x2hXRzLPMG9aznI",
5 "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbGc3YXBheTMwMDAwa2NvMnQ5NnUzaXVvIiwiaWF0IjoxNjgwOTIxNzcyLCJleHAiOjE2ODE1MjY1NzJ9.N50PYxSSD2IuAU7aW4AtHvV_sS0g0S-RtjJ6dI0H7mI",
6 "user": {
7 "id": "clg7apay30000kco2t96u3iuo",
8 "email": "test@example.com",
9 "firstname": "John",
10 "lastname": "Doe",
11 "role": "USER"
12 }
13 }
14 }
15}
3. 创建帖子
现在可以使用访问令牌来创建帖子。在 Playground 的 HTTP Headers 中添加 Authorization
头,值为 Bearer <accessToken>
。例如:
1{
2 "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbGc3YXBheTMwMDAwa2NvMnQ5NnUzaXVvIiwiaWF0IjoxNjgwOTIxNzcyLCJleHAiOjE2ODA5MjE4OTJ9.1e7Dk5wibQ4UCCcvzs_nyBOYkiB7x2hXRzLPMG9aznI"
3}
如果不设置访问令牌,会得到
Unauthorized
响应。
使用以下查询:
1mutation {
2 createPost(data: {
3 title: "My First Blog Post",
4 content: "This is my first blog post using GraphQL"
5 }) {
6 id
7 title
8 content
9 published
10 author {
11 id
12 email
13 firstname
14 lastname
15 }
16 }
17}
执行上述查询后,返回:
1{
2 "data": {
3 "createPost": {
4 "id": "clg7dg0r20003kco2tkcx98d4",
5 "title": "My First Blog Post",
6 "content": "This is my first blog post using GraphQL",
7 "published": true,
8 "author": {
9 "id": "clg7apay30000kco2t96u3iuo",
10 "email": "test@example.com",
11 "firstname": "John",
12 "lastname": "Doe"
13 }
14 }
15 }
16}
现在,我们已经创建了一个新帖子,并且可以在查询帖子列表中看到它。
4. 查询帖子列表
现在,我们想查询所有已发布的帖子的列表。在 GraphQL Schema 中,我们使用 publishedPosts
查询来获取已发布的帖子。publishedPosts
查询接受以下参数:
-
after
:分页游标,用于获取后续页的数据。 -
before
:分页游标,用于获取先前页的数据。 -
first
:要返回的第一页中的帖子数。 -
last
:要返回的最后一页中的帖子数。 -
orderBy
:用于排序帖子的字段和方向。 -
query
:用于搜索帖子的搜索字符串。 -
skip
:要跳过的帖子数。
使用以下查询:
1query {
2 publishedPosts(first: 10) {
3 edges {
4 node {
5 id
6 title
7 content
8 author {
9 id
10 email
11 firstname
12 lastname
13 }
14 }
15 }
16 }
17}
执行上述查询后,返回:
1{
2 "data": {
3 "publishedPosts": {
4 "edges": [
5 {
6 "node": {
7 "id": "clg78awi60001kc2b327lbr8x",
8 "title": "Join us for Prisma Day 2019 in Berlin",
9 "content": "https://www.prisma.io/day/",
10 "author": {
11 "id": "clg78awi60000kc2bu023og4d",
12 "email": "lisa@simpson.com",
13 "firstname": "Lisa",
14 "lastname": "Simpson"
15 }
16 }
17 },
18 {
19 "node": {
20 "id": "clg78awif0007kc2b1k85yo4r",
21 "title": "Subscribe to GraphQL Weekly for community news",
22 "content": "https://graphqlweekly.com/",
23 "author": {
24 "id": "clg78awif0006kc2b3wdoe1zu",
25 "email": "bart@simpson.com",
26 "firstname": "Bart",
27 "lastname": "Simpson"
28 }
29 }
30 },
31 {
32 "node": {
33 "id": "clg7dg0r20003kco2tkcx98d4",
34 "title": "My First Blog Post",
35 "content": "This is my first blog post using GraphQL",
36 "author": {
37 "id": "clg7apay30000kco2t96u3iuo",
38 "email": "test@example.com",
39 "firstname": "John",
40 "lastname": "Doe"
41 }
42 }
43 }
44 ]
45 }
46 }
47}
现在,我们已经成功查询到了所有已发布的帖子,并且可以在结果中看到我们创建的帖子。
5. 查询帖子详情
我们想查询我们刚刚创建的帖子的详细信息。在 GraphQL Schema 中,我们使用 post
查询来获取单个帖子。post
查询接受一个名为 postId
的字符串参数,并返回一个包含帖子信息的 Post
对象。
使用以下查询:
1query {
2 post(postId: "clg7dg0r20003kco2tkcx98d4") {
3 id
4 title
5 content
6 published
7 author {
8 id
9 email
10 firstname
11 lastname
12 }
13 }
14}
执行上述查询后,返回:
1{
2 "data": {
3 "post": {
4 "id": "clg7dg0r20003kco2tkcx98d4",
5 "title": "My First Blog Post",
6 "content": "This is my first blog post using GraphQL",
7 "published": true,
8 "author": {
9 "id": "clg7apay30000kco2t96u3iuo",
10 "email": "test@example.com",
11 "firstname": "John",
12 "lastname": "Doe"
13 }
14 }
15 }
16}
现在,我们已经成功查询到了我们刚刚创建的帖子的详细信息。
6. 更新帖子
现在我们想更新我们刚刚创建的帖子。在 GraphQL Schema 中,我们使用 updatePost
变更来更新帖子。updatePost
变更需要一个包含 postId
、title
和 content
的 UpdatePostInput
输入对象,并返回一个包含帖子信息的 Post
对象。
使用以下查询:
1mutation {
2 updatePost(data: {
3 postId: "clg7dg0r20003kco2tkcx98d4",
4 title: "My Updated Blog Post",
5 content: "This is my updated blog post using GraphQL"
6 }) {
7 id
8 title
9 content
10 published
11 author {
12 id
13 email
14 firstname
15 lastname
16 }
17 }
18}
执行上述查询后,返回:
1{
2 "error": {
3 "errors": [
4 {
5 "message": "Cannot query field \"updatePost\" on type \"Mutation\". Did you mean \"createPost\" or \"updateUser\"?",
6 "locations": [
7 {
8 "line": 2,
9 "column": 3
10 }
11 ],
12 "extensions": {
13 "code": "GRAPHQL_VALIDATION_FAILED",
14 "stacktrace": [
15 "GraphQLError: Cannot query field \"updatePost\" on type \"Mutation\". Did you mean \"createPost\" or \"updateUser\"?",
16 " at Object.Field (<SECRET>/node_modules/graphql/validation/rules/FieldsOnCorrectTypeRule.js:51:13)",
17 " at Object.enter (<SECRET>/node_modules/graphql/language/visitor.js:301:32)",
18 " at Object.enter (<SECRET>/node_modules/graphql/utilities/TypeInfo.js:391:27)",
19 " at visit (<SECRET>/node_modules/graphql/language/visitor.js:197:21)",
20 " at validate (<SECRET>/node_modules/graphql/validation/validate.js:91:24)",
21 " at processGraphQLRequest (<SECRET>/node_modules/@apollo/server/src/requestPipeline.ts:245:38)",
22 " at processTicksAndRejections (node:internal/process/task_queues:95:5)",
23 " at internalExecuteOperation (<SECRET>/node_modules/@apollo/server/src/ApolloServer.ts:1290:12)",
24 " at runHttpQuery (<SECRET>/node_modules/@apollo/server/src/runHttpQuery.ts:232:27)",
25 " at runPotentiallyBatchedHttpQuery (<SECRET>/node_modules/@apollo/server/src/httpBatching.ts:85:12)"
26 ]
27 }
28 }
29 ]
30 }
31}
这是因为还没有实现 updatePost
变更。下面我们来实现这个变更。
在文件 src/posts/posts.resolver.ts 添加如下代码:
1 @UseGuards(GqlAuthGuard)
2 @Mutation(() => Post)
3 async updatePost(
4 @UserEntity() user: User,
5 @Args('data') data: UpdatePostInput,
6 ) {
7 const post = await this.prisma.post.findUnique({
8 where: { id: data.postId },
9 });
10
11 if (!post || post.authorId !== user.id) {
12 throw new ForbiddenException('You can only update your own posts.');
13 }
14 return this.prisma.post.update({
15 where: { id: data.postId },
16 data: {
17 title: data.title,
18 content: data.content,
19 },
20 include: {
21 author: true,
22 },
23 });
24 }
创建如下 src/posts/dto/update-post.input.ts 文件:
1import { InputType, Field } from '@nestjs/graphql';
2import { IsNotEmpty } from 'class-validator';
3
4@InputType()
5export class UpdatePostInput {
6 @Field()
7 @IsNotEmpty()
8 postId: string;
9
10 @Field()
11 @IsNotEmpty()
12 title: string;
13
14 @Field()
15 @IsNotEmpty()
16 content: string;
17}
再次尝试,返回:
1{
2 "data": {
3 "updatePost": {
4 "id": "clg7dg0r20003kco2tkcx98d4",
5 "title": "My Updated Blog Post",
6 "content": "This is my updated blog post using GraphQL",
7 "published": true,
8 "author": {
9 "id": "clg7apay30000kco2t96u3iuo",
10 "email": "test@example.com",
11 "firstname": "John",
12 "lastname": "Doe"
13 }
14 }
15 }
16}
现在,我们已经成功更新了我们刚刚创建的帖子。
7. 删除帖子
现在我们想删除我们刚刚创建的帖子。在 GraphQL Schema 中,我们使用 deletePost
变更来删除帖子。deletePost
变更需要一个名为 postId
的字符串参数,并返回一个包含布尔值的 Boolean
对象,表示是否成功删除帖子。
我们首先实现它:
路径:src/posts/posts.resolver.ts
1 @UseGuards(GqlAuthGuard)
2 @Mutation(() => Boolean)
3 async deletePost(
4 @UserEntity() user: User,
5 @Args('postId') postId: string,
6 ) {
7 const post = await this.prisma.post.findUnique({
8 where: { id: postId },
9 });
10
11 if (!post || post.authorId !== user.id) {
12 throw new ForbiddenException('You can only delete your own posts.');
13 }
14
15 await this.prisma.post.delete({
16 where: { id: postId },
17 });
18
19 return true;
20 }
使用以下查询:
1mutation {
2 deletePost(postId: "clg7dg0r20003kco2tkcx98d4")
3}
执行上述查询后,返回:
1{
2 "data": {
3 "deletePost": true
4 }
5}
现在,我们已经成功删除了我们刚刚创建的帖子。
8. 注销
哈哈,根本没有注销功能。我们只需要删除 JWT 令牌即可。
本文使用 ChatGPT Plus - 3.5 Default 辅助编写。