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) Validation for Graphcache's opts.updates, opts.resolvers, opts.optimistic (#826)

* Ensure console mocks are reset after every test, without having to explicity do so.

* Graphcache: Wrap all `opts` validation in `if (process.env.NODE_ENV !== 'production')`.

* Graphcache: console.warn() if creating a store and updates has invalid mutations/subscriptions.

* Graphcache: console.warn() if creating a store with invalid resolvers.

* Graphcache: console.warn() if creating a store with invalid optimistic mutations.

* Graphcache: expand tests for query() and write().

* Graphcache: update changeset for new console.warn().

authored by

Amy Boyd and committed by
GitHub
f12b477e 98b6edc2

+481 -16
+3 -2
.changeset/thick-avocados-enjoy.md
··· 2 2 '@urql/exchange-graphcache': minor 3 3 --- 4 4 5 - Issue warnings when an unknown type has been included in Graphcache's opts.key configuration to help spot typos. 6 - See: [#820](https://github.com/FormidableLabs/urql/pull/820) 5 + Issue warnings when an unknown type or field has been included in Graphcache's `opts` configuration to help spot typos. 6 + Checks `opts.keys`, `opts.updates`, `opts.resolvers` and `opts.optimistic`. 7 + See: [#820](https://github.com/FormidableLabs/urql/pull/820) and [#826](https://github.com/FormidableLabs/urql/pull/826)
+43
docs/graphcache/errors.md
··· 289 289 290 290 Check whether your schema is up-to-date, or whether you're using an invalid 291 291 typename in `opts.keys`, maybe due to a typo. 292 + 293 + ## (21) Invalid mutation 294 + 295 + > Invalid mutation field `???` is not in the defined schema but the `updates` option is referencing it. 296 + 297 + When you're passing an introspected schema to the cache exchange, it is 298 + able to check whether your `opts.updates.Mutation` is valid. 299 + This error occurs when an unknown mutation field is found in `opts.updates.Mutation`. 300 + 301 + Check whether your schema is up-to-date, or whether you've got a typo in `opts.updates.Mutation`. 302 + 303 + ## (22) Invalid subscription 304 + 305 + > Invalid subscription field: `???` is not in the defined schema but the `updates` option is referencing it. 306 + 307 + When you're passing an introspected schema to the cache exchange, it is 308 + able to check whether your `opts.updates.Subscription` is valid. 309 + This error occurs when an unknown subscription field is found in `opts.updates.Subscription`. 310 + 311 + Check whether your schema is up-to-date, or whether you're using an invalid 312 + subscription name in `opts.updates.Subscription`, maybe due to a typo. 313 + 314 + ## (23) Invalid resolver 315 + 316 + > Invalid resolver: `???` is not in the defined schema, but the `resolvers` 317 + > option is referencing it. 318 + 319 + When you're passing an introspected schema to the cache exchange, it is 320 + able to check whether your `opts.resolvers` is valid. 321 + This error occurs when an unknown query, type or field is found in `opts.resolvers`. 322 + 323 + Check whether your schema is up-to-date, or whether you've got a typo in `opts.resolvers`. 324 + 325 + ## (24) Invalid optimistic mutation 326 + 327 + > Invalid optimistic mutation field: `???` is not a mutation field in the defined schema, 328 + > but the `optimistic` option is referencing it. 329 + 330 + When you're passing an introspected schema to the cache exchange, it is 331 + able to check whether your `opts.optimistic` is valid. 332 + This error occurs when a field in `opts.optimistic` is not in the schema's `Mutation` fields. 333 + 334 + Check whether your schema is up-to-date, or whether you've got a typo in `Mutation` or `opts.optimistic`.
+124 -1
exchanges/graphcache/src/ast/schemaPredicates.ts
··· 10 10 } from 'graphql'; 11 11 12 12 import { warn, invariant } from '../helpers/help'; 13 - import { KeyingConfig } from '../types'; 13 + import { 14 + KeyingConfig, 15 + UpdatesConfig, 16 + ResolverConfig, 17 + OptimisticMutationConfig, 18 + } from '../types'; 14 19 15 20 export const isFieldNullable = ( 16 21 schema: GraphQLSchema, ··· 131 136 }); 132 137 } 133 138 } 139 + 140 + export function expectValidUpdatesConfig( 141 + schema: GraphQLSchema, 142 + updates: UpdatesConfig 143 + ): void { 144 + if (process.env.NODE_ENV === 'production') { 145 + return; 146 + } 147 + 148 + /* eslint-disable prettier/prettier */ 149 + const schemaMutations = schema.getMutationType() 150 + ? Object.keys((schema.getMutationType() as GraphQLObjectType).toConfig().fields) 151 + : []; 152 + const schemaSubscriptions = schema.getSubscriptionType() 153 + ? Object.keys((schema.getSubscriptionType() as GraphQLObjectType).toConfig().fields) 154 + : []; 155 + const givenMutations = updates.Mutation 156 + ? Object.keys(updates.Mutation) 157 + : []; 158 + const givenSubscriptions = updates.Subscription 159 + ? Object.keys(updates.Subscription) 160 + : []; 161 + /* eslint-enable prettier/prettier */ 162 + 163 + for (const givenMutation of givenMutations) { 164 + if (schemaMutations.indexOf(givenMutation) === -1) { 165 + warn( 166 + 'Invalid mutation field: `' + 167 + givenMutation + 168 + '` is not in the defined schema, but the `updates.Mutation` option is referencing it.', 169 + 21 170 + ); 171 + } 172 + } 173 + 174 + for (const givenSubscription of givenSubscriptions) { 175 + if (schemaSubscriptions.indexOf(givenSubscription) === -1) { 176 + warn( 177 + 'Invalid subscription field: `' + 178 + givenSubscription + 179 + '` is not in the defined schema, but the `updates.Subscription` option is referencing it.', 180 + 22 181 + ); 182 + } 183 + } 184 + } 185 + 186 + function warnAboutResolver(name: string): void { 187 + warn( 188 + `Invalid resolver: \`${name}\` is not in the defined schema, but the \`resolvers\` option is referencing it.`, 189 + 23 190 + ); 191 + } 192 + 193 + export function expectValidResolversConfig( 194 + schema: GraphQLSchema, 195 + resolvers: ResolverConfig 196 + ): void { 197 + if (process.env.NODE_ENV === 'production') { 198 + return; 199 + } 200 + 201 + const validTypes = Object.keys(schema.getTypeMap()); 202 + 203 + for (const key in resolvers) { 204 + if (key === 'Query') { 205 + const queryType = schema.getQueryType(); 206 + if (queryType) { 207 + const validQueries = Object.keys(queryType.toConfig().fields); 208 + for (const resolverQuery in resolvers.Query) { 209 + if (validQueries.indexOf(resolverQuery) === -1) { 210 + warnAboutResolver('Query.' + resolverQuery); 211 + } 212 + } 213 + } else { 214 + warnAboutResolver('Query'); 215 + } 216 + } else { 217 + if (validTypes.indexOf(key) === -1) { 218 + warnAboutResolver(key); 219 + } else { 220 + const validTypeProperties = Object.keys( 221 + (schema.getType(key) as GraphQLObjectType).getFields() 222 + ); 223 + const resolverProperties = Object.keys(resolvers[key]); 224 + for (const resolverProperty of resolverProperties) { 225 + if (validTypeProperties.indexOf(resolverProperty) === -1) { 226 + warnAboutResolver(key + '.' + resolverProperty); 227 + } 228 + } 229 + } 230 + } 231 + } 232 + } 233 + 234 + export function expectValidOptimisticMutationsConfig( 235 + schema: GraphQLSchema, 236 + optimisticMutations: OptimisticMutationConfig 237 + ): void { 238 + if (process.env.NODE_ENV === 'production') { 239 + return; 240 + } 241 + 242 + const validMutations = schema.getMutationType() 243 + ? Object.keys( 244 + (schema.getMutationType() as GraphQLObjectType).toConfig().fields 245 + ) 246 + : []; 247 + 248 + for (const mutation in optimisticMutations) { 249 + if (validMutations.indexOf(mutation) === -1) { 250 + warn( 251 + `Invalid optimistic mutation field: \`${mutation}\` is not a mutation field in the defined schema, but the \`optimistic\` option is referencing it.`, 252 + 24 253 + ); 254 + } 255 + } 256 + }
+5 -1
exchanges/graphcache/src/helpers/help.ts
··· 24 24 | 17 25 25 | 18 26 26 | 19 27 - | 20; 27 + | 20 28 + | 21 29 + | 22 30 + | 23 31 + | 24; 28 32 29 33 type DebugNode = ExecutableDefinitionNode | InlineFragmentNode; 30 34
+9 -2
exchanges/graphcache/src/operations/query.test.ts
··· 41 41 ], 42 42 } 43 43 ); 44 - 45 - jest.resetAllMocks(); 46 44 }); 47 45 48 46 it('test partial results', () => { ··· 155 153 __typename: 'Query', 156 154 todos: [{ __typename: 'Todo', id: '0', text: 'Solve bug' }], 157 155 }); 156 + 157 + expect(console.warn).not.toHaveBeenCalled(); 158 + expect(console.error).not.toHaveBeenCalled(); 158 159 }); 159 160 160 161 it('should respect altered root types', () => { ··· 186 187 __typename: 'query_root', 187 188 todos: [{ __typename: 'Todo', id: '0', text: 'Solve bug' }], 188 189 }); 190 + 191 + expect(console.warn).not.toHaveBeenCalled(); 192 + expect(console.error).not.toHaveBeenCalled(); 189 193 }); 190 194 191 195 it('should allow subsequent read when first result was null', () => { ··· 247 251 __typename: 'query_root', 248 252 todos: [null], 249 253 }); 254 + 255 + expect(console.warn).not.toHaveBeenCalled(); 256 + expect(console.error).not.toHaveBeenCalled(); 250 257 }); 251 258 });
+36 -3
exchanges/graphcache/src/operations/write.test.ts
··· 44 44 jest.clearAllMocks(); 45 45 }); 46 46 47 + it('should not crash for valid writes', async () => { 48 + const VALID_TODO_QUERY = gql` 49 + mutation { 50 + toggleTodo { 51 + id 52 + text 53 + complete 54 + } 55 + } 56 + `; 57 + write( 58 + store, 59 + { query: VALID_TODO_QUERY }, 60 + { 61 + __typename: 'Mutation', 62 + toggleTodo: { 63 + __typename: 'Todo', 64 + id: '0', 65 + text: 'Teach', 66 + complete: true, 67 + }, 68 + } 69 + ); 70 + expect(console.warn).not.toHaveBeenCalled(); 71 + expect(console.error).not.toHaveBeenCalled(); 72 + }); 73 + 47 74 it('should warn once for invalid fields on an entity', () => { 48 75 const INVALID_TODO_QUERY = gql` 49 76 mutation { ··· 82 109 } 83 110 ); 84 111 expect(console.warn).toHaveBeenCalledTimes(1); 85 - expect((console.warn as any).mock.calls[0][0]).toMatch(/incomplete/); 112 + expect((console.warn as any).mock.calls[0][0]).toMatch( 113 + /The field `incomplete` does not exist on `Todo`/ 114 + ); 86 115 }); 87 116 88 117 it('should warn once for invalid fields on an entity', () => { ··· 131 160 ); 132 161 133 162 expect(console.warn).toHaveBeenCalledTimes(1); 134 - expect((console.warn as any).mock.calls[0][0]).toMatch(/writer/); 163 + expect((console.warn as any).mock.calls[0][0]).toMatch( 164 + /The field `writer` does not exist on `Todo`/ 165 + ); 135 166 }); 136 167 137 168 it('should skip undefined values that are expected', () => { ··· 146 177 write(store, { query }, { field: undefined } as any); 147 178 // Because of us writing an undefined field 148 179 expect(console.warn).toHaveBeenCalledTimes(2); 149 - expect((console.warn as any).mock.calls[0][0]).toMatch(/undefined/); 180 + expect((console.warn as any).mock.calls[0][0]).toMatch( 181 + /The field `field` does not exist on `Query`/ 182 + ); 150 183 151 184 InMemoryData.initDataState(store.data, null); 152 185 // The field must still be `'test'`
+207 -2
exchanges/graphcache/src/store/store.test.ts
··· 5 5 import { write, writeOptimistic } from '../operations/write'; 6 6 import * as InMemoryData from './data'; 7 7 import { Store } from './store'; 8 + import { noop } from '../test-utils/utils'; 8 9 9 10 const Appointment = gql` 10 11 query appointment($id: String) { ··· 91 92 }); 92 93 }); 93 94 95 + describe('Store with UpdatesConfig', () => { 96 + it("sets the store's updates field to the given argument", () => { 97 + const updatesOption = { 98 + Mutation: { 99 + toggleTodo: noop, 100 + }, 101 + Subscription: { 102 + newTodo: noop, 103 + }, 104 + }; 105 + 106 + const store = new Store({ 107 + updates: updatesOption, 108 + }); 109 + 110 + expect(store.updates.Mutation).toBe(updatesOption.Mutation); 111 + expect(store.updates.Subscription).toBe(updatesOption.Subscription); 112 + }); 113 + 114 + it("sets the store's updates field to an empty default if not provided", () => { 115 + const store = new Store({}); 116 + 117 + expect(store.updates.Mutation).toEqual({}); 118 + expect(store.updates.Subscription).toEqual({}); 119 + }); 120 + 121 + it('should not warn if Mutation/Subscription operations do exist in the schema', function () { 122 + new Store({ 123 + schema: require('../test-utils/simple_schema.json'), 124 + updates: { 125 + Mutation: { 126 + toggleTodo: noop, 127 + }, 128 + Subscription: { 129 + newTodo: noop, 130 + }, 131 + }, 132 + }); 133 + 134 + expect(console.warn).not.toBeCalled(); 135 + }); 136 + 137 + it("should warn if Mutation operations don't exist in the schema", function () { 138 + new Store({ 139 + schema: require('../test-utils/simple_schema.json'), 140 + updates: { 141 + Mutation: { 142 + doTheChaChaSlide: noop, 143 + }, 144 + }, 145 + }); 146 + 147 + expect(console.warn).toBeCalledTimes(1); 148 + const warnMessage = mocked(console.warn).mock.calls[0][0]; 149 + expect(warnMessage).toContain( 150 + 'Invalid mutation field: `doTheChaChaSlide` is not in the defined schema, but the `updates.Mutation` option is referencing it.' 151 + ); 152 + expect(warnMessage).toContain('https://bit.ly/2XbVrpR#21'); 153 + }); 154 + 155 + it("should warn if Subscription operations don't exist in the schema", function () { 156 + new Store({ 157 + schema: require('../test-utils/simple_schema.json'), 158 + updates: { 159 + Subscription: { 160 + someoneDidTheChaChaSlide: noop, 161 + }, 162 + }, 163 + }); 164 + 165 + expect(console.warn).toBeCalledTimes(1); 166 + const warnMessage = mocked(console.warn).mock.calls[0][0]; 167 + expect(warnMessage).toContain( 168 + 'Invalid subscription field: `someoneDidTheChaChaSlide` is not in the defined schema, but the `updates.Subscription` option is referencing it.' 169 + ); 170 + expect(warnMessage).toContain('https://bit.ly/2XbVrpR#22'); 171 + }); 172 + }); 173 + 94 174 describe('Store with KeyingConfig', () => { 95 175 it('generates keys from custom keying function', () => { 96 176 const store = new Store({ ··· 138 218 }); 139 219 }); 140 220 221 + describe('Store with ResolverConfig', () => { 222 + it("sets the store's resolvers field to the given argument", () => { 223 + const resolversOption = { 224 + Query: { 225 + latestTodo: () => 'todo', 226 + }, 227 + }; 228 + 229 + const store = new Store({ 230 + resolvers: resolversOption, 231 + }); 232 + 233 + expect(store.resolvers).toBe(resolversOption); 234 + }); 235 + 236 + it("sets the store's resolvers field to an empty default if not provided", () => { 237 + const store = new Store({}); 238 + 239 + expect(store.resolvers).toEqual({}); 240 + }); 241 + 242 + it('should not warn if resolvers do exist in the schema', function () { 243 + new Store({ 244 + schema: require('../test-utils/simple_schema.json'), 245 + resolvers: { 246 + Query: { 247 + latestTodo: () => 'todo', 248 + todos: () => ['todo 1', 'todo 2'], 249 + }, 250 + Todo: { 251 + text: todo => (todo.text as string).toUpperCase(), 252 + author: todo => (todo.author as string).toUpperCase(), 253 + }, 254 + }, 255 + }); 256 + 257 + expect(console.warn).not.toBeCalled(); 258 + }); 259 + 260 + it("should warn if a Query doesn't exist in the schema", function () { 261 + new Store({ 262 + schema: require('../test-utils/simple_schema.json'), 263 + resolvers: { 264 + Query: { 265 + todos: () => ['todo 1', 'todo 2'], 266 + // This query should be warned about. 267 + findDeletedTodos: () => ['todo 1', 'todo 2'], 268 + }, 269 + }, 270 + }); 271 + 272 + expect(console.warn).toBeCalledTimes(1); 273 + const warnMessage = mocked(console.warn).mock.calls[0][0]; 274 + expect(warnMessage).toContain( 275 + 'Invalid resolver: `Query.findDeletedTodos` is not in the defined schema, but the `resolvers` option is referencing it' 276 + ); 277 + expect(warnMessage).toContain('https://bit.ly/2XbVrpR#23'); 278 + }); 279 + 280 + it("should warn if a type doesn't exist in the schema", function () { 281 + new Store({ 282 + schema: require('../test-utils/simple_schema.json'), 283 + resolvers: { 284 + Todo: { 285 + complete: () => true, 286 + }, 287 + // This type should be warned about. 288 + Dinosaur: { 289 + isExtinct: () => true, 290 + }, 291 + }, 292 + }); 293 + 294 + expect(console.warn).toBeCalledTimes(1); 295 + const warnMessage = mocked(console.warn).mock.calls[0][0]; 296 + expect(warnMessage).toContain( 297 + 'Invalid resolver: `Dinosaur` is not in the defined schema, but the `resolvers` option is referencing it' 298 + ); 299 + expect(warnMessage).toContain('https://bit.ly/2XbVrpR#23'); 300 + }); 301 + 302 + it("should warn if a type's property doesn't exist in the schema", function () { 303 + new Store({ 304 + schema: require('../test-utils/simple_schema.json'), 305 + resolvers: { 306 + Todo: { 307 + complete: () => true, 308 + // This property should be warned about. 309 + isAboutDinosaurs: () => true, 310 + }, 311 + }, 312 + }); 313 + 314 + expect(console.warn).toBeCalledTimes(1); 315 + const warnMessage = mocked(console.warn).mock.calls[0][0]; 316 + expect(warnMessage).toContain( 317 + 'Invalid resolver: `Todo.isAboutDinosaurs` is not in the defined schema, but the `resolvers` option is referencing it' 318 + ); 319 + expect(warnMessage).toContain('https://bit.ly/2XbVrpR#23'); 320 + }); 321 + }); 322 + 141 323 describe('Store with OptimisticMutationConfig', () => { 142 324 let store; 143 325 ··· 156 338 InMemoryData.initDataState(store.data, null); 157 339 }); 158 340 159 - it('Should resolve a property', () => { 341 + it('should resolve a property', () => { 160 342 const todoResult = store.resolve({ __typename: 'Todo', id: '0' }, 'text'); 161 343 expect(todoResult).toEqual('Go to the shops'); 162 344 const authorResult = store.resolve( ··· 180 362 InMemoryData.clearDataState(); 181 363 }); 182 364 183 - it('Should resolve a link property', () => { 365 + it('should resolve a link property', () => { 184 366 const parent = { 185 367 id: '0', 186 368 text: 'test', ··· 658 840 InMemoryData.initDataState(store.data, null); 659 841 expect(InMemoryData.readRecord('Query', 'base')).toBe(true); 660 842 InMemoryData.clearDataState(); 843 + }); 844 + 845 + it("should warn if an optimistic field doesn't exist in the schema's mutations", function () { 846 + new Store({ 847 + schema: require('../test-utils/simple_schema.json'), 848 + updates: { 849 + Mutation: { 850 + toggleTodo: noop, 851 + }, 852 + }, 853 + optimistic: { 854 + toggleTodo: () => null, 855 + // This field should be warned about. 856 + deleteTodo: () => null, 857 + }, 858 + }); 859 + 860 + expect(console.warn).toBeCalledTimes(1); 861 + const warnMessage = mocked(console.warn).mock.calls[0][0]; 862 + expect(warnMessage).toContain( 863 + 'Invalid optimistic mutation field: `deleteTodo` is not a mutation field in the defined schema, but the `optimistic` option is referencing it.' 864 + ); 865 + expect(warnMessage).toContain('https://bit.ly/2XbVrpR#24'); 661 866 }); 662 867 });
+25 -2
exchanges/graphcache/src/store/store.ts
··· 75 75 if (mutationType) mutationName = mutationType.name; 76 76 if (subscriptionType) subscriptionName = subscriptionType.name; 77 77 78 - if (this.keys) { 79 - SchemaPredicates.expectValidKeyingConfig(this.schema, this.keys); 78 + if (process.env.NODE_ENV !== 'production') { 79 + if (this.keys) { 80 + SchemaPredicates.expectValidKeyingConfig(this.schema, this.keys); 81 + } 82 + 83 + const hasUpdates = 84 + Object.keys(this.updates.Mutation).length > 0 || 85 + Object.keys(this.updates.Subscription).length > 0; 86 + if (hasUpdates) { 87 + SchemaPredicates.expectValidUpdatesConfig(this.schema, this.updates); 88 + } 89 + 90 + if (this.resolvers) { 91 + SchemaPredicates.expectValidResolversConfig( 92 + this.schema, 93 + this.resolvers 94 + ); 95 + } 96 + 97 + if (this.optimisticMutations) { 98 + SchemaPredicates.expectValidOptimisticMutationsConfig( 99 + this.schema, 100 + this.optimisticMutations 101 + ); 102 + } 80 103 } 81 104 } 82 105
+24 -1
exchanges/graphcache/src/test-utils/simple_schema.json
··· 6 6 "mutationType": { 7 7 "name": "Mutation" 8 8 }, 9 - "subscriptionType": null, 9 + "subscriptionType": { 10 + "name": "Subscription" 11 + }, 10 12 "types": [ 11 13 { 12 14 "kind": "OBJECT", ··· 234 236 "name": "Todo", 235 237 "ofType": null 236 238 } 239 + } 240 + ], 241 + "inputFields": null, 242 + "interfaces": [], 243 + "enumValues": null, 244 + "possibleTypes": null 245 + }, 246 + { 247 + "kind": "OBJECT", 248 + "name": "Subscription", 249 + "fields": [ 250 + { 251 + "name": "newTodo", 252 + "args": [], 253 + "type": { 254 + "kind": "OBJECT", 255 + "name": "Todo", 256 + "ofType": null 257 + }, 258 + "isDeprecated": false, 259 + "deprecationReason": null 237 260 } 238 261 ], 239 262 "inputFields": null,
+2
exchanges/graphcache/src/test-utils/utils.ts
··· 1 + // eslint-disable-next-line 2 + export const noop = () => {};
+1
scripts/jest/preset.js
··· 3 3 setupFiles: [ 4 4 require.resolve('./setup.js') 5 5 ], 6 + clearMocks: true, 6 7 transform: { 7 8 '^.+\\.tsx?$': 'ts-jest', 8 9 },
+2 -2
scripts/jest/setup.js
··· 1 + // This script is run before each `.test.ts` file. 2 + 1 3 global.AbortController = undefined; 2 4 global.fetch = jest.fn(); 3 5 4 6 process.on('unhandledRejection', error => { 5 7 throw error; 6 8 }); 7 - 8 - jest.restoreAllMocks(); 9 9 10 10 const originalConsole = console; 11 11 global.console = {