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:
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
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),
}
);
New Post
Using a built-in function to make a new post
fn::Post::new($body, $title, $tags)
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;
Edit Post
UPDATE $post_id MERGE {body: $body}
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)
};
};
New Comment
Using SurrealQL
to make a comment
RELATE ($auth.id)->comment->$post CONTENT { message: $message }
Text Search
Searching titles
SELECT * FROM Post WHERE title @@ 'SurrealDB';
Rank-searching for Post content
SELECT search::score(1) AS score FROM Post
WHERE body @1@ 'graph-based database'
ORDER BY score DESC;
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
}
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
'';
};
};
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)