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

(graphcache) - Fix uncached GraphQLError fields making it impossible to get first API result #1367

Merged
merged 3 commits into from
Feb 5, 2021
Merged
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
64 changes: 64 additions & 0 deletions exchanges/graphcache/src/cacheExchange.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ExchangeIO,
Operation,
OperationResult,
CombinedError,
} from '@urql/core';

import {
Expand Down Expand Up @@ -610,6 +611,69 @@ describe('data dependencies', () => {
jest.runAllTimers();
expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(2);
});

it('marks errored null fields as uncached but delivers them as expected', () => {
const client = createClient({ url: 'http://0.0.0.0' });
const { source: ops$, next } = makeSubject<Operation>();

const query = gql`
{
field
author {
id
}
}
`;

const operation = client.createRequestOperation('query', {
key: 1,
query,
});

const queryResult: OperationResult = {
operation,
data: {
__typename: 'Query',
field: 'test',
author: null,
},
error: new CombinedError({
graphQLErrors: [
{
message: 'Test',
path: ['author'],
},
],
}),
};

const reexecuteOperation = jest
.spyOn(client, 'reexecuteOperation')
.mockImplementation(next);

const response = jest.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) return queryResult;
return undefined as any;
}
);

const result = jest.fn();
const forward: ExchangeIO = ops$ => pipe(ops$, map(response));

pipe(
cacheExchange({})({ forward, client, dispatchDebug })(ops$),
tap(result),
publish
);

next(operation);

expect(response).toHaveBeenCalledTimes(1);
expect(result).toHaveBeenCalledTimes(1);
expect(reexecuteOperation).toHaveBeenCalledTimes(0);
expect(result.mock.calls[0][0]).toHaveProperty('data.author', null);
});
});

