Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.

fix(graphcache): Allow partial optimistic results (#3264)

Changed files
+60 -38
.changeset
exchanges
graphcache
+5
.changeset/five-icons-agree.md
···
+
---
+
'@urql/exchange-graphcache': patch
+
---
+
+
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
···
todos: [{ __typename: 'Todo', id: '0', text: 'Solve bug' }],
});
-
// The warning should be called for `__typename`
-
expect(console.warn).toHaveBeenCalledTimes(1);
expect(console.error).not.toHaveBeenCalled();
});
+5 -3
exchanges/graphcache/src/operations/write.test.ts
···
}
`;
-
write(store, { query }, { field: 'test' } as any);
// This should not overwrite the field
write(store, { query }, { field: undefined } as any);
// Because of us writing an undefined field
expect(console.warn).toHaveBeenCalledTimes(2);
-
expect((console.warn as any).mock.calls[0][0]).toMatch(
-
/The field `field` does not exist on `Query`/
+
+
expect((console.warn as any).mock.calls[1][0]).toMatch(
+
/Invalid undefined: The field at `field`/
);
+
write(store, { query }, { field: 'test' } as any);
+
write(store, { query }, { field: undefined } as any);
InMemoryData.initDataState('read', store.data, null);
// The field must still be `'test'`
expect(InMemoryData.readRecord('Query', 'field')).toBe('test');
+50 -33
exchanges/graphcache/src/operations/write.ts
···
const rootField = ctx.store.rootNames[entityKey!] || 'query';
const isRoot = !!ctx.store.rootNames[entityKey!];
-
const typename = isRoot ? entityKey : data.__typename;
+
let typename = isRoot ? entityKey : data.__typename;
+
if (!typename && entityKey && ctx.optimistic) {
+
typename = InMemoryData.readRecord(entityKey, '__typename') as
+
| string
+
| undefined;
+
}
+
if (!typename) {
warn(
"Couldn't find __typename when writing.\n" +
···
const fieldAlias = getFieldAlias(node);
let fieldValue = data[ctx.optimistic ? fieldName : fieldAlias];
-
// Development check of undefined fields
-
if (process.env.NODE_ENV !== 'production') {
-
if (
-
rootField === 'query' &&
-
fieldValue === undefined &&
-
!deferRef &&
-
!ctx.optimistic
-
) {
-
const expected =
-
node.selectionSet === undefined
-
? 'scalar (number, boolean, etc)'
-
: 'selection set';
-
-
warn(
-
'Invalid undefined: The field at `' +
-
fieldKey +
-
'` is `undefined`, but the GraphQL query expects a ' +
-
expected +
-
' for this field.',
-
13
-
);
-
-
continue; // Skip this field
-
} else if (ctx.store.schema && typename && fieldName !== '__typename') {
-
isFieldAvailableOnType(ctx.store.schema, typename, fieldName);
-
}
-
}
-
if (
// Skip typename fields and assume they've already been written above
fieldName === '__typename' ||
···
(deferRef || (ctx.optimistic && rootField === 'query')))
) {
continue;
+
}
+
+
if (process.env.NODE_ENV !== 'production') {
+
if (ctx.store.schema && typename && fieldName !== '__typename') {
+
isFieldAvailableOnType(ctx.store.schema, typename, fieldName);
+
}
}
// Add the current alias to the walked path before processing the field's value
···
fieldValue = ensureData(resolver(fieldArgs || {}, ctx.store, ctx));
}
+
if (fieldValue === undefined) {
+
if (process.env.NODE_ENV !== 'production') {
+
if (
+
!entityKey ||
+
!InMemoryData.hasField(entityKey, fieldKey) ||
+
(ctx.optimistic && !InMemoryData.readRecord(entityKey, '__typename'))
+
) {
+
const expected =
+
node.selectionSet === undefined
+
? 'scalar (number, boolean, etc)'
+
: 'selection set';
+
+
warn(
+
'Invalid undefined: The field at `' +
+
fieldKey +
+
'` is `undefined`, but the GraphQL query expects a ' +
+
expected +
+
' for this field.',
+
13
+
);
+
}
+
}
+
+
continue; // Skip this field
+
}
+
if (node.selectionSet) {
// Process the field and write links for the child entities that have been written
if (entityKey && rootField === 'query') {
···
ctx,
getSelectionSet(node),
ensureData(fieldValue),
-
key
+
key,
+
ctx.optimistic
+
? InMemoryData.readLink(entityKey || typename, fieldKey)
+
: undefined
);
InMemoryData.writeLink(entityKey || typename, fieldKey, link);
} else {
···
ctx: Context,
select: SelectionSet,
data: null | Data | NullArray<Data>,
-
parentFieldKey?: string
+
parentFieldKey?: string,
+
prevLink?: Link
): Link | undefined => {
if (Array.isArray(data)) {
const newData = new Array(data.length);
···
? joinKeys(parentFieldKey, `${i}`)
: undefined;
// Recursively write array data
-
const links = writeField(ctx, select, data[i], indexKey);
+
const prevIndex = prevLink != null ? prevLink[i] : undefined;
+
const links = writeField(ctx, select, data[i], indexKey, prevIndex);
// Link cannot be expressed as a recursive type
newData[i] = links as string | null;
// After processing the field, remove the current index from the path
···
return getFieldError(ctx) ? undefined : null;
}
-
const entityKey = ctx.store.keyOfEntity(data);
+
const entityKey =
+
ctx.store.keyOfEntity(data) ||
+
(typeof prevLink === 'string' ? prevLink : null);
const typename = data.__typename;
if (