14. 变异
10m

概述

我们已经看到了特定且表达能力强的查询的强大功能,这些查询可以让我们一次性检索到我们想要的确切数据。但是 中只是等式的一部分。

当我们想要实际更改、插入或删除数据时,我们需要使用新的工具:

在本课中,我们将

  • 探索 语法并编写一个 来创建一个新的房源
  • 了解 input 类型
  • 了解 响应最佳实践

Airlock 中的变异

继续我们的 Airlock API 中的下一个功能:创建新的房源。

A mockup showing a view that lets us create a new listing by specifying its pertinent properties

我们的 REST API 配备了一个端点,允许我们创建新的房源: POST /listings

此方法接受我们请求主体上的 listing 属性。它应该包含与房源相关的所有属性,包括其便利设施。房源在数据库中创建后,此端点会将新创建的房源返回给我们。

好的,现在如何在 中启用此功能?

设计变异

就像 Query 类型一样,Mutation 类型是我们的 Schema 的入口点。它遵循与我们到目前为止一直在使用的 相同的语法。

我们使用 type 关键字声明 Mutation 类型,然后是名称 Mutation。在大括号内,我们有我们的入口点,即用于修改数据的

让我们打开 schema.graphql 并添加一个新的 Mutation 类型。

schema.graphql
type Mutation {
}

对于 Mutation,我们建议从一个描述更新 的特定操作的动词开始(例如 adddeletecreate),然后是 操作的数据。

我们将探索如何 创建 一个新的房源,因此我们将调用此 createListing

type Mutation {
"Creates a new listing"
createListing: #TODO
}

对于 createListing 的返回类型 ,我们 可以 返回 Listing 类型;它是我们希望 操作的 。但是,我们建议遵循 响应的统一 Response 类型。让我们看看它在一个新的类型中是什么样子。

Mutation 响应类型

的返回类型 Mutation 通常以 的名称开头,后跟 PayloadResponse。不要忘记类型名称应该用 PascalCase 格式化!

按照惯例,我们将我们的类型命名为 CreateListingResponse

type CreateListingResponse {
}

我们应该返回我们正在修改的 (在本例中为 Listing),以便客户端可以访问更新后的对象。

type CreateListingResponse {
listing: Listing
}

注意:虽然我们的 操作单个 Listing 对象,但 也可以一次更改和返回多个对象。

请注意,listing 返回一个 Listing 类型,该类型 可以null,因为我们的 可能失败。

为了解决可能发生的任何部分错误并向客户端返回有用的信息,我们可以包含一些额外的 在响应类型中。

  • code:一个 Int,它指的是响应的状态,类似于 HTTP 状态代码。

  • success:一个 Boolean 标志,指示 负责的所有更新是否成功。

  • message:一个 String,用于在客户端显示有关 结果的信息。如果变异仅部分成功并且通用错误消息无法说明全部情况,这特别有用。

让我们也为这些 添加注释,以便使我们的 API 文档更有用。

type CreateListingResponse {
"Similar to HTTP status code, represents the status of the mutation"
code: Int!
"Indicates whether the mutation was successful"
success: Boolean!
"Human-readable message for the UI"
message: String!
"The newly created listing"
listing: Listing
}

最后,我们可以将我们 的返回类型设置为这种新的 CreateListingResponse 类型,并使其不可为空。以下是 createListing 应该是什么样子:

type Mutation {
"Creates a new listing"
createListing: CreateListingResponse!
}

Mutation 输入

为了创建一个新的房源,我们的 需要接收一些输入。

让我们考虑一下这个 createListing 会期望什么样的输入。我们需要房源本身的所有详细信息,例如 titlecostPerNightnumOfBedsamenities 等等。

我们之前在 Query.listing 中使用过一个 : 我们传递了一个名为 id 的单个

type Query {
listing(id: ID!): Listing
}

但是 createListing 接受多个 。我们可以解决这个问题的一种方法是将每个参数逐一添加到我们的 createListing 中。但是这种方法可能会变得笨拙且难以理解。相反,使用 输入类型作为 ,对于 来说是一个好习惯。

探索 input 类型

input 类型中 是一个特殊的 ,它将一组 组合在一起,然后可以作为另一个 的参数使用。使用 input 类型有助于我们对 进行分组和理解,特别是对于

要定义输入类型,请使用 input 关键字,后跟名称和花括号 ({})。在花括号内,我们像往常一样列出 和类型。请注意,输入类型的字段只能是 、枚举或其他输入类型。

input CreateListingInput {
}

接下来,我们将添加属性。为了完善我们将要创建的房源,让我们发送所有相关详细信息。我们需要:titledescriptionnumOfBedscostPerNightclosedForBookings

input CreateListingInput {
"The listing's title"
title: String!
"The listing's description"
description: String!
"The number of beds available"
numOfBeds: Int!
"The cost per night"
costPerNight: Float!
"Indicates whether listing is closed for bookings (on hiatus)"
closedForBookings: Boolean
}

