loading...

Chapter 2: Let's get into Fauna: a guide to understand Fauna while creating a social media database

sertge profile image Sergio Andrés Jaime Sierra ・14 min read

In the first chapter of this series, we had the chance to walk through the Fauna and Next.js tutorials and started a simple website with a basic login mechanic. In this chapter, we are going to create the functions within Fauna to sign up new users, create following relationships between users, create posts, like them and comment them, we have our first approach to Fauna’s permissions system by granting users permissions to execute the functions mentioned.

Create new users

Last time, we did register our user using the Fauna dashboard. This is an impractical approach because we want our site to be autonomous and that transaction should be done from the user interface in our website. This is what the API called signup does.

What does this API do?

This API imports the query commands from fauna

1 import { query as q } from 'faunadb'
Enter fullscreen mode Exit fullscreen mode

And then, uses the Create command to create a new document on the Users collection,

16  user = await serverClient.query(
17    q.Create(q.Collection('User'), { //errata: our collection is called Users 
18      credentials: { password },
19      data: { email },
20    })
21  )
Enter fullscreen mode Exit fullscreen mode

As you can see, it’s very similar to the way we created our user with the Fauna’s dashboard. The main difference here is that we need to prepend every command with q. so it’s recognized as a Fauna’s method.

After creating the user, the API logs the user and returns the login token

31  const loginRes = await serverClient.query(
32    q.Login(user.ref, {
33    password,
34  })
35  )
Enter fullscreen mode Exit fullscreen mode

The secret returned is very similar to the Server Key we created on the first chapter. We need to save it on a cookie so the user keeps it when navigating through the website.

41    const cookieSerialized = serializeFaunaCookie(loginRes.secret)
42
43    res.setHeader('Set-Cookie', cookieSerialized)
Enter fullscreen mode Exit fullscreen mode

As you can see, we needed to call serverClient twice, the first time to create the user and the second time to log into the account. Even if two queries doesn’t look like a lot of calls, the user creation API is a good point to start using User Defined Functions (UDF), so, why don’t we try to make it a single database query to reduce latency on these requests? After this, we can understand how to do the same on larger functions that require an important amount of reads/writes.

Start using User Defined Functions (UDF) to improve your site's performance

Fauna allows you to create your own functions using the FQL methods described here, don’t panic, you don’t have to learn them all just yet. Most of the time we will be using Match, Paginate, Select and Get methods to get all the documents we require. Let’s now navigate to Fauna and create some simple functions.

create new function on Fauna, part 1

Let’s open our database from the Fauna’s dashboard, open the functions menu and click on New Function. Usually Fauna gives you a short example function’s body by default, it goes:

Query(
  Lambda(
    "x", Add(Var("x"), Var("x"))
  )
)
Enter fullscreen mode Exit fullscreen mode

Let’s explain it by parts:

  • Query: it’s only parameter is a lambda function and its purpose is to prevent the lambda function for immediate execution. It encases the function definition.
  • Lambda: this method has two parameters, the first one is the set of arguments the function can get (in this case, x), and the second one is the lambda function, which means the commands we will execute. All argument names should be strings, also, if you need to use more than one parameter, you should put all names in an array (e.g [“x”,”y”]).
  • Add: In the example code provided by Fauna, they use Add as the only method used, this returns the arithmetic sum of all the arguments. However, we will change this part to login the user.
  • Var: Everytime we make a reference to an existing variable, we need to call the method Var and put as argument the name of the variable as a string.

Ok, the functional part is Lambda, so let’s transform the default function’s body and make it a function for creating users.

Query(
  Lambda(
    ["email","password"],
    Let(
      {
        user:Create( //create the user
          Collection("Users"),
          { 
            credentials: { password: Var("password") },
            data: {
              email: Var("email"),
              posts: 0,
              activeSince: Now()
            }
        }),
        userRef: Select(
          "ref",
          Var("user")
        ),
      },
      Login(Var("userRef"), {
        password: Var("password"),
        data: {
          message: "first Login"
        }
      })
    )
  )
)
Enter fullscreen mode Exit fullscreen mode

This time, I changed the parameters in the Lambda function to show you how to put more than one variable. In this case, email is the user’s email and password is the user’s password.
The method Let allows you to create an object with temporal variables (represented as the object’s fields) and use them in the second argument by calling the method Var. We create a field named user and define it as the response for creating a new user on the Users collection with the data provided and some additional fields (for reference). The response of Create is the created document.

