DrillLab
第 02 / 17 节LESSON 02 / 17约 14 分钟~14 min

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.

3 个练习3 exercisesFederation · 第 1 部分Federation · Part 1
这一页有什么On this page6
学完这节你会After this lesson you can
  • 说清四个参数各是什么,什么时候用哪个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
这在考试里考什么What the exam does with this

你要写的四个 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.

这节课要看的真实文件Real files this lesson looks at高亮行 = 需要你动手改的文件Highlighted rows are the files you edit
graphql-federation-practice/node-subgraph/src/index.jscontext 在这里被构造,键名以它为准The context is built here, and these key names are the ones that count
JavaScriptindex.js源项目From source
1import { ApolloServer } from '@apollo/server';
2import { startStandaloneServer } from '@apollo/server/standalone';
3import { buildSubgraphSchema } from '@apollo/subgraph';
4import { readFileSync } from 'fs';
5import { fileURLToPath } from 'url';
6import { dirname, join } from 'path';
7import gql from 'graphql-tag';
8import { resolvers, createShippingInfoLoader, createOrderLoader } from './resolvers/orderResolvers.js';
9import { OrderDataSource, InventoryDataSource, ShippingDataSource } from './dataSources/orderDataSource.js';
10
11const __filename = fileURLToPath(import.meta.url);
12const __dirname = dirname(__filename);
13
14const typeDefs = gql(readFileSync(join(__dirname, 'schema.graphql'), { encoding: 'utf-8' }));
15const schema = buildSubgraphSchema([{ typeDefs, resolvers }]);
16
17const server = new ApolloServer({
18 schema,
19 formatError: formattedError => {
20 console.error('GraphQL Error:', {
21 message: formattedError.message,
22 code: formattedError.extensions?.code,
23 path: formattedError.path,
24 correlationId: formattedError.extensions?.correlationId
25 });
26 return formattedError;
27 }
28});
29
30const { url } = await startStandaloneServer(server, {
31 listen: { port: 4000, host: '0.0.0.0' },
32 context: async ({ req }) => {
33 const correlationId = req.headers['x-correlation-id'] ||
34 `corr-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
35
36 const orderDataSource = new OrderDataSource();
37 const inventoryDataSource = new InventoryDataSource();
38 const shippingDataSource = new ShippingDataSource();
39
40 const shippingInfoLoader = createShippingInfoLoader(shippingDataSource);
41 const orderLoader = createOrderLoader(orderDataSource);
42
43 return {
44 dataSources: { orderDataSource, inventoryDataSource, shippingDataSource },
45 loaders: { shippingInfoLoader, orderLoader },
46 correlationId
47 };
48 }
49});
50
51console.log(`Subgraph ready at ${url}`);
52console.log(`Federation SDL available at ${url}?query={_service{sdl}}`);
Source: graphql-federation-practice/node-subgraph/src/index.js
graphql-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.

JavaScriptorderResolvers.js源项目From source
1import DataLoader from 'dataloader';
2import { GraphQLError } from 'graphql';
3
4// Custom error codes
5const ErrorCodes = {
6 ORDER_NOT_FOUND: 'ORDER_NOT_FOUND',
7 INVALID_INPUT: 'INVALID_INPUT',
8 INVENTORY_ERROR: 'INVENTORY_ERROR',
9 SERVICE_ERROR: 'SERVICE_ERROR'
10};
11
12// DataLoader for batching shipping info requests
13function createShippingInfoLoader(shippingDataSource) {
14 return new DataLoader(async orderIds => {
15 console.log(`[DataLoader] Batching ${orderIds.length} shipping info requests`);
16
17 const shippingInfos = await Promise.all(
18 orderIds.map(id => shippingDataSource.getShippingInfo(id))
19 );
20
21 return shippingInfos;
22 });
23}
24
25// DataLoader for batching order requests
26function createOrderLoader(orderDataSource) {
27 return new DataLoader(async orderIds => {
28 console.log(`[DataLoader] Batching ${orderIds.length} order requests`);
29
30 const orders = await Promise.all(
31 orderIds.map(id => orderDataSource.getOrderById(id))
32 );
33
34 return orders;
35 });
36}
37
38export const resolvers = {
39 User: {
40 // Reference Resolver - extend User entity from Accounts subgraph
41 __resolveReference(user, { dataSources, loaders }) {
42 return { id: user.id };
43 },
44
45 // Field resolver with caching
46 async orders(user, _, { dataSources, loaders, correlationId }) {
47 // TODO: Implement orders resolver with proper error handling and correlation ID tracing
48 return [];
49 }
50 },
51
52 Order: {
53 // Field resolver for shipping info with DataLoader
54 async shippingInfo(parent, _, { dataSources, loaders, correlationId }) {
55 // TODO: Implement shipping info resolver using DataLoader to prevent N+1 queries
56 return null;
57 }
58 },
59
60 Query: {
61 async order(_, { id }, { dataSources, loaders, correlationId }) {
62 // TODO: Implement order query using DataLoader with structured error handling
63 return null;
64 },
65
66 async orders(_, { userId }, { dataSources, correlationId }) {
67 // TODO: Implement orders query with error handling and correlation ID logging
68 return [];
69 }
70 },
71
72 Mutation: {
73 // Mutation provided for reference - candidates focus on Query resolvers
74 async createOrder(_, { userId, items }, { dataSources, correlationId }) {
75 try {
76 console.log(`[${correlationId}] Creating order for userId: ${userId}`);
77
78 if (!userId || !items || items.length === 0) {
79 throw new GraphQLError('Invalid order input', {
80 extensions: {
81 code: ErrorCodes.INVALID_INPUT,
82 correlationId
83 }
84 });
85 }
86
87 const order = await dataSources.orderAPI.createOrder({ userId, items });
88 console.log(`[${correlationId}] Order created: ${order.id}`);
89
90 return order;
91 } catch (error) {
92 console.error(`[${correlationId}] Error creating order:`, error.message);
93 throw new GraphQLError('Failed to create order', {
94 extensions: {
95 code: ErrorCodes.SERVICE_ERROR,
96 correlationId,
97 originalError: error.message
98 }
99 });
100 }
101 }
102 }
103};
104
105export { createShippingInfoLoader, createOrderLoader };
Source: graphql-federation-practice/node-subgraph/src/resolvers/orderResolvers.js
§01

四个参数The four arguments

位置惯用名是什么这个项目里的用法
1parent上一层字段的返回值Order.shippingInfo 里的parent 就是那个 order 对象, 所以能用 parent.id
2args查询里传的参数Query.orders(_, { userId }, ...) —— 解构出 userId
3context每个请求共享的一个袋子数据源、DataLoader、correlationId 全在这里
4info本次查询的 AST 等元信息这个项目里完全没用到

用不到的参数写成 _ —— 这是约定,不是语法。真实代码里async orders(_, { userId }, { dataSources })那个下划线就是「我不需要 parent」。

注意 顶层的 Query / Mutation字段没有有意义的 parent(是 undefined 或根值), 所以它们的第一个参数总是 _。 而 Order.shippingInfo 这种字段 resolver的 parent 非常重要 —— 它就是「哪个 order」。

PositionUsual nameWhat it isHow this project uses it
1parentwhat the field above returnedinside Order.shippingInfo the parent is that order object, so parent.id works
2argsthe arguments the query passed inQuery.orders(_, { userId }, ...) — destructure userId out of it
3contextone bag shared by a single requestdata sources, DataLoaders and correlationId all live here
4infothe query AST and other metadatanever 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”.

§02

parent 是怎么来的Where parent comes from

上一层返回什么,下一层的 parent 就是什么。Whatever the level above returns becomes the parent of the level below.

执行器是一层一层往下走的:

  1. Query.orders,它返回[order456, order457]
  2. 客户端还查了 shippingInfo, 于是执行器对数组里每个元素Order.shippingInfo, 把那个元素作为 parent 传进去。
  3. 所以 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:

  1. It calls Query.orders, which returns [order456, order457].
  2. The client also asked for shippingInfo, so the executor calls Order.shippingInfo once per element of that array, passing the element in as parent.
  3. So parent.id is 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).

JavaScript为什么只有 shippingInfo 需要 resolverWhy only shippingInfo needs a resolver源项目From source
1// OrderDataSource 的种子数据 —— 注意它没有 shippingInfo 字段
2{
3 id: 'order-456',
4 userId: '123',
5 status: 'SHIPPED',
6 totalAmount: 299.99,
7 items: [{ productId: 'prod-789', quantity: 2, price: 149.99 }],
8 createdAt: '2026-01-01T10:30:00Z'
9}
10// ↑ id / userId / status / totalAmount / items / createdAt 六个字段
11// 靠默认 resolver 自动取值,不用写
12// ↑ shippingInfo 不在这里 → 必须自己写 resolver 去 ShippingDataSource 取
1// The seed data of OrderDataSource — note there is no shippingInfo field
2{
3 id: 'order-456',
4 userId: '123',
5 status: 'SHIPPED',
6 totalAmount: 299.99,
7 items: [{ productId: 'prod-789', quantity: 2, price: 149.99 }],
8 createdAt: '2026-01-01T10:30:00Z'
9}
10// ↑ six fields: id / userId / status / totalAmount / items / createdAt.
11// The default resolver reads them for you, so you write nothing
12// ↑ shippingInfo is not here → write a resolver that asks ShippingDataSource
Source: graphql-federation-practice/node-subgraph/src/dataSources/orderDataSource.js
§03

context:读 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 里你能直接从第三个参数里解构出这三个键 —— 下面的代码块和那张表就是它的确切形状。

三个数据源的确切名字:orderDataSourceinventoryDataSourceshippingDataSource不是 orderAPI, 不是 orderService(项目里的 starter 代码就写错成了 orderAPI —— 这是三个埋雷之一。)

两个 loader 的确切名字:shippingInfoLoaderorderLoader

为什么 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.

JavaScriptsrc/index.js(全文)src/index.js (full file)源项目From source
1import { ApolloServer } from '@apollo/server';
2import { startStandaloneServer } from '@apollo/server/standalone';
3import { buildSubgraphSchema } from '@apollo/subgraph';
4import { readFileSync } from 'fs';
5import { fileURLToPath } from 'url';
6import { dirname, join } from 'path';
7import gql from 'graphql-tag';
8import { resolvers, createShippingInfoLoader, createOrderLoader } from './resolvers/orderResolvers.js';
9import { OrderDataSource, InventoryDataSource, ShippingDataSource } from './dataSources/orderDataSource.js';
10
11const __filename = fileURLToPath(import.meta.url);
12const __dirname = dirname(__filename);
13
14const typeDefs = gql(readFileSync(join(__dirname, 'schema.graphql'), { encoding: 'utf-8' }));
15const schema = buildSubgraphSchema([{ typeDefs, resolvers }]);
16
17const server = new ApolloServer({
18 schema,
19 formatError: formattedError => {
20 console.error('GraphQL Error:', {
21 message: formattedError.message,
22 code: formattedError.extensions?.code,
23 path: formattedError.path,
24 correlationId: formattedError.extensions?.correlationId
25 });
26 return formattedError;
27 }
28});
29
30const { url } = await startStandaloneServer(server, {
31 listen: { port: 4000, host: '0.0.0.0' },
32 context: async ({ req }) => {
33 const correlationId = req.headers['x-correlation-id'] ||
34 `corr-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
35
36 const orderDataSource = new OrderDataSource();
37 const inventoryDataSource = new InventoryDataSource();
38 const shippingDataSource = new ShippingDataSource();
39
40 const shippingInfoLoader = createShippingInfoLoader(shippingDataSource);
41 const orderLoader = createOrderLoader(orderDataSource);
42
43 return {
44 dataSources: { orderDataSource, inventoryDataSource, shippingDataSource },
45 loaders: { shippingInfoLoader, orderLoader },
46 correlationId
47 };
48 }
49});
50
51console.log(`Subgraph ready at ${url}`);
52console.log(`Federation SDL available at ${url}?query={_service{sdl}}`);
Source: graphql-federation-practice/node-subgraph/src/index.js
JavaScript抄在纸上的那张表The table to copy onto paper已跑通Verified
1// context 的确切结构(从 index.js 第 47–51 行读出来的)
2{
3 dataSources: {
4 orderDataSource, // getOrder(id) / getOrdersByUserId(userId) / createOrder(userId, items)
5 inventoryDataSource, // getInventoryStatus(ids) / getProductPrice(productId)
6 shippingDataSource // getShippingInfo(orderId)
7 },
8 loaders: {
9 shippingInfoLoader, // .load(orderId)
10 orderLoader // .load(orderId)
11 },
12 correlationId // 字符串,用于把一次请求的所有日志串起来
13}
1// The exact shape of context (read off lines 47–51 of index.js)
2{
3 dataSources: {
4 orderDataSource, // getOrder(id) / getOrdersByUserId(userId) / createOrder(userId, items)
5 inventoryDataSource, // getInventoryStatus(ids) / getProductPrice(productId)
6 shippingDataSource // getShippingInfo(orderId)
7 },
8 loaders: {
9 shippingInfoLoader, // .load(orderId)
10 orderLoader // .load(orderId)
11 },
12 correlationId // a string that ties together all logs of one request
13}
写 resolver 之前把这张表抄下来。三个埋雷里有两个就是「名字对不上」—— starter 代码里写了 dataSources.orderAPI(不存在)和 orderDataSource.getOrderById(不存在)。Copy this table down before you write any resolver. Two of the three planted bugs are just names that do not match: the starter code writes dataSources.orderAPI (does not exist) and orderDataSource.getOrderById (does not exist).
§04

