loading...

The Guide to implement Geo Search in your React Native App with AWS Amplify

rpostulart profile image rpostulart ・11 min read

In this guide I am going to show how you can apply advanced GEO search in React Native in combination with AWS Amplify. More and more apps are using the GPS of a device or location of a user to show relevant content. This tutorial and in specific the AWS amplify implementation is even applicable for React or any other framework.

I will show how you can achieve GEO Polygon Search within your React Native App. With Polygon you can find a certain location in an area of coordinates. The other GEO option which I will show is GEO line Search. This is comparing two coordinates within a line distance.

Scenario 1
We are going to create an running event. You can only participate if you are living in that area where the event is taking place, otherwise you will not be able to find the event. With this I will cover Polygon search.

Scenario 2
In this scenario you can find other runners that live in a certain distance from you. You can connect to them to invite them to train together. You will enter the coordinates where you are living, set a distance and find all runners living in that distance.

What will not be covered?
You can use the GPS of your phone to show only the runners in a distance from your active GPS position, but that will not be covered in the tutorial.

Maybe you want to expose the data to each guest user or maybe only to logged in users. You can use AWS Amplify Auth to set up your corresponding authentication, but this will not be covered here. I will use Cognito to authenicate the API. If you want to know how to set up authenication in your app please follow this extensive guide from @dabit3 :

The Complete React Native Guide to User Authentication with the Amplify Framework: Click here

With approval from @dabit3 I also borrowed some steps about how to initiate AWS Amplify and React Native. Thx!

Getting Started

Set up React Native

First, we'll create the React Native application we'll be working with.

If using Expo

$ npx expo init geosearch

> Choose a template: blank

$ cd geosearch

$ npm install aws-amplify aws-amplify-react-native

If using React Native CLI

$ npx react-native init geosearch

$ cd geosearch

$ npm install aws-amplify aws-amplify-react-native 


Set up AWS Amplify

We first need to have the AWS Amplify CLI installed. The Amplify CLI is a command line tool that allows you to create & deploy various AWS services.

To install the CLI, we'll run the following command:

$ npm install -g @aws-amplify/cli

Next, we'll configure the CLI with a user from our AWS account:

$ amplify configure

For a video walkthrough of the process of configuring the CLI, click

Now we can now initialize a new Amplify project from within the root of our React Native application:

$ amplify init

Here we'll be guided through a series of steps:

  • Enter a name for the project: amplifygeo (or your preferred project name)
  • Enter a name for the environment: local (or your preferred environment name)
  • Choose your default editor: Visual Studio Code (or your text editor)
  • Choose the type of app that you're building: javascript
  • What javascript framework are you using: react-native
  • Source Directory Path: /
  • Distribution Directory Path: build
  • Build Command: npm run-script build
  • Start Command: npm run-script start
  • Do you want to use an AWS profile? Y
  • Please choose the profile you want to use: YOUR_USER_PROFILE
  • Now, our Amplify project has been created & we can move on to the next steps.

Add Graphql to your project

Your React Native App is up and running and AWS Amplify is configured. Amplify comes with different services which you can use to enrich your app. We are focussing mostly on the API service. So let’s add an API.

Amplify add api

These steps will take place:

  • Select Graphql
  • Enter a name for the API: geoAPI (your preferred API name)
  • Select an authorisation type for the API: Amazon Cognito User Pool ( Because we are using this app with authenticated users only, but you can choose other options)
  • Select at do you want to use the default authentication and security configuration: Default configuration
  • How do you want users to be able to sign in? Username (with this also the AWS Amplify Auth module will be enabled)
  • Do you want to configure advanced settings? *No, I am done. *
  • Do you have an annotated GraphQL schema? n
  • Do you want a guided schema creation?: n
  • Provide a custom type name: event

You API and your schema definition have been created now. You can find it in you project directory:

Amplify > backend > api > name of your api

Open your schema.graphql and remove all the code and replace it with this code:

type Event @model @searchable {
  date: String!
  description: String!
  maxParticipants: Int!
  polygon: Location!
}

type Runner @model @searchable {
  name: String!
  age: Int!
  coordinates: Coordinates!
}

type Coordinates {
  lat: Float!
  lon: Float!
}

type Location {
  type: String!
  coordinates: [[[Float]]]!
}