We also create a field called userRef in which we Select the field ref of our newly created user (this is equivalent to using user.ref in javascript). After defining our binding variables, we set the second parameter of Let to Login the user, this means, the Let method will return the result of Login.
When you login a user, you can provide additional data, we did put a field called message and put the string first login. You can be more creative and include relevant information for metrics like user’s IP, language, web browser, etc.

Let's name our function as signupUser and save it.

Next step: call this function from the signup API on our local repository.
If you don't have this repository yet, you can create it with the _create-next-app. Just run this from your command shell

npx create-next-app --example with-cookie-auth-fauna with-cookie-auth-fauna-app
or
yarn create next-app --example with-cookie-auth-fauna with-cookie-auth-fauna-app

Update Signup API on local app to call new function instead of executing methods

Replace the method in the first serverClientquery to

q.Call(q.Function('signupUser'), [ password , email ])
Enter fullscreen mode Exit fullscreen mode

This function will return the result from Login the user, thus, the second query is unnecessary and you can delete it.

Remove the section using the Query method for Login

When we test it, we should have the same behaviour we had before adding the UDF.

What did we achieve with this? On the API, we reduced the queries to Fauna from 2 to 1, reducing some lines of code. On the database, we reduced the data sent and received by performing both processes on the same query, we didn’t need to receive any information to perform the Login as we used the Let method.
If we have additional processes like adding tasks, roles, historic data, etc. we will have even better performance when we use UDFs.

In this case it doesn’t seem as much, but when we start expanding our database, we will have more efficiency by having UDFs vs performing many database queries.

You can make these functions available in your repository by adding them to a setup file. This way, when you are setting up a similar database for another server, you can recreate the same structure with just a single command. If you already have some experience with Next.js, you can adapt this example from Fauna’s developer team. Otherwise, wait until Chapter 3 when we will summarize all our progress in a setup script for Javascript that we will be able to track on your repository of choice.

Use UDF to follow users and post contents with less connection requests

Setting-up: Following relationships

We have the methods for creating new users, but there’s not a lot we can do with that. It’s time to add following relationships between our users. In this series, we will use a follower/followee relationship in which a user can follow another, this is not necessarily reciprocal.

Use the Fauna's dasboard to create a new collection and name it Followers, leave the other fields with their default values. Create two new indexes for your new collection, name them followers_by_followee and followees_by_follower. We will make the first index return all the followers of an user and the later index will return the people a user is following (followees). Let’s also make a third index called is_followee_of_user, with this, we can find if a user is already following another one and make unique the document related to the following condition.

  1. Create Followers collection:
    Fauna Dashboard: Create a new collection named Followers
    Also, you can do it from the Fauna’s shell using the method CreateCollection:
    CreateCollection({name:”Followers”})

  2. Create Index followers_by_followee:
    Fauna Dashboard: create new index named followers_by_followee
    Create it from the Fauna’s shell using CreateIndex:

CreateIndex({
  name: "followers_by_followee",
  unique: false,
  serialized: true,
  source: Collection("Followers"),
  terms: [
    {
      field: ["data", "followee"]
    }
  ],
  values: [
    {
      field: ["data", "follower"]
    }
  ]
})
Enter fullscreen mode Exit fullscreen mode
  1. Create Index followees_by_follower: Fauna Dashboard: Create new index named followees_by_follower

And with Fauna Shell:

CreateIndex({
  name: "followees_by_follower",
  unique: false,
  serialized: true,
  source: Collection("Followers"),
  terms: [
    {
      field: ["data", "follower"]
    }
  ],
  values: [
    {
      field: ["data", "followee"]
    }
  ]
})
Enter fullscreen mode Exit fullscreen mode
  1. Create the index is_followee_of_user: Fauna Dashboard: Create new index named is_followee_of_user

With this, we prepared our database to handle follower/followee relationships between all users, now, let’s prepare to post content.

Setting-up: User’s posts

Create a new collection and name it Posts. At this point, I think you’ve already got the hang of it.
Create an index for this collection and name it posts_by_owner. This index will have the field owner as term and the value field will be empty.

Create index named posts_by_owner

Now, our database can contain something more than users. Let’s create some functions to follow users and to create posts.

Follow users

CreateFunction({
  name: followUsers
  role: null,
  body: Query(
    Lambda(
      "followee",
      If(
        IsEmpty(
          Match(Index("is_followee_of_user"), [Identity(), Var("followee")])
        ),
        Do(
          Create(Collection("Followers"), {
            data: { follower: Identity(), followee: Var("followee") }
          }),
          { isFollowing: true }
        ),
        Do(
          Delete(
            Select(
              ["data", 0],
              Paginate(
                Match(Index("is_followee_of_user"), [
                  Identity(),
                  Var("followee")
                ])
              )
            )
          ),
          { isFollowing: false }
        )
      )
    )
  )
})
Enter fullscreen mode Exit fullscreen mode