我们还需要指定我们的房源提供的 amenities。请注意,我们在创建新房源的模型中,便利设施选项是预定义的;我们只能从现有选项中选择。在幕后,这些便利设施中的每一个都有一个唯一的 ID 标识符。出于这个原因,我们将添加一个 amenityIds CreateListingInput 中,类型为 [ID!]!

input CreateListingInput {
"The listing's title"
title: String!
"The listing's description"
description: String!
"The number of beds available"
numOfBeds: Int!
"The cost per night"
costPerNight: Float!
"Indicates whether listing is closed for bookings (on hiatus)"
closedForBookings: Boolean
"The Listing's amenities"
amenities: [ID!]!
}

注意: 您可以了解有关 input 类型的更多信息,以及其他 类型和功能,请访问 Side Quest: Intermediate Schema Design

使用 input

要在模式中使用 input 类型,我们可以将其设置为 的类型。例如,我们可以更新 createListing 以使用 CreateListingInput 类型,如下所示:

type Mutation {
"Creates a new listing"
createListing(input: CreateListingInput!): CreateListingResponse!
}

请注意,CreateListingInput 是非空的。要运行此 ,我们实际上需要一些输入!

构建 ListingAPI 方法

与其他请求一样,我们将构建一个方法来管理此 REST API 调用以创建新的房源。

回到 datasources/listing-api.ts 中,让我们为此 添加一个新方法。

datasources/listing-api.ts
createListing() {
// TODO
}

这次,因为端点使用的是 POST 方法,我们将使用 this.post 来自 RESTDataSource 的辅助方法,而不是 this.get

接下来,我们将端点传递给此方法:"listings"

listing-api.ts
createListing() {
return this.post("listings");
}

我们的方法将接收一个新的 listing ,其类型为 CreateListingInput。我们可以从 types.ts 导入此类型。

import { Amenity, Listing, CreateListingInput } from "../types";

然后,让我们更新 createListing 方法以接收新的房源。

createListing(listing: CreateListingInput) {
// method logic
}

我们可以将第二个 传递给 this.post 方法以指定请求主体。现在让我们添加这些花括号,并指定一个名为 body 的属性,它是一个另一个对象。

listing-api.ts
createListing(listing: CreateListingInput) {
return this.post("listings", {
body: {}
});
}

在这个对象内,我们将传递我们的 listing

return this.post("listings", {
body: { listing },
});

注意: 我们使用简写属性表示法来表示隐式键,因为我们用匹配的键 (listing) 命名了常量。

现在我们可以处理方法的返回类型。当房源成功创建时,此端点将返回一个 Promise,它解析为新创建的房源对象。因此,我们可以使用 Promise<Listing> 作为方法的返回类型。

createListing(listing: CreateListingInput): Promise<Listing> {
return this.post("listings", {
body: {
listing
}
});
}

在解析器中连接各个部分

跳回到 resolvers.ts 文件。我们将向 resolvers 对象添加一个新条目,名为 Mutation

resolvers.ts
Mutation: {
// TODO
},

让我们添加我们的 createListing

resolvers.ts
Mutation: {
createListing: (parent, args, contextValue, info) => {
// TODO
}
},

我们不需要这里的 parentinfo 参数,因此我们将用 _ 替换 parent 并完全删除 info

resolvers.ts
createListing: (_, args, contextValue) => {
// TODO
},

我们从 Mutation.createListing 模式 中知道,此 将接收一个名为 input。我们将为此属性解构 args。同时,我们也将为 contextValue 解构 dataSources 属性。

resolvers.ts
createListing: (_, { input }, { dataSources }) => {
// TODO
},

现在,我们将从 listingAPI 中调用 createListing 方法,并将我们的 input 传递给它。

resolvers.ts
createListing: (_, { input }, { dataSources }) => {
dataSources.listingAPI.createListing(input);
},

我们还没有从此方法返回任何内容,因此我们可能会看到一些错误。在我们的模式中,我们指定 Mutation.createListing 应该返回一个 CreateListingResponse 类型。

schema.graphql
type CreateListingResponse {
"Similar to HTTP status code, represents the status of the mutation"
code: Int!
"Indicates whether the mutation was successful"
success: Boolean!
"Human-readable message for the UI"
message: String!
"The newly created listing"
listing: Listing
}

这意味着我们从 返回的对象需要匹配此形状。让我们先设置一下在我们的 成功的情况下将要返回的对象。

resolvers.ts
createListing: (_, { input }, { dataSources }) => {
dataSources.listingAPI.createListing(input);
// everything succeeds with the mutation
return {
code: 200,
success: true,
message: "Listing successfully created!",
listing: null, // We don't have this value yet
}
},

检查响应

现在,让我们看一下 API 调用的响应。我们需要使我们的整个 async,以便 await 调用 dataSources.listingAPI.createListing() 的结果。

resolvers.ts
createListing: async (_, { input }, { dataSources }) => {
const response = await dataSources.listingAPI.createListing(input);
console.log(response);
// everything succeeds with the mutation
return {
code: 200,
success: true,
message: "Listing successfully created!",
listing: null, // We don't have this value yet
}
},

沙盒 中,让我们一起创建一个

mutation CreateListing($input: CreateListingInput!) {
createListing(input: $input) {
code
success
message
}
}

