Workarounds for AppSync Subscriptions triggers via Lambda functions
Tags: aws, developement, serverless, appsync
Published: 2023-10-02 16:56:13

[AWS AppSync](https://aws.amazon.com/appsync/) is a serverless GraphQL offering by AWS, previously I authored a blog about [AWS AppSync 101](https://blog.theserverlessterminal.com/aws-appsync-101) which gets you up to speed with the capabilities of AppSync and how you can leverage them in your serverless applications.

One query that stands out is that -

> AppSync subscriptions work amazing when client makes mutations but I have some downstreaming patterns in my application where the backend uses subscriptions to send data from back-end to front-end, how do I achieve this with AppSync?

## Ask ChatGPT

Well, this does look simple so why not ask ChatGPT?

> What would be the better way to handle AppSync Subscriptions which are triggered with Lambda functions?

### ChatGPT's solution

> Use the appropriate trigger:
> 
> To use the appropriate trigger, you can create a trigger on the event that is relevant to your use case. For example, if you want to trigger a subscription when a record is added to a database, you can use a database trigger to invoke your Lambda function.
> 
> Here's an example using AWS Lambda and Amazon DynamoDB:
> 
> * Create a DynamoDB table with a stream enabled.
>     
> * Create a Lambda function that is triggered by the DynamoDB stream.
>     
> * In the Lambda function, write code that parses the stream event and invokes the AppSync subscription.
>     

And also, ChatGPT generated the code snippet for this.

```javascript
// Sample code for a Lambda function that is triggered by a DynamoDB stream

exports.handler = async (event) => {
  // Parse the DynamoDB stream event
  const records = event.Records.map((record) => {
    return {
      eventName: record.eventName,
      record: record.dynamodb.NewImage,
    };
  });

  // Invoke the AppSync subscription
  const appSyncClient = new AWSAppSyncClient({
    url: 'https://my-appsync-endpoint.aws.com/graphql',
    region: 'us-east-1',
    auth: {
      type: 'AWS_IAM',
      credentials: AWS.config.credentials,
    },
  });

  const mutation = gql`
    subscription OnRecordAdded {
      onRecordAdded {
        id
        name
      }
    }
  `;

  const observable = appSyncClient.subscribe({ query: mutation });
  observable.subscribe((data) => console.log('Subscription data', data));
};
```

And the GraphQL schema for this was also generated by AppSync.

```javascript
type Record {
  id: ID!
  name: String!
}

type Subscription {
  onRecordAdded: Record!
}

type Mutation {
  addRecord(name: String!): Record!
}

type Query {
  getRecord(id: ID!): Record
}
```

## Let's validate ChatGPT 's response

Do you notice something from ChatGPT's response? Now let's put on the *Serverless architect* lens and see how this would be implemented.

### GraphQL schema

The schema generated by ChatGPT is a generic one that looks good at first look but when from the application's perspective, all the operations - `query`, `mutation` and `subscription` are accessible based on the default authorization set on AppSync.

Ensure the subscription is defined for a specific mutation, this is to be noted when you have multiple mutations. Although this looks good in this single mutation schema however it's good to define them with `@aws_subscribe` directive.

```javascript
type Subscription {
  onRecordAdded: Record
  @aws_subscribe(mutations: ["addRecord"])
}
```

Irrespective of what authorization is used in your application for the client (be it `Cognito User Pool`, `API Key`, `Lambda authorizers` or `AWS IAM role`), define the mutation which would be invoked from the Lambda function as `AWS IAM role`.

```javascript
type Mutation {
  addRecord(name: String!): Record!
  @aws_iam
}
```

### GraphQL resolvers

The conversation with ChatGPT didn't initially give me a resolver for the mutation `addRecord`, but a follow-up query did generate a VTL resolver (understand JS resolver is a very new, and fair guess that model doesn't know what JS resolver is at this point).

> This VTL resolver defines the `PutItem` operation for the `Mutation` type in the AppSync schema. It uses the `$ctx.args` variable to extract the `name` argument from the mutation, and then generates a new `id` for the item using the `$util.autoId()` function. The resolver constructs a DynamoDB `PutItem` request using these values, and then returns the request in JSON format.

```javascript
#set($name = $ctx.args.name)
#set($id = $util.autoId())
{
    "version": "2018-05-29",
    "operation": "PutItem",
    "tableName": "my-table",
    "item": {
        "id": { "S": "$id" },
        "name": { "S": "$name" }
    }
}
```