type Query {
  nearbyEvent(location: LocationEventInput!): EventConnection
  searchByDistance(location: LocationRunnerInput!, km: Int): RunnerConnection
}

input LocationEventInput {
  type: String!
  coordinates: [Float]!
}

input LocationRunnerInput {
  lat: Float!
  lon: Float!
}

type EventConnection {
  items: [Event]
  total: Int
  nextToken: String
}

type RunnerConnection {
  items: [Runner]
  total: Int
  nextToken: String
}

The @model will create a DynamoDB for you and the @searchable will create an ElasticSearch cluster for you. Each time you add, update or delete records in DynamoDB, these changes will be pushed to ElasticSearch. There are more directives possible, for the full set look at the AWS Amplify docs. With this schema you also add two queries:
1) to search events with coordinates that are positioned in the event area. With a input of type and coordinates
2) a query to search other runners by distance from your geo point. An input of lat, lon and kilometer.

Save your schema.graphql and close the file.

Now in you API directory open the resolvers directory and create these 4 files:

Query.nearbyEvent.req.vtl

## Query.nearbyEvent.req.vtl
## Objects of type Event will be stored in the /event index

#set( $indexPath = "/event/doc/_search" )

{
    "version": "2017-02-28",
    "operation": "GET",
    "path": "$indexPath.toLowerCase()",
    "params": {
        "body": {
          "query": {
                  "geo_shape": {
                    "polygon": {
                      "relation": "intersects",
                      "shape": {
                        "type": "${ctx.args.location.type}",
                        "coordinates": $ctx.args.location.coordinates

                      }
                    }
                  }
            }
      }
    }
}

Query.nearbyEvent.res.vtl

## Query.nearbyEvent.res.vtl

#set( $items = [] )
#foreach( $entry in $context.result.hits.hits )
  #if( !$foreach.hasNext )
    #set( $nextToken = "$entry.sort.get(0)" )
  #end
  $util.qr($items.add($entry.get("_source")))
#end
$util.toJson({
  "items": $items,
  "total": $ctx.result.hits.total,
  "nextToken": $nextToken
})

Query.searchByDistance.req.vtl

## Query.searchByDistance.req.vtl
## Objects of type Runner will be stored in the /runner index

#set( $indexPath = "/runner/doc/_search" )
#set( $distance = $util.defaultIfNull($ctx.args.km, 200) )
{
    "version": "2017-02-28",
    "operation": "GET",
    "path": "$indexPath.toLowerCase()",
    "params": {
        "body": {
            "query": {
                "bool" : {
                    "filter" : {
                        "geo_distance" : {
                            "distance" : "${distance}km",
                            "coordinates" : $util.toJson($ctx.args.location)
                        },

                    }
                }
            }
        }
    }
}

Query.searchByDistance.res.vtl

## Query.searchByDistance.res.vtl

#set( $items = [] )
#foreach( $entry in $context.result.hits.hits )
  #if( !$foreach.hasNext )
    #set( $nextToken = "$entry.sort.get(0)" )
  #end
  $util.qr($items.add($entry.get("_source")))
#end
$util.toJson({
  "items": $items,
  "total": $ctx.result.hits.total,
  "nextToken": $nextToken
})

You have now successfully created your resolvers which will act as a glue between GraphQL and ElasticSearch.

You need to make your application aware of these resolvers and provide the S3 path where you will deploy them. Therefor you go to your API directory and open the stacks directory. Open the file *CustomerResources.json. Find these lines:

"Resources": {
    "EmptyResource": {
      "Type": "Custom::EmptyResource",
      "Condition": "AlwaysFalse"
    }

Add a comma and paste this code:

"QuerynearbyEvent": {
      "Type": "AWS::AppSync::Resolver",
      "Properties": {
        "ApiId": {
          "Ref": "AppSyncApiId"
        },
        "DataSourceName": "ElasticSearchDomain",
        "TypeName": "Query",
        "FieldName": "nearbyEvent",
        "RequestMappingTemplateS3Location": {
          "Fn::Sub": [
            "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.nearbyEvent.req.vtl",
            {
              "S3DeploymentBucket": {
                "Ref": "S3DeploymentBucket"
              },
              "S3DeploymentRootKey": {
                "Ref": "S3DeploymentRootKey"
              }
            }
          ]
        },
        "ResponseMappingTemplateS3Location": {
          "Fn::Sub": [
            "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.nearbyEvent.res.vtl",
            {
              "S3DeploymentBucket": {
                "Ref": "S3DeploymentBucket"
              },
              "S3DeploymentRootKey": {
                "Ref": "S3DeploymentRootKey"
              }
            }
          ]
        }
      }
    },
"searchByDistance": {
      "Type": "AWS::AppSync::Resolver",
      "Properties": {
        "ApiId": {
          "Ref": "AppSyncApiId"
        },
        "DataSourceName": "ElasticSearchDomain",
        "TypeName": "Query",
        "FieldName": "searchByDistance",
        "RequestMappingTemplateS3Location": {
          "Fn::Sub": [
            "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.searchByDistance.req.vtl",
            {
              "S3DeploymentBucket": {
                "Ref": "S3DeploymentBucket"
              },
              "S3DeploymentRootKey": {
                "Ref": "S3DeploymentRootKey"
              }
            }
          ]
        },
        "ResponseMappingTemplateS3Location": {
          "Fn::Sub": [
            "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.searchByDistance.res.vtl",
            {
              "S3DeploymentBucket": {
                "Ref": "S3DeploymentBucket"
              },
              "S3DeploymentRootKey": {
                "Ref": "S3DeploymentRootKey"
              }
            }
          ]
        }
      }
    }


Wow! You have created your whole backend as code. It’s now time to deploy. Go to the root of your project directory and deploy with:

Amplify push
  • Do you want to generate code for your newly created GraphQL API (Y/n): Y
  • Choose the code generation language target: javascript
  • Enter the file name pattern of graphql queries, mutations and subscriptions: enter (the default path)
  • Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions: Y
  • Enter maximum statement depth [increase from default if your schema is deeply nested]: enter (the default value of 2)

Configure ElasticSearch

After you pushed your backend to AWS you need to do some additional task in the console. You need to map the polygon field of your Event type in ElasticSearch to a GEO shape. I have not figured out a way to do this as code, so tips are welcome. This means that each time you deploy your backend to a new environment you need to do this action manually once.

  • Log into the AWS console
  • Search for ElasticSearch Service
  • Click on the domain name of your cluster
  • Copy the Domain ARN
  • Click on the tab Modify access policy and add this code:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": "es:*",
      "Resource": "<YOUR DOMAIN ARN>/*"
    }
  ]
}

This will open Kibana access to the internet without a password. This means everybody van query and mutate your data. Make sure you change this later if you don't want to end up in the situation like Adobe did:

Adobe left 7.5 million Creative Cloud user records exposed online click here to read the article

  • Go back to the dashboard of the ElasticSearch Service
  • Click again on the domain
  • Click on the Kibana link
  • Click on Dev Tools and paste this code below in the console by replacing the other content:
PUT /event
    {
       "mappings": {
            "doc": { 
                "properties": {
                    "polygon": {
                    "type": "geo_shape"
                   }
                }
            }
        }
     }

PUT /runner
    {
        "mappings": {
            "doc": { 
                "properties": {
                    "coordinates": {
                     "type": "geo_point"
                      }
                 }
            }
        }
    }

You have now created the mappings! As soon as you create a running event via your app or Appsync console it will be added to dynamoDB, streamed to ElasticSearch and the polygon field will be mapped to a GEO shape. Good Job!

Add some data via AppSync

Let’s add some data which you can use in your app. Go to the AppSync service in the console.

  • Go to AWS AppSync via the console.
  • Open your project
  • Click on Queries
  • Log in with a Cognito user by clicking on the button 'Login via Cognito User Pools' (You can create a user via Cognito in the console or via your App)
  • Add the following code an run the code:
mutation PutEvent {
  createEvent( input: {
    date: "12-12-2019", 
    description: "Run event for kids",
    maxParticipants: 140,
    polygon:{
      type: "Polygon",
      coordinates:[[[52.357156, 4.888768],[52.354334, 4.888238],[52.353505, 4.900297],[52.356831, 4.902110],[52.357156, 4.888768]]]
    }
  }
  ){
    date
    description
    maxParticipants
    polygon { type, coordinates }
  }
}

now make another event in the same area:

mutation PutEvent {
  createEvent( input: {
    date: "08-12-2019", 
    description: "The Amsterdam running Event of the year",
    maxParticipants: 50,
    polygon:{
      type: "Polygon",
      coordinates:[[[52.357156, 4.888768],[52.354334, 4.888238],[52.353505, 4.900297],[52.356831, 4.902110],[52.357156, 4.888768]]]
    }
  }
  ){
    date
    description
    maxParticipants
    polygon { type, coordinates }
  }
}

Let's create some awesome Runners:

mutation PutRunners {
  createRunner(input:{
    name: "Ramon"
    age: 34
    coordinates:{
      lat:52.355986, 
      lon:4.892747
    }
  }){
    name
    age
    coordinates {lat,lon}
  }
}

And another one:

mutation PutRunners {
  createRunner(input:{
    name: "Niels"
    age: 40
    coordinates:{
      lat:52.370216, 
      lon:4.895168
    }
  }){
    name
    age
    coordinates {lat,lon}
  }
}

You will find now events and runners in DynamoDB as well in ElasticSearch. Your GraphQL, DynamoDB and ElasticSearch are working good!

Let's see the data in action

We are going to query the data via the the AWS Appsync query console.

Let's first query with the coordinates of Ramon's location:

query GetEventsNearBY {
  nearbyEvent(location:{
    type: "point"
    coordinates:[52.355986,4.892747] 
  })
  {
   items {
    date
    description
    maxParticipants
  } 
  }
}

Cool, you see the two events in the area where Ramon is living.
Now let's change the coordinates and let's see if Niels will not see these events, because he is living outside the area:

query GetEventsNearBY {
  nearbyEvent(location:{
    type: "point"
    coordinates:[52.370216,4.895168] 
  })
  {
   items {
    date
    description
    maxParticipants
  } 
  }
}

And it's working, there are no results

Alt Text

Now let's test scenario 2. Where we want to find runner is a certain distance from my location (coordinates):

query GetRunnersByDistance {
  searchByDistance(location:{lat:52.355986, lon: 4.892747}, km: 50)
  {
    items {
      name
      age
    }
  }
}

You will get two results.

{
  "data": {
    "searchByDistance": {
      "items": [
        {
          "name": "Niels",
          "age": 40
        },
        {
          "name": "Ramon",
          "age": 34
        }
      ]
    }
  }
}

Now let's make the distance smaller to 1 km:

query GetRunnersByDistance {
  searchByDistance(location:{lat:52.355986, lon: 4.892747}, km: 1)
  {
    items {
      name
      age
    }
  }
}

and the expected result is that you will only see Ramon:

{
  "data": {
    "searchByDistance": {
      "items": [
        {
          "name": "Ramon",
          "age": 34
        }
      ]
    }
  }
}

You whole backend is up and running, tested and you are now ready to built your front end application. This can be any framework like: IOS, Android, React Native, VueJS, React, Angular or Ionic.

Build the React Native app

We are going to build a very simple react native application, so that you at least can see it working in your app. Be prepared: this will not be a great user experienced design.

Go to the root of your project and open App.js and replace it with this code:

import React from "react";
import { withAuthenticator } from "aws-amplify-react-native";
import Amplify from "aws-amplify";

// Get the aws resources configuration parameters
import awsconfig from "./aws-exports"; // if you are using Amplify CLI
import Main from "./src/Main";

Amplify.configure(awsconfig);

class App extends React.Component {
  render() {
    return <Main />;
  }
}

export default withAuthenticator(App);

This will import everything you need an wraps your app with a HOC withAuthenticator. This creates login and signup functionality for your app.

Now create a file in the src folder with this name: Main.js and paste the following code:

import React from "react";
import { Text, View, FlatList, TextInput } from "react-native";
import * as queries from "./graphql/queries.js";
import { API, graphqlOperation } from "aws-amplify";

class Main extends React.Component {
  state = { events: [], runners: [] };

  async componentDidMount() {
    await this.loadNearByEvents();
  }

  async loadNearByEvents() {
    const input = {
      type: "point",
      coordinates: [52.355986, 4.892747] // normally you set these coordinates dynamically from a profile page or active GPS location.
    };

    const result = await API.graphql(
      graphqlOperation(queries.nearbyEvent, { location: input })
    )
      .then(result => {
        return result.data.nearbyEvent.items;
      })
      .catch(err => console.log(err));

    this.setState({
      events: result
    });
  }