describe('optimistic updates', () => {
Expand Down
8 changes: 7 additions & 1 deletion exchanges/graphcache/src/cacheExchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,13 @@ export const cacheExchange = (opts?: CacheExchangeOpts): Exchange => ({
).dependencies;
collectPendingOperations(pendingOperations, writeDependencies);

const queryResult = query(store, operation, result.data, key);
const queryResult = query(
store,
operation,
result.data,
result.error,
key
);
result.data = queryResult.data;
if (operation.kind === 'query') {
// Collect the query's dependencies for future pending operation updates
Expand Down
52 changes: 31 additions & 21 deletions exchanges/graphcache/src/operations/query.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { FieldNode, DocumentNode, FragmentDefinitionNode } from 'graphql';
import { CombinedError } from '@urql/core';

import {
getSelectionSet,
getName,
SelectionSet,
getFragmentTypeName,
getFieldAlias,
} from '../ast';

import {
getFragments,
getMainOperation,
normalizeVariables,
Expand Down Expand Up @@ -44,6 +42,7 @@ import {
ensureData,
makeContext,
updateContext,
getFieldError,
} from './shared';

import {
Expand All @@ -62,18 +61,20 @@ export const query = (
store: Store,
request: OperationRequest,
data?: Data,
error?: CombinedError | undefined,
key?: number
): QueryResult => {
initDataState('read', store.data, (data && key) || null);
const result = read(store, request, data);
const result = read(store, request, data, error);
clearDataState();
return result;
};

export const read = (
store: Store,
request: OperationRequest,
input?: Data
input?: Data,
error?: CombinedError | undefined
): QueryResult => {
const operation = getMainOperation(request.query);
const rootKey = store.rootFields[operation.operation];
Expand All @@ -84,7 +85,9 @@ export const read = (
normalizeVariables(operation, request.variables),
getFragments(request.query),
rootKey,
rootKey
rootKey,
false,
error
);

if (process.env.NODE_ENV !== 'production') {
Expand Down Expand Up @@ -117,21 +120,22 @@ const readRoot = (
select: SelectionSet,
originalData: Data
): Data => {
if (typeof originalData.__typename !== 'string') {
const typename = ctx.store.rootNames[entityKey]
? entityKey
: originalData.__typename;
if (typeof typename !== 'string') {
return originalData;
}

const iterate = makeSelectionIterator(entityKey, entityKey, select, ctx);
const data = {} as Data;
data.__typename = originalData.__typename;
const data = { __typename: typename };

let node: FieldNode | void;
while ((node = iterate())) {
const fieldAlias = getFieldAlias(node);
const fieldValue = originalData[fieldAlias];
// Add the current alias to the walked path before processing the field's value
if (process.env.NODE_ENV !== 'production')
ctx.__internal.path.push(fieldAlias);
ctx.__internal.path.push(fieldAlias);
// Process the root field's value
if (node.selectionSet && fieldValue !== null) {
const fieldData = ensureData(fieldValue);
Expand All @@ -140,7 +144,7 @@ const readRoot = (
data[fieldAlias] = fieldValue;
}
// After processing the field, remove the current alias from the path again
if (process.env.NODE_ENV !== 'production') ctx.__internal.path.pop();
ctx.__internal.path.pop();
}

return data;
Expand All @@ -155,11 +159,11 @@ const readRootField = (
const newData = new Array(originalData.length);
for (let i = 0, l = originalData.length; i < l; i++) {
// Add the current index to the walked path before reading the field's value
if (process.env.NODE_ENV !== 'production') ctx.__internal.path.push(i);
ctx.__internal.path.push(i);
// Recursively read the root field's value
newData[i] = readRootField(ctx, select, originalData[i]);
// After processing the field, remove the current index from the path
if (process.env.NODE_ENV !== 'production') ctx.__internal.path.pop();
ctx.__internal.path.pop();
}

return newData;
Expand Down Expand Up @@ -312,8 +316,7 @@ const readSelection = (
// means that the value is missing from the cache
let dataFieldValue: void | DataField;
// Add the current alias to the walked path before processing the field's value
if (process.env.NODE_ENV !== 'production')
ctx.__internal.path.push(fieldAlias);
ctx.__internal.path.push(fieldAlias);

if (resultValue !== undefined && node.selectionSet === undefined) {
// The field is a scalar and can be retrieved directly from the result
Expand Down Expand Up @@ -396,8 +399,15 @@ const readSelection = (
}
}

// If we have an error registered for the current field change undefined values to null
if (dataFieldValue === undefined && !!getFieldError(ctx)) {
hasPartials = true;
dataFieldValue = null;
}

// After processing the field, remove the current alias from the path again
if (process.env.NODE_ENV !== 'production') ctx.__internal.path.pop();
ctx.__internal.path.pop();

// Now that dataFieldValue has been retrieved it'll be set on data
// If it's uncached (undefined) but nullable we can continue assembling
// a partial query result
Expand Down Expand Up @@ -442,7 +452,7 @@ const resolveResolverResult = (
const data = new Array(result.length);
for (let i = 0, l = result.length; i < l; i++) {
// Add the current index to the walked path before reading the field's value
if (process.env.NODE_ENV !== 'production') ctx.__internal.path.push(i);
ctx.__internal.path.push(i);
// Recursively read resolver result
const childResult = resolveResolverResult(
ctx,
Expand All @@ -455,7 +465,7 @@ const resolveResolverResult = (
result[i]
);
// After processing the field, remove the current index from the path
if (process.env.NODE_ENV !== 'production') ctx.__internal.path.pop();
ctx.__internal.path.pop();
// Check the result for cache-missed values
if (childResult === undefined && !_isListNullable) {
return undefined;
Expand Down Expand Up @@ -504,7 +514,7 @@ const resolveLink = (
const newLink = new Array(link.length);
for (let i = 0, l = link.length; i < l; i++) {
// Add the current index to the walked path before reading the field's value
if (process.env.NODE_ENV !== 'production') ctx.__internal.path.push(i);
ctx.__internal.path.push(i);
// Recursively read the link
const childLink = resolveLink(
ctx,
Expand All @@ -515,7 +525,7 @@ const resolveLink = (
prevData != null ? prevData[i] : undefined
);
// After processing the field, remove the current index from the path
if (process.env.NODE_ENV !== 'production') ctx.__internal.path.pop();
ctx.__internal.path.pop();
// Check the result for cache-missed values
if (childLink === undefined && !_isListNullable) {
return undefined;
Expand Down