DEV Community

Cover image for Posta: Building a Social Blogging Backend with SurrealDB, Nix, and Nushell
Chris Dawkins
Chris Dawkins

Posted on

Posta: Building a Social Blogging Backend with SurrealDB, Nix, and Nushell

This is a quick blog about my experience building a social blogging backend with SurrealDB.

I have a personal project that needs a way of simply storing and organizing text posts with the ability to perform full-text-search on these posts. I decided to dive deeper into SurrealDB and, after the obligatory feature creep, built an open source solution that does all this and more!

What is Posta?

Posta is a social blogging backend built with SurrealDB. The project consists of a SurrealDB schema and uses Nix and Nushell to setup and run tests to assert correct functionality.

Some of the features that Posta offers:

  • User signup/signin
  • Publish/edit posts
  • Leave comments
  • Favorite posts
  • Subscribe to authors/tags
  • Blocking/muting
  • Full-text-search

The Stack

SurrealDB

SurrealDB offers several features that make it a strong candidate for building a database backend:

  • Multi-Model Database: SurrealDB supports multiple data models, including document-based, graph-based, and relational data.
  • Full-Text Indexing: SurrealDB provides full-text indexing capabilities, allowing efficient searching through text content.
  • Advanced Access Permissions: SurrealDB offers customizable access permissions at both the table and row levels.
  • Live Queries and Real-Time Data Sync: SurrealDB supports live queries and real-time data synchronization, allowing users to receive updates as soon as they occur.

You can load posta.surql into the Surrealist app to explore and learn more about Posta.

Nushell

Nushell has built-in testing, http, and data handling, making it a suitable choice for writing tests.

The tests are just scripts that setup the environment, run tests, and then teardown.

Nix

Nix is a package manager and build tool that is useful for bundling everything together in a reproducible way.

A note about contributing to open source

While doing research for these projects I came across some small bugs in another project. I made the fixes and opened some PRs which were accepted.

Programmers can be intimidated or unsure of where to start with open source contributions. You can discover many small bugs or typos while reading documentation or source code. These contributions are small but often times are also meaningful. So, don't be afraid of starting small!

Schema Design

The schema leverages heavy use of graph-based design to allow for bi-directional queries, compositional design, and relationship properties.

Simple authorization is done using the PERMISSIONS clause with more complex authorization handled using EVENT.

Below is an visualization of the schema using Surrealist:

schema

Usage

Here are a handful of code snippets and usages.

New user

curl -X POST -H "Accept: application/json" -d '{
    "ns":"posta",
    "db":"posta",
    "ac":"Author",
    "user":"john.doe",
    "email_address":"john.doe@example.com",
    "pass":"123456"
}' http://localhost:8000/signup
Enter fullscreen mode Exit fullscreen mode

User Signup

Implementation within Posta:

DEFINE ACCESS Author ON DATABASE TYPE RECORD
    SIGNIN (
        SELECT * FROM Author WHERE email IS $email AND crypto::argon2::compare(pass, $pass)
    )
    SIGNUP (
        CREATE Author CONTENT {
            name: $name,
            email_address: $email_address,
            pass: crypto::argon2::generate($pass),
        }
    );
Enter fullscreen mode Exit fullscreen mode

ACCESS

New Post

Using a built-in function to make a new post

fn::Post::new($body, $title, $tags)
Enter fullscreen mode Exit fullscreen mode

Function definition in schema:

DEFINE FUNCTION fn::Post::new($body: string, $title: string, $tags: array<string>) -> object {
    LET $post = CREATE ONLY Post CONTENT {
        body: $body,
        title: $title,
    } RETURN AFTER;

    FOR $tag IN $tags {
        LET $tag = string::lowercase($tag);
        -- Make new `Tag` if `$tag` doesn't exist
        IF (array::is_empty(SELECT * FROM Tag WHERE tag IS $tag)) {
            CREATE ONLY Tag CONTENT { tag: $tag };
        };

        LET $post_id = $post.id;
        LET $tag_id = (SELECT id FROM Tag WHERE tag IS $tag);

        RELATE $post_id->tagged->$tag_id;
    };

    RETURN $post;
} PERMISSIONS FULL;
Enter fullscreen mode Exit fullscreen mode

FUNCTION

Edit Post

UPDATE $post_id MERGE {body: $body}
Enter fullscreen mode Exit fullscreen mode

EVENT that checks for authorization and creates a diff object when a post is edited.

DEFINE EVENT post_edited ON TABLE Post WHEN $event IS "UPDATE" THEN {
    IF array::is_empty(SELECT id FROM published WHERE in IS $auth.id AND out IS $value.id) {
        THROW "Cannot edit unowned resource";
    };

    RELATE ($auth.id)->edited->($value.id) CONTENT {
        diff_ops: value::diff($before, $after)
    };
};
Enter fullscreen mode Exit fullscreen mode

New Comment

Using SurrealQL to make a comment

RELATE ($auth.id)->comment->$post CONTENT { message: $message }
Enter fullscreen mode Exit fullscreen mode

Text Search

Searching titles

SELECT * FROM Post WHERE title @@ 'SurrealDB';
Enter fullscreen mode Exit fullscreen mode

Rank-searching for Post content

SELECT search::score(1) AS score FROM Post
    WHERE body @1@ 'graph-based database'
    ORDER BY score DESC;
Enter fullscreen mode Exit fullscreen mode

Search functions

Testing

Testing is done using Nushell to setup the environment, push queries over http, and assert conditions.

Here is a test function that tests comment posting:

@test
def comment_post [] {
    let database = surrealdb_setup

    let authors = $database.bind | make_random_authors 2 | each {|it| $it | insert bind ($database | get bind)}

    let post = make_new_post $authors.0 | get result
    let message = random chars

    let comment = {
        query: "RELATE ($auth.id)->comment->$post CONTENT { message: $message };"
        args: {
            post: ($post | get id)
            message: $message
        }}
        | send_query $authors.1
        | first

    assert equal $comment.status "OK"

    let comments = {
        query: "SELECT * FROM comment"
        args: {}}
        | send_query $authors.1
        | first

    assert equal $comments.status "OK"
    assert length $comments.result 1
    assert equal ($comments | get result | first | get message) $message

    surrealdb_teardown
}
Enter fullscreen mode Exit fullscreen mode

These tests get wrapped with nix for CI and other uses.

checks = {
  posta-tests = with pkgs;
    stdenv.mkDerivation {
      inherit system;
      name = "posta tests";
      src = ./.;
      buildInputs = [
        nushell
        surrealdb
      ];
      buildPhase = ''
        ${nushell}/bin/nu \
          --no-config-file \
          ./tests/mod.nu
      '';
      installPhase = ''
        touch $out
      '';
    };
};
Enter fullscreen mode Exit fullscreen mode

Conclusion

Don't be afraid of writing your own code and your own bespoke solutions instead of just prompting or using a pre-made solution. You can discover new projects, learn new techniques, gain valuable experience, and meet and collaborate with new people. It's fun to build new things and try new technologies!

Top comments (0)