  async loadRunnersByDistance(kmInput) {
    // normally you set these coordinates dynamically from a profile page or active GPS location.
    const input = {
      lat: 52.355986,
      lon: 4.892747
    };

    const result = await API.graphql(
      graphqlOperation(queries.searchByDistance, {
        location: input,
        km: kmInput
      })
    )
      .then(result => {
        return result.data.searchByDistance.items;
      })
      .catch(err => console.log(err));

    this.setState({
      runners: result
    });
  }

  render() {
    return (
      <View style={{ marginTop: 80, marginLeft: 10, marginRight: 10 }}>
        <Text style={{ fontSize: 20, marginBottom: 5 }}>Events</Text>
        <FlatList
          data={this.state.events}
          renderItem={({ item }) => (
            <View style={{ marginBottom: 20 }}>
              <Text>Date: {item.date}</Text>
              <Text>Description: {item.description}</Text>
              <Text>Max Participants: {item.maxParticipants}</Text>
            </View>
          )}
        />
        <Text style={{ fontSize: 20, marginBottom: 5 }}>
          Runners in the area
        </Text>
        <Text>Distance (KM)</Text>
        <TextInput
          style={{
            borderRadius: 4,
            borderWidth: 0.5,
            borderColor: "#d6d7da",
            width: 200,
            padding: 10
          }}
          onChangeText={text => this.loadRunnersByDistance(text)}
        />
        <FlatList
          data={this.state.runners}
          renderItem={({ item }) => (
            <View style={{ marginBottom: 20 }}>
              <Text>Name: {item.name}</Text>
              <Text>Age: {item.age}</Text>
            </View>
          )}
        />
      </View>
    );
  }
}

export default Main;

You app is ready and you can start it from your root project with:

expo start

When you change to distance from 1 to 2 you will see it shows only the runners in that distance
Alt Text

See github for the actual code: https://github.com/rpostulart/geoSearch

Conclusion

In this guide you have seen how easily it is to set up a backend and front end for your App. You have now the skills to play around with GEO Searches while we discovered point distance and polygon, but there is more out there. You added static coordinates, so you can also continue to make them more dynamic.

I really love AWS Amplify. It gives you the power as a developer to fully focus on delivering business value and innovation to your customers and not spending time on configuration.

In this guide we are using AWS ElasticSearch Service. Be aware that this is an expensive service. I really hope that the AWS Amplify, AWS DynamoDB or AWS ElasticSearch team comes up with a less expensive solution like for example serverless ElasticSearch.

Let me know what you think about this guide.

Posted on by:

rpostulart profile

rpostulart

@rpostulart

IT Engineering Lead who loves to work on side projects with React, GatsbyJS, React Native, NodeJS and AWS Amplify.

Discussion

markdown guide
 

The best way to add the ES mapping is to use a cloudformation custom resource lambda, you can write an inline lambda function that makes the right API calls when the template is first deployed, if you make the ES domain a dependency of the custom resource Lambda then it will always run after the ES domain has been setup.

 

Ok nice, I was not aware of that. Do you have some more info / referencence documentation about this?

 

there are some gists floating around to adapt, most look like this block of JSON for the cloudformation custom resources file. You likely want to lock down the permission a bit more.

