Using AWS as a Back End: The Data Store API

In this tutorial, you’ll extend the Isolation Nation app from the previous tutorial, adding analytics and real-time chat functionality using AWS Pinpoint and AWS Amplify DataStore. By Tom Elliott.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 5 of this article. Click here to view the first page.

Updating Data in DynamoDB

You may have noticed that all your test users have the SW1A location in the Locations list. Instead, your app needs to ask people where they live. Sadly, not everyone can live in Buckingham Palace!

Open HomeScreenViewModel.swift. At the top of the file, import the Amplify library:

import Amplify

HomeScreenViewModel publishes a property called userPostcodeState. This wraps an optional String in a Loading enum.

Navigate to fetchUser(). Note how userPostcodeState is set to .loaded, with a hard-coded associated value of SW1A 1AA. Replace this line with the following:

// 1
userPostcodeState = .loading(nil)
// 2
_ = Amplify.API.query(request: .get(User.self, byId: userID)) { [self] event in
  // 3
  DispatchQueue.main.async {
    // 4
    switch event {
    case .failure(let error):
      logger?.logError(error.localizedDescription)
      userPostcodeState = .errored(error)
      return
    case .success(let result):
      switch result {
      case .failure(let resultError):
        logger?.logError(resultError.localizedDescription)
        userPostcodeState = .errored(resultError)
        return
      case .success(let user):
        // 5
        guard 
          let user = user, 
          let postcode = user.postcode 
        else {
          userPostcodeState = .loaded(nil)
          return
        }
        // 6
        userPostcodeState = .loaded(postcode)
      }
    }
  }
}

Here’s what this code is doing:

  1. First, set userPostcodeState to loading.
  2. Then, fetch the user from DynamoDB.
  3. Dispatch to the main queue, as you should always modify published vars from the main thread.
  4. Check for errors in the usual manner.
  5. If the request is successful, check to see if the user model has a postcode set. If not, set userPostcodeState to nil.
  6. If so, set userPostcodeState to loaded, with the user’s postcode as an associated value.

Build and run. This time, when your test user logs in, the app will present a screen inviting the user to enter a postcode.

Enter your Postcode

If you’re wondering how the app decided to display this screen, look in HomeScreen.swift. Notice how that view renders SetPostcodeView if the postcode is nil.

Open SetPostcodeView.swift in the Home group. This is a fairly simple view. TextField collects the user’s postcode. And Button asks the view model to perform the addPostCode action when tapped.

Now, open HomeScreenViewModel.swift again. Find addPostCode(_:) at the bottom of the file and write its implementation:

// 1
_ = Amplify.API.query(request: .get(User.self, byId: userID)) { [self] event in
  DispatchQueue.main.async {
    switch event {
    case .failure(let error):
      logger?.logError("Error occurred: \(error.localizedDescription )")
      userPostcodeState = .errored(error)
    case .success(let result):
      switch result {
      case .failure(let resultError):
        logger?
          .logError("Error occurred: \(resultError.localizedDescription )")
        userPostcodeState = .errored(resultError)
      case .success(let user):
        guard var user = user else {
          let error = IsolationNationError.noRecordReturnedFromAPI
          userPostcodeState = .errored(error)
          return
        }

        // 2
        user.postcode = postcode
        // 3 (Replace me later)
        _ = Amplify.API.mutate(request: .update(user)) { event in
          // 4
          DispatchQueue.main.async {
            switch event {
            case .failure(let error):
              logger?
                .logError("Error occurred: \(error.localizedDescription )")
              userPostcodeState = .errored(error)
            case .success(let result):
              switch result {
              case .failure(let resultError):
                logger?.logError(
                  "Error occurred: \(resultError.localizedDescription )")
                userPostcodeState = .errored(resultError)
              case .success(let savedUser):
                // 5
                userPostcodeState = .loaded(savedUser.postcode)
              }
            }
          }
        }
      }
    }
  }
}

Again, this looks like a lot of code. But most of it is just checking whether the requests succeeded and handling errors if not:

  1. You call Amplify.API.query to request the user by ID in the usual manner.
  2. If successful, you modify the fetched user model by setting the postcode to the value entered by the user.
  3. Then you call Amplify.API.mutate to mutate the existing model.
  4. You handle the response. Then you switch to the main thread again and check for failures.
  5. If successful, you set userPostcodeState to the saved value.

Build and run again. When presented with the view to collect the user’s postcode, enter SW1A 1AA and tap Update. After a second, the app will present the Locations screen again with the SW1A thread showing in the list.

Entered postcode

Now type the following into your terminal:

amplify console api

