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.

feat(graphcache): add logger interface (#3444)

authored by

Jovi De Croock and committed by
GitHub
1fee86f0 d6e435c5

+143 -59
+5
.changeset/tender-files-tie.md
··· 1 + --- 2 + '@urql/exchange-graphcache': minor 3 + --- 4 + 5 + Add optional `logger` to the options, this allows you to filter out warnings or disable them all together
+1
docs/api/graphcache.md
··· 32 32 | `optimistic` | A mapping of mutation fields to resolvers that may be used to provide _Graphcache_ with an optimistic result for a given mutation field that should be applied to the cached data temporarily. | 33 33 | `schema` | A serialized GraphQL schema that is used by _Graphcache_ to resolve partial data, interfaces, and enums. The schema also used to provide helpful warnings for [schema awareness](../graphcache/schema-awareness.md). | 34 34 | `storage` | A persisted storage interface that may be provided to preserve cache data for [offline support](../graphcache/offline.md). | 35 + | `logger` | A function that will be invoked for warning/debug/... logs | 35 36 36 37 The `@urql/exchange-graphcache` package also exports the `offlineExchange`; which is identical to 37 38 the `cacheExchange` but activates [offline support](../graphcache/offline.md) when the `storage` option is passed.
+10 -5
exchanges/graphcache/src/ast/schemaPredicates.test.ts
··· 57 57 58 58 it('should indicate nullability', () => { 59 59 expect( 60 - SchemaPredicates.isFieldNullable(schema, 'Todo', 'text') 60 + SchemaPredicates.isFieldNullable(schema, 'Todo', 'text', undefined) 61 61 ).toBeFalsy(); 62 62 expect( 63 - SchemaPredicates.isFieldNullable(schema, 'Todo', 'complete') 63 + SchemaPredicates.isFieldNullable(schema, 'Todo', 'complete', undefined) 64 64 ).toBeTruthy(); 65 65 expect( 66 - SchemaPredicates.isFieldNullable(schema, 'Todo', 'author') 66 + SchemaPredicates.isFieldNullable(schema, 'Todo', 'author', undefined) 67 67 ).toBeTruthy(); 68 68 }); 69 69 ··· 89 89 90 90 it('should throw if a requested type does not exist', () => { 91 91 expect(() => 92 - SchemaPredicates.isFieldNullable(schema, 'SomeInvalidType', 'complete') 92 + SchemaPredicates.isFieldNullable( 93 + schema, 94 + 'SomeInvalidType', 95 + 'complete', 96 + undefined 97 + ) 93 98 ).toThrow( 94 99 'The type `SomeInvalidType` is not an object in the defined schema, but the GraphQL document is traversing it.\nhttps://bit.ly/2XbVrpR#3' 95 100 ); ··· 97 102 98 103 it('should warn in console if a requested field does not exist', () => { 99 104 expect( 100 - SchemaPredicates.isFieldNullable(schema, 'Todo', 'goof') 105 + SchemaPredicates.isFieldNullable(schema, 'Todo', 'goof', undefined) 101 106 ).toBeFalsy(); 102 107 103 108 expect(console.warn).toBeCalledTimes(1);
+43 -25
exchanges/graphcache/src/ast/schemaPredicates.ts
··· 12 12 UpdatesConfig, 13 13 ResolverConfig, 14 14 OptimisticMutationConfig, 15 + Logger, 15 16 } from '../types'; 16 17 17 18 const BUILTIN_NAME = '__'; ··· 19 20 export const isFieldNullable = ( 20 21 schema: SchemaIntrospector, 21 22 typename: string, 22 - fieldName: string 23 + fieldName: string, 24 + logger: Logger | undefined 23 25 ): boolean => { 24 - const field = getField(schema, typename, fieldName); 26 + const field = getField(schema, typename, fieldName, logger); 25 27 return !!field && field.type.kind !== 'NON_NULL'; 26 28 }; 27 29 28 30 export const isListNullable = ( 29 31 schema: SchemaIntrospector, 30 32 typename: string, 31 - fieldName: string 33 + fieldName: string, 34 + logger: Logger | undefined 32 35 ): boolean => { 33 - const field = getField(schema, typename, fieldName); 36 + const field = getField(schema, typename, fieldName, logger); 34 37 if (!field) return false; 35 38 const ofType = 36 39 field.type.kind === 'NON_NULL' ? field.type.ofType : field.type; ··· 40 43 export const isFieldAvailableOnType = ( 41 44 schema: SchemaIntrospector, 42 45 typename: string, 43 - fieldName: string 46 + fieldName: string, 47 + logger: Logger | undefined 44 48 ): boolean => 45 49 fieldName.indexOf(BUILTIN_NAME) === 0 || 46 50 typename.indexOf(BUILTIN_NAME) === 0 || 47 - !!getField(schema, typename, fieldName); 51 + !!getField(schema, typename, fieldName, logger); 48 52 49 53 export const isInterfaceOfType = ( 50 54 schema: SchemaIntrospector, ··· 70 74 const getField = ( 71 75 schema: SchemaIntrospector, 72 76 typename: string, 73 - fieldName: string 77 + fieldName: string, 78 + logger: Logger | undefined 74 79 ) => { 75 80 if ( 76 81 fieldName.indexOf(BUILTIN_NAME) === 0 || ··· 90 95 '`, ' + 91 96 'but the GraphQL document expects it to exist.\n' + 92 97 'Traversal will continue, however this may lead to undefined behavior!', 93 - 4 98 + 4, 99 + logger 94 100 ); 95 101 } 96 102 ··· 124 130 125 131 export function expectValidKeyingConfig( 126 132 schema: SchemaIntrospector, 127 - keys: KeyingConfig 133 + keys: KeyingConfig, 134 + logger: Logger | undefined 128 135 ): void { 129 136 if (process.env.NODE_ENV !== 'production') { 130 137 for (const key in keys) { ··· 133 140 'Invalid Object type: The type `' + 134 141 key + 135 142 '` is not an object in the defined schema, but the `keys` option is referencing it.', 136 - 20 143 + 20, 144 + logger 137 145 ); 138 146 } 139 147 } ··· 142 150 143 151 export function expectValidUpdatesConfig( 144 152 schema: SchemaIntrospector, 145 - updates: UpdatesConfig 153 + updates: UpdatesConfig, 154 + logger: Logger | undefined 146 155 ): void { 147 156 if (process.env.NODE_ENV === 'production') { 148 157 return; ··· 175 184 typename + 176 185 '` is not an object in the defined schema, but the `updates` config is referencing it.' + 177 186 addition, 178 - 21 187 + 21, 188 + logger 179 189 ); 180 190 } 181 191 ··· 188 198 '` on `' + 189 199 typename + 190 200 '` is not in the defined schema, but the `updates` config is referencing it.', 191 - 22 201 + 22, 202 + logger 192 203 ); 193 204 } 194 205 } 195 206 } 196 207 } 197 208 198 - function warnAboutResolver(name: string): void { 209 + function warnAboutResolver(name: string, logger: Logger | undefined): void { 199 210 warn( 200 211 `Invalid resolver: \`${name}\` is not in the defined schema, but the \`resolvers\` option is referencing it.`, 201 - 23 212 + 23, 213 + logger 202 214 ); 203 215 } 204 216 205 217 function warnAboutAbstractResolver( 206 218 name: string, 207 - kind: 'UNION' | 'INTERFACE' 219 + kind: 'UNION' | 'INTERFACE', 220 + logger: Logger | undefined 208 221 ): void { 209 222 warn( 210 223 `Invalid resolver: \`${name}\` does not match to a concrete type in the schema, but the \`resolvers\` option is referencing it. Implement the resolver for the types that ${ 211 224 kind === 'UNION' ? 'make up the union' : 'implement the interface' 212 225 } instead.`, 213 - 26 226 + 26, 227 + logger 214 228 ); 215 229 } 216 230 217 231 export function expectValidResolversConfig( 218 232 schema: SchemaIntrospector, 219 - resolvers: ResolverConfig 233 + resolvers: ResolverConfig, 234 + logger: Logger | undefined 220 235 ): void { 221 236 if (process.env.NODE_ENV === 'production') { 222 237 return; ··· 230 245 ).fields(); 231 246 for (const resolverQuery in resolvers.Query || {}) { 232 247 if (!validQueries[resolverQuery]) { 233 - warnAboutResolver('Query.' + resolverQuery); 248 + warnAboutResolver('Query.' + resolverQuery, logger); 234 249 } 235 250 } 236 251 } else { 237 - warnAboutResolver('Query'); 252 + warnAboutResolver('Query', logger); 238 253 } 239 254 } else { 240 255 if (!schema.types!.has(key)) { 241 - warnAboutResolver(key); 256 + warnAboutResolver(key, logger); 242 257 } else if ( 243 258 schema.types!.get(key)!.kind === 'INTERFACE' || 244 259 schema.types!.get(key)!.kind === 'UNION' 245 260 ) { 246 261 warnAboutAbstractResolver( 247 262 key, 248 - schema.types!.get(key)!.kind as 'INTERFACE' | 'UNION' 263 + schema.types!.get(key)!.kind as 'INTERFACE' | 'UNION', 264 + logger 249 265 ); 250 266 } else { 251 267 const validTypeProperties = ( ··· 253 269 ).fields(); 254 270 for (const resolverProperty in resolvers[key] || {}) { 255 271 if (!validTypeProperties[resolverProperty]) { 256 - warnAboutResolver(key + '.' + resolverProperty); 272 + warnAboutResolver(key + '.' + resolverProperty, logger); 257 273 } 258 274 } 259 275 } ··· 263 279 264 280 export function expectValidOptimisticMutationsConfig( 265 281 schema: SchemaIntrospector, 266 - optimisticMutations: OptimisticMutationConfig 282 + optimisticMutations: OptimisticMutationConfig, 283 + logger: Logger | undefined 267 284 ): void { 268 285 if (process.env.NODE_ENV === 'production') { 269 286 return; ··· 277 294 if (!validMutations[mutation]) { 278 295 warn( 279 296 `Invalid optimistic mutation field: \`${mutation}\` is not a mutation field in the defined schema, but the \`optimistic\` option is referencing it.`, 280 - 24 297 + 24, 298 + logger 281 299 ); 282 300 } 283 301 }
+11 -2
exchanges/graphcache/src/helpers/help.ts
··· 7 7 ExecutableDefinitionNode, 8 8 InlineFragmentNode, 9 9 } from '@0no-co/graphql.web'; 10 + import type { Logger } from '../types'; 10 11 import { Kind } from '@0no-co/graphql.web'; 11 12 12 13 export type ErrorCode = ··· 89 90 } 90 91 } 91 92 92 - export function warn(message: string, code: ErrorCode) { 93 + export function warn( 94 + message: string, 95 + code: ErrorCode, 96 + logger: Logger | undefined 97 + ) { 93 98 if (!cache.has(message)) { 94 - console.warn(message + getDebugOutput() + helpUrl + code); 99 + if (logger) { 100 + logger('warn', message + getDebugOutput() + helpUrl + code); 101 + } else { 102 + console.warn(message + getDebugOutput() + helpUrl + code); 103 + } 95 104 cache.add(message); 96 105 } 97 106 }
+25 -12
exchanges/graphcache/src/operations/query.ts
··· 235 235 ' but could only find ' + 236 236 Object.keys(fragments).join(', ') + 237 237 '.', 238 - 6 238 + 6, 239 + store.logger 239 240 ); 240 241 241 242 return null; ··· 247 248 warn( 248 249 'readFragment(...) was called with an empty fragment.\n' + 249 250 'You have to call it with at least one fragment in your GraphQL document.', 250 - 6 251 + 6, 252 + store.logger 251 253 ); 252 254 253 255 return null; ··· 264 266 'You have to pass an `id` or `_id` field or create a custom `keys` config for `' + 265 267 typename + 266 268 '`.', 267 - 7 269 + 7, 270 + store.logger 268 271 ); 269 272 270 273 return null; ··· 327 330 if (fieldResolver && directiveResolver) { 328 331 warn( 329 332 `A resolver and directive is being used at "${typename}.${fieldName}" simultaneously. Only the directive will apply.`, 330 - 28 333 + 28, 334 + ctx.store.logger 331 335 ); 332 336 } 333 337 ··· 356 360 ctx.store.rootFields.subscription + 357 361 '` types are special ' + 358 362 'Operation Root Types and cannot be read back from the cache.', 359 - 25 363 + 25, 364 + store.logger 360 365 ); 361 366 } 362 367 ··· 373 378 entityKey + 374 379 '` returned an ' + 375 380 'invalid typename that could not be reconciled with the cache.', 376 - 8 381 + 8, 382 + store.logger 377 383 ); 378 384 379 385 return; ··· 406 412 const resultValue = result ? result[fieldName] : undefined; 407 413 408 414 if (process.env.NODE_ENV !== 'production' && store.schema && typename) { 409 - isFieldAvailableOnType(store.schema, typename, fieldName); 415 + isFieldAvailableOnType( 416 + store.schema, 417 + typename, 418 + fieldName, 419 + ctx.store.logger 420 + ); 410 421 } 411 422 412 423 // Add the current alias to the walked path before processing the field's value ··· 466 477 if ( 467 478 store.schema && 468 479 dataFieldValue === null && 469 - !isFieldNullable(store.schema, typename, fieldName) 480 + !isFieldNullable(store.schema, typename, fieldName, ctx.store.logger) 470 481 ) { 471 482 // Special case for when null is not a valid value for the 472 483 // current field ··· 519 530 dataFieldValue === undefined && 520 531 (directives.optional || 521 532 !!getFieldError(ctx) || 522 - (store.schema && isFieldNullable(store.schema, typename, fieldName))) 533 + (store.schema && 534 + isFieldNullable(store.schema, typename, fieldName, ctx.store.logger))) 523 535 ) { 524 536 // The field is uncached or has errored, so it'll be set to null and skipped 525 537 ctx.partial = true; ··· 570 582 // Check whether values of the list may be null; for resolvers we assume 571 583 // that they can be, since it's user-provided data 572 584 const _isListNullable = store.schema 573 - ? isListNullable(store.schema, typename, fieldName) 585 + ? isListNullable(store.schema, typename, fieldName, ctx.store.logger) 574 586 : false; 575 587 const hasPartials = ctx.partial; 576 588 const data = InMemoryData.makeData(prevData, true); ··· 622 634 key + 623 635 '` is a scalar (number, boolean, etc)' + 624 636 ', but the GraphQL query expects a selection set for this field.', 625 - 9 637 + 9, 638 + ctx.store.logger 626 639 ); 627 640 628 641 return undefined; ··· 641 654 if (Array.isArray(link)) { 642 655 const { store } = ctx; 643 656 const _isListNullable = store.schema 644 - ? isListNullable(store.schema, typename, fieldName) 657 + ? isListNullable(store.schema, typename, fieldName, ctx.store.logger) 645 658 : false; 646 659 const newLink = InMemoryData.makeData(prevData, true); 647 660 const hasPartials = ctx.partial;
+9 -4
exchanges/graphcache/src/operations/shared.ts
··· 25 25 Link, 26 26 Entity, 27 27 Data, 28 + Logger, 28 29 } from '../types'; 29 30 30 31 export interface Context { ··· 117 118 node: FormattedNode<InlineFragmentNode | FragmentDefinitionNode>, 118 119 typename: void | string, 119 120 entityKey: string, 120 - vars: Variables 121 + vars: Variables, 122 + logger?: Logger 121 123 ) => { 122 124 if (!typename) return false; 123 125 const typeCondition = getTypeCondition(node); ··· 134 136 '` may be an ' + 135 137 'interface.\nA schema needs to be defined for this match to be deterministic, ' + 136 138 'otherwise the fragment will be matched heuristically!', 137 - 16 139 + 16, 140 + logger 138 141 ); 139 142 140 143 return ( ··· 192 195 fragment, 193 196 typename, 194 197 entityKey, 195 - ctx.variables 198 + ctx.variables, 199 + ctx.store.logger 196 200 )); 197 201 if (isMatching) { 198 202 if (process.env.NODE_ENV !== 'production') ··· 234 238 '\nYou have to pass an `id` or `_id` field or create a custom `keys` config for `' + 235 239 ref.__typename + 236 240 '`.', 237 - 12 241 + 12, 242 + store.logger 238 243 ); 239 244 } 240 245
+18 -7
exchanges/graphcache/src/operations/write.ts
··· 147 147 ' but could only find ' + 148 148 Object.keys(fragments).join(', ') + 149 149 '.', 150 - 11 150 + 11, 151 + store.logger 151 152 ); 152 153 153 154 return null; ··· 159 160 warn( 160 161 'writeFragment(...) was called with an empty fragment.\n' + 161 162 'You have to call it with at least one fragment in your GraphQL document.', 162 - 11 163 + 11, 164 + store.logger 163 165 ); 164 166 165 167 return null; ··· 175 177 'You have to pass an `id` or `_id` field or create a custom `keys` config for `' + 176 178 typename + 177 179 '`.', 178 - 12 180 + 12, 181 + store.logger 179 182 ); 180 183 } 181 184 ··· 223 226 warn( 224 227 "Couldn't find __typename when writing.\n" + 225 228 "If you're writing to the cache manually have to pass a `__typename` property on each entity in your data.", 226 - 14 229 + 14, 230 + ctx.store.logger 227 231 ); 228 232 return; 229 233 } else if (!isRoot && entityKey) { ··· 260 264 261 265 if (process.env.NODE_ENV !== 'production') { 262 266 if (ctx.store.schema && typename && fieldName !== '__typename') { 263 - isFieldAvailableOnType(ctx.store.schema, typename, fieldName); 267 + isFieldAvailableOnType( 268 + ctx.store.schema, 269 + typename, 270 + fieldName, 271 + ctx.store.logger 272 + ); 264 273 } 265 274 } 266 275 ··· 309 318 '` is `undefined`, but the GraphQL query expects a ' + 310 319 expected + 311 320 ' for this field.', 312 - 13 321 + 13, 322 + ctx.store.logger 313 323 ); 314 324 } 315 325 } ··· 426 436 'If this is intentional, create a `keys` config for `' + 427 437 typename + 428 438 '` that always returns null.', 429 - 15 439 + 15, 440 + ctx.store.logger 430 441 ); 431 442 } 432 443
+8 -4
exchanges/graphcache/src/store/store.ts
··· 17 17 Entity, 18 18 CacheExchangeOpts, 19 19 DirectivesConfig, 20 + Logger, 20 21 } from '../types'; 21 22 22 23 import { invariant } from '../helpers/help'; ··· 48 49 { 49 50 data: InMemoryData.InMemoryData; 50 51 52 + logger?: Logger; 51 53 directives: DirectivesConfig; 52 54 resolvers: ResolverConfig; 53 55 updates: UpdatesConfig; ··· 62 64 constructor(opts?: C) { 63 65 if (!opts) opts = {} as C; 64 66 67 + this.logger = opts.logger; 65 68 this.resolvers = opts.resolvers || {}; 66 69 this.directives = opts.directives || {}; 67 70 this.optimisticMutations = opts.optimistic || {}; ··· 100 103 this.data = InMemoryData.make(queryName); 101 104 102 105 if (this.schema && process.env.NODE_ENV !== 'production') { 103 - expectValidKeyingConfig(this.schema, this.keys); 104 - expectValidUpdatesConfig(this.schema, this.updates); 105 - expectValidResolversConfig(this.schema, this.resolvers); 106 + expectValidKeyingConfig(this.schema, this.keys, this.logger); 107 + expectValidUpdatesConfig(this.schema, this.updates, this.logger); 108 + expectValidResolversConfig(this.schema, this.resolvers, this.logger); 106 109 expectValidOptimisticMutationsConfig( 107 110 this.schema, 108 - this.optimisticMutations 111 + this.optimisticMutations, 112 + this.logger 109 113 ); 110 114 } 111 115 }
+13
exchanges/graphcache/src/types.ts
··· 541 541 | null 542 542 | undefined; 543 543 544 + export type Logger = ( 545 + severity: 'debug' | 'error' | 'warn', 546 + message: string 547 + ) => void; 548 + 544 549 /** Input parameters for the {@link cacheExchange}. */ 545 550 export type CacheExchangeOpts = { 551 + /** Configure a custom-logger for graphcache, this function wll be called with a severity and a message. 552 + * 553 + * @remarks 554 + * By default we will invoke `console.warn` for warnings during development, however you might want to opt 555 + * out of this because you are re-using urql for a different library. This setting allows you to stub the logger 556 + * function or filter to only logs you want. 557 + */ 558 + logger?: Logger; 546 559 /** Configures update functions which are called when the mapped fields are written to the cache. 547 560 * 548 561 * @remarks