zino-hofmann / graphql-flutter

A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package.
https://zino-hofmann.github.io/graphql-flutter
MIT License
3.25k stars 620 forks source link

Query for data that is in the cache always hit the network #1247

Closed cspecter closed 2 years ago

cspecter commented 2 years ago

Our GraphQL server uses a relay style system. We have a setup like this for a table of products. We have one view in our app where it shows a list (feed) of products and one view that shows a product detail. Functionally these pull in the same data. Our app hits the feed view first, then the user can click on a product to get a detail. The setup looks like this:

# Product type
type Product {
  id: UUID!
  name: String
  slug: String
  releaseDate: String
  ...OtherData
}

#Product fragment
fragment ProductFrag on Product {
  __typename
  id
  name
  slug
  releaseDate
  ...OtherData
}

# Query to get the full product feed
query GetProducts($after: String, $first: Int, $releaseDate: String, $orderBy: [ProductOrderBy]) {
  products: productCollection(after: $after, first: $first, filter: { releaseDate: { gt: $releaseDate } } orderBy: $orderBy) {
    totalCount
    __typename
    edges {
        __typename
        cursor
        node {
            ...ProductFrag
        }
    }
    pageInfo {
      endCursor
      hasNextPage
      startCursor
      hasPreviousPage
    }
  }
}

#Query to get a single product
query GetProductBySlug($slug: String!) {
  products: productCollection(first: 1, filter: {slug: {eq: $slug}}) {
    __typename
    edges {
        __typename
        node {
            ...ProductFrag
        }
    }
  }
}

This returns data that looks like this in both cases:

//Feed return
{
  productCollection: {
  totalCount: 7,
  __typename: 'ProductCollectionConnection',
  edges: [
    {
      __typename: 'ProductCollectionEdge',
      node: {
        __typename: 'Product',
        id: '2c749d82-d6d3-4f8e-a281-4b9afe5465b4',
        name: 'Some Product',
        slug: 'some-product',
        ...OtherData
      }
    },
    ...OtherProductEdgesInList
    ]
  }
}

//Single Item return
{
  productCollection: {
  totalCount: 1,
  __typename: 'ProductCollectionConnection',
  edges: [
    {
      __typename: 'ProductCollectionEdge',
      node: {
        __typename: 'Product',
        id: '2c749d82-d6d3-4f8e-a281-4b9afe5465b4',
        name: 'Some Product',
        slug: 'some-product',
        ...OtherData
      }
    }
    ]
  }
}

In the first case, for the GetProducts query we are asking for things that are past a certain data with the ability to pass in cursors. In the second case, for the GetProductBySlug, we want just a single product referenced by it's slug.

After loading the GetProducts list, I see the data for the individual product is stored in the cache Product:2c749d82-d6d3-4f8e-a281-4b9afe5465b4. When we go to the next page and run the GetProductBySlug query, it always hit the network first, even if the data for the product it wants is in the cache and the policy is set to FetchPolicy.cacheFirst. We want to grab the individual product by it's slug in order to have a friendly deep link.

