Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.

fix(graphcache): incorrectly matching all concrete types (#3603)

Changed files
+139 -9
.changeset
exchanges
graphcache
src
+5
.changeset/red-eyes-lick.md
···
+
---
+
'@urql/exchange-graphcache': patch
+
---
+
+
Fix where we would incorrectly match all fragment concrete types because they belong to the abstract type
+119
exchanges/graphcache/src/cacheExchange.test.ts
···
expect(failingData).not.toMatchObject({ hasNext: true });
});
});
+
+
describe('abstract types', () => {
+
it('works with two responses giving different concrete types for a union', () => {
+
const query = gql`
+
query ($id: ID!) {
+
field(id: $id) {
+
id
+
union {
+
... on Type1 {
+
id
+
name
+
__typename
+
}
+
... on Type2 {
+
id
+
title
+
__typename
+
}
+
}
+
__typename
+
}
+
}
+
`;
+
const client = createClient({
+
url: 'http://0.0.0.0',
+
exchanges: [],
+
});
+
const { source: ops$, next } = makeSubject<Operation>();
+
const operation1 = client.createRequestOperation('query', {
+
key: 1,
+
query,
+
variables: { id: '1' },
+
});
+
const operation2 = client.createRequestOperation('query', {
+
key: 2,
+
query,
+
variables: { id: '2' },
+
});
+
const queryResult1: OperationResult = {
+
...queryResponse,
+
operation: operation1,
+
data: {
+
__typename: 'Query',
+
field: {
+
id: '1',
+
__typename: 'Todo',
+
union: {
+
id: '1',
+
name: 'test',
+
__typename: 'Type1',
+
},
+
},
+
},
+
};
+
+
const queryResult2: OperationResult = {
+
...queryResponse,
+
operation: operation2,
+
data: {
+
__typename: 'Query',
+
field: {
+
id: '2',
+
__typename: 'Todo',
+
union: {
+
id: '2',
+
title: 'test',
+
__typename: 'Type2',
+
},
+
},
+
},
+
};
+
+
vi.spyOn(client, 'reexecuteOperation').mockImplementation(next);
+
const response = vi.fn((forwardOp: Operation): OperationResult => {
+
if (forwardOp.key === 1) return queryResult1;
+
if (forwardOp.key === 2) return queryResult2;
+
return undefined as any;
+
});
+
+
const result = vi.fn();
+
const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);
+
+
pipe(
+
cacheExchange({})({ forward, client, dispatchDebug })(ops$),
+
tap(result),
+
publish
+
);
+
+
next(operation1);
+
expect(response).toHaveBeenCalledTimes(1);
+
expect(result).toHaveBeenCalledTimes(1);
+
expect(result.mock.calls[0][0].data).toEqual({
+
field: {
+
__typename: 'Todo',
+
id: '1',
+
union: {
+
__typename: 'Type1',
+
id: '1',
+
name: 'test',
+
},
+
},
+
});
+
+
next(operation2);
+
expect(response).toHaveBeenCalledTimes(2);
+
expect(result).toHaveBeenCalledTimes(2);
+
expect(result.mock.calls[1][0].data).toEqual({
+
field: {
+
__typename: 'Todo',
+
id: '2',
+
union: {
+
__typename: 'Type2',
+
id: '2',
+
title: 'test',
+
},
+
},
+
});
+
});
+
});
+12 -9
exchanges/graphcache/src/operations/shared.ts
···
currentOptimistic,
writeConcreteType,
getConcreteTypes,
+
isSeenConcreteType,
} from '../store/data';
import { keyOfField } from '../store/keys';
import type { Store } from '../store/store';
···
logger
);
-
return (
-
currentOperation === 'write' ||
-
!getSelectionSet(node).some(node => {
-
if (node.kind !== Kind.FIELD) return false;
-
const fieldKey = keyOfField(getName(node), getFieldArguments(node, vars));
-
return !hasField(entityKey, fieldKey);
-
})
-
);
+
return !getSelectionSet(node).some(node => {
+
if (node.kind !== Kind.FIELD) return false;
+
const fieldKey = keyOfField(getName(node), getFieldArguments(node, vars));
+
return !hasField(entityKey, fieldKey);
+
});
};
interface SelectionIterator {
···
ctx.store.logger
));
-
if (isMatching) {
+
if (isMatching || currentOperation === 'write') {
if (process.env.NODE_ENV !== 'production')
pushDebugNode(typename, fragment);
const isFragmentOptional = isOptional(select);
if (
+
isMatching &&
fragment.typeCondition &&
typename !== fragment.typeCondition.name.value
) {
···
const isFragmentMatching = (typeCondition: string, typename: string | void) => {
if (!typename) return false;
if (typeCondition === typename) return true;
+
+
const isProbableAbstractType = !isSeenConcreteType(typeCondition);
+
if (!isProbableAbstractType) return false;
+
const types = getConcreteTypes(typeCondition);
return types.size && types.has(typename);
};
+3
exchanges/graphcache/src/store/data.ts
···
export const getConcreteTypes = (typename: string): Set<string> =>
currentData!.abstractToConcreteMap.get(typename) || DEFAULT_EMPTY_SET;
+
export const isSeenConcreteType = (typename: string): boolean =>
+
currentData!.types.has(typename);
+
export const writeConcreteType = (
abstractType: string,
concreteType: string