Operating with AppSync from AWS Lambda
I wasn’t always sure about AWS Lambda as the best option to provide the full back-end of a medium-large app. Nevertheless, my opinion has recently changed (probably thanks to the enthusiasm of my colleague Luke and the discovery of the framework Serverless) and while I won’t go through the advantages of using a serverless architecture in this article, I will admit I’ve become a true believer in its potential and I’m convinced it will be part of the chosen architecture solution for most of our apps in the future.
For our latest hackathon adventure, the team opted for a serverless back-end composed by an AppSync layer which would work as a gateway to our services, complemented with AWS Lambda functions for specific tasks. If you are interested in the front-end side, please read (and clap) James’s amazing article to find out how he got AWS AppSync, React Native and React Apollo playing nicely all together.
Using AppSync from our Lambda functions
Placing ourselves in the back-end side, one of the tasks approached by Lambda aimed to retrieve Cafe products from our iZettle account and import them into the app database. The logic needed to get some information (e.g. does the product already exist?) from AppSync (using a GraphQL query) to subsequently persist it with a mutation.
This article written by Adrian Hall explains extremely well how to trigger an AppSync operation (a mutation) from AWS Lambda and it shows how powerful this approach is since you’ll be able to notify subscribed clients in real time. That’s something we wanted (it’s pretty cool allowing users to order a recently added new type of coffee, isn’t it?), however, in his example, he is based in the use of IAM roles as the authentication method in AppSync. Due to our requirements, we decided to go for Cognito User Pool to authenticate calls to AppSync because that would allow us to access to the user information in our resolvers, and control exactly the data allowed to be retrieved/updated once the user had been identified.
Create the Server App Client
For our example, we’ll assume the Cognito User Pool has already been created. So we’ll need to create a new App client for the pool making sure the option Enable sign-in API for server-based authentication (ADMIN_NO_SRP_AUTH) is checked. This authentication solution is designed to be used from server-side secure apps as an alternative of SRP (Secure Remote Password) to avoid some of the expensive calculations of this protocol, making use of AWS Credentials instead (you’ll need an IAM role with the adequate access to Cognito User Pools to carry out admin authentication actions).
The client app can be created either via UI:
Or as part of the stack created and deployed with CloudFormation through the Serverless framework:
OurBackendPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
ClientName: "Our backend"
GenerateSecret: false
RefreshTokenValidity: 30
UserPoolId: { Ref: OurCognitoUserPool }
ExplicitAuthFlows:
- ADMIN_NO_SRP_AUTH
Get a JW Token to authenticate the Cognito user
Now, we’ll be able to get JSON Web Tokens that will give access to the benefits of AppSync:
import * as AWS from "aws-sdk";
import { credentials } from "./aws-exports";/**
* Gets a Cognito user access token using ADMIN_NO_SRP_AUTH auth.
*
* @param awsConfig - Aws stack info.
*
* @returns - The access token.
*/
export const getCognitoAdminToken = (awsConfig: {
cognitoClientId: string,
cognitoPoolId: string,
cognitoUserName: string,
cognitoPassword: string
}): Promise => {
return new Promise((resolve, reject) => {
const {
cognitoClientId,
cognitoPoolId,
cognitoUserName,
cognitoPassword
} = awsConfig; const authRequestParams: AWS.CognitoIdentityServiceProvider.AdminInitiateAuthRequest = {
AuthFlow: "ADMIN_NO_SRP_AUTH",
ClientId: cognitoClientId,
UserPoolId: cognitoPoolId,
AuthParameters: {
USERNAME: cognitoUserName,
PASSWORD: cognitoPassword,
}
};
const awsCognito = new AWS.CognitoIdentityServiceProvider(credentials);
awsCognito.adminInitiateAuth(authRequestParams, (err, data) => {
if (err) {
reject(err);
return;
}
if (!data ||
!data.AuthenticationResult ||
!data.AuthenticationResult.AccessToken
) {
reject(new Error("Access token not found in adminInitiateAuth response"));
return;
} resolve(data.AuthenticationResult.AccessToken);
});
});
}
Note: for the given example, we assumed there is an existing user in the user pool that has been verified. In the next article, I’ll explain how to automate its creation.
Operate with AppSync
To actually communicate our function with AppSync we made use of the package aws-appsync which works as a wrapper of Apollo and allows authentication through Cognito pools:
import { AUTH_TYPE } from "aws-appsync/lib/link/auth-link";
import AWSAppSyncClient, { createAppSyncLink } from "aws-appsync/lib/client";
import { createHttpLink } from "apollo-link-http";
import fetch from "node-fetch";
import { NormalizedCacheObject } from "apollo-cache-inmemory";import { getCognitoAdminToken } from "./cognito-access";
import { awsCognitoConfig } from "./aws-exports";
/**
* Returns an initiated AppSync Client instance.
*
* @param appSyncConfig - The AppSync config.
*
* @returns - The AppSync Client instance.
*/
const initiateClient = (appSyncConfig: {
graphQLEndpoint: string,
region: string
}): Promise<AWSAppSyncClient> => {
return new Promise((resolve, reject) => {
const {
graphQLEndpoint,
region
} = appSyncConfig; try {
getCognitoAdminToken(awsCognitoConfig).then((error: Error, jwtToken: string) => {
if (error) {
reject(error);
return;
} const appSyncClientOptions = {
url: graphQLEndpoint,
region: region,
// Use the Cognito User Pool as the auth method and
// the token previously retrieved.
auth: {
type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS,
jwtToken
},
// Required when using from Lambda.
disableOffline: true,
// Required by the type, not used in our use case.
complexObjectsCredentials: () => { return null }
}; const apolloClientOptions = {
link: createAppSyncLink({
...appSyncClientOptions,
resultsFetcherLink: createHttpLink({ uri: appSyncClientOptions.url, fetch })
})
};
const appSyncClient = new AWSAppSyncClient(appSyncClientOptions, apolloClientOptions); resolve(appSyncClient); });
} catch (error) {
reject(error);
}
});
}
Once the AppSync Client is initialised, we can perform queries…
import gql from "graphql-tag";
import { appSyncConfig } from "./aws-exports";const query = `query GetEntity($id: ID!) {
getEntity(id: $id) {
id
name
}
}`;
const graphQLQuery = gql(query);initiateClient(appSyncConfig)
.then((client) => {
client.hydrated()
.then((hydratedClient) => {
hydratedClient.query({
query: graphQLQuery,
// Required when using from Lambda.
fetchPolicy: 'network-only',
variables: { id: entityId }
)
.then((data) => {
// Use the data returned by the query...
});
});
});
or, even better, the mutations we were looking for…
import gql from "graphql-tag";
import { appSyncConfig } from "./aws-exports";const mutation = `mutation UpdateEntity($input: UpdateEntityInput!) {
updateEntity(input: $input) {
id
name
}
}`;
const graphQLMutation = gql(mutation);initiateClient(appSyncConfig)
.then((client) => {
client.hydrated()
.then((hydratedClient) => {
hydratedClient.mutate({
mutation: graphQLMutation,
fetchPolicy: 'no-cache',
variables: { input: { id: entityId, name: entityName }
)
.then((data) => {
// Use the data returned by the mutation...
});
});
});
Any client subscribed to the data being mutated will be notified, keeping your users up to date with the latest changes.
In the next chapter, we’ll run through some of the things that can be improved:
- Automation: Creation of the resources needed for this solution with CloudFormation and Serverless framework.
- Performance: Reuse of access tokens across functions calls, speeding up the authentication process.
Originally published in: https://www.gravitywell.co.uk/latest/aws/posts/operating-with-appsync-aws-lambda/