This function toggles the follow/unfollow state of the users. If you already follow a user, you’ll stop following it, if you are not a follower, you’ll become one. Also, this function returns the new following status as true or false.

Create Post

CreateFunction({
  name: "createPost",
  role: null,
  body: Query(
    Lambda(
      "description",
      Create(Collection("Posts"), {
        data: {
          description: Var("description"),
          date: Now(),
          owner: Identity(),
          likes: 0,
          comments: 0
        }
      })
    )
  )
})
Enter fullscreen mode Exit fullscreen mode

With this function, you can create a new post and put initial values like the date it was posted as well as set the amount of likes and comments to 0.

List Users

CreateFunction({
  name: "listUsers",
  role: null,
  body: Query(
    Lambda(
      "cursor",
      Map(
        Paginate(Reverse(Documents(Collection("Users"))), {
          after: Var("cursor")
        }),
        Lambda("ref", {
          userId: Select("id", Var("ref")),
          isFollowee: IsNonEmpty(
            Match(Index("is_followee_of_user"), [Identity(), Var("ref")])
          ),
        isSelf: Equals(Identity(), Var("ref"))
        })
      )
    )
  )
})
Enter fullscreen mode Exit fullscreen mode

This function brings all the users, due to the nature of Paginate, every function call will return a page of 64 documents by default, in case we need the next page of 64 users, we can send a cursor variable containing the ref of the last user from the last result. Also, we can change the size of every page as we need. The response will contain a field called data which is an array of objects containing the fields userId (a string with the reference of the user), isFollowee (a boolean stating if you are following this user), and isSelf (a boolean indicating whether this user is you).

We’ve got several functions and indexes, but our users have permissions to none of them, all they can do is get their own user id. Let’s use the Fauna dashboard and the hints they provide to help us set the permissions for everyone.

First, let’s get to the manage roles section:
Fauna Dashboard: Create a new role under the Security / manage roles option

Click on new custom role and name it basicUser, then start adding the collections and functions, add everything except the index called users_by_email and the function called signupUser.

Fauna Dashboard: Selecting a collection to add permits

Fauna’s ABAC (Atribute-Based Access Control) will grant the documents of a collection all the permits that you grant. An authenticated document (in this case user) can have one or more roles, if any role grants permission to perform a certain action, the action will be performed when required.

After you finish adding your collections and indexes, you should see this:

Fauna Dashboard: permissions view after adding all required indexes and collections

