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

Configure Feed

Select the types of activity you want to include in your feed.

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

authored by

Jovi De Croock and committed by
GitHub
befdf48d d07602d2

+139 -9
+5
.changeset/red-eyes-lick.md
··· 1 + --- 2 + '@urql/exchange-graphcache': patch 3 + --- 4 + 5 + Fix where we would incorrectly match all fragment concrete types because they belong to the abstract type
+119
exchanges/graphcache/src/cacheExchange.test.ts
··· 3852 3852 expect(failingData).not.toMatchObject({ hasNext: true }); 3853 3853 }); 3854 3854 }); 3855 + 3856 + describe('abstract types', () => { 3857 + it('works with two responses giving different concrete types for a union', () => { 3858 + const query = gql` 3859 + query ($id: ID!) { 3860 + field(id: $id) { 3861 + id 3862 + union { 3863 + ... on Type1 { 3864 + id 3865 + name 3866 + __typename 3867 + } 3868 + ... on Type2 { 3869 + id 3870 + title 3871 + __typename 3872 + } 3873 + } 3874 + __typename 3875 + } 3876 + } 3877 + `; 3878 + const client = createClient({ 3879 + url: 'http://0.0.0.0', 3880 + exchanges: [], 3881 + }); 3882 + const { source: ops$, next } = makeSubject<Operation>(); 3883 + const operation1 = client.createRequestOperation('query', { 3884 + key: 1, 3885 + query, 3886 + variables: { id: '1' }, 3887 + }); 3888 + const operation2 = client.createRequestOperation('query', { 3889 + key: 2, 3890 + query, 3891 + variables: { id: '2' }, 3892 + }); 3893 + const queryResult1: OperationResult = { 3894 + ...queryResponse, 3895 + operation: operation1, 3896 + data: { 3897 + __typename: 'Query', 3898 + field: { 3899 + id: '1', 3900 + __typename: 'Todo', 3901 + union: { 3902 + id: '1', 3903 + name: 'test', 3904 + __typename: 'Type1', 3905 + }, 3906 + }, 3907 + }, 3908 + }; 3909 + 3910 + const queryResult2: OperationResult = { 3911 + ...queryResponse, 3912 + operation: operation2, 3913 + data: { 3914 + __typename: 'Query', 3915 + field: { 3916 + id: '2', 3917 + __typename: 'Todo', 3918 + union: { 3919 + id: '2', 3920 + title: 'test', 3921 + __typename: 'Type2', 3922 + }, 3923 + }, 3924 + }, 3925 + }; 3926 + 3927 + vi.spyOn(client, 'reexecuteOperation').mockImplementation(next); 3928 + const response = vi.fn((forwardOp: Operation): OperationResult => { 3929 + if (forwardOp.key === 1) return queryResult1; 3930 + if (forwardOp.key === 2) return queryResult2; 3931 + return undefined as any; 3932 + }); 3933 + 3934 + const result = vi.fn(); 3935 + const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); 3936 + 3937 + pipe( 3938 + cacheExchange({})({ forward, client, dispatchDebug })(ops$), 3939 + tap(result), 3940 + publish 3941 + ); 3942 + 3943 + next(operation1); 3944 + expect(response).toHaveBeenCalledTimes(1); 3945 + expect(result).toHaveBeenCalledTimes(1); 3946 + expect(result.mock.calls[0][0].data).toEqual({ 3947 + field: { 3948 + __typename: 'Todo', 3949 + id: '1', 3950 + union: { 3951 + __typename: 'Type1', 3952 + id: '1', 3953 + name: 'test', 3954 + }, 3955 + }, 3956 + }); 3957 + 3958 + next(operation2); 3959 + expect(response).toHaveBeenCalledTimes(2); 3960 + expect(result).toHaveBeenCalledTimes(2); 3961 + expect(result.mock.calls[1][0].data).toEqual({ 3962 + field: { 3963 + __typename: 'Todo', 3964 + id: '2', 3965 + union: { 3966 + __typename: 'Type2', 3967 + id: '2', 3968 + title: 'test', 3969 + }, 3970 + }, 3971 + }); 3972 + }); 3973 + });
+12 -9
exchanges/graphcache/src/operations/shared.ts
··· 23 23 currentOptimistic, 24 24 writeConcreteType, 25 25 getConcreteTypes, 26 + isSeenConcreteType, 26 27 } from '../store/data'; 27 28 import { keyOfField } from '../store/keys'; 28 29 import type { Store } from '../store/store'; ··· 153 154 logger 154 155 ); 155 156 156 - return ( 157 - currentOperation === 'write' || 158 - !getSelectionSet(node).some(node => { 159 - if (node.kind !== Kind.FIELD) return false; 160 - const fieldKey = keyOfField(getName(node), getFieldArguments(node, vars)); 161 - return !hasField(entityKey, fieldKey); 162 - }) 163 - ); 157 + return !getSelectionSet(node).some(node => { 158 + if (node.kind !== Kind.FIELD) return false; 159 + const fieldKey = keyOfField(getName(node), getFieldArguments(node, vars)); 160 + return !hasField(entityKey, fieldKey); 161 + }); 164 162 }; 165 163 166 164 interface SelectionIterator { ··· 238 236 ctx.store.logger 239 237 )); 240 238 241 - if (isMatching) { 239 + if (isMatching || currentOperation === 'write') { 242 240 if (process.env.NODE_ENV !== 'production') 243 241 pushDebugNode(typename, fragment); 244 242 const isFragmentOptional = isOptional(select); 245 243 if ( 244 + isMatching && 246 245 fragment.typeCondition && 247 246 typename !== fragment.typeCondition.name.value 248 247 ) { ··· 272 271 const isFragmentMatching = (typeCondition: string, typename: string | void) => { 273 272 if (!typename) return false; 274 273 if (typeCondition === typename) return true; 274 + 275 + const isProbableAbstractType = !isSeenConcreteType(typeCondition); 276 + if (!isProbableAbstractType) return false; 277 + 275 278 const types = getConcreteTypes(typeCondition); 276 279 return types.size && types.has(typename); 277 280 };
+3
exchanges/graphcache/src/store/data.ts
··· 492 492 export const getConcreteTypes = (typename: string): Set<string> => 493 493 currentData!.abstractToConcreteMap.get(typename) || DEFAULT_EMPTY_SET; 494 494 495 + export const isSeenConcreteType = (typename: string): boolean => 496 + currentData!.types.has(typename); 497 + 495 498 export const writeConcreteType = ( 496 499 abstractType: string, 497 500 concreteType: string