Orders subgraph:四个 resolver + DataLoaderOrders subgraph: four resolvers and DataLoader
题面The problem
先把要求读完,再动手。Read every requirement before you start.
空目录开始,搭出一个 Apollo Federation subgraph, 实现四个 resolver 加一个 mutation,让 10 个测试全过, 并且 _service 和 _entities 都能正常工作。不要打开源项目的 orderResolvers.js。
Starting from an empty directory, build an Apollo Federation subgraph. Write four resolvers plus one mutation, get all 10 tests passing, and make both _service and _entities work. Do not open orderResolvers.js from the source project.
- 用 @apollo/server + @apollo/subgraph 起一个 subgraph,监听 4000Start a subgraph with @apollo/server + @apollo/subgraph, listening on 4000
- schema 从 .graphql 文件读入,用 buildSubgraphSchema 组装Read the schema from a .graphql file and assemble it with buildSubgraphSchema
- 每个请求构造 context:三个数据源、两个 DataLoader、一个 correlationIdBuild the context per request: three data sources, two DataLoaders, one correlationId
- correlationId 优先取请求头 x-correlation-id,没有就生成Take correlationId from the x-correlation-id request header, and generate one when it is absent
- 实现 User.__resolveReference:把 representation 变成本地对象Write User.__resolveReference: turn the representation into a local object
- 实现 User.orders:按 user.id 取订单,[Order!]! 所以绝不返回 nullWrite User.orders: read orders by user.id; the type is [Order!]!, so never return null
- 实现 Order.shippingInfo:必须走 DataLoader 防 N+1;可空,找不到返回 nullWrite Order.shippingInfo: it must go through the DataLoader to prevent N+1; it is nullable, so return null when nothing is found
- 实现 Query.order:走 DataLoader;找不到抛带 ORDER_NOT_FOUND 的 GraphQLErrorWrite Query.order: go through the DataLoader; when nothing is found, throw a GraphQLError carrying ORDER_NOT_FOUND
- 实现 Query.orders:校验 userId;[Order!]! 所以兜底 []Write Query.orders: validate userId; the type is [Order!]!, so fall back to []
- 实现 Mutation.createOrder:先查商品价格补全 items,再创建;校验失败抛 INVALID_INPUTWrite Mutation.createOrder: look up product prices to complete items first, then create; throw INVALID_INPUT when validation fails
- 两个 DataLoader 的 batch 函数:返回数组的长度与顺序必须和 keys 一一对应The batch function of both DataLoaders: the array it returns must match keys in both length and order
- 所有 resolver 都用 try/catch,catch 第一行放行已有的 GraphQLErrorWrap every resolver in try/catch, and let an existing GraphQLError pass through on the first line of catch
- 所有日志和错误 extensions 里带上 correlationIdCarry correlationId in every log line and in the extensions of every error
预计 90 分钟。这个数字是照「读完题就开始写、不查资料」估的 —— 第一次超时很正常,第二次要压进去。Budget 90 minutes. Overrunning on the first pass is normal; the second pass should fit.
工作区Workspace
这道题要真起一个 Apollo subgraph 服务,跑 npm test 里的十个断言,还要用 _service / _entities 两个联邦入口验证。浏览器沙箱起不了服务端进程 —— 所以这里只给命令和期望输出。This one needs a real Apollo subgraph process, the ten assertions in npm test, and verification through the _service and _entities federation entry points. A browser sandbox cannot start a server process, so you get the commands and the expected output instead.
跑完自己对一遍期望输出,然后在下面打勾。这里不给假编辑器 —— 装个能跑的样子只会让你以为练过了。Compare the output yourself, then tick it off below. No fake editor here.
展开讲解Walkthrough
下面是《先读题:四个 TODO、三处埋雷、十个测试》那一节的原文 —— 和课程里是同一份内容,不是另写的摘要。卡住了再展开。Below is the actual text of the lesson “先读题:四个 TODO、三处埋雷、十个测试” — the same content as in the course, not a rewritten summary. Expand it when you stall.
展开《先读题:四个 TODO、三处埋雷、十个测试》(6 段 · 约 15 分钟)Expand “先读题:四个 TODO、三处埋雷、十个测试” (6 sections · ~15 min)
完整那一节(含练习、常见错误、迁移模式)在The full lesson (with exercises, common mistakes and transfer patterns) is at 先读题:四个 TODO、三处埋雷、十个测试。
题面原文The task text, as given
README 里 Task 1 的部分,一个字没改:
注意最后那句 integration issues —— 这是在暗示「starter 代码里有本来就坏掉的地方」。 它没说有几个、在哪。
The Task 1 section of the README, not a word changed:
Look at that last sentence, integration issues — it is hinting that parts of the starter code are broken to begin with. It does not say how many, or where.
graphql-federation-practice/README.md四个 TODO:README 只列了三个,代码里有四个Four TODOs: the README lists three, the code has four
这是第一个需要自己发现的地方。This is the first thing you have to notice on your own.
README 列了三条(User.orders、Order.shippingInfo、Query.orders), 但打开代码会发现还有一个:Query.order(单个订单)也是 TODO。
| 位置 | TODO 原文里的关键词 | README 提到了吗 | 有测试吗 |
|---|---|---|---|
User.orders | proper error handling + correlation ID tracing | ✅ | ✅ 2 条 |
Order.shippingInfo | using DataLoader to prevent N+1 queries | ✅ | ✅ 2 条 |
Query.order | using DataLoader with structured error handling | ❌ 没提 | ❌ 没有 |
Query.orders | error handling + correlation ID logging | ✅ | ✅ 2 条 |
Query.order 既没在 README 里被提到, 也没有测试。但代码里的 TODO 明确要求实现它。不实现它不会有任何测试变红 —— 但人工 review 会看到一个没做的 TODO。照代码里的 TODO 做,别只照 README。
The README lists three (User.orders, Order.shippingInfo, Query.orders), but open the code and there is a fourth one: Query.order (a single order) is a TODO too.
| Where | Key words in the TODO itself | Named in the README? | Any tests? |
|---|---|---|---|
User.orders | proper error handling + correlation ID tracing | ✅ | ✅ 2 of them |
Order.shippingInfo | using DataLoader to prevent N+1 queries | ✅ | ✅ 2 of them |
Query.order | using DataLoader with structured error handling | ❌ never mentioned | ❌ none |
Query.orders | error handling + correlation ID logging | ✅ | ✅ 2 of them |
Query.order is neither named in the README nor covered by a test. But the TODO in the code asks for it in plain words. Skipping it turns no test red — a human reviewer, though, sees an unfinished TODO. Work from the TODOs in the code, not only from the README.
graphql-federation-practice/node-subgraph/src/resolvers/orderResolvers.js写代码前先抄这张表Copy this table before you write code
三个埋雷里有两个就是「名字对不上」。抄一遍表,两个都能避掉。Two of the three planted bugs are just names that do not match. Copy the table once and you avoid both.
context 的结构(来自 index.js):
数据源的方法(来自dataSources/orderDataSource.js):
这张表值得在开始写之前真的抄一遍。 审计发现 starter 代码里有两处名字是错的 (orderAPI、getOrderById), 而它们都是「听起来非常合理」的名字 —— 靠直觉写就会中招,靠核对就不会。
The shape of context (from index.js):
The methods on the data sources (from dataSources/orderDataSource.js):
This table is worth actually copying out before you write anything. The audit found two wrong names in the starter code (orderAPI and getOrderById), and both of them sound entirely reasonable — write on instinct and you walk right into them, check the names and you never do.
graphql-federation-practice/node-subgraph/src/index.jsgraphql-federation-practice/node-subgraph/src/dataSources/orderDataSource.js跑基线:6 failed / 4 passedRun the baseline: 6 failed / 4 passed
改代码之前先知道起点。而且这个起点本身就在教你东西。Know your starting point before you change anything. The starting point already teaches you something.
node-subgraph 目录里原本没有node_modules,所以第一步必须npm install。然后 npm test(这个项目有 test script,和 React 那个不同)。
审计实测结果:
The node-subgraph directory ships with no node_modules, so step one has to be npm install. Then npm test (this project does have a test script, unlike the React one).
What the audit actually measured:
graphql-federation-practice/node-subgraph4 个通过里有 3 个是假通过3 of the 4 passing tests are not real passes
这是这门考试最重要的一课。This is the most important lesson in this exam.
逐条对照那 10 个测试:
| 测试 | 基线 | 为什么 |
|---|---|---|
| User.orders 返回用户订单 | ✕ | TODO 返回 [] |
| User.orders 无订单用户返回 [] | ✓ | 假通过:TODO 恰好返回 [] |
| Order.shippingInfo 返回物流 | ✕ | TODO 返回 null |
| Order.shippingInfo 无物流返回 null | ✓ | 假通过 |
| Query.orders 返回指定用户订单 | ✕ | TODO 返回 [] |
| Query.orders 无订单返回 [] | ✓ | 假通过 |
| Mutation.createOrder 成功 | ✕ | 埋雷 2(orderAPI 不存在) |
| DataLoader 批量取 order | ✕ | 埋雷 1(getOrderById 不存在) |
| DataLoader 批量取 shipping | ✓ | 这个 loader 本来就是对的 |
| 校验失败返回结构化错误 | ✕ | 埋雷 3(catch 把 INVALID_INPUT 吞成 SERVICE_ERROR) |
三个「假通过」的共同点:断言的都是「返回空」。而空实现正好就返回空。所以这三条测试对你的实现完全没有约束力 —— 它们从第一秒就是绿的,改完之后还是绿的, 但中间你可能写出了完全错误的代码。
怎么办?把注意力放在那 6 个红的上, 以及那些「测试没覆盖」的要求(correlation id 日志、Query.order、DataLoader 的使用)。红转绿是及格线,测试之外的要求才是分差。
Go through the ten tests one by one:
| Test | Baseline | Why |
|---|---|---|
| User.orders returns a user’s orders | ✕ | the TODO returns [] |
| User.orders returns [] for a user with none | ✓ | fake pass: the TODO happens to return [] |
| Order.shippingInfo returns shipping info | ✕ | the TODO returns null |
| Order.shippingInfo returns null when there is none | ✓ | fake pass |
| Query.orders returns one user’s orders | ✕ | the TODO returns [] |
| Query.orders returns [] when there are none | ✓ | fake pass |
| Mutation.createOrder succeeds | ✕ | planted bug 2 (orderAPI does not exist) |
| DataLoader batches order requests | ✕ | planted bug 1 (getOrderById does not exist) |
| DataLoader batches shipping requests | ✓ | this loader was correct all along |
| validation failure returns a structured error | ✕ | planted bug 3 (the catch swallows INVALID_INPUT into SERVICE_ERROR) |
What the three fake passes have in common: every one of them asserts “returns nothing”. And an empty implementation returns exactly nothing. So those three tests put no constraint at all on your implementation — green from the first second, still green when you are done, and in between you could have written completely wrong code.
So what do you do? Put your attention on the six red ones, and on the requirements no test covers at all (correlation id logging, Query.order, using DataLoader). Turning red to green is the pass mark; the requirements outside the tests are where the points differ.
只改一个文件Change one file only
README 的文件结构图标得很清楚。node-subgraph 下面:
src/resolvers/orderResolvers.js——EDIT THISsrc/dataSources/orderDataSource.js—— PROVIDEDsrc/schema.graphql—— PROVIDEDsrc/index.js—— PROVIDED__tests__/resolvers.test.js—— PROVIDED
PROVIDED 的意思是「别动」。判卷时这些文件很可能被替换回原版 —— 你改了 orderDataSource.js 加一个getOrderById 方法,判卷时那个方法就消失了, 你的 loader 又挂了。
所以埋雷 1 的正确修法是改 loader 里的调用, 不是给数据源加方法。这个判断在考场上值好几分。
The file tree in the README is explicit about this. Under node-subgraph:
src/resolvers/orderResolvers.js— EDIT THISsrc/dataSources/orderDataSource.js— PROVIDEDsrc/schema.graphql— PROVIDEDsrc/index.js— PROVIDED__tests__/resolvers.test.js— PROVIDED
PROVIDED means hands off. When your submission is graded, those files are quite likely swapped back to the originals — add a getOrderById method to orderDataSource.js and the method vanishes at grading time, breaking your loader all over again.
So the right fix for planted bug 1 is to change the call inside the loader, not to add a method to the data source. That judgement is worth several points in the exam.
参考答案Reference solution
提示是一级一级放的。四级看完还写不出来,再开答案门。The hints come one level at a time. If all four leave you stuck, open the answer.
这份答案在本机真跑过测试。但先确认你自己动手写过一遍 —— 读懂答案和写出答案是两种能力,考场上考的是后一种。This answer really was run here and its tests passed. But write it yourself first — reading an answer and producing one are two different skills, and the exam tests the second.