Each row represents a collection, index or function. Each column stands for an action.
+Over the Collections your actions are Read / Write(update) / Create / Delete / Read History / Write on History / Unrestricted (do all)
+Over the indexes, you can Read / Unrestricted access (read all index's records, even for documents you can’t directly access)
+You can Call functions

Now, let’s grant the permissions for these items, click on the red X to turn it into a green checkmark on the next items:
+Read permissions for collections Users, Followers, Posts.
+Create permissions on Followers and Posts (we will change that later).
+Delete permissions on Followers.
+Read permissions on all indexes.
+Call permissions on all functions.

Finally, let’s click on the Membership tab on the upper side of the window to define who will have the permissions we’ve just set.

Select the Users collection as the users will be the ones with these permissions, now you can click on the new Row with the name Users to set a predicate function. In this case, all users will have permissions, so let’s just make the function return true all the time.

Fauna Dashboard: Predicate function for the Users collection, make it return true

It’s done. Click save and we are ready to test our new functions using Next.js

For this, let’s clone this repository
https://github.com/Sertge/fauna-example

In this repository, we updated the example from Fauna in this address
https://github.com/vercel/next.js/tree/canary/examples/with-cookie-auth-fauna

To include the APIs that will call the functions we’ve just created. When you’re done cloning, add your Fauna’s server key to the environment variable, input the command npm install or yarn, and then npm run dev or yarn dev to run your local server.

When you clone this repository and run it, you should see some additional options in your header

New navigation bar on project, it contains new options: Users, New Post, Feed

From Signup, create some new users and click on the Users option from the header.
Here, you can see a list of all users on the database and will have the option to follow/unfollow them, also, you can follow yourself.

Navigation: Users tab shows the users

And finally, you can add some posts and see them on the database from the tab called New post

Navigation: Add a post

Fauna Dashboard: See the new post from the Posts collection

Great, now we can create some follower/followee relationships, see which users we are following as well as the ones we aren’t and we can create some posts, all of this by using Fauna’s UDF. In the next section, we will harvest even more power from Fauna.

Interact with your followers, have them like and comment your posts

On a social network, we want to have our wall/feed populated with the users we follow and keep up to date with them. Also, we want to let them know we admire their work by liking or commenting on their posts. It’s time to add a collection called Likes and another one called Comments as well as create the functions to post comments, toggle the like/unlike status on a post, and finally, grant permissions to our users so they can do all of this.

+Use the dashboard to create a new collection and name it Likes or use CreateCollection({name:’Likes’}) on the Shell.
+Create a new collection and name it Comments.
+Create a new Index for the collection Likes and name it likes_by_post_id, use the field postId as search term.

Fauna Dashboard: Creating the index likes_by_post_id

You can also run the command on the shell

CreateIndex({
  name: "likes_by_post_id",
  unique: false,
  serialized: true,
  source: Collection("Likes"),
  terms: [
    {
      field: ["data", "postId"]
    }
  ]
})
Enter fullscreen mode Exit fullscreen mode

+Create another index for the Comments collection and name it comments_by_post_id, it's the same as the previous step, just change the collection and the name.
+Create an index for the Likes collection called is_liked_by_user and use the owner and the postId as terms. Make it unique by marking the Unique field, so a user can only like a post once.

Fauna Dashboard: a new index called is_liked_by_user

Let’s also add a function to post comments and name it postComment

CreateFunction({
  "name": "postComment",
  "role": null,
  "body": Query(
    Lambda(
      ["postId", "description"],
      Create(Collection("Comments"), {
        data: {
          owner: Identity(),
          date: Now(),
          likes: 0,
          description: Var("description"),
          postId: Var("postId")
        }
      })
    )
  )
})
Enter fullscreen mode Exit fullscreen mode

A function to toggle the like/unlike status on the post

CreateFunction({
  name: "toggleLike",
  role: null,
  body: Query(
    Lambda(
      "postId",
      If(
        IsEmpty(Match(Index("is_liked_by_user"), [Identity(), Var("postId")])),
        Do(
          Create(Collection("Likes"), {
            data: { owner: Identity(), postId: Var("postId"), date: Now() }
          }),
          { isFollowing: true }
        ),
        Do(
          Delete(
            Select(
              ["data", 0],
              Paginate(
                Match(Index("is_liked_by_user"), [Identity(), Var("postId")])
              )
            )
          ),
          { isFollowing: false }
        )
      )
    )
  )
})
Enter fullscreen mode Exit fullscreen mode

A function to get the posts of all the users you follow (feed):

CreateFunction({
  name: "getFeed",
  role: null,
  body: Query(
    Lambda(
      "cursor",
      Map(
        Paginate(
          Reverse(
            Join(
              Match(Index("followees_by_follower"), Identity()),
              Index("posts_by_owner")
            )
          )
        ),
        Lambda(
          "post",
          Merge(Select("data", Get(Var("post"))), {
            isLiked: IsNonEmpty(
              Match(Index("is_liked_by_user"), [Identity(), Var("post")])
            ),
            postId: Var("post"),
            userIsOwner: Equals(
              Identity(),
              Select(["data", "owner"], Get(Var("post")))
            )
          })
        )
      )
    )
  )
})
Enter fullscreen mode Exit fullscreen mode

And finally, a function to get the comments from a post:

CreateFunction({
  name: "getComments",
  role: null,
  body: Query(
    Lambda(
      "postId",
      Map(
        Paginate(Match(Index("comments_by_post_id"), Var("postId"))),
        Lambda(
          "comment",
          Merge(Select("data", Var("comment")), {
            isLiked: IsNonEmpty(
              Match(Index("is_liked_by_user"), [Identity(), Var("comment")])
            ),
            commentId: Var("comment"),
            userIsOwner: Equals(
              Identity(),
              Select(["data", "owner"], Get(Var("comment")))
            )
          })
        )
      )
    )
  )
})
Enter fullscreen mode Exit fullscreen mode

We’re almost there, it’s time to add the permissions so our regular users are able to use these collections, indexes and functions. Add the following permissions:

+Likes: Read, Create, Delete.
+Comments: Read, Create
+All new indexes: Read
+All new functions: Call

Now, you can head to the feed and get all the posts your followees have posted:

Navigation: Feed page with some posts

Click on any post to open it and see all the comments:
Navigation: A post with its comments, it includes the user's Id

Now, we have some of the functions a small social network requires to work. In the next chapter, we will display more information, add the stories, chats, and use Fauna's ABAC to limit the user’s actions, giving us some control within our application. Also, we will learn how to create ABAC predicate functions and debug them.

Discussion

pic
Editor guide