Repository: FormidableLabs/urql (ts)

Issue:
graphcache: support inline fragments without type conditions
Graphcache has an uncovered edge case that is misbehaving related to inline fragments.

```graphql
query A {
  ... {
    field
  }
}

query B {
  ... @skip(if: false) {
    field
  }
}
```

The `typeCondition` for `InlineFragmentNode` is optional, yet when it's missing the fragment is skipped in both of the above conditions.

Pull request description:
(graphcache) - Fix handling of conditionless inline fragment
Fix #1039

- Add test suite case for conditionless inline fragment
- Fix `isFragmentHeuristicallyMatching`
- Fix `isInterfaceOfType`
- Use `node` in `isInterfaceOfType` for consistency
- Update tests in `schemaPredicates.test.ts`
- Add test suite case for directives (while I'm at it)

Patch:
diff --git a/.changeset/small-worms-bake.md b/.changeset/small-worms-bake.md
new file mode 100644
index 00000000..a862dbc8
--- /dev/null
+++ b/.changeset/small-worms-bake.md
@@ -0,0 +1,5 @@
+---
+'@urql/exchange-graphcache': patch
+---
+
+Fix inline fragments being skipped when they were missing a full type condition as per the GraphQL spec (e.g `{ ... { field } }`)
diff --git a/exchanges/graphcache/src/ast/node.ts b/exchanges/graphcache/src/ast/node.ts
index 19341456..1a5427ae 100644
--- a/exchanges/graphcache/src/ast/node.ts
+++ b/exchanges/graphcache/src/ast/node.ts
@@ -26,11 +26,9 @@ export const getSelectionSet = (node: {
   selectionSet?: SelectionSetNode;
 }): SelectionSet => (node.selectionSet ? node.selectionSet.selections : []);
 
-export const getTypeCondition = ({
-  typeCondition,
-}: {
+export const getTypeCondition = (node: {
   typeCondition?: NamedTypeNode;
-}): string | null => (typeCondition ? getName(typeCondition) : null);
+}): string | null => (node.typeCondition ? getName(node.typeCondition) : null);
 
 export const isFieldNode = (node: SelectionNode): node is FieldNode =>
   node.kind === Kind.FIELD;
diff --git a/exchanges/graphcache/src/ast/schemaPredicates.ts b/exchanges/graphcache/src/ast/schemaPredicates.ts
index dec17d01..4b5f2d3e 100644
--- a/exchanges/graphcache/src/ast/schemaPredicates.ts
+++ b/exchanges/graphcache/src/ast/schemaPredicates.ts
@@ -2,6 +2,8 @@ import {
   isNullableType,
   isListType,
   isNonNullType,
+  InlineFragmentNode,
+  FragmentDefinitionNode,
   GraphQLSchema,
   GraphQLAbstractType,
   GraphQLObjectType,
@@ -10,6 +12,7 @@ import {
 } from 'graphql';
 
 import { warn, invariant } from '../helpers/help';
+import { getTypeCondition } from './node';
 import {
   KeyingConfig,
   UpdateResolver,
@@ -51,11 +54,12 @@ export const isFieldAvailableOnType = (
 
 export const isInterfaceOfType = (
   schema: GraphQLSchema,
-  typeCondition: null | string,
+  node: InlineFragmentNode | FragmentDefinitionNode,
   typename: string | void
 ): boolean => {
-  if (!typename || !typeCondition) return false;
-  if (typename === typeCondition) return true;
+  if (!typename) return false;
+  const typeCondition = getTypeCondition(node);
+  if (!typeCondition || typename === typeCondition) return true;
 
   const abstractType = schema.getType(typeCondition);
   const objectType = schema.getType(typename);
diff --git a/exchanges/graphcache/src/operations/shared.ts b/exchanges/graphcache/src/operations/shared.ts
index c8936c5d..ba896626 100644
--- a/exchanges/graphcache/src/operations/shared.ts
+++ b/exchanges/graphcache/src/operations/shared.ts
@@ -67,7 +67,7 @@ const isFragmentHeuristicallyMatching = (
 ) => {
   if (!typename) return false;
   const typeCondition = getTypeCondition(node);
-  if (typename === typeCondition) return true;
+  if (!typeCondition || typename === typeCondition) return true;
 
   warn(
     'Heuristic Fragment Matching: A fragment is trying to match against the `' +
@@ -127,11 +127,7 @@ export const makeSelectionIterator = (
               }
 
               const isMatching = ctx.store.schema
-                ? isInterfaceOfType(
-                    ctx.store.schema,
-                    getTypeCondition(fragmentNode),
-                    typename
-                  )
+                ? isInterfaceOfType(ctx.store.schema, fragmentNode, typename)
                 : isFragmentHeuristicallyMatching(
                     fragmentNode,
                     typename,


Test patch:
diff --git a/exchanges/graphcache/src/ast/schemaPredicates.test.ts b/exchanges/graphcache/src/ast/schemaPredicates.test.ts
index b1c836d0..0b7b7d8f 100644
--- a/exchanges/graphcache/src/ast/schemaPredicates.test.ts
+++ b/exchanges/graphcache/src/ast/schemaPredicates.test.ts
@@ -1,4 +1,4 @@
-import { buildClientSchema } from 'graphql';
+import { Kind, InlineFragmentNode, buildClientSchema } from 'graphql';
 import { mocked } from 'ts-jest/utils';
 import * as SchemaPredicates from './schemaPredicates';
 
@@ -6,25 +6,46 @@ describe('SchemaPredicates', () => {
   // eslint-disable-next-line
   const schema = buildClientSchema(require('../test-utils/simple_schema.json'));
 
+  const frag = (value: string): InlineFragmentNode => ({
+    kind: Kind.INLINE_FRAGMENT,
+    typeCondition: {
+      kind: Kind.NAMED_TYPE,
+      name: {
+        kind: Kind.NAME,
+        value,
+      },
+    },
+    selectionSet: {
+      kind: Kind.SELECTION_SET,
+      selections: [],
+    },
+  });
+
   it('should match fragments by interface/union', () => {
     expect(
-      SchemaPredicates.isInterfaceOfType(schema, 'ITodo', 'BigTodo')
+      SchemaPredicates.isInterfaceOfType(schema, frag('ITodo'), 'BigTodo')
     ).toBeTruthy();
     expect(
-      SchemaPredicates.isInterfaceOfType(schema, 'ITodo', 'SmallTodo')
+      SchemaPredicates.isInterfaceOfType(schema, frag('ITodo'), 'SmallTodo')
     ).toBeTruthy();
     expect(
-      SchemaPredicates.isInterfaceOfType(schema, 'Search', 'BigTodo')
+      SchemaPredicates.isInterfaceOfType(schema, frag('Search'), 'BigTodo')
     ).toBeTruthy();
     expect(
-      SchemaPredicates.isInterfaceOfType(schema, 'Search', 'SmallTodo')
+      SchemaPredicates.isInterfaceOfType(schema, frag('Search'), 'SmallTodo')
     ).toBeTruthy();
     expect(
-      SchemaPredicates.isInterfaceOfType(schema, 'ITodo', 'Todo')
+      SchemaPredicates.isInterfaceOfType(schema, frag('ITodo'), 'Todo')
     ).toBeFalsy();
     expect(
-      SchemaPredicates.isInterfaceOfType(schema, 'Search', 'Todo')
+      SchemaPredicates.isInterfaceOfType(schema, frag('Search'), 'Todo')
     ).toBeFalsy();
+
+    const typeConditionLess = frag('Type');
+    (typeConditionLess as any).typeCondition = undefined;
+    expect(
+      SchemaPredicates.isInterfaceOfType(schema, typeConditionLess, 'Todo')
+    ).toBeTruthy();
   });
 
   it('should indicate nullability', () => {
@@ -41,17 +62,21 @@ describe('SchemaPredicates', () => {
 
   it('should handle unions of objects', () => {
     expect(
-      SchemaPredicates.isInterfaceOfType(schema, 'LatestTodoResult', 'Todo')
+      SchemaPredicates.isInterfaceOfType(
+        schema,
+        frag('LatestTodoResult'),
+        'Todo'
+      )
     ).toBeTruthy();
     expect(
       SchemaPredicates.isInterfaceOfType(
         schema,
-        'LatestTodoResult',
+        frag('LatestTodoResult'),
         'NoTodosError'
       )
     ).toBeTruthy();
     expect(
-      SchemaPredicates.isInterfaceOfType(schema, 'Todo', 'NoTodosError')
+      SchemaPredicates.isInterfaceOfType(schema, frag('Todo'), 'NoTodosError')
     ).toBeFalsy();
   });
 
diff --git a/exchanges/graphcache/src/test-utils/suite.test.ts b/exchanges/graphcache/src/test-utils/suite.test.ts
index 03424f65..0a8fe5a5 100644
--- a/exchanges/graphcache/src/test-utils/suite.test.ts
+++ b/exchanges/graphcache/src/test-utils/suite.test.ts
@@ -44,6 +44,44 @@ it('aliased field on query', () => {
   });
 });
 
+it('@skip directive on field on query', () => {
+  expectCacheIntegrity({
+    query: gql`
+      {
+        __typename
+        intA @skip(if: true)
+        intB @skip(if: false)
+      }
+    `,
+    data: { __typename: 'Query', intB: 2 },
+  });
+});
+
+it('@include directive on field on query', () => {
+  expectCacheIntegrity({
+    query: gql`
+      {
+        __typename
+        intA @include(if: true)
+        intB @include(if: false)
+      }
+    `,
+    data: { __typename: 'Query', intA: 2 },
+  });
+});
+
+it('random directive on field on query', () => {
+  expectCacheIntegrity({
+    query: gql`
+      {
+        __typename
+        int @shouldntMatter
+      }
+    `,
+    data: { __typename: 'Query', int: 1 },
+  });
+});
+
 it('json on query', () => {
   expectCacheIntegrity({
     query: gql`
@@ -259,6 +297,39 @@ it('entity list on query and inline fragment', () => {
   });
 });
 
+it('conditionless inline fragment', () => {
+  expectCacheIntegrity({
+    query: gql`
+      {
+        __typename
+        ... {
+          test
+        }
+      }
+    `,
+    data: {
+      __typename: 'Query',
+      test: true,
+    },
+  });
+});
+
+it('skipped conditionless inline fragment', () => {
+  expectCacheIntegrity({
+    query: gql`
+      {
+        __typename
+        ... @skip(if: true) {
+          test
+        }
+      }
+    `,
+    data: {
+      __typename: 'Query',
+    },
+  });
+});
+
 it('entity list on query and spread fragment', () => {
   expectCacheIntegrity({
     query: gql`
@@ -284,6 +355,24 @@ it('entity list on query and spread fragment', () => {
   });
 });
 
+it('skipped spread fragment', () => {
+  expectCacheIntegrity({
+    query: gql`
+      query Test {
+        __typename
+        ...TestFragment @skip(if: true)
+      }
+
+      fragment TestFragment on Query {
+        test
+      }
+    `,
+    data: {
+      __typename: 'Query',
+    },
+  });
+});
+
 it('embedded objects on entities', () => {
   expectCacheIntegrity({
     query: gql`