···11+---
22+'@urql/exchange-graphcache': patch
33+---
44+55+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
···164164 todos: [{ __typename: 'Todo', id: '0', text: 'Solve bug' }],
165165 });
166166167167- // The warning should be called for `__typename`
168168- expect(console.warn).toHaveBeenCalledTimes(1);
169167 expect(console.error).not.toHaveBeenCalled();
170168 });
171169
+5-3
exchanges/graphcache/src/operations/write.test.ts
···162162 }
163163 `;
164164165165- write(store, { query }, { field: 'test' } as any);
166165 // This should not overwrite the field
167166 write(store, { query }, { field: undefined } as any);
168167 // Because of us writing an undefined field
169168 expect(console.warn).toHaveBeenCalledTimes(2);
170170- expect((console.warn as any).mock.calls[0][0]).toMatch(
171171- /The field `field` does not exist on `Query`/
169169+170170+ expect((console.warn as any).mock.calls[1][0]).toMatch(
171171+ /Invalid undefined: The field at `field`/
172172 );
173173174174+ write(store, { query }, { field: 'test' } as any);
175175+ write(store, { query }, { field: undefined } as any);
174176 InMemoryData.initDataState('read', store.data, null);
175177 // The field must still be `'test'`
176178 expect(InMemoryData.readRecord('Query', 'field')).toBe('test');
+50-33
exchanges/graphcache/src/operations/write.ts
···210210 const rootField = ctx.store.rootNames[entityKey!] || 'query';
211211 const isRoot = !!ctx.store.rootNames[entityKey!];
212212213213- const typename = isRoot ? entityKey : data.__typename;
213213+ let typename = isRoot ? entityKey : data.__typename;
214214+ if (!typename && entityKey && ctx.optimistic) {
215215+ typename = InMemoryData.readRecord(entityKey, '__typename') as
216216+ | string
217217+ | undefined;
218218+ }
219219+214220 if (!typename) {
215221 warn(
216222 "Couldn't find __typename when writing.\n" +
···239245 const fieldAlias = getFieldAlias(node);
240246 let fieldValue = data[ctx.optimistic ? fieldName : fieldAlias];
241247242242- // Development check of undefined fields
243243- if (process.env.NODE_ENV !== 'production') {
244244- if (
245245- rootField === 'query' &&
246246- fieldValue === undefined &&
247247- !deferRef &&
248248- !ctx.optimistic
249249- ) {
250250- const expected =
251251- node.selectionSet === undefined
252252- ? 'scalar (number, boolean, etc)'
253253- : 'selection set';
254254-255255- warn(
256256- 'Invalid undefined: The field at `' +
257257- fieldKey +
258258- '` is `undefined`, but the GraphQL query expects a ' +
259259- expected +
260260- ' for this field.',
261261- 13
262262- );
263263-264264- continue; // Skip this field
265265- } else if (ctx.store.schema && typename && fieldName !== '__typename') {
266266- isFieldAvailableOnType(ctx.store.schema, typename, fieldName);
267267- }
268268- }
269269-270248 if (
271249 // Skip typename fields and assume they've already been written above
272250 fieldName === '__typename' ||
···276254 (deferRef || (ctx.optimistic && rootField === 'query')))
277255 ) {
278256 continue;
257257+ }
258258+259259+ if (process.env.NODE_ENV !== 'production') {
260260+ if (ctx.store.schema && typename && fieldName !== '__typename') {
261261+ isFieldAvailableOnType(ctx.store.schema, typename, fieldName);
262262+ }
279263 }
280264281265 // Add the current alias to the walked path before processing the field's value
···298282 fieldValue = ensureData(resolver(fieldArgs || {}, ctx.store, ctx));
299283 }
300284285285+ if (fieldValue === undefined) {
286286+ if (process.env.NODE_ENV !== 'production') {
287287+ if (
288288+ !entityKey ||
289289+ !InMemoryData.hasField(entityKey, fieldKey) ||
290290+ (ctx.optimistic && !InMemoryData.readRecord(entityKey, '__typename'))
291291+ ) {
292292+ const expected =
293293+ node.selectionSet === undefined
294294+ ? 'scalar (number, boolean, etc)'
295295+ : 'selection set';
296296+297297+ warn(
298298+ 'Invalid undefined: The field at `' +
299299+ fieldKey +
300300+ '` is `undefined`, but the GraphQL query expects a ' +
301301+ expected +
302302+ ' for this field.',
303303+ 13
304304+ );
305305+ }
306306+ }
307307+308308+ continue; // Skip this field
309309+ }
310310+301311 if (node.selectionSet) {
302312 // Process the field and write links for the child entities that have been written
303313 if (entityKey && rootField === 'query') {
···306316 ctx,
307317 getSelectionSet(node),
308318 ensureData(fieldValue),
309309- key
319319+ key,
320320+ ctx.optimistic
321321+ ? InMemoryData.readLink(entityKey || typename, fieldKey)
322322+ : undefined
310323 );
311324 InMemoryData.writeLink(entityKey || typename, fieldKey, link);
312325 } else {
···353366 ctx: Context,
354367 select: SelectionSet,
355368 data: null | Data | NullArray<Data>,
356356- parentFieldKey?: string
369369+ parentFieldKey?: string,
370370+ prevLink?: Link
357371): Link | undefined => {
358372 if (Array.isArray(data)) {
359373 const newData = new Array(data.length);
···365379 ? joinKeys(parentFieldKey, `${i}`)
366380 : undefined;
367381 // Recursively write array data
368368- const links = writeField(ctx, select, data[i], indexKey);
382382+ const prevIndex = prevLink != null ? prevLink[i] : undefined;
383383+ const links = writeField(ctx, select, data[i], indexKey, prevIndex);
369384 // Link cannot be expressed as a recursive type
370385 newData[i] = links as string | null;
371386 // After processing the field, remove the current index from the path
···377392 return getFieldError(ctx) ? undefined : null;
378393 }
379394380380- const entityKey = ctx.store.keyOfEntity(data);
395395+ const entityKey =
396396+ ctx.store.keyOfEntity(data) ||
397397+ (typeof prevLink === 'string' ? prevLink : null);
381398 const typename = data.__typename;
382399383400 if (