DrillLab
第 08 / 17 节LESSON 08 / 17约 15 分钟~15 min

先读题:四个 TODO、三处埋雷、十个测试Read the task first: four TODOs, three planted bugs, ten tests

在写第一行 resolver 之前,把要改什么、别人给了什么、判卷标准是什么全摸清。Before writing the first line of a resolver, find out what to change, what is already given, and how it will be graded.

3 个练习3 exercisesFederation · 第 3 部分Federation · Part 3
这一页有什么On this page8
学完这节你会After this lesson you can
  • 复述四个 TODO 各自的要求Restate what each of the four TODOs asks for
  • 抄出一张「数据源方法名 + context 键名」的对照表Copy out one reference table of data source method names and context key names
  • 跑出基线测试并读懂那 6 个失败Run the baseline tests and read the 6 failures
  • 认出「4 个通过里有 3 个是假通过」这件事See that 3 of the 4 passing tests are not real passes
这在考试里考什么What the exam does with this

这一节本身就是考点。README 有一句「The starter code also contains related TODOs and integration issues that may need attention」—— 那三处埋雷不会有人告诉你在哪,只能靠核对。This lesson is itself part of the exam. The README says: The starter code also contains related TODOs and integration issues that may need attention. Nobody tells you where the three planted bugs are; you find them by checking names one by one.

这节课要看的真实文件Real files this lesson looks at高亮行 = 需要你动手改的文件Highlighted rows are the files you edit
graphql-federation-practice/README.md任务清单与 EDIT THIS / PROVIDED 标注The task list and the EDIT THIS / PROVIDED markers
TextREADME.md源项目From source
1# GraphQL Federation Resolver & Microservice Integration
2
3## Overview
4
5TechFlow Inc. operates a distributed microservices architecture with a GraphQL Supergraph (Apollo Federation) serving as the unified API gateway. You will implement a GraphQL subgraph resolver in Node.js and a backend microservice in Java/Spring Boot.
6
7## File Structure
8
9```text
10.
11├── node-subgraph/
12│ ├── src/
13│ │ ├── resolvers/orderResolvers.js (EDIT THIS)
14│ │ ├── dataSources/orderDataSource.js (PROVIDED)
15│ │ ├── schema.graphql (PROVIDED)
16│ │ └── index.js (PROVIDED)
17│ ├── package.json (PROVIDED)
18│ └── __tests__/resolvers.test.js (PROVIDED)
19├── java-service/
20│ ├── src/main/java/com/techflow/orders/
21│ │ ├── controller/OrderController.java (EDIT THIS)
22│ │ ├── service/OrderService.java (PROVIDED)
23│ │ ├── exception/ (PROVIDED)
24│ │ ├── config/ (PROVIDED)
25│ │ ├── dto/ (PROVIDED)
26│ │ ├── model/ (PROVIDED)
27│ │ └── repository/ (PROVIDED)
28│ ├── src/test/java/com/techflow/orders/OrderControllerTest.java
29│ └── pom.xml
30└── QUESTIONS.md (EDIT THIS)
31```
32
33## How to Run and Test
34
35### Node.js GraphQL Subgraph
36
37```bash
38cd node-subgraph
39npm install
40npm start
41```
42
43```bash
44npm test
45```
46
47### Java Spring Boot Service
48
49```bash
50cd java-service
51mvn spring-boot:run
52```
53
54```bash
55mvn test
56```
57
58The tests are provided to help validate incremental progress.
59
60# [REQUIRED] Your Tasks
61
62## Task 1: GraphQL Subgraph Resolver with Federation
63
64Implement resolver logic in `node-subgraph/src/resolvers/orderResolvers.js`:
65
66- Implement `User.orders` with proper error handling
67- Implement `Order.shippingInfo` using the provided DataLoader
68- Implement `Query.orders` to fetch orders by user ID
69
70The starter code also contains related TODOs and integration issues that may need attention.
71
72## Task 2: Spring Boot REST Controller
73
74Implement REST endpoints in `java-service/src/main/java/com/techflow/orders/controller/OrderController.java`:
75
76- `GET /api/orders`
77- `GET /api/orders/{id}`
78- `GET /api/orders/user/{userId}`
79- `POST /api/orders`
80- `PATCH /api/orders/{id}/status`
81- `DELETE /api/orders/{id}`
82
83Use the provided `OrderService` for business logic.
84
85## Written Questions
86
87Answer the questions in `QUESTIONS.md`.
Source: graphql-federation-practice/README.md
graphql-federation-practice/node-subgraph/src/resolvers/orderResolvers.js唯一要改的文件The only file you change

