Search code examples
graphqlaws-appsync

How can I response to client based on what fields they are querying in graphql?


I am using AWS appsync for graphql server and have schema like:

type Order {
  id: ID!
  price: Int
  refundAmount: Int
  period: String!
}
  

query orders (userId: ID!) [Order]

It is to support query orders based on user id. It responses an array of orders for different time period. The response could be:

[{
  id: xxx
  price: 100
  refundAmount: 10
  period: '2021-01-01'
},{
  id: xxx
  price: 200
  refundAmount: 0
  period: '2021-01-03'
},
...
]

If the price and refundAmount in the period is 0, I won't response empty element in the array. In the above example, there is price and refundAmount on 2021-01-02, so there is no such element in the array.

My problem is how can I response the data based on what frontend queries? If customer only query refundAmount field in the response, I don't want to response 2021-01-03 period. How do I know what fields frontend wants to show in the response?

e.g.

If clients send this query:

query {
   orders (userId: "someUserId") {
      refundAmount
   }
}

I will response below data but I don't want the second one to be there since the value is 0.

[{
  id: xxx
  refundAmount: 10
  period: '2021-01-01'
},{
  id: xxx
  refundAmount: 0
  period: '2021-01-03'
}
]

Solution

  • My problem is how can I response the data based on what frontend queries?

    GraphQL will do that out of the box for you provided you have the resolvers for the fields in the query. Look at appropriate resolver based on your underlying data source.

    How do I know what fields frontend wants to show in the response?

    This is what the frontend decides, it can send a different query based on the fields it is interested. A few examples below.

    If the frontend is interested in only one field i.e. refundAmount, then it would send a query something like this.

    query {
       orders (userId: "someUserId") {
          refundAmount
       }
    }
    

    If it is interested in more than 1 field say price and refundAmount then the query would be something like this

    query {
       orders (userId: "someUserId") {
          price,
          refundAmount
       }
    }
    

    Update: Filter response:

    Now based on the updated question, you need to enhance your resolver to do this additional filtering.

    • The resolver can do this filtering always (Kind of hard coded like refundAmount > 0 )
    • Support a filter criteria in the query model query orders (userId: ID!, OrderFilterInput) [Order] and the define the criteria based on which you want to filter. Then support those filter criteria in the resolvers to query the underlying data source. Also take the filter criteria from the client.

    Look at the ModelPostFilterInput generated model on this example.

    Edit 2: Adds changed Schema for a filter

    Let's say you change your Schema to support filtering and there is no additional VTL request/response mappers and you directly talk to a Lambda.

    So this is how the Schema would look like (of course you would have your mutations and subscriptions and are omitted here.)

    input IntFilterInput { # This is all the kind of filtering you want to support for Int data types
        ne: Int
        eq: Int
        le: Int
        lt: Int
        ge: Int
        gt: Int
    }
    
    type Order {
        id: ID!
        price: Int
        refundAmount: Int
        period: String!
    }
    
    input OrderFilterInput { # This only supports filter by refundAmount. You can add more filters if you need them.
        refundAmount: IntFilterInput
    }
    
    type Query {
        orders(userId: ID!, filter: OrderFilterInput): [Order] # Here you add an optional filter input
    }
    
    schema {
        query: Query
    }
    

    Let's say you attached the Lambda resolver at the Query orders. In this case, the Lambda would need to return an array/list of Orders.

    If you are further sending this query to some table/api, you need to understand the filter, and create an appropriate query or api call for the downstream system.

    I showing a simple Lambda with hard coded response. If we bring in the Filter, this is what changes.

    const getFilterFunction = (operator, key, value) => {
        switch (operator) {
            case "ne":
                return x => x[key] != value
            case "eq":
                return x => x[key] == value
            case "le":
                return x => x[key] <= value
            case "lt":
                return x => x[key] < value
            case "ge":
                return x => x[key] >= value
            case "gt":
                return x => x[key] > value
            default:
                throw Error("Unsupported filter operation");
        }
    }
    
    
    exports.handler = async(event) => {
    
        let response = [{
            "id": "xxx1",
            "refundAmount": 10,
            "period": '2021-01-01'
        }, {
            "id": "xxx2",
            "refundAmount": 0,
            "period": '2021-01-03'
        }]
        const filter = event.arguments.filter; 
        if (filter) { // If possible send the filter to your downstream system rather handling in the Lambda
            if (filter.refundAmount) {
                const refundAmountFilters = Object.keys(filter.refundAmount)
                    .map(operator => getFilterFunction(operator + "", "refundAmount", filter.refundAmount[operator]));
                refundAmountFilters.forEach(filterFunction => { response = response.filter(filterFunction) });
            }
        }
    
        return response; // You don't have to return individual fields the query asks for. It is taken care by AppSync. Just return a list of orders.
    };
    

    With the above in place, you can send various queries like

    query MyQuery {
      orders(userId: "1") { #without any filters
        id
        refundAmount
      }
    }
    
    query MyQuery {
      orders(userId: "1", filter: {refundAmount: {ne: 0}}) { # The filter you are interested
        id
        refundAmount
      }
    }
    
    query MyQuery {
      orders(userId: "1", filter: {refundAmount: {ne: 0, gt: 5}}) { # Mix and Match filters
        id
        refundAmount
      }
    }
    

    You don't have to support all the operators for filtering and you can focus only on ne or != and further simplify things. Look at this blog for a more simple version where the filter operation is assumed.

    Finally the other possibility to filter without modifying the Schema is to change your Lambda only to ensure it returns a filtered set of results either doing the filtering itself or sending an appropriate query/request to the underlying system to do the filtering.