概述
尽管我们已经定义了Listing
实体 在我们的 reviews
子图 中,并为其贡献了 字段,但我们还没有告诉它如何解析这些字段。
在本课中,我们将
- 创建一个引用 解析器
- 用数据填充
reviews
和overallRating
字段。
引用解析器
当我们 查询 特定的列表及其评论数据时,我们需要一种方法来告诉 reviews
子图 哪个 列表是我们正在谈论的。然后 reviews
子图 将理解要提供哪些数据!
为了做到这一点,路由器 传递了我们在上一课中谈到的 实体 表示: reviews
子图 为了拼凑出它正在获取数据的列表,所需的最小信息。
{"__typename": "Listing","id": "listing-3"}
但在 reviews
子图 中,这个 实体 表示实际上 去 了哪里呢?
这正是我们缺少的:引用 解析器。引用解析器是一种特殊的方法,可以接收 实体 表示,来自 路由器,创建一个新的 Listing
实例,并将其返回。
让我们看看代码中的样子,并逐步完成这个过程。
添加 __resolveReference
回到 reviews
子图 中,我们需要告诉我们的服务器当它接收到特定列表的 实体 表示时该怎么办。我们将通过在 Listing
的 解析器 地图中的新条目下定义一个名为 __resolveReference
的特殊函数来实现。
打开 reviews/src/resolvers.ts
。让我们添加 Listing
键和引用 解析器,如下所示。
// Query, Mutation entries aboveListing: {__resolveReference: () => {},}
您将在我们定义的这个新函数上看到一个错误。这是因为 __resolveReference
不是我们如在 reviews
模式文件中所述的 Listing
类型上的已知属性。
Object literal may only specify known properties, and '__resolveReference'does not exist in type 'ListingResolvers<any, Listing>'.
我们可以通过在我们的 codegen.ts
中添加一个额外的属性来解决这个问题。在 config
对象中,我们可以添加一个名为 federation
的新键,并将它的值设置为 true
。这使我们的 codegen 过程能够考虑我们 解析器 的一些特定于联合的要求:例如解析 实体 表示,从 路由器 接收!
config: {contextType: "./context#DataSourceContext",federation: true},
您可能在您的 resolvers.ts
文件中看不到任何变化;但这次,TypeScript 对我们的函数还没有返回任何东西感到很不安。接下来让我们处理这个问题。
我们将接收 实体 表示作为我们 __resolveReference
函数中的一个参数,名为 representation
。然后,我们将把它记录下来,并返回它。
__resolveReference: (representation) => {console.log(representation)return representation;},
即使有了这个变化,我们的函数仍然有另一个错误。当我们悬停在 __resolveReference
上时,我们会看到什么。
Property 'reviews' is missing in type '{ __typename: "Listing"; }& GraphQLRecursivePick<Listing, { id: true; }>' but required in type 'Listing'.
这个很长的错误告诉我们一个很清楚的事情: __resolveReference
函数接收的 "listing"(来自 路由器 的 实体 表示)缺少我们在模式文件中所说的它应该具有的某些基本属性。从本质上讲,我们的 codegen 过程 期望 我们在 resolvers.ts
文件中使用的所有 Listing
对象具有以下所有 字段。
type Listing @key(fields: "id") {id: ID!"The submitted reviews for this listing"reviews: [Review!]!"The overall calculated rating for a listing"overallRating: Float}
然而,实体 表示仅包含两个属性: __typename
和 id
。
我们需要修复 codegen 过程对 Listing
的理解,当它作为 实体 表示进入 __resolveReference
函数时。为此,我们将使用 模型。
介绍模型
在 GraphQL 模式 中,定义了 对象类型 之间的关系以及我们如何从一个对象到另一个对象。通过从一个对象到另一个对象,我们可以构建详细的查询,这些查询在单个客户端请求中获取我们所需的一切。
这是 解析器 函数的任务,使这种魔法成为可能:它们需要自由地接收可能与我们模式中的类型不匹配的数据,并执行将数据返回到我们 期望 的类型的逻辑。
为了保持类型安全,我们需要澄清我们的数据类型在解析器接收到的数据(来自数据源,或者来自路由器,就像我们的实体表示)和解析器返回给客户端的数据之间的区别。
在我们的例子中,我们的__resolveReference
解析器认为它将接收一个看起来像我们Listing
GraphQL类型的对象;所以我们需要使用一个模型来重新定义它的预期。
添加一个ListingEntityRepresentation
模型
我们将创建一个新的模型,让我们更准确地描述列表实体表示所采用的形状。然后我们将该模型集成到我们的代码生成流程中,并解决我们的类型错误。
在reviews/src
目录中,创建一个名为models.ts
的文件。
📦 src┣ 📂 datasources┣ 📂 sequelize┣ 📄 context.ts┣ 📄 graphql.d.ts┣ 📄 index.ts┣ 📄 models.ts┣ 📄 resolvers.ts┣ 📄 schema.graphql┗ 📄 types.ts
在里面,我们将定义一个名为ListingEntityRepresentation
的基本模型。我们将赋予它我们关心的单个属性:id
。
export type ListingEntityRepresentation = {id: string;};
接下来,我们将这个模型添加到我们的代码生成流程中,在codegen.ts
中。
在config
键下,我们将添加另一个名为mappers
的属性。在这里,我们可以指定每个模型的路径,这些模型将用作特定GraphQL类型的映射器。这意味着,当代码生成器看到我们在我们的解析器中使用特定 GraphQL 对象时,它将使用我们提供的模型作为该对象应该具有的属性的参考。
config: {contextType: "./context#DataSourceContext",federation: true,mappers: {Listing: "./models#ListingEntityRepresentation"}},
让我们停止运行的reviews
服务器并重新启动它,以确保应用了新的代码生成设置。这应该可以解决我们的错误!
现在让我们试一试。让我们回到 Sandbox 并再次尝试我们的查询。
query GetListingAndReviews {listing(id: "listing-1") {titledescriptionnumOfBedsamenities {namecategory}overallRatingreviews {idtext}}}
当我们运行查询时,我们仍然不会得到任何与评论相关的数据;但是,当我们检查我们的reviews
子图的终端时,我们将看到我们的实体表示已经到达并被打印出来了!
{__typename=Listing, id=listing-1}
我们的reviews
子图正在成功地从路由器接收列表表示;这意味着我们知道哪些列表要提供数据。现在,我们只需要定义解析器,以用于字段,即reviews
和overallRating
,这是reviews
子图负责的。
添加Listing.reviews
让我们首先解决reviews
方法。
返回到reviews/src/resolvers.ts
。让我们清理掉__resolveReference
中的console.log
语句。然后,在__resolveReference
的下方,我们将为字段添加一个新的解析器,用于Listing.reviews
。
Listing: {__resolveReference: (representation) => {return representation;},reviews: () => {}}
此方法应该使用来自实体表示的id
来查找并返回数据库中所有相关的评论。由于此解析器解析实体类型上的字段,我们知道链中的前一个解析器是__resolveReference
解析器,我们刚刚定义了它。这意味着我们可以使用parent
(每个解析器接收的第一个位置参数)访问它的返回值(Listing
实例)。
让我们为parent
解构id
属性。
reviews: ({ id }) => {// TODO};
我们的解析器已经可以访问服务器的dataSources
上的一个属性,名为reviewsDb
,它是一个类,提供了一些对底层评论数据库进行操作的方法。让我们为解析器的第三个位置参数,contextValue
解构dataSources
属性。
reviews: ({ id }, _, { dataSources }) => {// TODO};
有了我们的列表id
,我们可以查询数据库中所有与该列表关联的评论。该reviewsDb
类实例提供了一个名为getReviewsByListing
的方法,用于根据特定列表 ID 查询评论。让我们调用此方法,并将从我们的parent
参数获得的id
属性作为参数传递。
reviews: ({ id }, _, { dataSources }) => {return dataSources.reviewsDb.getReviewsByListing(id);};
The overallRating
method
进入overallRating
解析器!
自己尝试一下。(提示:查看datasources/reviews.ts
文件以获取有用的方法!)
准备好了之后,将你的代码与我们完成的解析器进行比较。
overallRating: ({ id }, _, { dataSources }) => {return dataSources.reviewsDb.getOverallRatingForListing(id);},
运行我们的梦想查询
在rover dev
进程仍在运行的情况下,让我们在https://127.0.0.1:4002
尝试我们的查询。
query GetListingAndReviews {listing(id: "listing-1") {titledescriptionnumOfBedsamenities {namecategory}overallRatingreviews {idtext}}}
提交查询,然后...我们获得了数据!🎉 我们已经将评论与列表关联,并让我们的梦想查询变为现实!
查看查询计划
让我们查看查询计划,看看这些数据是如何整合在一起的。我们将看到,首先,路由器将从listings
获取数据,然后使用这些数据来构建对reviews
的请求。最后一步是将来自这两个子图的响应扁平化为Listing
类型的单个实例,以用于我们查询的listing
字段!
仍然看到reviews: null
?尝试重启你的reviews
服务器!在端口4002
上运行我们的路由器的rover dev
进程会自动刷新。
练习
关键要点
- 任何贡献 字段 到 实体 的 子图 需要为该实体定义一个引用 解析器 方法。每当 路由器 需要从另一个子图访问实体的字段时,就会调用此方法。
- 一个 实体 表示是一个对象, 路由器 使用它来表示实体的特定实例。它包含实体的类型及其关键 字段。
-
__resolveReference
函数从 路由器 接收一个特定 GraphQL 类型的 实体 表示。这个表示包含 子图 需要了解如何填充它所贡献的 字段 的所有数据。
接下来
太棒了!我们的 listings
和 reviews
服务现在正在协同工作,使用相同的 Listing
实体。每个 子图 贡献自己的 字段,而我们的本地 rover dev
进程启动的 路由器 会将响应打包给我们。重点是 本地 这个词;为了让我们的更改真正“生效”(至少在 教程 的意义上),我们需要告诉 GraphOS 关于这些更改!
在下节课中,我们将看看 如何 使用 模式检查 和 发布 安全可靠地实现这些更改。
分享您对本课的疑问和评论
本课程目前处于
您需要一个 GitHub 帐户才能在下方发布评论。没有帐户? 请在我们的 Odyssey 论坛中发布评论。