非空、列表,和那个没有 price 的 inputNon-null, lists, and the input that has no price
schema 里两处细节,直接决定四个 TODO 里三个的对错。Two details in the schema decide whether three of the four TODOs are right.
这一页有什么On this page5
- 读懂 ! 和 [] 的四种组合各是什么意思Read the four combinations of ! and [] and say what each means
- 解释为什么 [Order!]! 的 resolver 必须写 ?? []Explain why a resolver for [Order!]! must end with ?? []
- 看出 OrderItemInput 少了 price 会导致什么See what goes wrong because OrderItemInput has no price
- 知道非空字段返回 null 时错误会怎样向上冒泡Know how the error moves upward when a non-null field returns null
这一节讲的两处细节,是这门考试最典型的「不读 schema 就必错」的地方。审计时实测确认:createOrder 不补 price,测试直接失败。The two details in this lesson are the clearest case of what you get wrong by not reading the schema. Measured during the audit: if createOrder does not fill in price, the test fails.
graphql-federation-practice/node-subgraph/src/schema.graphql非空标记与 input 定义The non-null markers and the input definitions
graphql-federation-practice/node-subgraph/src/schema.graphqlgraphql-federation-practice/node-subgraph/src/dataSources/orderDataSource.jscreateOrder 里那行乘法暴露了 price 的必要性The multiplication inside createOrder shows why price is needed
graphql-federation-practice/node-subgraph/src/dataSources/orderDataSource.js! 和 [] 的四种组合The four combinations of ! and []
默认可空,加 ! 才不可空。列表和元素各有自己的可空性。Fields are nullable by default; ! makes them non-null. The list and its elements each have their own nullability.
GraphQL 里所有类型默认可空。! 是「保证不为 null」。 列表的方括号和元素各能带一个 !, 所以有四种组合:
| 写法 | 列表本身能是 null 吗 | 元素能是 null 吗 | 合法的值举例 |
|---|---|---|---|
[Order] | 能 | 能 | null、[]、[a, null] |
[Order!] | 能 | 不能 | null、[]、[a, b] |
[Order]! | 不能 | 能 | []、[a, null] |
[Order!]! | 不能 | 不能 | []、[a, b] |
这份 schema 里有两处用了最严格的[Order!]!:User.orders 和 Query.orders。 两个都是你要实现的 TODO。
实践结论:这两个 resolver 绝对不能返回null 或 undefined。「没有订单」的正确表达是空数组 [], 不是 null。所以真实答案里都写了return orders ?? []。
Every type in GraphQL is nullable by default. ! means “guaranteed not to be null”. The brackets of a list and the elements inside it can each carry their own !, which gives four combinations:
| Written as | Can the list be null | Can an element be null | Legal values |
|---|---|---|---|
[Order] | yes | yes | null, [], [a, null] |
[Order!] | yes | no | null, [], [a, b] |
[Order]! | no | yes | [], [a, null] |
[Order!]! | no | no | [], [a, b] |
This schema uses the strictest form, [Order!]!, in two places: User.orders and Query.orders. Both of them are TODOs you have to implement.
Practical conclusion: those two resolvers must never return null or undefined. The right way to say “no orders” is an empty array [], not null. Which is why both real answers write return orders ?? [].
graphql-federation-practice/node-subgraph/src/schema.graphql非空字段返回 null 会怎样:错误向上冒泡What happens when a non-null field returns null: the error moves upward
不是「那个字段变成 null」,是整块数据被丢掉。The field does not just become null. The whole block of data is dropped.
如果 Query.orders 返回了 null, GraphQL 执行器不会容忍 —— 它会:
- 在
errors数组里加一条Cannot return null for non-nullable field Query.orders。 - 把这个字段的值设为 null,然后往上冒泡 —— 如果父字段也是非空的,父字段也变 null,一直往上, 直到遇到一个可空的祖先,或者到根节点让整个
data变成null。
所以一个 resolver 忘了兜底,可能导致整个响应的 data 变成 null —— 客户端拿不到任何数据,即使其他字段都好着。 这就是为什么 schema 设计里「该可空的地方就标可空」很重要, 也是为什么这两个列表字段必须 ?? []。
反过来,Order.shippingInfo 是可空的, 所以「order-999 没有物流信息」这种情况返回 null完全正常,不会报错。数据源那边正是这么设计的 ——getShippingInfo 只有 order-456/457 有数据, 其余返回 null。测试也直接断言了这一点。
If Query.orders returns null, the GraphQL executor will not put up with it. It:
- adds
Cannot return null for non-nullable field Query.ordersto theerrorsarray. - sets that field to null and bubbles upward — if the parent field is also non-nullable, the parent becomes null too, and so on up the tree until it reaches a nullable ancestor, or hits the root and turns the whole
dataintonull.
So one resolver forgetting its fallback can turn the data of the whole response into null — the client gets nothing back, even though every other field was fine. That is why “mark it nullable where it should be nullable” matters in schema design, and why those two list fields need ?? [].
The other direction: Order.shippingInfo is nullable, so returning null for “order-999 has no shipping info” is perfectly normal and raises no error. The data source is built that way on purpose — getShippingInfo only has data for order-456 and order-457 and returns null for everything else. A test asserts exactly this.
OrderItemInput 少了 price —— 这是个陷阱OrderItemInput has no price — this is a trap
两个文件放在一起看,才能发现问题。You only see the problem when you read the two files side by side.
先看 schema 里的 input:只有 productId 和 quantity。 客户端调 createOrder 时不传 price(合理 —— 价格不能让客户端说)。
再看数据源的 createOrder:它内部要算 sum + item.price * item.quantity。
问题来了:如果 resolver 把客户端传来的 items 原样交给数据源,那 item.price 是undefined,undefined * 2 得到 NaN,totalAmount 变成 NaN。 而 totalAmount: Float! 收到 NaN 会序列化失败。
解法:resolver 必须先去InventoryDataSource.getProductPrice(productId)查每个商品的价格,把 items 补全之后再交给数据源。
这就是 InventoryDataSource 存在的原因 —— 它不是干扰项。(getInventoryStatus 才是干扰项, 没有任何地方需要它。)
测试怎么抓这个的?expect(order.items[0].price).toBeDefined() 和expect(order.totalAmount).toBeGreaterThan(0)。 审计时实测:不补 price,这个测试失败。
First, the input in the schema: only productId and quantity. A client calling createOrder never sends price — which is reasonable, the client does not get to name the price.
Now the createOrder in the data source: internally it computes sum + item.price * item.quantity.
Here is the problem: if the resolver hands the client’s items straight to the data source, then item.price is undefined, undefined * 2 gives NaN, and totalAmount becomes NaN. And totalAmount: Float! cannot serialise a NaN.
The fix: the resolver has to look up each product’s price with InventoryDataSource.getProductPrice(productId) and complete the items before handing them to the data source.
That is why InventoryDataSource exists — it is not a distractor. (getInventoryStatus is the distractor; nothing anywhere needs it.)
How does the test catch this? expect(order.items[0].price).toBeDefined() and expect(order.totalAmount).toBeGreaterThan(0). Measured during the audit: skip the price lookup and this test fails.
graphql-federation-practice/node-subgraph/src/schema.graphqlgraphql-federation-practice/node-subgraph/src/dataSources/orderDataSource.jsgraphql-federation-practice/node-subgraph/src/dataSources/orderDataSource.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.
schema 写的是 orders(userId: ID!): [Order!]!。 user 999 没有任何订单。resolver 该返回什么?
The schema says orders(userId: ID!): [Order!]!. User 999 has no orders at all. What should the resolver return?
客户端调 createOrder(userId: "789", items: [{ productId: "prod-789", quantity: 2 }])。 如果 resolver 把 items 原样交给orderDataSource.createOrder,会怎样?
The client calls createOrder(userId: "789", items: [{ productId: "prod-789", quantity: 2 }]). What happens if the resolver hands items straight to orderDataSource.createOrder?
照 schema 的非空标记,给每个 resolver 填上正确的返回表达式。 想清楚「这个字段能不能是 null」。
Go by the non-null markers in the schema and write the right return expression for each resolver. Decide first whether the field is allowed to be null.
换一道题也能用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.
- GraphQL 默认可空,加上 ! 才不可空;列表和元素各有自己的可空性。GraphQL fields are nullable by default; ! makes them non-null. The list and its elements each have their own nullability.
- [Order!]! 的 resolver 必须 ?? [] —— 「没有」的正确表达是空数组。A resolver for [Order!]! must use ?? []. Here an empty array is how you say there is nothing.
- 非空字段返回 null 会向上冒泡,可能让整个 data 变成 null。When a non-null field returns null the error moves upward and can turn the whole data object into null.
- shippingInfo 可空,测试断言 toBeNull —— 所以要显式 ?? null,别让 undefined 漏出去。shippingInfo is nullable and the test asserts toBeNull, so write ?? null explicitly and do not let undefined through.
- OrderItemInput 没有 price,而数据源要用它算总价 → resolver 必须先查 getProductPrice。OrderItemInput has no price, but the data source needs it to compute the total, so the resolver must call getProductPrice first.