resolver 的四个参数The four arguments of a resolver
(parent, args, context, info) —— 这四个东西是整门考试的操作台。(parent, args, context, info) — you use these four in every task of this exam.
这一页有什么On this page6
- 说清四个参数各是什么,什么时候用哪个Explain what each of the four arguments is and when to use which
- 解释 parent 是从哪来的Explain where parent comes from
- 从真实 index.js 里读出 context 的确切结构Read the exact shape of context out of the real index.js
- 知道字段没有 resolver 时会发生什么Know what happens when a field has no resolver
你要写的四个 TODO,全部是「从 context 里取数据源、用 parent 或 args 里的 id 去取数」。context 的键名写错(orderAPI vs orderDataSource)是这个项目里真实存在的埋雷之一。All four TODOs you have to write do the same thing: take a data source from context, then fetch data using an id from parent or args. Getting a key name in context wrong (orderAPI instead of orderDataSource) is one of the bugs actually planted in this project.
graphql-federation-practice/node-subgraph/src/index.jscontext 在这里被构造,键名以它为准The context is built here, and these key names are the ones that count
graphql-federation-practice/node-subgraph/src/index.jsgraphql-federation-practice/node-subgraph/src/resolvers/orderResolvers.js四个 TODO 的位置Where the four TODOs are
提醒:源项目在磁盘上是做完的版本 —— 下面就是答案。想自己先写一遍的话,现在关上。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四个参数The four arguments
| 位置 | 惯用名 | 是什么 | 这个项目里的用法 |
|---|---|---|---|
| 1 | parent | 上一层字段的返回值 | Order.shippingInfo 里的parent 就是那个 order 对象, 所以能用 parent.id |
| 2 | args | 查询里传的参数 | Query.orders(_, { userId }, ...) —— 解构出 userId |
| 3 | context | 每个请求共享的一个袋子 | 数据源、DataLoader、correlationId 全在这里 |
| 4 | info | 本次查询的 AST 等元信息 | 这个项目里完全没用到 |
用不到的参数写成 _ —— 这是约定,不是语法。真实代码里async orders(_, { userId }, { dataSources })那个下划线就是「我不需要 parent」。
注意 顶层的 Query / Mutation字段没有有意义的 parent(是 undefined 或根值), 所以它们的第一个参数总是 _。 而 Order.shippingInfo 这种字段 resolver的 parent 非常重要 —— 它就是「哪个 order」。
| Position | Usual name | What it is | How this project uses it |
|---|---|---|---|
| 1 | parent | what the field above returned | inside Order.shippingInfo the parent is that order object, so parent.id works |
| 2 | args | the arguments the query passed in | Query.orders(_, { userId }, ...) — destructure userId out of it |
| 3 | context | one bag shared by a single request | data sources, DataLoaders and correlationId all live here |
| 4 | info | the query AST and other metadata | never touched in this project |
Write _ for parameters you do not use — that is a convention, not syntax. In the real code, async orders(_, { userId }, { dataSources }) uses that underscore to say “I do not need parent”.
Note that top-level Query and Mutation fields have no meaningful parent (it is undefined or the root value), so their first parameter is always _. For a field resolver like Order.shippingInfo, parent matters a lot — it is “which order”.
parent 是怎么来的Where parent comes from
上一层返回什么,下一层的 parent 就是什么。Whatever the level above returns becomes the parent of the level below.
执行器是一层一层往下走的:
- 调
Query.orders,它返回[order456, order457]。 - 客户端还查了
shippingInfo, 于是执行器对数组里每个元素调Order.shippingInfo, 把那个元素作为parent传进去。 - 所以
parent.id就是"order-456"或"order-457"。
字段没有 resolver 时会怎样?执行器会用默认 resolver: 直接取 parent[字段名]。
这解释了一件重要的事:Order 有 7 个字段, 但 resolvers.Order 里只写了shippingInfo 一个。 其余 6 个(id、userId、status……)不需要写 —— 因为数据源返回的对象上正好有这些同名属性,默认 resolver 直接取就行。
而 shippingInfo 必须自己写, 因为数据源返回的 order 对象上没有这个属性 (物流信息在另一个服务里)。
The executor walks one layer at a time:
- It calls
Query.orders, which returns[order456, order457]. - The client also asked for
shippingInfo, so the executor callsOrder.shippingInfoonce per element of that array, passing the element in asparent. - So
parent.idis either"order-456"or"order-457".
What happens to a field with no resolver? The executor falls back to the default resolver: it reads parent[fieldName] and returns that.
That explains something important. Order has seven fields, but resolvers.Order defines only shippingInfo. The other six (id, userId, status and so on) need no resolver — the object the data source returns already has properties with exactly those names, so the default resolver picks them up.
And shippingInfo has to be written by hand, because the order object from the data source does not have that property (shipping lives in another service).
graphql-federation-practice/node-subgraph/src/dataSources/orderDataSource.jscontext:读 index.js 拿到确切的键名context: read index.js to get the exact key names
这一段是全门考试最该抄在纸上的东西。This is the part of the exam most worth copying onto paper.
context 是在 index.js 里每个请求现场构造的。看清它的三个键:
所以在 resolver 里你能直接从第三个参数里解构出这三个键 —— 下面的代码块和那张表就是它的确切形状。
三个数据源的确切名字:orderDataSource、inventoryDataSource、shippingDataSource。不是 orderAPI, 不是 orderService。(项目里的 starter 代码就写错成了 orderAPI —— 这是三个埋雷之一。)
两个 loader 的确切名字:shippingInfoLoader、orderLoader。
为什么 loader 要每请求新建?DataLoader 自带缓存。如果建在模块顶层, 第一个请求缓存的数据会被第二个请求看到 —— 跨请求数据泄漏,而且数据永远不刷新。 所以正确做法就是像这里一样,在 context函数里 new。
context is built fresh inside index.js for every request. Look closely at its three keys:
Which is why a resolver can destructure those three keys straight out of its third argument — the code block and the table below are its exact shape.
The exact names of the three data sources: orderDataSource, inventoryDataSource, shippingDataSource. Not orderAPI, not orderService. (The starter code in the project gets this wrong and writes orderAPI — one of the three planted bugs.)
The exact names of the two loaders: shippingInfoLoader and orderLoader.
Why do the loaders have to be new for every request? DataLoader caches by design. Build one at module top level and whatever the first request cached is visible to the second — data leaking across requests, and data that never refreshes. So the right move is exactly what happens here: new them inside the context function.
graphql-federation-practice/node-subgraph/src/index.jscorrelationId:为什么每个 TODO 都提到它correlationId: why every TODO mentions it
四个 TODO 里有两个明确写了correlation ID tracing / correlation ID logging。 这不是装饰。
在微服务系统里,一个用户请求会经过 Router → subgraph A → subgraph B → 数据库,每一跳都在打日志。 出问题时你需要把同一次请求的所有日志找出来 ——correlation id 就是那根线。
index.js 里的逻辑是:优先用客户端传来的x-correlation-id 请求头,没有就自己生成一个。 这样调用方(比如 Router)传下来的 id 会被沿用, 整条链路的日志能串起来。
你要做的很简单:在 resolver 的日志和错误里带上它。console.log(`[${correlationId}] ...`), 以及 extensions: { code, correlationId }。 Java 那道题里也有同一套思路(用 SLF4J 的 MDC 实现)。
Two of the four TODOs spell it out: correlation ID tracing / correlation ID logging. That is not decoration.
In a microservice system one user request travels Router → subgraph A → subgraph B → database, and every hop writes logs. When something breaks you need every log line that belongs to that one request — the correlation id is that thread.
The logic in index.js: use the x-correlation-id header from the client if it is there, otherwise generate one. That way an id handed down by the caller (the Router, say) gets reused and the logs of the whole chain line up.
Your part is simple: carry it in the logs and errors of your resolvers. console.log(`[${correlationId}] ...`), plus extensions: { code, correlationId }. The Java question uses the same idea, implemented with SLF4J’s MDC.
graphql-federation-practice/node-subgraph/src/index.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.
照 index.js 里 context 的真实结构, 哪个写法能拿到订单数据源?
Going by the real shape of context in index.js, which of these reads the order data source?
Order.shippingInfo 需要知道「是哪个 order 的物流」。 这个 order 的 id 从哪个参数拿?
Order.shippingInfo has to know which order the shipping belongs to. Which argument holds that order's id?
客户端发 { orders(userId:"123") { id shippingInfo { status } } }, 数据源里 user 123 有两个订单。把服务端的动作排序。
The client sends { orders(userId:"123") { id shippingInfo { status } } }, and in the data source user 123 has two orders. Put the server's steps in order.
换一道题也能用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.
- 四个参数:parent(上一层返回值)、args(查询参数)、context(请求级袋子)、info(这个项目没用)。The four arguments: parent (what the level above returned), args (the query arguments), context (per-request shared data), info (not used in this project).
- 顶层 Query/Mutation 的 parent 无意义,写成 _;字段 resolver 的 parent 至关重要。On top-level Query and Mutation the parent means nothing, so write it as _. On a field resolver the parent matters a lot.
- context 的确切键名:dataSources.{orderDataSource, inventoryDataSource, shippingDataSource}、loaders.{shippingInfoLoader, orderLoader}、correlationId。The exact keys in context: dataSources.{orderDataSource, inventoryDataSource, shippingDataSource}, loaders.{shippingInfoLoader, orderLoader}, correlationId.
- 数据源上有同名属性的字段不用写 resolver;shippingInfo 没有,所以必须写。A field whose name already exists on the data source needs no resolver. shippingInfo is not there, so you must write one.
- DataLoader 必须每请求新建,否则缓存跨请求泄漏。A DataLoader must be created once per request, otherwise its cache leaks from one request into the next.