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.

(graphcache) - Increase consistency of __typename in cached results (#1185)

* Add `__typename` field as instructed by selection sets

The `__typename` field does not need to be added to root
types by default and must not be added first, as it shouldn't
be necessary. Instead we can assume that it's in the result
and copy it over once it's reached in the selection iteration.

* Update testing fixtures for query.test.ts

* Add changeset

* Update various test suites

* Restore skipped __typename field on write

* Update __typename field assignment to add alias

* Format documents before reading from cache in Store

* Add warning for writing selection sets without __typename

* Check test suites for silent warnings

* Update changeset

* Update Changeset to minor bump

I believe this should be a minor bump since we're adding a new
warning.

authored by

Phil Pluckthun and committed by
GitHub
b608a41b 403a263a

+145 -43
+9
.changeset/smart-lizards-crash.md
··· 1 + --- 2 + '@urql/exchange-graphcache': minor 3 + --- 4 + 5 + Increase the consistency of when and how the `__typename` field is added to results. Instead of 6 + adding it by default and automatically first, the `__typename` field will now be added along with 7 + the usual selection set. The `write` operation now automatically issues a warning if `__typename` 8 + isn't present where it's expected more often, which helps in debugging. Also the `__typename` field 9 + may now not proactively be added to root results, e.g. `"Query"`.
+11
docs/graphcache/errors.md
··· 194 194 GraphQL results should never contain an `undefined` value, so this warning will let you 195 195 know which part of your result did contain `undefined`. 196 196 197 + ## (14) Couldn't find \_\_typename when writing. 198 + 199 + > Couldn't find **typename when writing. 200 + > If you're writing to the cache manually have to pass a `**typename` property on each entity in your data. 201 + 202 + You probably have called `cache.writeFragment` or `cache.updateQuery` with data that is missing a 203 + `__typename` field for an entity where your document contains a selection set. The cache won't be 204 + able to generate a key for entities that are missing the `__typename` field. 205 + 206 + Please make sure that you include enough properties on your data so that `write` can generate a key. 207 + 197 208 ## (15) Invalid key 198 209 199 210 > Invalid key: The GraphQL query at the field at `???` has a selection set,
+1 -1
exchanges/graphcache/default-storage/package.json
··· 15 15 "./package.json": "./package.json" 16 16 }, 17 17 "dependencies": { 18 - "@urql/core": ">=1.14.1", 18 + "@urql/core": ">=1.15.2", 19 19 "wonka": "^4.0.14" 20 20 } 21 21 }
+1 -1
exchanges/graphcache/src/cacheExchange.test.ts
··· 1011 1011 expect(reexec).toHaveBeenCalledTimes(1); 1012 1012 1013 1013 jest.runAllTimers(); 1014 + 1014 1015 expect(updates.Mutation.addAuthor).toHaveBeenCalledTimes(2); 1015 1016 expect(response).toHaveBeenCalledTimes(2); 1016 1017 expect(result).toHaveBeenCalledTimes(4); ··· 1456 1457 expect(reexec).toHaveBeenCalledTimes(1); 1457 1458 expect(result.mock.calls[1][0].stale).toBe(true); 1458 1459 expect(result.mock.calls[1][0].data).toEqual({ 1459 - __typename: 'Query', 1460 1460 todos: [ 1461 1461 { 1462 1462 __typename: 'Todo',
+14
exchanges/graphcache/src/extras/relayPagination.test.ts
··· 20 20 it('works with forward pagination', () => { 21 21 const Pagination = gql` 22 22 query($cursor: String) { 23 + __typename 23 24 items(first: 1, after: $cursor) { 24 25 __typename 25 26 edges { ··· 97 98 it('works with backwards pagination', () => { 98 99 const Pagination = gql` 99 100 query($cursor: String) { 101 + __typename 100 102 items(last: 1, before: $cursor) { 101 103 __typename 102 104 edges { ··· 174 176 it('handles duplicate edges', () => { 175 177 const Pagination = gql` 176 178 query($cursor: String) { 179 + __typename 177 180 items(first: 2, after: $cursor) { 178 181 __typename 179 182 edges { ··· 259 262 it('works with simultaneous forward and backward pagination (outwards merging)', () => { 260 263 const Pagination = gql` 261 264 query($first: Int, $last: Int, $before: String, $after: String) { 265 + __typename 262 266 items(first: $first, last: $last, before: $before, after: $after) { 263 267 __typename 264 268 edges { ··· 389 393 it('works with simultaneous forward and backward pagination (inwards merging)', () => { 390 394 const Pagination = gql` 391 395 query($first: Int, $last: Int, $before: String, $after: String) { 396 + __typename 392 397 items(first: $first, last: $last, before: $before, after: $after) { 393 398 __typename 394 399 edges { ··· 652 657 it('returns other fields on the same level as the edges', () => { 653 658 const Pagination = gql` 654 659 query { 660 + __typename 655 661 items(first: 1) { 662 + __typename 656 663 totalCount 657 664 } 658 665 } ··· 691 698 it('returns a subset of the cached items if the query requests less items than the cached ones', () => { 692 699 const Pagination = gql` 693 700 query($first: Int, $last: Int, $before: String, $after: String) { 701 + __typename 694 702 items(first: $first, last: $last, before: $before, after: $after) { 695 703 __typename 696 704 edges { ··· 754 762 it("returns the cached items even if they don't fullfil the query", () => { 755 763 const Pagination = gql` 756 764 query($first: Int, $last: Int, $before: String, $after: String) { 765 + __typename 757 766 items(first: $first, last: $last, before: $before, after: $after) { 758 767 __typename 759 768 edges { ··· 821 830 it('returns the cached items even when they come from a different query', () => { 822 831 const Pagination = gql` 823 832 query($first: Int, $last: Int, $before: String, $after: String) { 833 + __typename 824 834 items(first: $first, last: $last, before: $before, after: $after) { 825 835 __typename 826 836 edges { ··· 884 894 it('caches and retrieves correctly queries with inwards pagination', () => { 885 895 const Pagination = gql` 886 896 query($first: Int, $last: Int, $before: String, $after: String) { 897 + __typename 887 898 items(first: $first, last: $last, before: $before, after: $after) { 888 899 __typename 889 900 edges { ··· 951 962 it('does not include a previous result when adding parameters', () => { 952 963 const Pagination = gql` 953 964 query($first: Int, $filter: String) { 965 + __typename 954 966 items(first: $first, filter: $filter) { 955 967 __typename 956 968 edges { ··· 1033 1045 it('Works with edges absent from query', () => { 1034 1046 const Pagination = gql` 1035 1047 query($first: Int, $last: Int, $before: String, $after: String) { 1048 + __typename 1036 1049 items(first: $first, last: $last, before: $before, after: $after) { 1037 1050 __typename 1038 1051 nodes { ··· 1088 1101 it('Works with nodes absent from query', () => { 1089 1102 const Pagination = gql` 1090 1103 query($first: Int, $last: Int, $before: String, $after: String) { 1104 + __typename 1091 1105 items(first: $first, last: $last, before: $before, after: $after) { 1092 1106 __typename 1093 1107 edges {
+8
exchanges/graphcache/src/extras/simplePagination.test.ts
··· 6 6 it('works with forward pagination', () => { 7 7 const Pagination = gql` 8 8 query($skip: Number, $limit: Number) { 9 + __typename 9 10 persons(skip: $skip, limit: $limit) { 10 11 __typename 11 12 id ··· 76 77 it('works with backwards pagination', () => { 77 78 const Pagination = gql` 78 79 query($skip: Number, $limit: Number) { 80 + __typename 79 81 persons(skip: $skip, limit: $limit) { 80 82 __typename 81 83 id ··· 146 148 it('handles duplicates', () => { 147 149 const Pagination = gql` 148 150 query($skip: Number, $limit: Number) { 151 + __typename 149 152 persons(skip: $skip, limit: $limit) { 150 153 __typename 151 154 id ··· 204 207 it('should not return previous result when adding a parameter', () => { 205 208 const Pagination = gql` 206 209 query($skip: Number, $limit: Number, $filter: String) { 210 + __typename 207 211 persons(skip: $skip, limit: $limit, filter: $filter) { 208 212 __typename 209 213 id ··· 255 259 it('should preserve the correct order in forward pagination', () => { 256 260 const Pagination = gql` 257 261 query($skip: Number, $limit: Number) { 262 + __typename 258 263 persons(skip: $skip, limit: $limit) { 259 264 __typename 260 265 id ··· 313 318 it('should preserve the correct order in backward pagination', () => { 314 319 const Pagination = gql` 315 320 query($skip: Number, $limit: Number) { 321 + __typename 316 322 persons(skip: $skip, limit: $limit) { 317 323 __typename 318 324 id ··· 362 368 query: Pagination, 363 369 variables: { skip: 3, limit: 3 }, 364 370 }); 371 + 365 372 expect(result.data).toEqual({ 366 373 __typename: 'Query', 367 374 persons: [...pageTwo.persons, ...pageOne.persons], ··· 371 378 it('prevents overlapping of pagination on different arguments', () => { 372 379 const Pagination = gql` 373 380 query($skip: Number, $limit: Number, $filter: string) { 381 + __typename 374 382 persons(skip: $skip, limit: $limit, filter: $filter) { 375 383 __typename 376 384 id
+1
exchanges/graphcache/src/helpers/help.ts
··· 19 19 | 11 20 20 | 12 21 21 | 13 22 + | 14 22 23 | 15 23 24 | 16 24 25 | 17
+2 -2
exchanges/graphcache/src/offlineExchange.test.ts
··· 33 33 authors { 34 34 id 35 35 name 36 + __typename 36 37 } 37 38 } 38 39 `; ··· 41 42 __typename: 'Query', 42 43 authors: [ 43 44 { 44 - __typename: 'Author', 45 45 id: '123', 46 46 name: 'Me', 47 + __typename: 'Author', 47 48 }, 48 49 ], 49 50 }; ··· 189 190 next(queryOp); 190 191 expect(result).toBeCalledTimes(2); 191 192 expect(result.mock.calls[1][0].data).toEqual({ 192 - __typename: 'Query', 193 193 authors: [{ id: '123', name: 'URQL', __typename: 'Author' }], 194 194 }); 195 195 });
+11 -2
exchanges/graphcache/src/operations/query.test.ts
··· 55 55 const result = query(store, { query: TODO_QUERY }); 56 56 expect(result.partial).toBe(true); 57 57 expect(result.data).toEqual({ 58 - __typename: 'Query', 59 58 todos: [ 60 59 { 61 60 id: '0', ··· 135 134 it('should not crash for valid queries', () => { 136 135 const VALID_QUERY = gql` 137 136 query getTodos { 137 + __typename 138 138 todos { 139 + __typename 139 140 id 140 141 text 141 142 } ··· 162 163 todos: [{ __typename: 'Todo', id: '0', text: 'Solve bug' }], 163 164 }); 164 165 165 - expect(console.warn).not.toHaveBeenCalled(); 166 + // The warning should be called for `__typename` 167 + expect(console.warn).toHaveBeenCalledTimes(1); 166 168 expect(console.error).not.toHaveBeenCalled(); 167 169 }); 168 170 169 171 it('should respect altered root types', () => { 170 172 const QUERY = gql` 171 173 query getTodos { 174 + __typename 172 175 todos { 176 + __typename 173 177 id 174 178 text 175 179 } ··· 203 207 it('should allow subsequent read when first result was null', () => { 204 208 const QUERY_WRITE = gql` 205 209 query writeTodos { 210 + __typename 206 211 todos { 212 + __typename 207 213 ...ValidRead 208 214 } 209 215 } ··· 215 221 216 222 const QUERY_READ = gql` 217 223 query getTodos { 224 + __typename 218 225 todos { 226 + __typename 219 227 ...MissingRead 220 228 } 221 229 todos { 230 + __typename 222 231 id 223 232 } 224 233 }
+4 -4
exchanges/graphcache/src/operations/query.ts
··· 271 271 return; 272 272 } 273 273 274 - // The following closely mirrors readSelection, but differs only slightly for the 275 - // sake of resolving from an existing resolver result 276 - data.__typename = typename; 277 274 const iterate = makeSelectionIterator(typename, entityKey, select, ctx); 278 275 279 276 let node: FieldNode | void; ··· 298 295 // means that the value is missing from the cache 299 296 let dataFieldValue: void | DataField; 300 297 301 - if (resultValue !== undefined && node.selectionSet === undefined) { 298 + if (fieldName === '__typename') { 299 + data[fieldAlias] = typename; 300 + continue; 301 + } else if (resultValue !== undefined && node.selectionSet === undefined) { 302 302 // The field is a scalar and can be retrieved directly from the result 303 303 dataFieldValue = resultValue; 304 304 } else if (
+1 -1
exchanges/graphcache/src/operations/shared.ts
··· 149 149 ))(); 150 150 } 151 151 } 152 - } else if (getName(node) !== '__typename') { 152 + } else { 153 153 return node; 154 154 } 155 155 }
+1 -18
exchanges/graphcache/src/operations/write.test.ts
··· 148 148 } 149 149 ); 150 150 // Because of us indicating Todo:Writer as a scalar 151 - expect(console.warn).toHaveBeenCalledTimes(1); 152 - write( 153 - store, 154 - { query: INVALID_TODO_QUERY }, 155 - { 156 - __typename: 'Mutation', 157 - toggleTodo: { 158 - __typename: 'Todo', 159 - id: '0', 160 - text: 'Teach', 161 - writer: { 162 - id: '0', 163 - }, 164 - }, 165 - } 166 - ); 167 - 168 - expect(console.warn).toHaveBeenCalledTimes(1); 151 + expect(console.warn).toHaveBeenCalledTimes(2); 169 152 expect((console.warn as any).mock.calls[0][0]).toMatch( 170 153 /The field `writer` does not exist on `Todo`/ 171 154 );
+9 -2
exchanges/graphcache/src/operations/write.ts
··· 194 194 const isRoot = !isQuery && !!ctx.store.rootNames[entityKey!]; 195 195 const typename = isRoot || isQuery ? entityKey : data.__typename; 196 196 if (!typename) { 197 + warn( 198 + "Couldn't find __typename when writing.\n" + 199 + "If you're writing to the cache manually have to pass a `__typename` property on each entity in your data.", 200 + 14 201 + ); 197 202 return; 198 203 } else if (!isRoot && !isQuery && entityKey) { 199 204 InMemoryData.writeRecord(entityKey, '__typename', typename); ··· 236 241 ); 237 242 238 243 continue; // Skip this field 239 - } else if (ctx.store.schema && typename) { 244 + } else if (ctx.store.schema && typename && fieldName !== '__typename') { 240 245 isFieldAvailableOnType(ctx.store.schema, typename, fieldName); 241 246 } 242 247 } 243 248 244 - if (ctx.optimistic && isRoot) { 249 + if (fieldName === '__typename') { 250 + continue; 251 + } else if (ctx.optimistic && isRoot) { 245 252 const resolver = ctx.store.optimisticMutations[fieldName]; 246 253 247 254 if (!resolver) continue;
+41 -2
exchanges/graphcache/src/store/store.test.ts
··· 2 2 3 3 import gql from 'graphql-tag'; 4 4 import { minifyIntrospectionQuery } from '@urql/introspection'; 5 + import { maskTypename } from '@urql/core'; 5 6 import { mocked } from 'ts-jest/utils'; 6 7 7 8 import { Data, StorageAdapter } from '../types'; ··· 14 15 15 16 const Appointment = gql` 16 17 query appointment($id: String) { 18 + __typename 17 19 appointment(id: $id) { 18 20 __typename 19 21 id ··· 41 43 42 44 const TodosWithoutTypename = gql` 43 45 query { 46 + __typename 44 47 todos { 45 48 id 46 49 text ··· 87 90 // NOTE: This is the query without __typename annotations 88 91 write(store, { query: TodosWithoutTypename }, todosData); 89 92 const result = query(store, { query: TodosWithoutTypename }); 90 - expect(result.data).toEqual(todosData); 93 + expect(result.data).toEqual({ 94 + ...maskTypename(todosData), 95 + __typename: 'Query', 96 + }); 91 97 }); 92 98 }); 93 99 ··· 473 479 id 474 480 text 475 481 complete 482 + __typename 476 483 } 477 484 `, 478 485 { id: '0' } ··· 714 721 it('should be able to persist embedded data', () => { 715 722 const EmbeddedAppointment = gql` 716 723 query appointment($id: String) { 724 + __typename 717 725 appointment(id: $id) { 718 726 __typename 719 727 info ··· 803 811 InMemoryData.clearDataState(); 804 812 }); 805 813 806 - it("should warn if an optimistic field doesn't exist in the schema's mutations", function () { 814 + it("should warn if an optimistic field doesn't exist in the schema's mutations", () => { 807 815 new Store({ 808 816 schema: minifyIntrospectionQuery( 809 817 require('../test-utils/simple_schema.json') ··· 875 883 876 884 query(store, { query: parse(getIntrospectionQuery()) }, schema); 877 885 expect(console.warn).toBeCalledTimes(0); 886 + }); 887 + 888 + it('should warn when __typename is missing when store.writeFragment is called', () => { 889 + InMemoryData.initDataState('write', store.data, null); 890 + 891 + store.writeFragment( 892 + parse(` 893 + fragment _ on Test { 894 + __typename 895 + id 896 + sub { 897 + id 898 + } 899 + } 900 + `), 901 + { 902 + id: 'test', 903 + sub: { 904 + id: 'test', 905 + }, 906 + } 907 + ); 908 + 909 + InMemoryData.clearDataState(); 910 + 911 + expect(console.warn).toBeCalledTimes(1); 912 + const warnMessage = mocked(console.warn).mock.calls[0][0]; 913 + expect(warnMessage).toContain( 914 + "Couldn't find __typename when writing.\nIf you're writing to the cache manually have to pass a `__typename` property on each entity in your data." 915 + ); 916 + expect(warnMessage).toContain('https://bit.ly/2XbVrpR#14'); 878 917 }); 879 918 });
+12 -5
exchanges/graphcache/src/store/store.ts
··· 5 5 GraphQLSchema, 6 6 } from 'graphql'; 7 7 8 - import { TypedDocumentNode, createRequest } from '@urql/core'; 8 + import { TypedDocumentNode, formatDocument, createRequest } from '@urql/core'; 9 9 10 10 import { 11 11 Cache, ··· 177 177 updater: (data: T | null) => T | null 178 178 ): void { 179 179 const request = createRequest<T, V>(input.query, input.variables as any); 180 + request.query = formatDocument(request.query); 180 181 const output = updater(this.readQuery(request)); 181 182 if (output !== null) { 182 183 startWrite(this, request, output as any); ··· 184 185 } 185 186 186 187 readQuery<T = Data, V = Variables>(input: QueryInput<T, V>): T | null { 187 - return read(this, createRequest(input.query, input.variables!)) 188 - .data as T | null; 188 + const request = createRequest(input.query, input.variables!); 189 + request.query = formatDocument(request.query); 190 + return read(this, request).data as T | null; 189 191 } 190 192 191 193 readFragment<T = Data, V = Variables>( ··· 193 195 entity: string | Data | T, 194 196 variables?: V 195 197 ): T | null { 196 - return readFragment(this, fragment, entity, variables as any) as T | null; 198 + return readFragment( 199 + this, 200 + formatDocument(fragment), 201 + entity, 202 + variables as any 203 + ) as T | null; 197 204 } 198 205 199 206 writeFragment<T = Data, V = Variables>( ··· 201 208 data: T, 202 209 variables?: V 203 210 ): void { 204 - writeFragment(this, fragment, data, variables as any); 211 + writeFragment(this, formatDocument(fragment), data, variables as any); 205 212 } 206 213 }
+6 -1
exchanges/graphcache/src/test-utils/examples-1.test.ts
··· 63 63 } 64 64 `; 65 65 66 + afterEach(() => { 67 + expect(console.warn).not.toHaveBeenCalled(); 68 + }); 69 + 66 70 it('passes the "getting-started" example', () => { 67 71 const store = new Store(); 68 72 const todosData = { ··· 146 150 147 151 const GetWithVariables = gql` 148 152 query { 153 + __typename 149 154 todo(first: null) { 150 155 __typename 151 156 id ··· 155 160 156 161 const GetWithoutVariables = gql` 157 162 query { 163 + __typename 158 164 todo { 159 165 __typename 160 166 id ··· 203 209 id: '0', 204 210 text: 'Go to the shops', 205 211 complete: false, 206 - __typename: 'Todo', 207 212 }, 208 213 }); 209 214 });
+7 -3
exchanges/graphcache/src/test-utils/examples-2.test.ts
··· 51 51 } 52 52 `; 53 53 54 + afterEach(() => { 55 + expect(console.warn).not.toHaveBeenCalled(); 56 + }); 57 + 54 58 it('allows custom resolvers to resolve nested, unkeyed data', () => { 55 59 const store = new Store({ 56 60 resolvers: { ··· 98 102 expect(res.partial).toBe(false); 99 103 100 104 expect(res.data).toEqual({ 101 - __typename: 'Query', 102 105 todos: { 103 106 __typename: 'TodosConnection', 104 107 edges: [ ··· 161 164 const res = query(store, { query: Pagination }); 162 165 expect(res.partial).toBe(false); 163 166 expect(res.data).toEqual({ 164 - __typename: 'Query', 165 167 todos: { 166 168 __typename: 'TodosConnection', 167 169 edges: [ ··· 186 188 187 189 it('allows custom resolvers to resolve mixed data (keyable and unkeyable)', () => { 188 190 const store = new Store({ 191 + keys: { 192 + TodoDetails: () => null, 193 + }, 189 194 resolvers: { 190 195 Query: { 191 196 todo: () => ({ ··· 224 229 expect(res.partial).toBe(false); 225 230 expect(res.dependencies).toHaveProperty('Author:x', true); 226 231 expect(res.data).toEqual({ 227 - __typename: 'Query', 228 232 todo: { 229 233 __typename: 'Todo', 230 234 id: '1',
+4 -1
exchanges/graphcache/src/test-utils/examples-3.test.ts
··· 2 2 import { query, write } from '../operations'; 3 3 import { Store } from '../store'; 4 4 5 + afterEach(() => { 6 + expect(console.warn).not.toHaveBeenCalled(); 7 + }); 8 + 5 9 it('allows viewer fields to overwrite the root Query data', () => { 6 10 const store = new Store(); 7 11 const get = gql` ··· 47 51 48 52 expect(res.partial).toBe(false); 49 53 expect(res.data).toEqual({ 50 - __typename: 'Query', 51 54 int: 43, 52 55 }); 53 56 });
+2
exchanges/graphcache/src/test-utils/suite.test.ts
··· 256 256 expectCacheIntegrity({ 257 257 query: gql` 258 258 { 259 + __typename 259 260 items { 260 261 __typename 261 262 id ··· 377 378 expectCacheIntegrity({ 378 379 query: gql` 379 380 { 381 + __typename 380 382 user { 381 383 __typename 382 384 id