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.24k stars 613 forks source link

Amplify Subscriptions support #1163

Open ZRunner opened 2 years ago

ZRunner commented 2 years ago

I cannot seem to be able to use GraphQL subscriptions with my AWS server, by using Cognito authentication.
Queries and mutations work fine as far as I can tell, but I couldn't find how to connect to the amplify websocket.

Here is the code I tried, based on this comment: https://github.com/zino-hofmann/graphql-flutter/issues/682#issuecomment-759078492 (which I had to edit because Cognito tokens are not static)

import 'dart:async';
import 'dart:convert';

import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:flutter/material.dart';
import 'package:gql/language.dart';
import 'package:graphql_flutter/graphql_flutter.dart';

String toBase64(Map data) => base64.encode(utf8.encode(jsonEncode(data)));

const apiId = "XXX";
const zone = "eu-west-1";

class AppSyncRequest extends RequestSerializer {
  final Map<String, dynamic> authHeader;

  const AppSyncRequest({
    required this.authHeader,
  });

  @override
  Map<String, dynamic> serializeRequest(Request request) => {
        "data": jsonEncode({
          "query": printNode(request.operation.document),
          "variables": request.variables,
        }),
        "extensions": {
          "authorization": authHeader,
        }
      };
}

ValueNotifier<GraphQLClient> buildClient() {
  Future<String?> _fetchSession() async {
    try {
      final session = await Amplify.Auth.fetchAuthSession(
              options: CognitoSessionOptions(getAWSCredentials: true))
          as CognitoAuthSession;
      if (!session.isSignedIn) {
        return null;
      }
      return session.userPoolTokens?.accessToken;
    } on AuthException catch (e) {
      debugPrintStack(label: e.toString());
    }
    return null;
  }

  Future<String?> _getIdToken() async {
    try {
      final session = await Amplify.Auth.fetchAuthSession(
              options: CognitoSessionOptions(getAWSCredentials: true))
          as CognitoAuthSession;
      if (!session.isSignedIn) {
        return null;
      }
      return session.userPoolTokens?.idToken;
    } on AuthException catch (e) {
      debugPrintStack(label: e.toString());
    }
    return null;
  }

  final HttpLink httpLink =
      HttpLink("https://$apiId.appsync-api.$zone.amazonaws.com/graphql");

  final WebSocketLink wsLink = WebSocketLink(
    'wss://$apiId.appsync-realtime-api.$zone.amazonaws.com/graphql',
    config: SocketClientConfig(
      initialPayload: () async {
        final token = await _fetchSession();
        return {
          "headers": {
            "Authorization": '$token',
            "host": "$apiId.appsync-api.$zone.amazonaws.com",
          },
        };
      },
    ),
  );

  final AuthLink authLink = AuthLink(
    getToken: _fetchSession,
  );

  // final Link link = authLink.concat(httpLink).concat(wsLink);
  final Link link = Link.split((request) => request.isSubscription,
      authLink.concat(wsLink), authLink.concat(httpLink));

  ValueNotifier<GraphQLClient> client = ValueNotifier(
    GraphQLClient(
      link: link,
      defaultPolicies:
          DefaultPolicies(query: Policies(fetch: FetchPolicy.cacheAndNetwork)),
      cache: GraphQLCache(
        partialDataPolicy: PartialDataCachePolicy.accept,
        store: HiveStore(),
      ),
    ),
  );
  return client;
}

class ClientProvider extends StatefulWidget {
  final Widget child;

  const ClientProvider({
    Key? key,
    required this.child,
  }) : super(key: key);

  @override
  State<ClientProvider> createState() => _ClientProviderState();
}

class _ClientProviderState extends State<ClientProvider> {
  @override
  Widget build(BuildContext context) {
    return GraphQLProvider(
      client: buildClient(),
      child: widget.child,
    );
  }
}

Naturally in both ways my Subscription widget doesn't work (infinite loading state).

Device / execution context

vincenzopalazzo commented 2 years ago

Mh! I'm new with amplify but I can reproduce it maybe this weekend.

Sorry about that!

matthintosh commented 2 years ago

+1 It will be very useful for using your lib in flutter instead of AWS Amplify which is very far from your implementation of GraphQL...

vincenzopalazzo commented 2 years ago

We are evaluating on how to manage this issue, I will work on it 🫡

vincenzopalazzo commented 2 years ago

We have a problem with ws right now, and we are introducing integration testing, after this, if the problem is not solved we will build an Amplify to reproduce and work on this problem!

Thanks to report it

temirlanalmassov commented 2 years ago
 final httpLink = HttpLink(AppConstants.urlHttpLink); //'http://somelink.com'
    final wsLink = WebSocketLink(
      AppConstants.urlWsLink, // 'wss://somelink.com/'
      config: SocketClientConfig(
        autoReconnect: true,
        initialPayload: () async {
          final token = await _getRealToken();
          return {
              'authToken': token,
            };
        },
      ),
    );

    final authLink = AuthLink(
      getToken: _getToken,
    );

    final Link link = Link.split(
      (request) => request.isSubscription,
      authLink.concat(wsLink),
      authLink.concat(httpLink),
    );

    _client = GraphQLClient(
      link: link,
      cache: GraphQLCache(
        store: InMemoryStore(),
      ),
    );

This works for me @ZRunner I don't know why it doesn't work with you. Yes it already spam my console about disconnecting, but it reconnects and getting messages

vincenzopalazzo commented 2 years ago

@temirlanalmassov

Will be useful have the version of the package that you are using

banaszeknorbert commented 2 years ago

@temirlanalmassov where I can find web socket link?

temirlanalmassov commented 2 years ago

@temirlanalmassov

Will be useful have the version of the package that you are using

graphql_flutter: ^5.1.0

temirlanalmassov commented 2 years ago

@temirlanalmassov where I can find web socket link?

I think you can ask it from your BackEnd side.

  static const urlHttpLink = 'https://$domain/graphql/';
  static const urlWsLink = 'wss://$domain/ws/graphql/';

For example I am using it like this

vincenzopalazzo commented 2 years ago

graphql_flutter: ^5.1.0

@temirlanalmassov I suggest to use the last beta version, and also I'm starting to reproduce the problem with a AWS amplify project I hope to have this up and running soon.

But in the meanwhile, there can you check what happens if you change the protocol that the library is using to initiate the connection?

vincenzopalazzo commented 2 years ago

@temirlanalmassov I think you are wrong about the API, look to the AWS docs https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html#handshake-details-to-establish-the-websocket-connection

In addition, this looks like an error with the old protocol, and I need to make double-check if AWS has a custom protocol or it is based on a well-known protocol.

I was able to reproduce but I will research these days on what protocol is supported by AWS and try to fix this asap.

For the moment it is reproduced in https://github.com/zino-hofmann/graphql-flutter/pull/1206

alexboulay commented 1 year ago

@ZRunner Did you get this to work?

ZRunner commented 1 year ago

@ZRunner Did you get this to work?

My company has decided to move to react native due to the lack of maturity of the Flutter ecosystem. So I'm afraid I won't be of any help here, sorry.

alexboulay commented 1 year ago

@ZRunner thats ok! good luck with react native!

i ended up simply creating 2 client instances and it worked fine!

thanks for responding quickly! :)