apollographql / graphql-tag

A JavaScript template literal tag that parses GraphQL queries
MIT License
2.32k stars 175 forks source link

Double Stringify() for AWS AppSync AWSJSON attributes #807

Open QAnders opened 11 months ago

QAnders commented 11 months ago

graphql-tag has served us well, and it's a great module, but I've been trying to figure this one out, and it seems related to graphql-tag...

We are running AWS AppSync and it's a bit finnicky with JSON data as input, so it could be some AWS thing as well...

We have an invoice object, on which we run mutations (e.g. create the invoice in the DB), and inside the invoice object we have other attributes that are JSON.

In the AppSync schema the attributes are defined as AWSJSON which will take a stringify'd JSON as input.

In the code though, calling the mutation I have to stringify it twice to get it working, like the below sample:

const query = `
    mutation createOutgoingInvoice{
      outgoinginvoice(
        input: {
          invoice_data: {
            currency: "${currency}",
            references: ${JSON.stringify(JSON.stringify(references))}
          },
          org_nr: "SE123"
        }
      ) {
        id
      }
    }
  `;

// query: gql(query)

The mutation works and the invoice is created, but there's something weird going on... Has anyone else come across this using AppSync?

jerelmiller commented 11 months ago

Hey @QAnders 👋

What type is that references variable? Would you be able to share a runnable reproduction, or a more complete example that shows the behavior you're seeing? Its difficult to tell if this is because of JS strings, graphql-tag, or something else. Any more info you can provide would be super helpful.

EDIT:

I'm willing to bet this is more a property of string interpolation in JavaScript and not so much about graphql-tag behavior. JSON.stringify(...) is going to return a string, but keep in mind its an unquoted string, so your query is going to look something like this after the first JSON.stringify:

mutation createOutgoingInvoice{
  outgoinginvoice(
    input: {
      invoice_data: {
        currency: "USD",
        references: {"some": "field", "value": 2}
      },
      org_nr: "SE123"
    }
  ) {
    id
  }
}

Note how its set to the object, and again, this is because it returns an unquoted string. Since your GraphQL server expects a stringified JSON object, this is where your 2nd JSON.stringify is doing the work. Its stringifying the string, so it will add double quotes and escape all double quotes on the keys/string values.

While you can use interpolation to add variables to your query, I'd recommend that you use GraphQL variables to pass this value to your server. This allows your query to be a static string. Passing references via GraphQL variables should only require a single JSON.stringify()

mutation createOutgoingInvoice($currency: String, $references: AWSJSON) {
  outgoinginvoice(
    input: {
      invoice_data: {
        currency: $currency,
        references: $references
      },
      org_nr: "SE123"
    }
  ) {
    id
  }
}

Then in your request (not sure what library you're using to fetch, but here is the raw fetch version of it):

import { print } from 'graphql';

const mutation = gql`
  mutation createOutgoingInvoice {
    # ...
  }
`

fetch('/graphql', {
  method: 'POST',
  headers: {
    'content-type': 'application/json'
  },
  body: JSON.stringify({
    // NOTE: If you're using apollo or another library, this print is usually done for you
    query: print(mutation),
    variables: { 
      currency: 'USD',
      references: JSON.stringify(references)
    }
  })
)

See if you have better luck with this approach!

QAnders commented 11 months ago

Thanks for getting back to me! Much appreciated!

I did a lot (I mean a lot) of testing and various attempts in getting it working and I have too tried with variables, and that's actually where I struggled the longest as that made most sense to use... However I never got it working as it would seem AppSync rejects anything from variables, regardless of the "stringify()" number.

AppSync then returns an error like:

WrongType: argument 'input.invoice_data.bankaccount_data' with value 'StringValue{value='$bankaccount_data'}' is not a valid 'AWSJSON'

The only way (I found) to get it to accept anything in variables is to set the schema to String instead of AWSJSON but that is messing with my Lambda and I get some "weird" half parsed JSON in my Lambda. Using AWSJSON gives me a valid JSON object in my Lambda so I can't budge from that...

I guess I just have to live with the double stringify() and hopefully this issue can help some other poor sod struggling to get request with JSON into AppSync...

I added some logging to AppSync, and here we can clearly see that your point, @jerelmiller , is totally accurate on a working query:

GraphQL Query: mutation createOutgoingInvoice {
  outgoinginvoice(
    input: {invoice_data: {currency: "SEK", bankaccount_data: "[{\"bic\":null,\"iban\":null,\"account_name\":\"Bankgiro\",\"account_number\":\"... cut ...\"}]"

bankaccount_data is coming in as a "String" (as it is inclosed in "") and very clearly it is "double" stringify()'d.

Trying the same with variables I get this on a "double" stringify:

"bankaccount_data": "\"[{\\\"bic\\\":null,\\\"iban\\\":null,\\\"account_name\\\":\\\"Bankgiro\\\",\\\"account_number\\\":\\\... cut ...\\\"}]\""

Using only one stringify on the variables gives me the expected "string" in the logs, but I still get the AWSJSON error from AppSync:

"bankaccount_data": "[{\"bic\":null,\"iban\":null,\"account_name\":\"Bankgiro\",\"account_number\":\"... cut ...\"}]"

So, I can conclude, that something in AppSync and how it move the values from variables to the mutation query is messing it up and the only solution I can find is to have the JSON objects doubly stringify'd and in the mutation query itself...