Skip to content

Commit

Permalink
Implement nullability operators in Graphcache
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
kitten committed Oct 8, 2021
1 parent 83f5155 commit 82fdbdc
Show file tree
Hide file tree
Showing 2 changed files with 15 additions and 5 deletions.
13 changes: 9 additions & 4 deletions exchanges/graphcache/src/operations/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,14 +306,15 @@ const readSelection = (
let hasFields = false;
let hasPartials = false;
let hasChanged = typename !== input.__typename;
let node: FieldNode | void;
let node: ReturnType<typeof iterate>;
const output = makeData(input);
while ((node = iterate()) !== undefined) {
// Derive the needed data from our node.
const fieldName = getName(node);
const fieldArgs = getFieldArguments(node, ctx.variables);
const fieldAlias = getFieldAlias(node);
const fieldKey = keyOfField(fieldName, fieldArgs);
const fieldRequired = node.required || 'unset';
const key = joinKeys(entityKey, fieldKey);
const fieldValue = InMemoryData.readRecord(entityKey, fieldKey);
const resultValue = result ? result[fieldName] : undefined;
Expand Down Expand Up @@ -430,13 +431,17 @@ const readSelection = (
hasFields = true;
} else if (
dataFieldValue === undefined &&
((store.schema && isFieldNullable(store.schema, typename, fieldName)) ||
(fieldRequired === 'optional' ||
(store.schema && isFieldNullable(store.schema, typename, fieldName)) ||
!!getFieldError(ctx))
) {
// The field is uncached or has errored, so it'll be set to null and skipped
// The field is skipped since it's nullable & uncached, marked as optional, or has errored
hasPartials = true;
dataFieldValue = null;
} else if (dataFieldValue === undefined) {
} else if (
(fieldRequired === 'required' && dataFieldValue == null) ||
dataFieldValue === undefined
) {
// If the field isn't deferred or partial then we have to abort
ctx.__internal.path.pop();
return undefined;
Expand Down
7 changes: 6 additions & 1 deletion exchanges/graphcache/src/operations/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,13 @@ const isFragmentHeuristicallyMatching = (
});
};

export type RequiredStatus = 'required' | 'optional' | 'unset';
export interface ExtendedFieldNode extends FieldNode {
readonly required?: RequiredStatus;
}

interface SelectionIterator {
(): FieldNode | undefined;
(): ExtendedFieldNode | undefined;
}

export const makeSelectionIterator = (
Expand Down

0 comments on commit 82fdbdc

Please sign in to comment.