AWS News Blog
Amplify DataStore – Simplify Development of Offline Apps with GraphQL
|
The open source Amplify Framework is a command line tool and a library allowing web & mobile developers to easily provision and access cloud based services. For example, if I want to create a GraphQL API for my mobile application, I use amplify add api
on my development machine to configure the backend API. After answering a few questions, I type amplify push
to create an AWS AppSync API backend in the cloud. Amplify generates code allowing my app to easily access the newly created API. Amplify supports popular web frameworks, such as Angular, React, and Vue. It also supports mobile applications developed with React Native, Swift for iOS, or Java for Android. If you want to learn more about how to use Amplify for your mobile applications, feel free to attend one the workshops (iOS or React Native) we prepared for the re:Invent 2019 conference.
AWS customers told us the most difficult tasks when developing web & mobile applications is to synchronize data across devices and to handle offline operations. Ideally, when a device is offline, your customers should be able to continue to use your application, not only to access data but also to create and modify them. When the device comes back online, the application must reconnect to the backend, synchronize the data and resolve conflicts, if any. It requires a lot of undifferentiated code to correctly handle all edge cases, even when using AWS AppSync SDK’s on-device cache with offline mutations and delta sync.
Today, we are introducing Amplify DataStore, a persistent on-device storage repository for developers to write, read, and observe changes to data. Amplify DataStore allows developers to write apps leveraging distributed data without writing additional code for offline or online scenario. Amplify DataStore can be used as a stand-alone local datastore in web and mobile applications, with no connection to the cloud, or the need to have an AWS Account. However, when used with a cloud backend, Amplify DataStore transparently synchronizes data with an AWS AppSync API when network connectivity is available. Amplify DataStore automatically versions data, implements conflict detection and resolution in the cloud using AppSync. The toolchain also generates object definitions for my programming language based on the GraphQL schema developers provide.
Let’s see how it works.
I first install the Amplify CLI and create a React App. This is standard React, you can find the script on my git repo. I add Amplify DataStore to the app with npx amplify-app
. npx
is specific for NodeJS, Amplify DataStore also integrates with native mobile toolchains, such as the Gradle plugin for Android Studio and CocoaPods that creates custom XCode build phases for iOS.
Now that the scaffolding of my app is done, I add a GraphQL schema representing two entities: Posts
and Comments
on these posts. I install the dependencies and use AWS Amplify CLI to generate the source code for the objects defined in the GraphQL schema.
# add a graphql schema to amplify/backend/api/amplifyDatasource/schema.graphql
echo "enum PostStatus {
ACTIVE
INACTIVE
}
type Post @model {
id: ID!
title: String!
comments: [Comment] @connection(name: \"PostComments\")
rating: Int!
status: PostStatus!
}
type Comment @model {
id: ID!
content: String
post: Post @connection(name: \"PostComments\")
}" > amplify/backend/api/amplifyDatasource/schema.graphql
# install dependencies
npm i @aws-amplify/core @aws-amplify/datastore
# generate the source code representing the model
npm run amplify-modelgen
# create the API in the cloud
npm run amplify-push
@model
and @connection
are directives that the Amplify GraphQL Transformer uses to generate code. Objects annotated with @model are top level objects in your API, they are stored in DynamoDB, you can make them searchable, version them or restrict their access to authorised users only. @connection allows to express 1-n relationships between objects, similarly to what you would define when using a relational database (you can use the @key
directive to model n-n relationships).
The last step is to create the React app itself. I propose to download a very simple sample app to get started quickly:
# download a simple react app
curl -o src/App.js https://raw.githubusercontent.com/sebsto/amplify-datastore-js-e2e/master/src/App.js
# start the app
npm run start
I connect my browser to the app http://localhost:8080
and start to test the app.
The demo app provides a basic UI (as you can guess, I am not a graphic designer !) to create, query, and to delete items. Amplify DataStore provides developers with an easy to use API to store, query and delete data. Read and write are propagated in the background to your AppSync endpoint in the cloud. Amplify DataStore uses a local data store via a storage adapter, we ship IndexedDB for web and SQLite for mobile. Amplify DataStore is open source, you can add support for other database, if needed.
From a code perspective, interacting with data is as easy as invoking the save()
, delete()
, or query()
operations on the DataStore
object (this is a Javascript example, you would write similar code for Swift or Java). Notice that the query()
operation accepts filters based on Predicates
expressions, such as item.rating("gt", 4)
or Predicates.All
.
function onCreate() {
DataStore.save(
new Post({
title: `New title ${Date.now()}`,
rating: 1,
status: PostStatus.ACTIVE
})
);
}
function onDeleteAll() {
DataStore.delete(Post, Predicates.ALL);
}
async function onQuery(setPosts) {
const posts = await DataStore.query(Post, c => c.rating("gt", 4));
setPosts(posts)
}
async function listPosts(setPosts) {
const posts = await DataStore.query(Post, Predicates.ALL);
setPosts(posts);
}
I connect to Amazon DynamoDB console and observe the items are stored in my backend:
There is nothing to change in my code to support offline mode. To simulate offline mode, I turn off my wifi. I add two items in the app and turn on the wifi again. The app continues to operate as usual while offline. The only noticeable change is the _version
field is not updated while offline, as it is populated by the backend.
When the network is back, Amplify DataStore transparently synchronizes with the backend. I verify there are 5 items now in DynamoDB (the table name is different for each deployment, be sure to adjust the name for your table below):
aws dynamodb scan --table-name Post-raherug3frfibkwsuzphkexewa-amplify \
--filter-expression "#deleted <> :value" \
--expression-attribute-names '{"#deleted" : "_deleted"}' \
--expression-attribute-values '{":value" : { "BOOL": true} }' \
--query "Count"
5 // <= there are now 5 non deleted items in the table !
Amplify DataStore leverages GraphQL subscriptions to keep track of changes that happen on the backend. Your customers can modify the data from another device and Amplify DataStore takes care of synchronizing the local data store transparently. No GraphQL knowledge is required, Amplify DataStore takes care of the low level GraphQL API calls for you automatically. Real-time data, connections, scalability, fan-out and broadcasting are all handled by the Amplify client and AppSync, using WebSocket protocol under the cover.
We are effectively using GraphQL as a network protocol to dynamically transform model instances to GraphQL documents over HTTPS.
To refresh the UI when a change happens on the backend, I add the following code in the useEffect()
React hook. It uses the DataStore.observe()
method to register a callback function ( msg => { ... }
). Amplify DataStore calls this function when an instance of Post
changes on the backend.
const subscription = DataStore.observe(Post).subscribe(msg => {
console.log(msg.model, msg.opType, msg.element);
listPosts(setPosts);
});
Now, I open the AppSync console. I query existing Posts to retrieve a Post ID.
query ListPost {
listPosts(limit: 10) {
items {
id
title
status
rating
_version
}
}
}
I choose the first post in my app, the one starting with 7d8… and I send the following GraphQL mutation:
mutation UpdatePost {
updatePost(input: {
id: "7d80688f-898d-4fb6-a632-8cbe060b9691"
title: "updated title 13:56"
status: ACTIVE
rating: 7
_version: 1
}) {
id
title
status
rating
_lastChangedAt
_version
_deleted
}
}
Immediately, I see the app receiving the notification and refreshing its user interface.
Finally, I test with multiple devices. I first create a hosting environment for my app using amplify add hosting
and amplify publish
. Once the app is published, I open the iOS Simulator and Chrome side by side. Both apps initially display the same list of items. I create new items in both apps and observe the apps refreshing their UI in near real time. At the end of my test, I delete all items.
I verify there are no more items in DynamoDB (the table name is different for each deployment, be sure to adjust the name for your table below):
aws dynamodb scan --table-name Post-raherug3frfibkwsuzphkexewa-amplify \
--filter-expression "#deleted <> :value" \
--expression-attribute-names '{"#deleted" : "_deleted"}' \
--expression-attribute-values '{":value" : { "BOOL": true} }' \
--query "Count"
0 // <= all the items have been deleted !
When syncing local data with the backend, AWS AppSync keeps track of version numbers to detect conflicts. When there is a conflict, the default resolution strategy is to automerge the changes on the backend. Automerge is an easy strategy to resolve conflit without writing client-side code. For example, let’s pretend I have an initial Post
, and Bob & Alice update the post at the same time:
The original item: |
Alice updates the rating : |
At the same time, Bob updates the title : |
The final item after auto-merge is: |
Automerge strictly defines merging rules at field level, based on type information defined in the GraphQL schema. For example List
and Map
are merged, and conflicting updates on scalars (such as numbers and strings) preserve the value existing on the server. Developers can chose other conflict resolution strategies: optimistic concurrency (conflicting updates are rejected) or custom (an AWS Lambda function is called to decide what version is the correct one). You can choose the conflit resolution strategy with amplify update api
. You can read more about these different strategies in the AppSync documentation.
The full source code for this demo is available on my git repository. The app has less than 100 lines of code, 20% being just UI related. Notice that I did not write a single line of GraphQL code, everything happens in the Amplify DataStore.
Your Amplify DataStore cloud backend is available in all AWS Regions where AppSync is available, which, at the time I write this post are: US East (N. Virginia), US East (Ohio), US West (Oregon), Asia Pacific (Mumbai), Asia Pacific (Seoul), Asia Pacific (Singapore), Asia Pacific (Sydney), Asia Pacific (Tokyo), Europe (Frankfurt), Europe (Ireland), and Europe (London).
There is no additional charges to use Amplify DataStore in your application, you only pay for the backend resources you use, such as AppSync and DynamoDB (see here and here for the pricing detail). Both services have a free tier allowing you to discover and to experiment for free.
Amplify DataStore allows you to focus on the business value of your apps, instead of writing undifferentiated code. I can’t wait to discover the great applications you’re going to build with it.
-- seb