TODO 1 · User.ordersTODO 1 · User.orders
Federation 链路的终点。三行代码,但每一行都有理由。The last step of the Federation path. Three lines of code, and every line has a reason.
这一页有什么On this page10
- 01 这一问在要求什么What this task asks for
- 02 这一问真正考什么What this task actually tests
- 03 先想再写Think before you write
- 04 分步实现Building it step by step
- 05 catch 里为什么要先判断 instanceof GraphQLErrorWhy the catch block must check instanceof GraphQLError first
- 06 完整答案The full answer
- 07 怎么验证How to check it
- 练习 · 动手做Practice
- 常见错误Common mistakes
- 迁移模式Transfer
- 独立写出 User.ordersWrite User.orders without help
- 解释 user.id 是从哪来的Explain where user.id comes from
- 说清为什么必须 ?? [] 兜底Explain why ?? [] is required as a fallback
- 写出符合 TODO 要求的错误处理和 correlation id 日志Write the error handling and correlation id logging the TODO asks for
这是 Federation 那部分唯一一个要你写的 entity 字段。它的正确性直接决定「Router 能不能把用户和订单缝起来」。两个测试查它。This is the only entity field the Federation part asks you to write. Whether it is correct decides whether the Router can join users to their orders. Two tests check it.
graphql-federation-practice/node-subgraph/src/resolvers/orderResolvers.jsUser.orders
提醒:源项目在磁盘上是做完的版本 —— 下面就是答案。想自己先写一遍的话,现在关上。Heads up: on disk this project is the finished version — what follows is the answer. Close this if you want to write it yourself first.
graphql-federation-practice/node-subgraph/src/resolvers/orderResolvers.js这一问在要求什么What this task asks for
TODO 原文:Implement orders resolver with proper error handling and correlation ID tracing。
三个要求:
- 实现 —— 返回这个用户的订单列表。
- proper error handling —— 数据源出错时要抛结构化的
GraphQLError, 不能让原始异常裸奔到客户端。 - correlation ID tracing —— 日志和错误里都要带上
correlationId。
schema 那边的约束:orders: [Order!]!,双重非空 —— 绝不能返回 null。
The TODO, word for word: Implement orders resolver with proper error handling and correlation ID tracing.
Three requirements:
- Implement it — return this user’s list of orders.
- proper error handling — when the data source fails, throw a structured
GraphQLError; never let a raw exception run loose to the client. - correlation ID tracing — carry
correlationIdin both the logs and the errors.
The constraint from the schema: orders: [Order!]!, non-null twice over — never return null.
这一问真正考什么What this task actually tests
- 你知不知道 parent 是什么。
user.id来自__resolveReference的返回值, 而它只返回了{ id }。 - 你会不会核对方法名。是
getOrdersByUserId, 不是getOrders、不是findByUser。 - 你读没读 schema 的可空性。
[Order!]!决定了必须?? []。 - 你会不会区分「业务错误」和「系统错误」。下面会讲为什么 catch 里要先判断
instanceof GraphQLError。
- Whether you know what parent is.
user.idcomes from what__resolveReferencereturned, and that was only{ id }. - Whether you check method names. It is
getOrdersByUserId, notgetOrdersand notfindByUser. - Whether you read the nullability in the schema.
[Order!]!is what forces the?? []. - Whether you tell a business error apart from a system error. The next section explains why a catch has to check
instanceof GraphQLErrorfirst.
先想再写Think before you write
分步实现Building it step by step
第一步:最小可用版本。先让那个红的测试变绿。
第二步:加兜底。getOrdersByUserId 用 filter 实现, 找不到会返回 [] 而不是 undefined, 所以这里的 ?? [] 严格说是防御性的。但还是要写 —— schema 是双重非空,你不该依赖数据源的实现细节。 真实项目里数据源换个实现(比如换成 HTTP 调用)就可能返回 undefined。
第三步:加 try/catch 和日志。TODO 明确要求这两样。
第四步:处理「已经是 GraphQLError」的情况。这一步是最容易漏的,下一段专门讲。
Step one: the smallest version that works. Get that red test to green.
Step two: add the fallback. getOrdersByUserId is written with filter, so a miss gives you [] rather than undefined, which makes the ?? [] here strictly defensive. Write it anyway — the schema is non-null twice over and you should not lean on the data source’s implementation details. In a real project, swap that data source for an HTTP call and undefined becomes possible.
Step three: add try/catch and logging. The TODO asks for both in plain words.
Step four: handle the case where the error is already a GraphQLError. The easiest step to miss, and the next section is all about it.
catch 里为什么要先判断 instanceof GraphQLErrorWhy the catch block must check instanceof GraphQLError first
这是本门考试贯穿三处的一个模式,值得单独理解。This pattern shows up in three places in this exam, so it is worth learning on its own.
catch 会接住 try 块里任何抛出的东西 —— 包括你自己故意抛的那个结构化错误。
想一个场景:Query.orders 里你先校验 「userId 不能为空」,不合法就抛GraphQLError(code: INVALID_INPUT)。 然后自己的 catch 接住它,重新包成code: SERVICE_ERROR。
结果:客户端收到的是「服务器内部错误」, 而实际上是「你的输入不合法」。这是完全错误的信号 —— 客户端会重试(以为是临时故障), 而重试永远不会成功。
所以模式是:catch 的第一行先问「这个错误已经是 我精心构造过的了吗?」是就原样往上抛。
这个模式在项目里的三处都需要:Query.order(抛 ORDER_NOT_FOUND)、Query.orders(抛 INVALID_INPUT)、Mutation.createOrder(抛 INVALID_INPUT)。最后那个就是埋雷 3 —— starter 代码漏了这一行,测试直接失败。
User.orders 里其实没有自己抛业务错误, 所以这一行是防御性的。但统一写法比 「哪里需要哪里写」更可靠,也更容易 review。
A catch catches anything thrown inside the try block — including the structured error you threw on purpose.
Picture this: inside Query.orders you first check that userId is not empty and throw GraphQLError(code: INVALID_INPUT) when it is. Then your own catch grabs that error and rewraps it as code: SERVICE_ERROR.
Result: the client is told “internal server error” when the truth is “your input was invalid”. That is the wrong signal entirely — the client retries, thinking it hit a temporary glitch, and the retry can never succeed.
So the pattern is: the first line of the catch asks “is this error one I carefully built myself?” and rethrows it untouched if it is.
Three places in this project need the pattern: Query.order (throws ORDER_NOT_FOUND), Query.orders (throws INVALID_INPUT) and Mutation.createOrder (throws INVALID_INPUT). That last one is planted bug 3 — the starter code is missing this line, and a test fails because of it.
User.orders never throws a business error of its own, so the line is purely defensive there. But writing it the same way everywhere is more reliable than “add it where it is needed”, and easier to review.
完整答案The full answer
审计时实测:这样写之后两个相关测试通过。Measured during the audit: with this code the two related tests pass.
注意 _ 那个位置是 args ——User.orders 在 schema 里没有参数,所以用不到。
Note that the _ slot is where args goes — User.orders takes no arguments in the schema, so nothing there is needed.
怎么验证How to check it
单元测试直接调 resolver 函数:
但单元测试绕过了 federation 链路。想验证「Router 那条路也通」,用 _entities 查询 —— 审计时实测输出如下,两个订单都拿到了:
The unit tests call the resolver function directly:
But a unit test bypasses the federation path. To check that the Router’s route works as well, use an _entities query — here is what the audit actually measured, with both orders coming back:
graphql-federation-practice/node-subgraph/__tests__/resolvers.test.js动手做Get your hands on it
填空只是过渡。真正掌握的标准,是在没有答案的时候从头写出来 —— 所以做完 L2 之后一定要往 L3、L4 走。Filling blanks is a stepping stone. The real bar is writing it from nothing, so once L2 is comfortable, push on to L3 and L4.
四个空。第 2 个是数据源上的真实方法名, 第 4 个是那行最容易漏的防御。
Four blanks. The second is the method name that really exists on the data source; the fourth is the guard people most often forget.
按 TODO 的三条要求写完整实现。检查器会核对方法名、兜底、 错误处理和 correlation id。
Write the full implementation against the three requirements in the TODO. The checker looks at the method name, the fallback, the error handling and the correlation id.
- 用 user.id 去取该用户的订单Use user.id to fetch that user's orders
- 调用数据源上真实存在的方法Call a method that really exists on the data source
- 绝不返回 null 或 undefined(schema 是 [Order!]!)Never return null or undefined (the schema says [Order!]!)
- 用 try/catch 包住,失败时抛 GraphQLErrorWrap it in try/catch and throw a GraphQLError on failure
- 错误的 extensions 里带 code 和 correlationIdPut code and correlationId in the error's extensions
- 已经是 GraphQLError 的错误要原样往上抛,不要重新包装Rethrow an error that is already a GraphQLError untouched, without rewrapping it
- 日志里带上 correlationIdInclude correlationId in the log line
看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.
初学者常见的几种写法错误Mistakes beginners actually make
下面每一段都是「能编译、但结果不对」或者「一跑就炸」的真实写法。先自己看出问题在哪,再看解释。Every snippet below either compiles and gives the wrong answer, or blows up on the first run. Spot the problem yourself before reading the explanation.
orderLoader 是按 order id取单个订单的,不是按 user id 取列表。 传 "123" 进去会去找id === "123" 的 order —— 找不到,返回 undefined。loader 不是万能的,要看它 batch 函数里调的是什么。 这里该用
orderDataSource.getOrdersByUserId。orderLoader fetches one order by order id. It does not fetch a list by user id. Passing "123" makes it look for the order whose id === "123" — there is none, so it returns undefined.A loader only does what its batch function does. Here you need
orderDataSource.getOrdersByUserId.filter 实现, 找不到返回 [],所以恰好不会出问题。但你不该依赖这个实现细节 —— schema 是
[Order!]!, 而数据源随时可能换成 HTTP 调用(那时找不到可能返回undefined 或 null)。按 schema 的契约写,不按数据源的当前行为写。The data source in this project uses filter, so a miss returns[]. By luck, nothing breaks.But you should not rely on that detail. The schema says
[Order!]!, and the data source could become an HTTP call at any time — then a miss might return undefined or null. Write to the contract in the schema, not to how the data source behaves today.换一道题也能用Works on other problems too
考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.
- user.id 来自 __resolveReference 的返回值,parent 上只有这一个属性。user.id comes from what __resolveReference returned; it is the only property on parent.
- 方法名是 getOrdersByUserId —— 去数据源核对,别凭直觉。The method name is getOrdersByUserId. Check it against the data source instead of guessing.
- [Order!]! 决定必须 ?? [] 兜底,按 schema 契约写而不是按数据源当前行为。[Order!]! means you must fall back with ?? []. Write to the schema contract, not to how the data source behaves today.
- catch 第一行先放行已结构化的 GraphQLError,否则会把业务错误降级成系统错误。The first line of catch must let an existing GraphQLError through, otherwise a business error is turned into a system error.
- 单元测试直接调 resolver;想验 federation 链路要用 _entities 查询。Unit tests call the resolver directly. To check the Federation path, use the _entities query.