Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: (graphcache) - Nullability Operators #2018

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion exchanges/graphcache/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@urql/exchange-graphcache",
"version": "4.3.5",
"version": "4.3.6-spec-nullability-2",
"description": "A normalized and configurable cache exchange for urql",
"sideEffects": false,
"homepage": "https://formidable.com/open-source/urql/docs/graphcache",
Expand Down
12 changes: 11 additions & 1 deletion exchanges/graphcache/src/cacheExchange.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { visit, DocumentNode } from 'graphql';

import {
Exchange,
formatDocument,
formatDocument as core_formatDocument,
makeOperation,
Operation,
OperationResult,
Expand Down Expand Up @@ -30,6 +32,14 @@ import { filterVariables, getMainOperation } from './ast';
import { Store, noopDataState, hydrateData, reserveLayer } from './store';
import { Data, Dependencies, CacheExchangeOpts } from './types';

/** Modified to strip out nullability operators. */
const formatDocument = <T extends DocumentNode>(node: T): T =>
core_formatDocument(
visit(node, {
Field: field => ({ ...field, required: 'unset' }),
})
);

type OperationResultWithMeta = OperationResult & {
outcome: CacheOutcome;
dependencies: Dependencies;
Expand Down
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
14 changes: 12 additions & 2 deletions exchanges/graphcache/src/operations/write.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { FieldNode, DocumentNode, FragmentDefinitionNode } from 'graphql';
import { DocumentNode, FragmentDefinitionNode } from 'graphql';
import { CombinedError } from '@urql/core';

import {
getFragments,
getMainOperation,
normalizeVariables,
getFieldArguments,
isFieldNullable,
isFieldAvailableOnType,
getSelectionSet,
getName,
Expand Down Expand Up @@ -197,7 +198,7 @@ const writeSelection = (
ctx
);

let node: FieldNode | void;
let node: ReturnType<typeof iterate>;
while ((node = iterate())) {
const fieldName = getName(node);
const fieldArgs = getFieldArguments(node, ctx.variables);
Expand Down Expand Up @@ -255,6 +256,15 @@ const writeSelection = (
fieldValue = data[fieldAlias] = ensureData(
resolver(fieldArgs || {}, ctx.store, ctx)
);
} else if (
fieldValue == null &&
node.required === 'optional' &&
(!ctx.store.schema ||
!isFieldNullable(ctx.store.schema, typename, fieldName))
) {
// If the field has errored or it's marked as optional (and we can't assume it's nullable),
// we erase it instead of writing it as null
fieldValue = undefined as any;
}

if (node.selectionSet) {
Expand Down