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): Allow partial optimistic results (#3264)

authored by

Phil Pluckthun and committed by
GitHub
008c61bb 3a8fecbd

+60 -38
+5
.changeset/five-icons-agree.md
··· 1 + --- 2 + '@urql/exchange-graphcache': patch 3 + --- 4 + 5 + Make "Invalid undefined" warning heuristic smarter and allow for partial optimistic results. Previously, when a partial optimistic result would be passed, a warning would be issued, and in production, fields would be deleted from the cache. Instead, we now only issue a warning if these fields aren't cached already.
-2
exchanges/graphcache/src/operations/query.test.ts
··· 164 164 todos: [{ __typename: 'Todo', id: '0', text: 'Solve bug' }], 165 165 }); 166 166 167 - // The warning should be called for `__typename` 168 - expect(console.warn).toHaveBeenCalledTimes(1); 169 167 expect(console.error).not.toHaveBeenCalled(); 170 168 }); 171 169
+5 -3
exchanges/graphcache/src/operations/write.test.ts
··· 162 162 } 163 163 `; 164 164 165 - write(store, { query }, { field: 'test' } as any); 166 165 // This should not overwrite the field 167 166 write(store, { query }, { field: undefined } as any); 168 167 // Because of us writing an undefined field 169 168 expect(console.warn).toHaveBeenCalledTimes(2); 170 - expect((console.warn as any).mock.calls[0][0]).toMatch( 171 - /The field `field` does not exist on `Query`/ 169 + 170 + expect((console.warn as any).mock.calls[1][0]).toMatch( 171 + /Invalid undefined: The field at `field`/ 172 172 ); 173 173 174 + write(store, { query }, { field: 'test' } as any); 175 + write(store, { query }, { field: undefined } as any); 174 176 InMemoryData.initDataState('read', store.data, null); 175 177 // The field must still be `'test'` 176 178 expect(InMemoryData.readRecord('Query', 'field')).toBe('test');
+50 -33
exchanges/graphcache/src/operations/write.ts
··· 210 210 const rootField = ctx.store.rootNames[entityKey!] || 'query'; 211 211 const isRoot = !!ctx.store.rootNames[entityKey!]; 212 212 213 - const typename = isRoot ? entityKey : data.__typename; 213 + let typename = isRoot ? entityKey : data.__typename; 214 + if (!typename && entityKey && ctx.optimistic) { 215 + typename = InMemoryData.readRecord(entityKey, '__typename') as 216 + | string 217 + | undefined; 218 + } 219 + 214 220 if (!typename) { 215 221 warn( 216 222 "Couldn't find __typename when writing.\n" + ··· 239 245 const fieldAlias = getFieldAlias(node); 240 246 let fieldValue = data[ctx.optimistic ? fieldName : fieldAlias]; 241 247 242 - // Development check of undefined fields 243 - if (process.env.NODE_ENV !== 'production') { 244 - if ( 245 - rootField === 'query' && 246 - fieldValue === undefined && 247 - !deferRef && 248 - !ctx.optimistic 249 - ) { 250 - const expected = 251 - node.selectionSet === undefined 252 - ? 'scalar (number, boolean, etc)' 253 - : 'selection set'; 254 - 255 - warn( 256 - 'Invalid undefined: The field at `' + 257 - fieldKey + 258 - '` is `undefined`, but the GraphQL query expects a ' + 259 - expected + 260 - ' for this field.', 261 - 13 262 - ); 263 - 264 - continue; // Skip this field 265 - } else if (ctx.store.schema && typename && fieldName !== '__typename') { 266 - isFieldAvailableOnType(ctx.store.schema, typename, fieldName); 267 - } 268 - } 269 - 270 248 if ( 271 249 // Skip typename fields and assume they've already been written above 272 250 fieldName === '__typename' || ··· 276 254 (deferRef || (ctx.optimistic && rootField === 'query'))) 277 255 ) { 278 256 continue; 257 + } 258 + 259 + if (process.env.NODE_ENV !== 'production') { 260 + if (ctx.store.schema && typename && fieldName !== '__typename') { 261 + isFieldAvailableOnType(ctx.store.schema, typename, fieldName); 262 + } 279 263 } 280 264 281 265 // Add the current alias to the walked path before processing the field's value ··· 298 282 fieldValue = ensureData(resolver(fieldArgs || {}, ctx.store, ctx)); 299 283 } 300 284 285 + if (fieldValue === undefined) { 286 + if (process.env.NODE_ENV !== 'production') { 287 + if ( 288 + !entityKey || 289 + !InMemoryData.hasField(entityKey, fieldKey) || 290 + (ctx.optimistic && !InMemoryData.readRecord(entityKey, '__typename')) 291 + ) { 292 + const expected = 293 + node.selectionSet === undefined 294 + ? 'scalar (number, boolean, etc)' 295 + : 'selection set'; 296 + 297 + warn( 298 + 'Invalid undefined: The field at `' + 299 + fieldKey + 300 + '` is `undefined`, but the GraphQL query expects a ' + 301 + expected + 302 + ' for this field.', 303 + 13 304 + ); 305 + } 306 + } 307 + 308 + continue; // Skip this field 309 + } 310 + 301 311 if (node.selectionSet) { 302 312 // Process the field and write links for the child entities that have been written 303 313 if (entityKey && rootField === 'query') { ··· 306 316 ctx, 307 317 getSelectionSet(node), 308 318 ensureData(fieldValue), 309 - key 319 + key, 320 + ctx.optimistic 321 + ? InMemoryData.readLink(entityKey || typename, fieldKey) 322 + : undefined 310 323 ); 311 324 InMemoryData.writeLink(entityKey || typename, fieldKey, link); 312 325 } else { ··· 353 366 ctx: Context, 354 367 select: SelectionSet, 355 368 data: null | Data | NullArray<Data>, 356 - parentFieldKey?: string 369 + parentFieldKey?: string, 370 + prevLink?: Link 357 371 ): Link | undefined => { 358 372 if (Array.isArray(data)) { 359 373 const newData = new Array(data.length); ··· 365 379 ? joinKeys(parentFieldKey, `${i}`) 366 380 : undefined; 367 381 // Recursively write array data 368 - const links = writeField(ctx, select, data[i], indexKey); 382 + const prevIndex = prevLink != null ? prevLink[i] : undefined; 383 + const links = writeField(ctx, select, data[i], indexKey, prevIndex); 369 384 // Link cannot be expressed as a recursive type 370 385 newData[i] = links as string | null; 371 386 // After processing the field, remove the current index from the path ··· 377 392 return getFieldError(ctx) ? undefined : null; 378 393 } 379 394 380 - const entityKey = ctx.store.keyOfEntity(data); 395 + const entityKey = 396 + ctx.store.keyOfEntity(data) || 397 + (typeof prevLink === 'string' ? prevLink : null); 381 398 const typename = data.__typename; 382 399 383 400 if (