You may notice that the VTL resolver generated is doing a DynamoDB `PutItem` action even though the response ChatGPT gave didn't have the schema treating the `Record` type as a `model` or even give the steps to create a DynamoDB data source.

For the simplicity of things, let's create a simple VTL resolver that generates an ID and returns the name with the data source being a `None` type.

Request mapping template

```javascript
{
    "version": "2017-02-28",
    "payload": {
        "name": "$context.arguments.name",
        "id": "$util.autoId()"
    }
}
```

Response mapping template

```javascript
$util.toJson($context.result)
```

And the mutation `addRecord` is ready to be tested!

![Testing GraphQL mutation addRecord in AppSync console](https://cdn.hashnode.com/res/hashnode/image/upload/v1682155748549/eeddfd62-9008-417c-8656-d055c5ed93f3.png)

### Lambda function to trigger subscriptions on client-end

![Lambda function uses AppSync mutation to trigger a AppSync subscription on client-end](https://cdn.hashnode.com/res/hashnode/image/upload/v1682156671644/bb75a564-6ca9-4c1c-b0c5-2aa2c30b828d.png)

The solution that a *Serverless architect* would have come up with is the one above, where any Lambda trigger could invoke the Lambda function, and the Lambda function in turn uses an AppSync mutation and on the successful execution of the mutation, the subscription is internally invoked by AppSync.

The Lambda function generated by ChatGPT uses AppSync Client SDK. And it subscribes to an AppSync subscription rather than a mutation. 🤔 Why! 😵‍💫

```javascript
const observable = appSyncClient.subscribe({ query: mutation });
observable.subscribe((data) => console.log('Subscription data', data));
```

Firstly, create a Lambda layer with the npm dependencies -

```powershell
npm install aws-sdk aws-appsync graphql-tag isomorphic-fetch axios
```

Create your Lambda function with the environment variable set with the AppSync API endpoint.

```javascript
require('isomorphic-fetch');
const AWS = require('aws-sdk/global');
const AUTH_TYPE = require('aws-appsync').AUTH_TYPE;
const AWSAppSyncClient = require('aws-appsync').default;
const gql = require('graphql-tag');

const config = {
    url: process.env.APPSYNC_ENDPOINT,
    region: process.env.AWS_REGION,
    auth: {
        type: AUTH_TYPE.AWS_IAM,
        credentials: AWS.config.credentials,
    },
    disableOffline: true
};

const createTodo = gql`
    mutation MyMutation($name: String!) {
      addRecord(name: $name) {
        id
        name
      }
    }
`;

const client = new AWSAppSyncClient(config);

exports.handler = async function (event) {
    console.log("event ", event);
    try {
        const result = await client.mutate({
            mutation: createTodo,
            variables: {
                "name":event.name
            }
        });
        console.log("result ", result);
        return result
    } catch (error) {
        console.log("error ", error);
        return error
    }
};
```

`credentials: AWS.config.credentials` uses the IAM credentials from Lambda's runtime and for this to work, you would have to define

Update your Lambda's execution role with the policy -

```javascript
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "appsync:GraphQL",
            "Resource": "<Your AppSync ARN>"
        }
    ]
}
```

Back to your Lambda console, create an Event JSON.

```javascript
{
  "name": "This is a record from Lambda fn"
}
```

When testing the subscription from the AppSync console. The API by default uses `API Key` as authorization.

![Using AppSync console for testing subscriptions](https://cdn.hashnode.com/res/hashnode/image/upload/v1682169786774/f8231be5-f973-4640-b92e-e513d2d6e59a.gif)

Behind the scene, the event was tested from the Lambda function console and here the mutation is authorized by `AWS IAM`.

![Invoking the Lambda from Lambda console](https://cdn.hashnode.com/res/hashnode/image/upload/v1682169646933/1f8c3ebb-3e38-4115-8e4f-3b9a23d1491a.png)

## Wrap up

ChatGPT gave us a high-level solution in terms of what needs to be done but the how to execute and the minor details were missed out, this is exactly where Serverless architects would come into the picture to start building on top of what AI suggests.

The TL;DR of this is that you can start to get solutions from ChatGPT but the accuracy of how well it solves is something humans would have to intervene on. And building Lambda functions that invokes an AppSync mutation to trigger subscriptions would be helpful when you are relying on real-time sync from external sources other than the support data sources from AppSync.