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 OrderedTables, 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 OrderedTable 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 OrderedTable'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 OrderedTable 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: OrderedTable (User, OffsetDateTime) Post
userPostsTable = OrderedTable.named database "userPosts" Universal.ordering
getUserPosts =
rangeClosed.prefix userPostsTable OrderedTable.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, Storage, Remote, Random} UserRow
db.createUser storage userHandle name avatar =
use OrderedTable.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 OrderedTable have a transactional variant, suffixed with .tx
.
Because we're writing to a OrderedTable, 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, Storage} a
transact.random : Database
-> '{Transaction, Exception, Random} a
->{Exception, Storage, 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