{
  "ConfigureESCustom": {
    "Type": "Custom::ConfigureES",
    "Properties": {
      "ServiceToken": {
        "Fn::GetAtt": [
          "ConfigureES",
          "Arn"
        ]
      }
    }
  },
  "ConfigureES": {
    "Type": "AWS::Lambda::Function",
    "Properties": {
      "Environment": {
        "Variables": {
          "ES_ENDPOINT": {
            "Fn::ImportValue": {
              "Fn::Join": [
                ":",
                [
                  {
                    "Ref": "AppSyncApiId"
                  },
                  "GetAtt",
                  "Elasticsearch",
                  "DomainEndpoint"
                ]
              ]
            }
          },
          "ES_REGION": {
            "Ref": "AWS::Region"
          }
        }
      },
      "Code": {
            "ZipFile": "import base64
    import json
    import logging
    import string
    import boto3
    import os
    import time
    import datetime
    import traceback
    from urllib.parse import urlparse, quote
    from botocore.vendored import requests
    from botocore.auth import SigV4Auth
    from botocore.awsrequest import AWSRequest
    from botocore.credentials import get_credentials
    from botocore.endpoint import BotocoreHTTPSession
    from botocore.session import Session
    import cfnresponse

    logger = logging.getLogger()
    logger.setLevel(logging.INFO)

    # The following parameters are required to configure the ES cluster
    ES_ENDPOINT = os.environ['ES_ENDPOINT']
    ES_REGION = os.environ['ES_REGION']
    DEBUG = True if os.environ['DEBUG'] is not None else False

    def es_put(payload, region, creds, host, path, method='PUT', proto='https://'):
    '''Put index data to ES endpoint with SigV4 signed http headers'''
    req = AWSRequest(method=method, url=proto + host +
        quote(path), data=payload, headers={'Host': host, 'Content-Type': 'application/json'})
    SigV4Auth(creds, 'es', region).add_auth(req)
    http_session = BotocoreHTTPSession()
    res = http_session.send(req.prepare())
    return res._content

    def lambda_handler(event, context):
    logger.info('got event {}'.format(event))

    if event['RequestType'] == 'Create':
        # Get aws_region and credentials to post signed URL to ES
        es_region = ES_REGION or os.environ['AWS_REGION']
        session = Session({'region': es_region})
        creds = get_credentials(session)
        es_url = urlparse(ES_ENDPOINT)
        # Extract the domain name in ES_ENDPOINT
        es_endpoint = es_url.netloc or es_url.path
        es_put('', es_region, creds,
        es_endpoint, '/s12doctor')
        es_actions = []
        es_actions.append('')  # Add one empty line to force final \

        es_payload = '\
    '.join(es_actions)
        action = {\"properties\": {\"location\": {\"type\": \"geo_point\"}, \"coordinates\": {\"type\": \"geo_point\"}}}
        es_actions.append(json.dumps(action))
    es_actions.append('')  # Add one empty line to force final \

    es_payload = '\
'.join(es_actions)
    es_put(es_payload, es_region, creds,
      es_endpoint, '/INDEX_NAME_HERE/_mapping/doc')

  cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
"
      },
      "FunctionName": {
        "Fn::Join": [
          "-",
          [
            "ConfigureES",
            {
              "Ref": "env"
            }
          ]
        ]
      },
      "Handler": "index.lambda_handler",
      "Timeout": 30,
      "Role": {
        "Fn::GetAtt": [
          "LambdaRole",
          "Arn"
        ]
      },
      "Runtime": "python3.6",
      "Layers": [
        "arn:aws:lambda:eu-west-2:142628438157:layer:AWSLambda-Python-AWS-SDK:1"
      ]
    }
  },
  "LambdaRole": {
    "Type": "AWS::IAM::Role",
    "Properties": {
      "AssumeRolePolicyDocument": {
        "Version": "2012-10-17",
        "Statement": [
          {
            "Effect": "Allow",
            "Principal": {
              "Service": [
                "lambda.amazonaws.com"
              ]
            },
            "Action": [
              "sts:AssumeRole"
            ]
          }
        ]
      },
      "Path": "/",
      "Policies": [
        {
          "PolicyName": "lambda-logs",
          "PolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
              {
                "Effect": "Allow",
                "Action": [
                  "logs:CreateLogGroup",
                  "logs:CreateLogStream",
                  "logs:PutLogEvents"
                ],
                "Resource": [
                  "arn:aws:logs:*:*:*"
                ]
              }
            ]
          }
        },
        {
          "PolicyName": "ES",
          "PolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
              {
                "Effect": "Allow",
                "Action": [
                  "es:*"
                ],
                "Resource": {
                  "Fn::Join": [
                    "",
                    [
                      {
                        "Fn::ImportValue": {
                          "Fn::Join": [
                            ":",
                            [
                              {
                                "Ref": "AppSyncApiId"
                              },
                              "GetAtt",
                              "Elasticsearch",
                              "DomainArn"
                            ]
                          ]
                        }
                      },
                      "/*"
                    ]
                  ]
                }
              }
            ]
          }
        }
      ]
    }
  }
}

Thx, will look into it and update this post accordingly