When asked, select GraphQL. The AWS AppSync landing page will open in your browser. Select Data Sources. Click the link for the User table, and then select the Items tab.

AWS AppSync Console

Items in DynamoDB

Select the ID of the user for whom you just added a postcode. Notice that the postcode field now appears in the record.

Viewing the Postcode in the User record in DynamoDB

Open the record for your other user and note how the field is completely absent. This is an important feature of key-value databases like DynamoDB. They allow a flexible schema, which can be very useful for fast iterations of a new app. :]

In this section, you’ve added a GraphQL schema. You used AWS AppSync to generate a back end declaratively from that schema. You also used AppSync to read and write data to the underlying DynamoDB.

Designing a Chat Data Model

So far, you have an app with cloud-based login. It also reads and writes a user record to a cloud-based database. But it’s not very exciting for the user, is it? :]

It’s time to fix that! In the rest of this tutorial, you’ll be designing and building the chat features for your app.

Open schema.graphql in the AmplifyConfig group. Add the following Thread model to the bottom of the file:

# 1
type Thread
  @model
  # 2
  @key(
    fields: ["location"], 
    name: "byLocation", 
    queryField: "ThreadByLocation")
{
  id: ID!
  name: String!
  location: String!
  # 3
  messages: [Message] @connection(
    name: "ThreadMessages", 
    sortField: "createdAt")
  # 4
  associated: [UserThread] @connection(keyName: "byThread", fields: ["id"])
  createdAt: AWSDateTime!
}

Running through the model, this is what you’re doing:

  1. You define a Thread type. You use the @model directive to tell AppSync to create a DynamoDB table for this model.
  2. You add the @key directive, which adds a custom index in the DynamoDB database. In this case, you’re specifying that you want to be able to query for a Thread
  3. You add messages to your Thread model. messages contains an array of Message types. You use the @connection directive to specify a one-to-many connection between a Thread and its Messages. You’ll learn more about this later.
  4. You add an associated field which contains an array of UserThread objects. To support many-to-many connections in AppSync, you need to create a joining model. UserThread is the joining model to support the connection between users and threads.

Next, add the type definition for the Message type:

type Message
  @model
{
  id: ID!
  author: User! @connection(name: "UserMessages")
  body: String!
  thread: Thread @connection(name: "ThreadMessages")
  replies: [Reply] @connection(name: "MessageReplies", sortField: "createdAt")
  createdAt: AWSDateTime!
}

As you might expect, the Message type has a connection to the author, of type User. It also has connections to the Thread and any Replies for that Message. Note that the name for the Thread @connection matches the name provided in the thread type.

Next, add the definition for replies:

type Reply
  @model
{
  id: ID!
  author: User! @connection(name: "UserReplies")
  body: String!
  message: Message @connection(name: "MessageReplies")
  createdAt: AWSDateTime!
}

Nothing new here! This is similar to Message, above.

Now add the model for our UserThread type:

type UserThread
  @model
  # 1
  @key(name: "byUser", fields: ["userThreadUserId", "userThreadThreadId"])
  @key(name: "byThread", fields: ["userThreadThreadId", "userThreadUserId"])
{
  id: ID!
  # 2
  userThreadUserId: ID!
  userThreadThreadId: ID!
  # 3
  user: User! @connection(fields: ["userThreadUserId"])
  thread: Thread! @connection(fields: ["userThreadThreadId"])
  createdAt: AWSDateTime!
}

When creating a many-to-many connection with AppSync, you don’t create the connection on the types directly. Instead, you create a joining model. For your joining model to work, you must provide several things:

  1. You identify a key for each side of the model. The first field in the fields array defines the hash key for this key, and the second field is the sort key.
  2. For each type in the connection, you specify an ID field to hold the join data.
  3. You also provide a field of each type. This field uses the @connection directive to specify that the ID field from above is to be used to connect to the type.

Finally, add the following connections to the User type after the postcode so your users will have access to their data:

threads: [UserThread] @connection(keyName: "byUser", fields: ["id"])
messages: [Message] @connection(name: "UserMessages")
replies: [Reply] @connection(name: "UserReplies")

Build and run. This will take some time, as the Amplify Tools plugin is doing a lot of work:

  • It notices all the new GraphQL types.
  • It generates Swift models for you.
  • And it updates AppSync and DynamoDB in the cloud.

When the build is complete, look at your AmplifyModels group. It now contains model files for all the new types.

New generated models in Swift

Then open the DynamoDB tab in your browser, and confirm that tables also exist for each type.

New tables in DynamoDB

You now have a data model, and it’s reflected both in your code and in the cloud!