If we set the cache policy to FetchPolicy.cacheOnly we get this error CacheMissException(Could not resolve the given request against the cache.

I see in the cache that there is a Query key with the following:

"productCollection("{
      "after":null,
      "filter":{
         "releaseDate":{
            "gte":"2022-10-03T00:00:00.000"
         }
      },
      "first":10,
      "orderBy":[
         {
            "releaseDate":"AscNullsFirst"
         }
      ]
   }")":{
      "totalCount":7,
      "__typename":"ProductConnection",
      "edges":[
         {
            "__typename":"ProductEdge",
            "cursor":WyIyMDIyLTEwLTEyVDEyOjAwOjAwIiwgIjJjNzQ5ZDgyLWQ2ZDMtNGY4ZS1hMjgxLTRiOWFmZTU0
NjViNCJd,
            "node":{
               "$ref":"Product":2c749d82-d6d3-4f8e-a281-4b9afe5465b4
            }
         },
         {
            "__typename":"ProductEdge",
            "cursor":WyIyMDIyLTEwLTE5VDEyOjAwOjAwIiwgImZlMTQ5ODdlLTJjZGUtNDJkMC04ZmNjLTE4NGM3ZjNi
MGRjNiJd,
            "node":{
               "$ref":"Product":fe14987e-2cde-42d0-8fcc-184c7f3b0dc6
            }
         },
         {
            "__typename":"ProductEdge",
            "cursor":WyIyMDIyLTEwLTIyVDEyOjAwOjAwIiwgIjYxYzQ5NzZmLTg5YjEtNGVkYy05YWY4LWE5MjY0YWZj
MTMzYyJd,
            "node":{
               "$ref":"Product":61c4976f-89b1-4edc-9af8-a9264afc133c
            }
         },
         {
            "__typename":"ProductEdge",
            "cursor":WyIyMDIyLTEwLTI0VDEyOjAwOjAwIiwgIjY1ZDcyNWZjLTZlODgtNGM1MS1iNDg2LWYwMzY5ZTEz
YTk3NCJd,
            "node":{
               "$ref":"Product":65d725fc-6e88-4c51-b486-f0369e13a974
            }
         },
         {
            "__typename":"ProductEdge",
            "cursor":WyIyMDIyLTEwLTI4VDEyOjAwOjAwIiwgImZlOGZiMzFjLWRhYTMtNDU4ZS1iYmFhLWYxYjliNjRm
YTdiZSJd,
            "node":{
               "$ref":"Product":fe8fb31c-daa3-458e-bbaa-f1b9b64fa7be
            }
         },
         {
            "__typename":"ProductEdge",
            "cursor":WyIyMDIyLTEwLTI5VDEyOjAwOjAwIiwgImQ0NDYzNmEwLTQyY2EtNDkwNy1iMWE1LWMyZGEzYTBm
Yjc0MCJd,
            "node":{
               "$ref":"Product":d44636a0-42ca-4907-b1a5-c2da3a0fb740
            }
         },
         {
            "__typename":"ProductEdge",
            "cursor":WyIyMDIyLTExLTI2VDEyOjAwOjAwIiwgImI5NDBmNzI0LTRkNGMtNDU1NS1hYTZjLWUzNWY2ZTFk
NDgwNCJd,
            "node":{
               "$ref":"Product":b940f724-4d4c-4555-aa6c-e35f6e1d4804
            }
         }
      ],
      "pageInfo":{
         "endCursor":WyIyMDIyLTExLTI2VDEyOjAwOjAwIiwgImI5NDBmNzI0LTRkNGMtNDU1NS1hYTZjLWUzNWY2ZTFk
NDgwNCJd,
         "hasNextPage":false,
         "startCursor":WyIyMDIyLTEwLTEyVDEyOjAwOjAwIiwgIjJjNzQ5ZDgyLWQ2ZDMtNGY4ZS1hMjgxLTRiOWFmZTU0
NjViNCJd,
         "hasPreviousPage":true,
         "__typename":"PageInfo"
      }
   },

My assumption is that I can query the cache like our server, but I guess this is not the case. After it hits the server for the GetProductBySlug query, I see this has been added to the Query key in the cache:

"productCollection("{
      "filter":{
         "slug":{
            "eq":"some-product"
         }
      },
      "first":1
   }")":{
      "__typename":"ProductConnection",
      "edges":[
         {
            "__typename":"ProductEdge",
            "node":{
               "$ref":"Product":2c749d82-d6d3-4f8e-a281-4b9afe5465b4
            }
         }
      ]
   },

Once that is in there it will pull from the cache properly.

In our React web app using Apollo, making this kind of query is definitely pulling from the cache. Do I need some sort of custom Resolver here? Or to set the queries up another way?

Here is how our app is configured:

///GraphQL Provider
import 'dart:convert' show jsonDecode;

import "package:http/http.dart" as http;
import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:compute/compute.dart';
import 'package:hybrid_flutter_app/api/provider/websocket_isolate.dart';

/// GraphQL Client
ValueNotifier<GraphQLClient>? gqlclient;

/// Isolate for GraphQL queries
Future<Map<String, dynamic>?> isolateHttpResponseDecoder(
  http.Response httpResponse,
) async =>
    await compute(jsonDecode, httpResponse.body) as Map<String, dynamic>?;

///Initialization funtion
void getClient({
  required String uri,
  required String anon_key,
  required HiveStore hive,
}) {
  final headers = {
    'Bearer': 'Bearer ${anon_key}',
    'ApiKey': '${anon_key}',
    'Accept': 'application/json',
    'Content-Type': 'application/json',
  };

  final httpLink = HttpLink('$uri/graphql/v1',
      defaultHeaders: headers, httpResponseDecoder: isolateHttpResponseDecoder);

  final link = Link.from([
    DedupeLink(),
    httpLink,
  ]);

  gqlclient = ValueNotifier<GraphQLClient>(
    GraphQLClient(
      cache: GraphQLCache(store: hive),
      link: link,
    ),
  );
}
/// Main
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  GoRouter.setUrlPathStrategy(UrlPathStrategy.path);

  await initHiveForFlutter();

  final hive = HiveStore();

  await bootstrap(() {
    getClient(
        uri: dotenv.get('SUPABASE_URL'),
        anon_key: dotenv.get('ANON_KEY'),
        hive: hive);

    return GraphQLProvider(
      client: gqlclient,
      child: ProviderScope(child: App()),
    );
  });
}

Future<void> bootstrap(FutureOr<Widget> Function() builder) async {
  FlutterError.onError = (details) {
    log(details.exceptionAsString(), stackTrace: details.stack);
  };

  await runZonedGuarded(
    () async {
      runApp(await builder());
    },
    (error, stackTrace) => log(error.toString(), stackTrace: stackTrace),
  );
}
/// Hooks generated by `graphql_codegen` - GetProducts
// Variables for the Product Feed Query.
  final allProductsVariables = useState<Variables$Query$GetProducts>(
    Variables$Query$GetProducts(
      first: limit,
      filter: Input$ProductFilter(
          releaseDate: Input$DatetimeFilter(
        gte: date,
      )),
      orderBy: [
        Input$ProductOrderBy(releaseDate: Enum$OrderByDirection.AscNullsFirst),
      ],
    ),
  );

  // Query for the product feed.
  final allProducts = useQuery$GetProducts(
    Options$Query$GetProducts(
      variables: allProductsVariables.value,
      fetchPolicy: FetchPolicy.cacheAndNetwork,
    ),
  );
/// Hooks generated by `graphql_codegen` - GetProductBySlug
  final productResponse = useQuery$GetProductBySlug(
    Options$Query$GetProductBySlug(
      variables: Variables$Query$GetProductBySlug(slug: slug ?? ''),
      fetchPolicy: FetchPolicy.cacheFirst,
    ),
  );
vincenzopalazzo commented 2 years ago

What is your client configuration?

cspecter commented 2 years ago

Just added the relevant setup code.

budde377 commented 2 years ago

The problem is that you're changing the arguments so the cache have no idea of which product to fetch from the cache, so it needs to go to the network. You can solve this by writing to the cache with cache access methods defined in the readme.

cspecter commented 2 years ago

OK, thanks. We will look into that.