correlationId:为什么每个 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.

JavaScriptcorrelationId 的来源Where correlationId comes from源项目From source
1const correlationId = req.headers['x-correlation-id'] ||
2 `corr-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
Source: graphql-federation-practice/node-subgraph/src/index.js
「有就用调用方的,没有就自己造」是这类可观测性字段的标准做法 —— 保证整条链路共用一个 id。Use the caller's value if there is one, otherwise make your own. That is the standard pattern for this kind of observability field: it keeps one id across the whole chain of calls.
练习Practice

动手做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.

L1认出来Spot it从 context 里取订单数据源,正确写法是The right way to read the order data source out of context

index.js 里 context 的真实结构, 哪个写法能拿到订单数据源?

Going by the real shape of context in index.js, which of these reads the order data source?

先选一个选项Pick an option first
L1认出来Spot it这个 resolver 该用哪个参数Which argument should this resolver use

Order.shippingInfo 需要知道「是哪个 order 的物流」。 这个 order 的 id 从哪个参数拿?

Order.shippingInfo has to know which order the shipping belongs to. Which argument holds that order's id?

先选一个选项Pick an option first
L1排顺序Order it把 resolver 的调用顺序排对Put the resolver calls in the right order

客户端发 { 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.

1对每个 order 分别调 Order.shippingInfo(共 2 次)Call Order.shippingInfo once per order (2 calls in total)
2用 schema 校验查询:字段存不存在、userId 类型对不对Validate the query against the schema: do the fields exist, is the type of userId right
3DataLoader 把 2 次 load 合并成 1 次批量请求DataLoader merges the 2 load calls into 1 batched request
4调 Query.orders,拿到 [order-456, order-457]Call Query.orders and get back [order-456, order-457]
5按查询形状组装 JSON 返回Assemble the JSON response in the shape of the query
迁移Transfer

换一道题也能用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.

字段 resolver 需要「是哪一个」A field resolver needs to know which object it is on
用 parentUse parent
查询传了参数The query passes arguments
用 args,通常直接解构Use args, usually destructured directly
需要数据源 / loader / 请求级信息You need a data source, a loader, or per-request information
用 context,键名以 index.js 为准Use context; index.js is the source of truth for key names
数据源上已经有同名属性The data source already has a property with the same name
不用写 resolver,默认 resolver 会取No resolver needed; the default resolver reads it
TODO 里提到 correlation idA TODO mentions correlation id
日志和 error extensions 里都带上它Put it in the log line and in the error extensions
这节的要点What to take away
  1. 四个参数: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).
  2. 顶层 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.
  3. context 的确切键名:dataSources.{orderDataSource, inventoryDataSource, shippingDataSource}、loaders.{shippingInfoLoader, orderLoader}、correlationId。The exact keys in context: dataSources.{orderDataSource, inventoryDataSource, shippingDataSource}, loaders.{shippingInfoLoader, orderLoader}, correlationId.
  4. 数据源上有同名属性的字段不用写 resolver;shippingInfo 没有,所以必须写。A field whose name already exists on the data source needs no resolver. shippingInfo is not there, so you must write one.
  5. DataLoader 必须每请求新建,否则缓存跨请求泄漏。A DataLoader must be created once per request, otherwise its cache leaks from one request into the next.

接下来What next

  1. 把这一节的练习做掉Do this lesson’s exercises3 个,就在这一页上面 —— 别攒着最后一起做3 of them, further up this page — do not save them for later
    回到练习 ↑Back up to them ↑
  2. 接着看下一节Continue to the next lesson非空、列表,和那个没有 price 的 inputNon-null, lists, and the input that has no price
    下一节Next lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: GraphQL 是什么:一份 schema 加一堆 resolverWhat GraphQL is: one schema plus a set of resolvers