diff --git a/exchanges/graphcache/package.json b/exchanges/graphcache/package.json index adcb0d6467..37772b08ed 100644 --- a/exchanges/graphcache/package.json +++ b/exchanges/graphcache/package.json @@ -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", diff --git a/exchanges/graphcache/src/cacheExchange.ts b/exchanges/graphcache/src/cacheExchange.ts index aeb258498e..b7df1b3fc4 100644 --- a/exchanges/graphcache/src/cacheExchange.ts +++ b/exchanges/graphcache/src/cacheExchange.ts @@ -1,6 +1,8 @@ +import { visit, DocumentNode } from 'graphql'; + import { Exchange, - formatDocument, + formatDocument as core_formatDocument, makeOperation, Operation, OperationResult, @@ -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 = (node: T): T => + core_formatDocument( + visit(node, { + Field: field => ({ ...field, required: 'unset' }), + }) + ); + type OperationResultWithMeta = OperationResult & { outcome: CacheOutcome; dependencies: Dependencies; diff --git a/exchanges/graphcache/src/operations/query.ts b/exchanges/graphcache/src/operations/query.ts index a7bf781a73..1938aab5f0 100644 --- a/exchanges/graphcache/src/operations/query.ts +++ b/exchanges/graphcache/src/operations/query.ts @@ -306,7 +306,7 @@ const readSelection = ( let hasFields = false; let hasPartials = false; let hasChanged = typename !== input.__typename; - let node: FieldNode | void; + let node: ReturnType; const output = makeData(input); while ((node = iterate()) !== undefined) { // Derive the needed data from our node. @@ -314,6 +314,7 @@ const readSelection = ( 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; @@ -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; diff --git a/exchanges/graphcache/src/operations/shared.ts b/exchanges/graphcache/src/operations/shared.ts index a90013c341..0468933c71 100644 --- a/exchanges/graphcache/src/operations/shared.ts +++ b/exchanges/graphcache/src/operations/shared.ts @@ -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 = ( diff --git a/exchanges/graphcache/src/operations/write.ts b/exchanges/graphcache/src/operations/write.ts index ef7c6a6d8f..0644895646 100644 --- a/exchanges/graphcache/src/operations/write.ts +++ b/exchanges/graphcache/src/operations/write.ts @@ -1,4 +1,4 @@ -import { FieldNode, DocumentNode, FragmentDefinitionNode } from 'graphql'; +import { DocumentNode, FragmentDefinitionNode } from 'graphql'; import { CombinedError } from '@urql/core'; import { @@ -6,6 +6,7 @@ import { getMainOperation, normalizeVariables, getFieldArguments, + isFieldNullable, isFieldAvailableOnType, getSelectionSet, getName, @@ -197,7 +198,7 @@ const writeSelection = ( ctx ); - let node: FieldNode | void; + let node: ReturnType; while ((node = iterate())) { const fieldName = getName(node); const fieldArgs = getFieldArguments(node, ctx.variables); @@ -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) {