awslabs / aws-mobile-appsync-sdk-js

JavaScript library files for Offline, Sync, Sigv4. includes support for React Native
Apache License 2.0
921 stars 266 forks source link

Support for queries with union type responses #615

Closed aliwoodman closed 3 years ago

aliwoodman commented 3 years ago

Note: If your issue/feature-request/question is regarding the AWS AppSync service, please log it in the official AWS AppSync forum

Do you want to request a feature or report a bug? Report a (possible) bug

What is the current behavior? A query for a union type results in the following response from AWSAppSyncClient: { data: null, loading: false, networkStatus: 7, stale: true }

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem.

Our resolvers are implemented in AWS Lambda. We have set the fetchPolicy to 'network-only' and disableOffline to true when initialising the client.

GraphQL schema:

type Query {
  user(input: UserInput!): UserResponse!
}

input UserInput {
  userId: String!
}

union UserResponse = User | Error

type User {
  id: String!
  name: String!
}

type Error{
  error: String!
}

Query:

gql(`
    query User {
      user(input: {userId: "${userId}"}) {
        ... on User {
          __typename
          id
          name

        }
        ... on Error {
          __typename
          error
        }
      }
    }
  `);

Resolver:

Example of data returned:

{
  __typename: 'User',
  id: 'uuid-12345',
  name: 'some name',
}

or 

{
  __typename: 'Error',
  error: 'something went wrong'
}

If we know we are getting the 'User' response from the resolver and we remove the ...on Error part of the query, we successfully get data back from the AppSync client. If we leave the query as above, we get null data even when our logs show that the resolver is returning one of the types in the union.

What is the expected behavior?

We would expect this query to successfully return data without having to take this step. This works in the AppSync console but not when making the request from a lambda via apollo-client.

Which versions and which environment (browser, react-native, nodejs) / OS are affected by this issue? Did this work in previous versions?

node 12.16

aliwoodman commented 3 years ago

We've got this working now. We needed to tell the Apollo Client about the possible return types, via something called an IntrospectionFragmentMatcher - something not currently mentioned in the AWS docs we were following.

In case anyone else finds it useful, this is roughly how we now set it up and pass it into our AWSAppSyncClient configuration:

const introspectionQueryResultData = {
  "__schema": {
    "types": [
      {
        "kind": "INTERFACE",
        "name": "UserResponse",
        "possibleTypes": [{ "name": "User" }, { "name": "Error" }]
      }
    ]
  }
}

const fragmentMatcher = new IntrospectionFragmentMatcher({
    introspectionQueryResultData,
});

const cache = new InMemoryCache({
  fragmentMatcher,
});

const client = new AWSAppSyncClient(
  {
    url: APPSYNC_API_URL,
    region: REGION,
    auth: {
      type: AUTH_TYPE.AWS_IAM,
      credentials: AWS.config.credentials,
    },
    disableOffline: true,
  },
  {
    defaultOptions: {
      query: {
        fetchPolicy: 'network-only',
      },
    },
    cache,
  }
);
slikk66 commented 2 years ago
> We've got this working now. We needed to tell the Apollo Client about the possible return types, via something called an [IntrospectionFragmentMatcher](https://www.apollographql.com/docs/react/data/fragments/#using-fragments-with-unions-and-interfaces) - something not currently mentioned in the [AWS docs](https://docs.aws.amazon.com/appsync/latest/devguide/building-a-client-app-node.html) we were following.
> 
> In case anyone else finds it useful, this is roughly how we now set it up and pass it into our `AWSAppSyncClient` configuration:

Where can we import these classes? IntrospectionFragmentMatcher, InMemoryCache - trying this but it's not working:

import {InMemoryCache} from "aws-appsync/node_modules/apollo-cache-inmemory"

slikk66 commented 2 years ago

I added package apollo-cache-inmemory to my app and this seems to be working in typescript. Note I had to cast the cache as "any", it was asking for ApolloCache<NormalizedCacheObject> but not sure how/where to locate that, but this seems to work.

The creation of the fragmentTypes.json is taken from the schema.json pulled from my endpoint, more info here: https://www.apollographql.com/docs/react/v2/data/fragments/#fragments-on-unions-and-interfaces

current relevant package.json contents:

    "devDependencies": {
        "@types/aws-lambda": "^8.10.81",
        "@types/aws-sdk": "^2.7.0",
        "@types/lodash": "^4.14.175",
        "@types/uuid": "^8.3.1",
        "aws-sdk": "^2.1036.0",
        "typescript": "^4.4.3"
    },
    "dependencies": {
        "@types/node": "^16.9.6",
        "apollo-cache-inmemory": "^1.6.6",
        "autodetect-decoder-stream": "^2.0.0",
        "aws-appsync": "^4.1.1",
        "es6-promise": "^4.2.8",
        "graphql": "^15.5.0",
        "graphql-tag": "^2.11.0",
        "http": "^0.0.1-security",
        "isomorphic-fetch": "^3.0.0",
        "lodash": "^4.17.21",
        "mycompany-private-schema-package": "~1.0.0",
        "uuid": "^8.3.2",
        "ws": "^7.4.4"
    }
import AWS from "aws-sdk";
import AWSAppSyncClient from "aws-appsync";

import { IntrospectionFragmentMatcher, InMemoryCache } from "apollo-cache-inmemory";

import introspectionQueryResultData from "mycompany-private-schema-package/dist/fragmentTypes.json";

const region = process.env.AWS_REGION;
const endpoint = process.env.GRAPHQL_ENDPOINT;

export default class getAppSyncClient {
    constructor() {
        console.log("getAppSyncClient() INIT");
    }

    public async parse() {
        console.log("return AWSAppSyncClient()");

        const fragmentMatcher = new IntrospectionFragmentMatcher({
            introspectionQueryResultData,
        });

        const cache: any = new InMemoryCache({
            fragmentMatcher,
        });

        let client = new AWSAppSyncClient(
            {
                url: endpoint!,
                region: region!,
                auth: {
                    type: "AWS_IAM",
                    credentials: async () => {
                        let c = await new AWS.CredentialProviderChain().resolvePromise();
                        return c;
                    },
                },
                disableOffline: true,
            },
            {
                defaultOptions: {
                    query: {
                        fetchPolicy: "network-only",
                    },
                },
                cache,
            }
        );

        let the_client = await client.hydrated();

        return the_client;
    }
}
samkio commented 2 years ago

Thank you so much @slikk66 ; I've been trying all sorts of things to get this to work this afternoon. Stumbled upon this issue and saw your comment only 19 hours ago!

For the casting of the cache to any; this is due to different versions of apollo-cache-inmemory being used between aws-appsync and yourself. Here are my versions (I just matched my version to use the same as aws-appsync) that resolved the need for the cast:

  "aws-appsync": "4.1.4",
  "apollo-cache-inmemory": "1.3.12",