Skip to content

Step 2: Building the backend

Inside the backend, create a folder called src. Move to that folder, and create a new file called main.rs.

Let's add the top use statements:

use axum::{
    extract::{Path, Query, State},
    http::StatusCode,
    http::HeaderMap,
    response::{/*Html,*/ IntoResponse, Json},
    routing::{get, post},
    Router,
};
use golem_base_sdk::{
    entity::{Annotation, Create, Update},

    GolemBaseClient, GolemBaseRoClient, PrivateKeySigner, Url,
};
use golem_base_sdk::rpc::EntityMetaData;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::net::TcpListener;
use tower_http::cors::CorsLayer;
use alloy_primitives::B256;
use bytes::Bytes;
use dirs::config_dir;
use golem_base_sdk::Hash;

Next let's add a constant for later:

const GOLEM_BASE_APP_NAME: &str = "golembase-media_demo_v0.5";

The first represents out app name. You can update the version number as you update the code as necessary.

Now let's build some structures that represent the media types:

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")] // This tells serde to use the 'type' field to distinguish between variants
pub enum MediaItem {
    #[serde(rename = "book")]
    Book(Book),
    #[serde(rename = "movie")]
    Movie(Movie),
    #[serde(rename = "music")]
    Music(Music),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Book {
    pub title: String,
    pub description: String,
    pub author: String,
    pub genre: String,
    pub rating: u64,
    pub owned: bool,
    pub year: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Movie {
    pub title: String,
    pub description: String,
    pub director: String,
    pub genre: String,
    pub rating: u64,
    pub watched: bool,
    pub year: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Music {
    pub title: String,
    pub description: String,
    pub artist: String,
    pub genre: String,
    pub rating: u64,
    pub favorite: bool,
    pub year: u64,
}

These are the "Rust" versions of the data, as distinguished from how they'll actually be stored in the database.

Next, let's build the Searches structure, which is essentially an index for the data:

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Searches {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub entity_key: Option<String>,
    pub directors: Vec<String>,
    pub artists: Vec<String>,
    pub authors: Vec<String>,
    pub movie_genres: Vec<String>,
    pub music_genres: Vec<String>,
    pub book_genres: Vec<String>,
}

And then a map for the media types:

lazy_static::lazy_static! {
    static ref MEDIA_MAP: HashMap<&'static str, (&'static str, &'static str, &'static str)> = {
        let mut m = HashMap::new();
        m.insert("book", ("authors", "book_genres", "author"));
        m.insert("movie", ("directors", "movie_genres", "director"));
        m.insert("music", ("artists", "music_genres", "artist"));
        m
    };
}

Now let's put together an AppState type that maintains state between route calls. This holds the client that we use for accessing the GolemBase node.

struct AppState {
    client: GolemBaseClient,
}

Tip

Considering we only have one member in our AppState, client, we could in theory just pass client throughout the app. However, we're adding it as a member to the AppState in case you want to add on to this app later.

And next let's build a helper function for converting between Hashes stored as strings and B256 types, used internally:

fn parse_b256(thumbid: &str) -> B256 {

    let hex_str = thumbid.strip_prefix("0x")
        .or_else(|| thumbid.strip_prefix("0X"))
        .unwrap_or(thumbid);

    let bytes: [u8; 32] =
        <[u8; 32] as hex::FromHex>::from_hex(hex_str).expect("Invalid hex string for B256");

    B256::from(bytes)
}

Now let's get to the heart of how we're using GolemBase to store our data. Instead of storing the data directly as-is, we're going to convert the data from plain old objects to a golembase-style whereby the individual members are stored as StringAnnotations and NumericAnnotations inside GolemBase. To help us with this, let's build a couple of conversion routines that convert between those two types:

/// Transforms a plain object-like structure into an Annotations struct.
/// This is a simplified version for this specific app's needs.
fn transform_item_to_annotations(item: &MediaItem, indexes: &[&str]) -> (String, Vec<Annotation<String>>, Vec<Annotation<u64>>) {
    let data_as_string = serde_json::to_string(&item).unwrap();
    let mut string_annotations = vec![
        Annotation::new("app", GOLEM_BASE_APP_NAME.to_string()),
        Annotation::new("type", match item {
            MediaItem::Book(_) => "book",
            MediaItem::Movie(_) => "movie",
            MediaItem::Music(_) => "music",
        }.to_string()),
    ];
    let mut numeric_annotations = Vec::new();

    let item_map: HashMap<String, serde_json::Value> = serde_json::from_str(&data_as_string).unwrap();

    for key in indexes {
        if let Some(value) = item_map.get(&key.to_string()) {
            if let Some(num) = value.as_u64() {
                numeric_annotations.push(Annotation::new(key.to_string(), num));
            } else {
                string_annotations.push(Annotation::new(key.to_string(), value.to_string().replace("\"", "")));
            }
        }
    }

    (data_as_string, string_annotations, numeric_annotations)
}

fn transform_annotations_to_searches(metadata: &EntityMetaData, entity_key: String) -> Searches {
    let mut searches = Searches::default();

    for annot in &metadata.string_annotations {
        match annot.key.as_str() {
            "directors" => searches.directors = annot.value.split(',').map(|s| s.trim().to_string()).collect(),
            "artists" => searches.artists = annot.value.split(',').map(|s| s.trim().to_string()).collect(),
            "authors" => searches.authors = annot.value.split(',').map(|s| s.trim().to_string()).collect(),
            "movie_genres" => searches.movie_genres = annot.value.split(',').map(|s| s.trim().to_string()).collect(),
            "music_genres" => searches.music_genres = annot.value.split(',').map(|s| s.trim().to_string()).collect(),
            "book_genres" => searches.book_genres = annot.value.split(',').map(|s| s.trim().to_string()).collect(),
            _ => {}
        }
    }

    searches.entity_key = Some(entity_key);
    searches
}

And with that, let's move on to the next step!

Head to Step 3.