We've written the HTTP layer of the application; what remains is to implement the storage layer and the basic logic for the microblogging site. We'll give you some pointers and teach you how to interact with the database, but so long as the POSTs and GETs to your service return the expected results, the design of the schema and the service implementation is up to you.

📚 If you haven't read our tutorial for how to construct a relational database schema using BTrees, you should check that out before starting this exercise.

Schema design best practices and tips

Because we've started with the API layer, we already know the queries we'll be making and we can work backwards to design the database schema. You should write a few types that your database will store and retrieve:

type db.UserRow =
type db.PostRow =
type db.FollowingRow =

{- Plus any other types you need to
  represent IDs, the microblog domain, etc. -}

Questions you might consider in this process:

  • How much data normalization is preferable for your app? Should your keys be randomly generated IDs that reference the data in another table, or should an entire data blob be stored directly in the table?
  • How can you ensure the uniqueness of a given key in your BTree table? A random ID? Timestamps? Etc?
  • Are secondary sorts and filters ok to do in memory, or should you store the data in a way that makes these operations more efficient?
  • What database operations will require keeping multiple tables in sync? Cloud databases support transactions via the Transaction ability, but they have size constraints and too many transactions can lock down your tables.

Many of the tables will likely be BTree's—sorted key-value tables that support tuple key types and querying by ranges. As you're designing your schema consider leveraging both of those features. For example, if we needed to get a user's most recent posts, rather than storing all the user's posts in a list, we could store the posts in a BTree keyed by the User and the timestamp of the post. This way, we can query the table via the User prefix, to get all the posts in a sorted order:

userPostsTable: BTree (User, OffsetDateTime) Post
userPostsTable = Btree.named database "userPosts" Universal.ordering

getUserPosts =
  rangeClosed.prefix userPostsTable BTree.prefixOrdering target User targetUser
    |> Stream.toList

📝 Instructions

Once you've thought about the set of tables needed to support the app, update your deploy function to create the Database and tables.

cloud-start/main> edit ex3_microblog.deploy

Remember to Database.assign the database to the service environment after creating it!

Transactions and other abilities in database interactions

Once you've settled on your data types and schema, you can get started writing database interactions, let's look at an example that creates a user in the database. In this example, we've defined a UserRow type to represent a user in the database and have two tables which need to be kept in sync: one which links a UserHandle to a UserId and another which stores the UserRow data by its UserId.

db.createUser :
  db.Storage
  -> UserHandle
  -> Text
  -> Optional URI
  ->{Exception, State, Remote, Random} UserRow
db.createUser storage userHandle name avatar =
  use BTree.write tx
  userId = UserId (Remote.randomBytes 256 |> Bytes.toHex)
  timeStamp = !InstantToOffsetDateTime
  userRow = UserRow userId timeStamp timeStamp userHandle name avatar
  transact.random (db.Storage.database storage) do
    tx (usersTable storage) userId userRow
    tx (userHandleIdTable storage) userHandle userId
  userRow

We're using a custom Storage type to hold the tables and the database. (It helps keep our argument list cleaner when we have multiple tables to interact with.)

Cloud services don't have access to arbitrary IO, so generating timestamps or random ID's will require using the Remote ability. We're using now! and Remote.randomBytes instead of IO.

The db.createUser function also uses the Transaction ability to perform multiple operations that should fail or succeed together. Most functions defined for updating records in a BTree have a transactional variant, suffixed with .tx.

Because we're writing to a BTree, the transaction also requires the Random ability. Depending on what abilities your combined database operations require, run the transaction in one of the following:

transact        : Database
                  -> '{Transaction, Exception} a
                  ->{Exception, State} a
transact.random : Database
                  -> '{Transaction, Exception, Random} a
                  ->{Exception, State, Random} a

📝 Instructions

Write the remaining database functions for the microblogging site and call them inside your earlier service route functions to connect the API layer to the database layer.

Your service routes should do the following:

  • They should run the necessary database operations to create, read, update, and delete data.
  • They should translate the types returned from the database layer into the types expected by the API layer.
  • They should represent various errors from the database layer in terms of HTTP status codes.

We won't test for all of the possible workflows in your app, but we will check if your service can create a new user, have that user create a post, and then retrieve that post by its ID. We'll also check if your app allows a user to folllow another user, so that they can see a microblog feed of interesting posts.

When you're ready, update your codebase and submit your solution.

cloud-start/main> run submit.ex3_microblog.roundTrip