并将以下内容添加到 变量 面板中。

{
"input": {
"title": "Mars' top destination",
"description": "A really cool place to stay",
"costPerNight": 44.0,
"amenities": ["am-1", "am-2"],
"numOfBeds": 2
}
}

当我们运行 时,我们将看到此 确实按预期工作!我们可以清楚地看到我们在 codesuccessmessage 中为我们的成功路径设置的值。

在运行服务器的终端中,我们将看到 API 响应的日志记录:它就是我们新创建的清单!

{
id: '22ff64fe-41d4-4754-acbb-22ac89e27620',
title: "Mars' top destination",
description: 'A really cool place to stay',
costPerNight: 44,
hostId: null,
locationType: null,
numOfBeds: 2,
photoThumbnail: null,
isFeatured: null,
latitude: null,
longitude: null,
closedForBookings: null,
amenities: [
{
id: 'am-1',
category: 'Accommodation Details',
name: 'Interdimensional wifi'
},
{ id: 'am-2', category: 'Accommodation Details', name: 'Towel' }
]
}

这意味着我们可以将 response 的整个值作为 listing 属性返回到我们返回的对象中。

return {
code: 200,
success: true,
message: "Listing successfully created!",
listing: response,
};

现在让我们构建失败路径——当我们的 没有 按预期进行时。

处理失败路径

返回到我们的 Mutation.createListing 中,我们将考虑错误状态。

我们将从将我们到目前为止编写的所有内容都包装在 try/catch 块中开始。

resolvers.ts
try {
const response = await dataSources.listingAPI.createListing(input);
return {
code: 200,
success: true,
message: "Listing successfully created!",
listing: response,
};
} catch (err) {}

catch 块中,我们将返回一个包含一些不同属性的对象。

resolvers.ts
try {
// try block body
} catch (err) {
return {
code: 500,
success: false,
message: `Something went wrong: ${err.extensions.response.body}`,
listing: null,
};
}

以下是您的整个 函数应该看起来的样子。

为了测试我们的失败路径,让我们尝试使用一个不存在的便利设施 ID 创建一个清单。

回到 沙盒 中,让我们更新我们的 以包含更多有关我们将尝试创建的清单的信息。

注定会失败的变异
mutation CreateListing($input: CreateListingInput!) {
createListing(input: $input) {
code
success
message
listing {
title
description
costPerNight
amenities {
name
category
}
}
}
}

然后让我们更新我们的 变量 面板;这次,我们将更新清单的 "numOfBeds" 为零。

{
"input": {
"title": "Mars' top destination",
"description": "A really cool place to stay",
"costPerNight": 44.0,
"amenities": ["am-1", "am-2"],
"numOfBeds": 0
}
}

当我们运行 时,我们将看到我们预期的值: 失败,我们的错误消息也清晰可见。 Something went wrong: 503: Service Unavailable!

在我们的失败路径得到验证后,让我们还原对 变量 面板的更改——我们的清单至少需要有一张床可用!

{
"input": {
"title": "Mars' top destination",
"description": "A really cool place to stay",
"costPerNight": 44.0,
"amenities": ["am-1", "am-2"],
"numOfBeds": 2
}
}

现在让我们再次运行该 并看看 所有 的清单数据是否都已考虑在内。

假设会成功的变异
mutation CreateListing($input: CreateListingInput!) {
createListing(input: $input) {
code
success
message
listing {
title
description
costPerNight
amenities {
name
category
}
}
}
}

运行 并... 数据!

太棒了,您做到了!

练习

根据上面推荐的约定,以下哪些是变异的有效名称?
在变异响应类型 (CreateListingResponse) 中,为什么修改后的对象的返回类型 (Listing) 是可空的?
我们如何在模式中使用 input 类型?
为变异创建输入类型时,通常使用什么命名约定?

主要要点

  • 是写入 用于修改数据。
  • 命名 通常以描述动作的动词开头,例如 "add"、"delete" 或 "create"。
  • 创建一致的 响应类型是一个常见约定。
  • 中通常需要多个 来执行操作。为了将参数组合在一起,我们使用 GraphQL 输入类型来确保清晰度和可维护性。

旅程的终点

任务!

您创建了一个 API!您有一个可以正常工作的 ,里面塞满了星际清单,它使用 REST API 作为 。您已经编写了查询和 ,并在此过程中学习了一些常见的 GraphQL 约定。您还探索了如何在模式设计中使用 GraphQL 和输入类型。花点时间庆祝一下;您学到了很多东西!

但旅程并没有结束!当您准备好让您的 API 更进一步 时,请跳到本系列的下一门课程:使用 TypeScript 和 Apollo Server 的联邦

感谢您参加本课程;希望在下一门课程中见到您!

上一页

分享您关于本课的疑问和评论

本课程目前处于

测试版
.您的反馈有助于我们改进!如果您卡住或感到困惑,请告诉我们,我们会帮助您。所有评论都是公开的,必须遵守 Apollo 行为准则。请注意,已解决或已处理的评论可能会被删除。

您需要一个 GitHub 帐户才能在下方发布。还没有吗? 请改在 Odyssey 论坛中发布。