提醒:源项目在磁盘上是做完的版本 —— 下面就是答案。想自己先写一遍的话,现在关上。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
graphql-federation-practice/node-subgraph/__tests__/resolvers.test.js10 个判卷测试The ten tests that decide the marks
JavaScriptresolvers.test.js源项目From source
1import { describe, it, expect, beforeEach } from '@jest/globals';
2import { resolvers, createShippingInfoLoader, createOrderLoader } from '../src/resolvers/orderResolvers.js';
3import { OrderDataSource, InventoryDataSource, ShippingDataSource } from '../src/dataSources/orderDataSource.js';
4
5describe('Order Resolvers', () => {
6 let dataSources;
7 let loaders;
8 let context;
9
10 beforeEach(() => {
11 dataSources = {
12 orderDataSource: new OrderDataSource(),
13 inventoryDataSource: new InventoryDataSource(),
14 shippingDataSource: new ShippingDataSource()
15 };
16
17 loaders = {
18 shippingInfoLoader: createShippingInfoLoader(dataSources.shippingDataSource),
19 orderLoader: createOrderLoader(dataSources.orderDataSource)
20 };
21
22 context = {
23 dataSources,
24 loaders,
25 correlationId: 'test-correlation-id'
26 };
27 });
28
29 describe('User.orders resolver', () => {
30 it('should return orders for a user', async () => {
31 const user = { id: '123' };
32 const orders = await resolvers.User.orders(user, {}, context);
33
34 expect(orders).toBeDefined();
35 expect(Array.isArray(orders)).toBe(true);
36 expect(orders.length).toBeGreaterThan(0);
37 expect(orders[0]).toHaveProperty('id');
38 expect(orders[0]).toHaveProperty('userId', '123');
39 expect(orders[0]).toHaveProperty('status');
40 expect(orders[0]).toHaveProperty('totalAmount');
41 });
42
43 it('should return empty array for user with no orders', async () => {
44 const user = { id: '999' };
45 const orders = await resolvers.User.orders(user, {}, context);
46
47 expect(orders).toBeDefined();
48 expect(Array.isArray(orders)).toBe(true);
49 expect(orders.length).toBe(0);
50 });
51 });
52
53 describe('Order.shippingInfo resolver', () => {
54 it('should return shipping info for an order', async () => {
55 const order = { id: 'order-456' };
56 const shippingInfo = await resolvers.Order.shippingInfo(order, {}, context);
57
58 expect(shippingInfo).toBeDefined();
59 expect(shippingInfo).toHaveProperty('status');
60 expect(shippingInfo).toHaveProperty('trackingNumber');
61 });
62
63 it('should return null for order without shipping info', async () => {
64 const order = { id: 'order-999' };
65 const shippingInfo = await resolvers.Order.shippingInfo(order, {}, context);
66 expect(shippingInfo).toBeNull();
67 });
68 });
69
70 describe('Query.orders resolver', () => {
71 it('should return orders for a specific user', async () => {
72 const orders = await resolvers.Query.orders({}, { userId: '123' }, context);
73
74 expect(orders).toBeDefined();
75 expect(Array.isArray(orders)).toBe(true);
76 expect(orders.length).toBeGreaterThan(0);
77 expect(orders.every(order => order.userId === '123')).toBe(true);
78 });
79
80 it('should return empty array for user with no orders', async () => {
81 const orders = await resolvers.Query.orders({}, { userId: '999' }, context);
82
83 expect(orders).toBeDefined();
84 expect(Array.isArray(orders)).toBe(true);
85 expect(orders.length).toBe(0);
86 });
87 });
88
89 describe('Mutation.createOrder resolver', () => {
90 it('should create a new order successfully', async () => {
91 const input = {
92 userId: '789',
93 items: [{ productId: 'prod-789', quantity: 2 }]
94 };
95
96 const order = await resolvers.Mutation.createOrder({}, input, context);
97
98 expect(order).toBeDefined();
99 expect(order.id).toBeDefined();
100 expect(order.userId).toBe('789');
101 expect(order.status).toBe('PENDING');
102 expect(order.totalAmount).toBeGreaterThan(0);
103 expect(order.items.length).toBe(1);
104 expect(order.items[0].productId).toBe('prod-789');
105 expect(order.items[0].quantity).toBe(2);
106 expect(order.items[0].price).toBeDefined();
107 });
108 });
109
110 describe('DataLoader functionality', () => {
111 it('should batch multiple order requests', async () => {
112 const orderIds = ['order-456', 'order-457'];
113 const orders = await Promise.all(orderIds.map(id => loaders.orderLoader.load(id)));
114
115 expect(orders.length).toBe(2);
116 expect(orders[0].id).toBe('order-456');
117 expect(orders[1].id).toBe('order-457');
118 });
119
120 it('should batch multiple shipping info requests', async () => {
Source: graphql-federation-practice/node-subgraph/__tests__/resolvers.test.js

只显示前 120 行,整个文件共 144 行 —— 其余在本机打开看。First 120 of 144 lines — open the file locally for the rest.

§01

题面原文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.

TextREADME.md(Task 1 原文)README.md (the original Task 1 text)源项目From source
1## Task 1: GraphQL Subgraph Resolver with Federation
2
3Implement resolver logic in `node-subgraph/src/resolvers/orderResolvers.js`:
4
5- Implement `User.orders` with proper error handling
6- Implement `Order.shippingInfo` using the provided DataLoader
7- Implement `Query.orders` to fetch orders by user ID
8
9The starter code also contains related TODOs and integration issues
10that may need attention.
Source: graphql-federation-practice/README.md
§02

四个 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.ordersOrder.shippingInfoQuery.orders), 但打开代码会发现还有一个Query.order(单个订单)也是 TODO。

位置TODO 原文里的关键词README 提到了吗有测试吗
User.ordersproper error handling + correlation ID tracing✅ 2 条
Order.shippingInfousing DataLoader to prevent N+1 queries✅ 2 条
Query.orderusing DataLoader with structured error handling没提没有
Query.orderserror 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.

WhereKey words in the TODO itselfNamed in the README?Any tests?
User.ordersproper error handling + correlation ID tracing✅ 2 of them
Order.shippingInfousing DataLoader to prevent N+1 queries✅ 2 of them
Query.orderusing DataLoader with structured error handlingnever mentionednone
Query.orderserror 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.

JavaScriptsrc/resolvers/orderResolvers.js(starter 全貌,已标注)src/resolvers/orderResolvers.js (the whole starter, annotated)源项目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
15// DataLoader for batching order requests
16function createOrderLoader(orderDataSource) {
17 return new DataLoader(async orderIds => {
18 const orders = await Promise.all(
19 orderIds.map(id => orderDataSource.getOrderById(id)) // ← 埋雷 1
20 );
21 return orders;
22 });
23}
24
25export const resolvers = {
26 User: {
27 __resolveReference(user, { dataSources, loaders }) {
28 return { id: user.id }; // 已给好
29 },
30
31 async orders(user, _, { dataSources, loaders, correlationId }) {
32 // TODO: Implement orders resolver with proper error handling and correlation ID tracing
33 return []; // ← TODO 1
34 }
35 },
36
37 Order: {
38 async shippingInfo(parent, _, { dataSources, loaders, correlationId }) {
39 // TODO: Implement shipping info resolver using DataLoader to prevent N+1 queries
40 return null; // ← TODO 2
41 }
42 },
43
44 Query: {
45 async order(_, { id }, { dataSources, loaders, correlationId }) {
46 // TODO: Implement order query using DataLoader with structured error handling
47 return null; // ← TODO 3
48 },
49
50 async orders(_, { userId }, { dataSources, correlationId }) {
51 // TODO: Implement orders query with error handling and correlation ID logging
52 return []; // ← TODO 4
53 }
54 },
55
56 Mutation: {
57 async createOrder(_, { userId, items }, { dataSources, correlationId }) {
58 try {
59 console.log(`[${correlationId}] Creating order for userId: ${userId}`);
60
61 if (!userId || !items || items.length === 0) {
62 throw new GraphQLError('Invalid order input', {
63 extensions: { code: ErrorCodes.INVALID_INPUT, correlationId }
64 });
65 }
66
67 const order = await dataSources.orderAPI.createOrder({ userId, items }); // ← 埋雷 2
68 console.log(`[${correlationId}] Order created: ${order.id}`);
69
70 return order;
71 } catch (error) {
72 console.error(`[${correlationId}] Error creating order:`, error.message);
73 throw new GraphQLError('Failed to create order', { // ← 埋雷 3
74 extensions: {
75 code: ErrorCodes.SERVICE_ERROR,
76 correlationId,
77 originalError: error.message
78 }
79 });
80 }
81 }
82 }
83};
84
85export { createShippingInfoLoader, createOrderLoader };
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) { /* given to you, correct */ }
14
15// DataLoader for batching order requests
16function createOrderLoader(orderDataSource) {
17 return new DataLoader(async orderIds => {
18 const orders = await Promise.all(
19 orderIds.map(id => orderDataSource.getOrderById(id)) // ← planted bug 1
20 );
21 return orders;
22 });
23}
24
25export const resolvers = {
26 User: {
27 __resolveReference(user, { dataSources, loaders }) {
28 return { id: user.id }; // given to you
29 },
30
31 async orders(user, _, { dataSources, loaders, correlationId }) {
32 // TODO: Implement orders resolver with proper error handling and correlation ID tracing
33 return []; // ← TODO 1
34 }
35 },
36
37 Order: {
38 async shippingInfo(parent, _, { dataSources, loaders, correlationId }) {
39 // TODO: Implement shipping info resolver using DataLoader to prevent N+1 queries
40 return null; // ← TODO 2
41 }
42 },
43
44 Query: {
45 async order(_, { id }, { dataSources, loaders, correlationId }) {
46 // TODO: Implement order query using DataLoader with structured error handling
47 return null; // ← TODO 3
48 },
49
50 async orders(_, { userId }, { dataSources, correlationId }) {
51 // TODO: Implement orders query with error handling and correlation ID logging
52 return []; // ← TODO 4
53 }
54 },
55
56 Mutation: {
57 async createOrder(_, { userId, items }, { dataSources, correlationId }) {
58 try {
59 console.log(`[${correlationId}] Creating order for userId: ${userId}`);
60
61 if (!userId || !items || items.length === 0) {
62 throw new GraphQLError('Invalid order input', {
63 extensions: { code: ErrorCodes.INVALID_INPUT, correlationId }
64 });
65 }
66
67 const order = await dataSources.orderAPI.createOrder({ userId, items }); // ← planted bug 2
68 console.log(`[${correlationId}] Order created: ${order.id}`);
69
70 return order;
71 } catch (error) {
72 console.error(`[${correlationId}] Error creating order:`, error.message);
73 throw new GraphQLError('Failed to create order', { // ← planted bug 3
74 extensions: {
75 code: ErrorCodes.SERVICE_ERROR,
76 correlationId,
77 originalError: error.message
78 }
79 });
80 }
81 }
82 }
83};
84
85export { createShippingInfoLoader, createOrderLoader };
Source: graphql-federation-practice/node-subgraph/src/resolvers/orderResolvers.js
§03

写代码前先抄这张表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 代码里有两处名字是错的 (orderAPIgetOrderById), 而它们都是「听起来非常合理」的名字 —— 靠直觉写就会中招,靠核对就不会。

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.

JavaScriptcontext 的确切键名The exact key names in context源项目From source
1{
2 dataSources: { orderDataSource, inventoryDataSource, shippingDataSource },
3 loaders: { shippingInfoLoader, orderLoader },
4 correlationId
5}
Source: graphql-federation-practice/node-subgraph/src/index.js
Text三个数据源的方法与数据The methods and data of the three data sources源项目From source
1class OrderDataSource {
2 getOrder(id) → 一条 order,找不到返回 undefined
3 getOrdersByUserId(userId) → order 数组,找不到返回 []
4 createOrder(userId, items) → 新 order(内部要用 item.price 算总价!)
5
6 种子数据:
7 order-456 userId '123' SHIPPED 299.99
8 order-457 userId '123' DELIVERED 89.99
9 order-458 userId '456' PENDING 199.99
10}
11
12class InventoryDataSource {
13 getInventoryStatus(productIds) → 没有任何地方需要它(干扰项)
14 getProductPrice(productId) → 价格,未知商品兜底 99.99 ★ createOrder 要用
15}
16
17class ShippingDataSource {
18 getShippingInfo(orderId) → 物流信息,只有 order-456/457 有,其余 null
19}
1class OrderDataSource {
2 getOrder(id) → one order; undefined when not found
3 getOrdersByUserId(userId) → array of orders; [] when not found
4 createOrder(userId, items) → a new order (uses item.price for the total!)
5
6 Seed data:
7 order-456 userId '123' SHIPPED 299.99
8 order-457 userId '123' DELIVERED 89.99
9 order-458 userId '456' PENDING 199.99
10}
11
12class InventoryDataSource {
13 getInventoryStatus(productIds) → nothing anywhere needs it (a distractor)
14 getProductPrice(productId) → price; unknown product falls back to 99.99 ★ createOrder needs it
15}
16
17class ShippingDataSource {
18 getShippingInfo(orderId) → shipping info; only order-456/457 have it, others null
19}
Source: graphql-federation-practice/node-subgraph/src/dataSources/orderDataSource.js
§04

跑基线: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:

Terminal本机实测Measured on this machine源项目From source
1$ cd node-subgraph
2$ npm install
3added 424 packages in 11s
4
5$ npm test
6Tests: 6 failed, 4 passed, 10 total
Source: graphql-federation-practice/node-subgraph
§05

4 个通过里有 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:

TestBaselineWhy
User.orders returns a user’s ordersthe TODO returns []
User.orders returns [] for a user with nonefake pass: the TODO happens to return []
Order.shippingInfo returns shipping infothe TODO returns null
Order.shippingInfo returns null when there is nonefake pass
Query.orders returns one user’s ordersthe TODO returns []
Query.orders returns [] when there are nonefake pass
Mutation.createOrder succeedsplanted bug 2 (orderAPI does not exist)
DataLoader batches order requestsplanted bug 1 (getOrderById does not exist)
DataLoader batches shipping requeststhis loader was correct all along
validation failure returns a structured errorplanted 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.

§06

只改一个文件Change one file only

README 的文件结构图标得很清楚。node-subgraph 下面:

  • src/resolvers/orderResolvers.js ——EDIT THIS
  • src/dataSources/orderDataSource.js —— PROVIDED
  • src/schema.graphql —— PROVIDED
  • src/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 THIS
  • src/dataSources/orderDataSource.js — PROVIDED
  • src/schema.graphql — PROVIDED
  • src/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.

练习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为什么基线里有 4 个测试是通过的Why 4 tests already pass at the baseline

四个 TODO 全都只写了 return []return null,为什么还有 4 个测试通过?

All four TODOs contain nothing but return [] or return null. So why do 4 tests still pass?

先选一个选项Pick an option first
L1认出来Spot it埋雷 1 该在哪个文件修Which file should planted bug 1 be fixed in

createOrderLoader 调了不存在的orderDataSource.getOrderById(id)。 正确的修法是?

createOrderLoader calls orderDataSource.getOrderById(id), which does not exist. What is the right fix?

先选一个选项Pick an option first
L1排顺序Order it把 Task 1 的推进顺序排对Put the steps of Task 1 in the right order

拿到 node-subgraph,最合理的动作顺序?

You have just opened node-subgraph. What is the most sensible order to work in?

1逐个实现四个 TODOImplement the four TODOs one by one
2读 schema.graphql,记下四个返回类型的可空性Read schema.graphql and note the nullability of the four return types
3npm install,然后 npm test 拿到 6 failed / 4 passed 的基线npm install, then npm test to get the 6 failed / 4 passed baseline
4npm test 转绿,再写个 verify 脚本查 _service 和 _entitiesOnce npm test is green, write a verify script that checks _service and _entities
5读 index.js 和 dataSources,抄下 context 键名与方法名Read index.js and the dataSources, and copy out the context keys and method names
6修三处埋雷Fix the three planted bugs
迁移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.

README 说有「integration issues」The README mentions integration issues
逐个核对方法名、键名、签名Check method names, key names and signatures one by one
看到 EDIT THIS / PROVIDED 标注You see EDIT THIS / PROVIDED markers
只改 EDIT THIS 的文件Only change the files marked EDIT THIS
基线里有测试是绿的Some tests are already green in the baseline
判断是真通过还是「空实现恰好满足」Decide whether it really passes, or an empty implementation happens to satisfy it
代码里的 TODO 比 README 多The code has more TODOs than the README
以代码为准,README 可能不全Trust the code; the README may be incomplete
这节的要点What to take away
  1. 四个 TODO,README 只列了三个 —— Query.order 既没被提到也没有测试,但代码里要求实现。There are four TODOs but the README lists three. Query.order is neither mentioned nor tested, yet the code asks you to implement it.
  2. 开始写之前抄两张表:context 的键名、三个数据源的方法名。Before you start writing, copy two tables: the key names in context, and the method names on the three data sources.
  3. 基线是 6 failed / 4 passed,其中 3 个通过是「空实现恰好满足断言」的假通过。The baseline is 6 failed / 4 passed, and 3 of those passes only happen because an empty implementation satisfies the assertion.
  4. 只改 orderResolvers.js;其余文件 PROVIDED,判卷时可能被换回原版。Change only orderResolvers.js. The other files are marked PROVIDED and may be replaced with the originals during grading.
  5. 先修埋雷再写 TODO,否则埋雷的报错会干扰你判断自己的代码对不对。Fix the planted bugs before writing the TODOs, otherwise their errors make it hard to tell whether your own code is right.

接下来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 lessonTODO 1 · User.ordersTODO 1 · User.orders
    下一节Next lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: N+1 问题与 DataLoaderThe N+1 problem and DataLoader