Skip to content

How to handle AWSJSON in AppSync resolver

0

I have a subfield in an AppSync mutation which is an opaque object defined as AWSJSON, like this:

type AssetDetail {
  ...
  metadata: AWSJSON
}

type Mutation {
  saveAssetDetail(assetDetail: AssetDetail!): Boolean
} 

We've done things like this in other places in this project but this is the first time we've attempted it in a JavaScript resolver. The metadata field comes into the resolver as stringified JSON. My resolver code currently looks like this:

export function request(ctx: Context<{ assetDetail: AssetDetail }>) {
    const { guid, metadata, ...rest } = ctx.args.assetDetail;

    return ddb.update<AssetDetail>({
        key: {
            guid
        },
        update: {
            ...rest,
            metadata: ddb.operations.replace(JSON.parse(metadata))
        },
    });
}

When I test my resolver in the AppSync console, the resulting update statement looks fine:

{
  "update": {
    "expression": "SET #expName_1 = :expValue_1, #expName_2 = :expValue_2, #expName_3 = :expValue_3, #expName_4 = :expValue_4, #expName_5 = :expValue_5, #expName_6 = :expValue_6, #expName_7 = :expValue_7, #expName_8 = :expValue_8, #expName_9 = :expValue_9",
    "expressionNames": {
      "#expName_9": "metadata",
      ...
    },
    "expressionValues": {
      ":expValue_9": {
        "M": {
          "foo": {
            "S": "bar"
          }
        }
      },
      ...
    }
  },
  "operation": "UpdateItem",
  "key": {
    "guid": {
      "S": "3088FB20-8E10-499D-BC2A-1EF704ABC328"
    }
  }

but the ddb table always ends up with metadata: null. I even copy and pasted the output of my resolver test into a direct call to DynamoDB UpdateItem and it worked fine. So what is the correct way to handle this?

1 Answer
0

To handle AWSJSON in an AppSync JavaScript resolver, you're on the right track, but there are a few adjustments you can make to ensure the metadata is correctly stored in your DynamoDB table.

First, it's important to understand that AWSJSON is indeed received as a stringified JSON in the resolver. Your approach of using JSON.parse() is correct, but the issue might be in how you're applying the update operation.

Here's an improved version of your resolver code:

export function request(ctx) {
    const { guid, metadata, ...rest } = ctx.args.assetDetail;

    const updateExpression = {};
    Object.keys(rest).forEach(key => {
        updateExpression[key] = { Action: 'PUT', Value: rest[key] };
    });

    // Parse the metadata JSON string
    const parsedMetadata = JSON.parse(metadata);
    updateExpression.metadata = { Action: 'PUT', Value: parsedMetadata };

    return {
        operation: 'UpdateItem',
        key: {
            guid: { S: guid }
        },
        update: {
            expression: 'SET #metadata = :metadata, #rest = :rest',
            expressionNames: {
                '#metadata': 'metadata',
                '#rest': 'rest'
            },
            expressionValues: {
                ':metadata': { M: parsedMetadata },
                ':rest': { M: updateExpression }
            }
        }
    };
}

In this updated version:

  1. We parse the metadata JSON string using JSON.parse().
  2. We create an updateExpression object to handle all other fields.
  3. We use the 'UpdateItem' operation directly, which gives us more control over how the update is performed.
  4. We set the metadata as a map (M) in DynamoDB, which is the correct way to store JSON objects.
  5. We use expression attributes to avoid potential naming conflicts.

This approach should correctly store your metadata as a JSON object in the DynamoDB table, rather than as null.

Remember to adjust the key type (S for string) in the 'key' section if your guid is not a string.

Also, ensure that your IAM permissions for the resolver are set up correctly to allow the UpdateItem operation on your DynamoDB table.

If you're still experiencing issues, double-check that the incoming metadata is valid JSON and that there are no type mismatches between what's being sent and what DynamoDB expects.

Lastly, consider using AWS AppSync's built-in $util.toJson() and $util.parseJson() functions if you need to perform JSON operations in your resolver, as these are optimized for use within AppSync.

Sources
Configuring resolvers in AWS AppSync - AWS AppSync
Testing and debugging resolvers in AWS AppSync (JavaScript) - AWS AppSync
AWS AppSync JavaScript resolvers overview - AWS AppSync

answered 2 years ago

  • Ah ok, so that was my suspicion, that I would end up having to write out my updateExpress with names and values. So there's no way to utilize the much easier @aws-appsync/utils stuff?

    And how was I supposed to troubleshoot this on my own? As you can see in the test output from my resolver, the update expression LOOKS correct, metadata is a Map/M type.

    To your last point, I can't use $util.toJson since I believe that only exists in VTL resolvers.

  • Yeah this still isn't working. With this code verbatim I get the error Value for attribute '$[update][expressionValues][:metadata][M]' must be an object.

    And I'm not sure how this code would work anyway. There is no ddb prop called rest, I need that flattened out and as a sibling of metadata. :(

  • I really can't see why this doesn't work. I reduced my resolver code to a very simple:

        const { guid, metadata, ...rest } = ctx.args.assetDetail;
        const { assetID, revision, type } = rest;
    
        return {
            operation: 'UpdateItem',
            key: util.dynamodb.toMapValues({
                guid,
                SK: `ASSET_DETAIL#REV#${revision}#ASSET#${assetID}#TYPE#${type}`
            }),
            update: {
                expression: 'SET #metadata = :metadata',
                expressionNames: {
                    '#metadata': 'metadata'
                },
                expressionValues: util.dynamodb.toMapValues({
                    ':metadata': JSON.parse(metadata)
                })
            }
        };
    

    Running my resolver in AppSync shows this return from my request resolver, which according to the docs, looks like it should work:

    {
      "operation": "UpdateItem",
      "key": {
        "SK": {
          "S": "ASSET_DETAIL#REV#3#ASSET#vvMqjK8Sx9751D5Js6VNh#TYPE#rev"
        },
        "guid": {
          "S": "3088FB20-8E10-499D-BC2A-1EF704ABC328"
        }
      },
      "update": {
        "expression": "SET #metadata = :metadata",
        "expressionNames": {
          "#metadata": "metadata"
        },
        "expressionValues": {
          ":metadata": {
            "M": {
              "orderNumber": {
                "S": "CP1063456078"
              }
            }
          }
        }
      }
    }
    

    However, metadata is still getting set to null.

    I'm probably just going to rework this as a lambda. I've wasted too much time on this.

You are not logged in. Log in to post an answer.

A good answer clearly answers the question and provides constructive feedback and encourages professional growth in the question asker.