Skip to content

Commit 82fdbdc

Browse files
committed
Implement nullability operators in Graphcache
The ["Nullability RFC" for GraphQL](graphql/graphql-wg#694) allows fields to individually be marked as optional or required in a query by the client-side. ([See Strawman Proposal](graphql/graphql-spec#867)) If a field is marked as optional then it's allowed to be missing and `null`, which can control where missing values cascade to: ```graphql query { me { name? } } ``` If a field is marked as required it may never be allowed to become `null` and must cascade if it otherwise would have been set to `null`: ```graphql query { me { name! } } ``` In Graphcache, we imagine that the nullable field — which would be marked with `required: 'optional'` — can allow Graphcache to make more data nullable and hence partial, which enhances schema awareness, even if it's not actively used. The required fields — which would be marked with `required: 'required'` — would force Graphcache to include this data, regardless of what schema awareness may say, which also enhances partial data in the presence of schema awareness, since it increases what the cache may deliver. In other words, it guarantees a "forced outcome" in both cases, without having to look up whether a field is nullable in the schema. In the future, we may even derive the `RequiredStatus` of a `FieldNode` in an external place and never call `isFieldNullable` with a schema in the query traversal.
1 parent 83f5155 commit 82fdbdc

File tree

2 files changed

+15
-5
lines changed

2 files changed

+15
-5
lines changed

Diff for: exchanges/graphcache/src/operations/query.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -306,14 +306,15 @@ const readSelection = (
306306
let hasFields = false;
307307
let hasPartials = false;
308308
let hasChanged = typename !== input.__typename;
309-
let node: FieldNode | void;
309+
let node: ReturnType<typeof iterate>;
310310
const output = makeData(input);
311311
while ((node = iterate()) !== undefined) {
312312
// Derive the needed data from our node.
313313
const fieldName = getName(node);
314314
const fieldArgs = getFieldArguments(node, ctx.variables);
315315
const fieldAlias = getFieldAlias(node);
316316
const fieldKey = keyOfField(fieldName, fieldArgs);
317+
const fieldRequired = node.required || 'unset';
317318
const key = joinKeys(entityKey, fieldKey);
318319
const fieldValue = InMemoryData.readRecord(entityKey, fieldKey);
319320
const resultValue = result ? result[fieldName] : undefined;
@@ -430,13 +431,17 @@ const readSelection = (
430431
hasFields = true;
431432
} else if (
432433
dataFieldValue === undefined &&
433-
((store.schema && isFieldNullable(store.schema, typename, fieldName)) ||
434+
(fieldRequired === 'optional' ||
435+
(store.schema && isFieldNullable(store.schema, typename, fieldName)) ||
434436
!!getFieldError(ctx))
435437
) {
436-
// The field is uncached or has errored, so it'll be set to null and skipped
438+
// The field is skipped since it's nullable & uncached, marked as optional, or has errored
437439
hasPartials = true;
438440
dataFieldValue = null;
439-
} else if (dataFieldValue === undefined) {
441+
} else if (
442+
(fieldRequired === 'required' && dataFieldValue == null) ||
443+
dataFieldValue === undefined
444+
) {
440445
// If the field isn't deferred or partial then we have to abort
441446
ctx.__internal.path.pop();
442447
return undefined;

Diff for: exchanges/graphcache/src/operations/shared.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,13 @@ const isFragmentHeuristicallyMatching = (
148148
});
149149
};
150150

151+
export type RequiredStatus = 'required' | 'optional' | 'unset';
152+
export interface ExtendedFieldNode extends FieldNode {
153+
readonly required?: RequiredStatus;
154+
}
155+
151156
interface SelectionIterator {
152-
(): FieldNode | undefined;
157+
(): ExtendedFieldNode | undefined;
153158
}
154159

155160
export const makeSelectionIterator = (

0 commit comments

Comments
 (0)