aws-amplify / amplify-flutter

A declarative library with an easy-to-use interface for building Flutter applications on AWS.
https://docs.amplify.aws
Apache License 2.0
1.32k stars 248 forks source link

QUESTION: Is it possible to use an existing graphQL API not built with Amplify backend? #4589

Closed slikk66 closed 7 months ago

slikk66 commented 7 months ago

Hi guys, trying to use Flutter to build a frontend mobile app for an existing Appsync API + Cognito (built with Pulumi) and website (Typescript/Amplify Libraries/React on S3). On the Typescript/react side of things this is pretty trivial to implement passing in GraphQL strings and using the returned JSON. In the past I've tried using the Amplify IaC tools, but they've been too limiting, buggy, and just not needed since I prefer using Pulumi. But, the client libraries have been essential.

For last couple years I've been using custom backend paired with the frontend libraries and bypassing the amplify CLI/AWS setup. I was hoping to do the same with Amplify + Flutter. The backend is using all AppSync resolvers with datasources of Lambda and Dynamo.

So far it looks like the Amplify + Flutter system is very different in that it appears to essentially require the models/API adhere to a specific schema and toolset with built CRUD functions and amplify specific tags to generate everything.

https://docs.amplify.aws/flutter/build-a-backend/graphqlapi/custom-business-logic/#appsync-javascript-or-vtl-resolver This hasn't been much help, it mentions connecting to VTL/JS AppSync resolvers but then goes on to talk about modifying CDK (which I'm not using.. why is it built around one tool and not the service?) and also doesn't seem to indicate how to mark the fields (such as @http or @function) so it's still not clear why these custom functions would become codified in models and mine would not. It seems you're just overriding the backend amplify cdk configuration, but no modification needed on front end to indicate that the backend is custom.

Here's a very simplified example of my existing AppSync schema:

## INPUT TYPES
#################

input PaginationParamsInput {
    limit: Int
    reverse: Boolean
}

input InputCustomerModify {
    id: ID!
    email: String
    status: String
}

input InputEntityModify {
    id: ID!
    name: String
    status: String
}

## RESOURCES
#################

type PaginationParams @aws_iam @aws_cognito_user_pools {
    limit: Int
    reverse: Boolean
}

type QueryResultsCustomer @aws_iam @aws_cognito_user_pools {
    more: Boolean
    filterParams: [FilterParam]
    paginationParams: PaginationParams
    results: [Customer]
}

type FilterParam @aws_iam @aws_cognito_user_pools {
    key: String
    value: String
}

type Customer @aws_iam @aws_cognito_user_pools {
    id: ID!

    email: String
    status: String

    entities: [Entity]
}

type Entity @aws_iam @aws_cognito_user_pools {
    id: ID!
    name: String
    status: String
}

type RealtimeEvent @aws_iam @aws_cognito_user_pools {
    channel: String!
    data: String!
    date: AWSDateTime!
}

## ROOT
#################

type RootQuery @aws_iam @aws_cognito_user_pools {
    getCustomer(customerId: ID): Customer

    getCustomers(paginationParams: PaginationParamsInput): QueryResultsCustomer
    @aws_iam @aws_cognito_user_pools(cognito_groups: ["${env}-admin"])

}

type RootMutation @aws_iam @aws_cognito_user_pools {

        addCustomer(input: InputCustomerModify!): Customer
        @aws_iam @aws_cognito_user_pools(cognito_groups: ["${env}-admin"])

        updateCustomer(input: InputCustomerModify!): Customer
        @aws_iam @aws_cognito_user_pools(cognito_groups: ["${env}-admin"])

    ### REALTIME EVENT
        publishRealtimeEvent(
            data: String!
            channel: String!
        ): RealtimeEvent
}

type RootSubscription @aws_iam @aws_cognito_user_pools {
    subscribeToRealtimeEvent(channel: String!): RealtimeEvent
    @aws_subscribe(mutations: ["publishRealtimeEvent"])
}

schema {
    query: RootQuery
    mutation: RootMutation
    subscription: RootSubscription
}

I pulled down my schema using aws appsync get-introspection-schema added a bunch of @model directives (without them I was getting an error while generating) and generated the models, but I don't see the main RootMutation functions in the generated RootMutation or RootQuery code, the files exist but there are no methods.

At this point I'd be OK with using dynamic request objects -> JSON -> query/mutation -> JSON -> cast to Models, is that possible?

slikk66 commented 7 months ago

Well, I'd be happy to hear any additional advice (especially how I may improve this), but I did make some progress. Using my generated models, I was able to dig a bit into the code and figure out how to send random queries and map that to my models. Here's the result, maybe it will help someone!

Query Function:

  Future<Customer?> getCustomer() async {
    try {
      const document = '''
          query GetCustomer(\$customerId: ID!) {
              getCustomer(customerId: \$customerId) {
                  id
                  email
                  status
              }
          }
      ''';
      const variables = {"customerId": "abcdefg"};

      final call = await Amplify.API
          .query(
              request: GraphQLRequest<dynamic>(
            document: document,
            variables: variables,
          ))
          .response;
      final responseRaw = call.data;
      print(responseRaw);

      final customerDynamic = jsonDecode(responseRaw)['getCustomer'];
      print(customerDynamic);

      final customer = Customer.fromJson(customerDynamic);

      return customer;
    } on ApiException catch (e) {
      safePrint('Query failed: $e');
      return null;
    }
  }

Result (in console):

{"getCustomer":{"id":"abcdefg,"email":"me@gmail.com","status":"ACTIVE"}}
{id: abcdefg, email: me@gmail.com, status: ACTIVE}
Customer {id=abcdefg, email=me@gmail.com, status=ACTIVE, createdAt=null, updatedAt=null, queryResultsCustomerResultsId=null}

Next up.. figure out how to do subscriptions 😂

slikk66 commented 7 months ago

Subscriptions!

  StreamSubscription<GraphQLResponse<String>>? subscription;

  void subscribe() {
    const document = '''
      subscription SubscribeToRealtimeEvent(\$channel: String!) {
        subscribeToRealtimeEvent(channel: \$channel) {
          channel
          data
          date
        }
      }
    ''';
    const variables = {"channel": "123456"};
    final subscriptionRequest = GraphQLRequest<String>(document: document, variables: variables);

    final Stream<GraphQLResponse<String>> operation = Amplify.API.subscribe(
      subscriptionRequest,
      onEstablished: () => safePrint('Subscription established'),
    );
    subscription = operation.listen(
      (event) {
        safePrint('Subscription event data received: ${event.data}');
      },
      onError: (Object e) => safePrint('Error in subscription stream: $e'),
    );
  }

Output:

Subscription event data received:
{"subscribeToRealtimeEvent":{"channel":"123456","data":"{\"action\":\"ACT_ON_RESPONSE_RECEIVED\",\"params\":{\"itemId\":\"000000001\"}}","date":"2024-03-21T18:45:30.343Z"}}

I tried mapping these to the RealtimeEvent model, but I got this error message so I just moved it to String, easy enough to handle:

Error: ApiOperationException {                                                                                                                                                                                                              
  "message": "Decoding of the response type provided is currently unsupported",                                                                                                                                                             
  "recoverySuggestion": "Please provide a Model Type or type 'String'"                                                                                                                                                                      
}