mirror of
https://github.com/fluencelabs/fluid
synced 2025-06-19 08:11:21 +00:00
rust-workshop => backend-rust
This commit is contained in:
@ -3,7 +3,7 @@ import {query} from "../node_modules/db-connector/assembly/sqlite"
|
|||||||
|
|
||||||
// main handler for an application
|
// main handler for an application
|
||||||
export function handler(username: string): string {
|
export function handler(username: string): string {
|
||||||
// Create table for messages storage
|
// Create table for messages storage
|
||||||
query(`CREATE TABLE messages(msg text, username text)`);
|
query(`CREATE TABLE messages(msg text, username text)`);
|
||||||
|
|
||||||
// Insert message 'Hello, username!' using `username` as author's username
|
// Insert message 'Hello, username!' using `username` as author's username
|
||||||
|
@ -1,16 +1,31 @@
|
|||||||
# How to run
|
# Fluid – decentralized twitter-like feed built on Fluence with SQLite
|
||||||
```shell
|
## Workshop
|
||||||
|
### Step 0 – Framework
|
||||||
|
Here you can see the most basic Fluence app – just a hello-world.
|
||||||
|
|
||||||
# Download SQLite WASM module
|
Take a note of `#invocation_handler` macro, it marks function as an entry-point to your app.
|
||||||
mkdir wasm
|
|
||||||
wget https://github.com/fluencelabs/sqlite/releases/download/v0.2.0_w/sqlite3_0.2.0.wasm -O ./wasm/sqlite3_0.2.0.wasm
|
|
||||||
|
|
||||||
# Build fluid WASM module
|
### Step 1 – JSON API
|
||||||
cargo +nightly build --target wasm32-unknown-unknown --release
|
Now let's define some API for our feed backend. Let's use JSON as a communication format, and serde_json Rust library for handy serialization and deserialization.
|
||||||
cp target/wasm32-unknown-unknown/release/*.wasm ./wasm/
|
|
||||||
|
|
||||||
# Run it all on 30000 port with default Fluence API
|
Two enums – `Request` and `Response` in [api.rs](step1-json-api/src/api.rs) describe API for interaction with frontend.
|
||||||
docker run -it --rm -v $(pwd)/wasm:/code -p 30000:30000 fluencelabs/frun:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
Take a look at how to use frun [here](https://fluence.dev/docs/debugging) and look up HTTP API [here](https://fluence.dev/reference)
|
Method `parse` parses `Request` from JSON, and `serialize` converts `Response` back to JSON string, so it can be returned to frontend.
|
||||||
|
|
||||||
|
Here we also define [model.rs](step1-json-api/src/model.rs) – that's how we plan to use our database. Currently it is just a prototype, so we can implement our API in it's full.
|
||||||
|
|
||||||
|
### Step 2 – Database
|
||||||
|
Let's forget for a moment about API, and dive into using SQLite module.
|
||||||
|
|
||||||
|
- [ffi.rs](step2-database-only/src/ffi.rs) describes inter-module communication between Fluid and SQLite
|
||||||
|
- [databse.rs](step2-database-only/src/database.rs) provides us with `query` function that executes SQL query on SQLite module, and returns result as a String
|
||||||
|
|
||||||
|
Now, take a look at [lib.rs](step2-database-only/src/lib.rs) – a few SQL requests are sent there.
|
||||||
|
|
||||||
|
First one creates the table for messages, next goes message insertion, emulating a user who's creating a post. Then, we select all messages and log them.
|
||||||
|
|
||||||
|
But we decided to use JSON as a communication format, and SQLite allows us to query results in json. So the last query does exactly that – it is the same as usual `SELECT *`, but wraps result in json array.
|
||||||
|
|
||||||
|
### Step 3 – Tying everything together
|
||||||
|
Now let's use our SQLite knowledge to implement [model.rs](step3-finished-app/src/model.rs) so our API can really persists and read data. And voilà, we have our backend for almost twitter, but it can be easily decentralized!
|
||||||
|
|
@ -7,7 +7,7 @@ use crate::errors::err_msg;
|
|||||||
pub fn create_scheme() -> AppResult<()> {
|
pub fn create_scheme() -> AppResult<()> {
|
||||||
database::query("CREATE TABLE messages(msg text, username text)".to_string())
|
database::query("CREATE TABLE messages(msg text, username text)".to_string())
|
||||||
.map_err(|e| err_msg(&format!("Error creating table messages: {}", e)))
|
.map_err(|e| err_msg(&format!("Error creating table messages: {}", e)))
|
||||||
.map(|_| ())
|
.map(drop)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_post(msg: String, username: String) -> AppResult<()> {
|
pub fn add_post(msg: String, username: String) -> AppResult<()> {
|
||||||
@ -21,7 +21,7 @@ pub fn add_post(msg: String, username: String) -> AppResult<()> {
|
|||||||
msg, username, e
|
msg, username, e
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.map(|_| ())
|
.map(drop)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_all_posts() -> AppResult<String> {
|
pub fn get_all_posts() -> AppResult<String> {
|
@ -1,31 +0,0 @@
|
|||||||
# Fluid – decentralized twitter-like feed built on Fluence with SQLite
|
|
||||||
## Workshop
|
|
||||||
### Step 0 – Framework
|
|
||||||
Here you can see the most basic Fluence app – just a hello-world.
|
|
||||||
|
|
||||||
Take a note of `#invocation_handler` macro, it marks function as an entry-point to your app.
|
|
||||||
|
|
||||||
### Step 1 – JSON API
|
|
||||||
Now let's define some API for our feed backend. Let's use JSON as a communication format, and serde_json Rust library for handy serialization and deserialization.
|
|
||||||
|
|
||||||
Two enums – `Request` and `Response` in [api.rs](step1-json-api/src/api.rs) describe API for interaction with frontend.
|
|
||||||
|
|
||||||
Method `parse` parses `Request` from JSON, and `serialize` converts `Response` back to JSON string, so it can be returned to frontend.
|
|
||||||
|
|
||||||
Here we also define [model.rs](step1-json-api/src/model.rs) – that's how we plan to use our database. Currently it is just a prototype, so we can implement our API in it's full.
|
|
||||||
|
|
||||||
### Step 2 – Database
|
|
||||||
Let's forget for a moment about API, and dive into using SQLite module.
|
|
||||||
|
|
||||||
- [ffi.rs](step2-database-only/src/ffi.rs) describes inter-module communication between Fluid and SQLite
|
|
||||||
- [databse.rs](step2-database-only/src/database.rs) provides us with `query` function that executes SQL query on SQLite module, and returns result as a String
|
|
||||||
|
|
||||||
Now, take a look at [lib.rs](step2-database-only/src/lib.rs) – a few SQL requests are sent there.
|
|
||||||
|
|
||||||
First one creates the table for messages, next goes message insertion, emulating a user who's creating a post. Then, we select all messages and log them.
|
|
||||||
|
|
||||||
But we decided to use JSON as a communication format, and SQLite allows us to query results in json. So the last query does exactly that – it is the same as usual `SELECT *`, but wraps result in json array.
|
|
||||||
|
|
||||||
### Step 3 – Tying everything together
|
|
||||||
Now let's use our SQLite knowledge to implement [model.rs](step3-finished-app/src/model.rs) so our API can really persists and read data. And voilà, we have our backend for almost twitter, but it can be easily decentralized!
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "fluid"
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = ["Fluence Labs"]
|
|
||||||
publish = false
|
|
||||||
description = "Decentralized feed built on Fluence"
|
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "fluid"
|
|
||||||
path = "src/lib.rs"
|
|
||||||
crate-type = ["cdylib"]
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
debug = false
|
|
||||||
lto = true
|
|
||||||
opt-level = "z"
|
|
||||||
panic = "abort"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
serde = { version = "=1.0.88", features = ["derive"] }
|
|
||||||
serde_json = { version = "=1.0.38", features = ["raw_value"] }
|
|
||||||
log = "0.4"
|
|
||||||
fluence = { version = "0.1.6", features = ["wasm_logger"] }
|
|
@ -1,30 +0,0 @@
|
|||||||
use crate::errors::{err_msg, Error};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_json::value::RawValue;
|
|
||||||
|
|
||||||
pub type AppResult<T> = ::std::result::Result<T, Box<Error>>;
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(tag = "action")]
|
|
||||||
pub enum Request {
|
|
||||||
Post { message: String, username: String },
|
|
||||||
Fetch { username: Option<String> },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum Response {
|
|
||||||
Post { count: i32 },
|
|
||||||
Fetch { posts: Box<RawValue> },
|
|
||||||
Error { error: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse(s: String) -> AppResult<Request> {
|
|
||||||
serde_json::from_str(s.as_str())
|
|
||||||
.map_err(|e| err_msg(&format!("unable to parse json request from {}: {}", s, e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn serialize(response: &Response) -> String {
|
|
||||||
serde_json::to_string(response)
|
|
||||||
.unwrap_or(format!("Unable to serialize response {:?}", response))
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
use log;
|
|
||||||
|
|
||||||
use crate::api::AppResult;
|
|
||||||
use crate::errors::err_msg;
|
|
||||||
use crate::ffi;
|
|
||||||
|
|
||||||
// Execute query on SQLite
|
|
||||||
pub fn query(query: String) -> AppResult<String> {
|
|
||||||
log::debug!("executing query: '{}'", query);
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
// Convert query string to bytes
|
|
||||||
let query_bytes = query.as_bytes();
|
|
||||||
// Allocate memory for query in SQLite module
|
|
||||||
let query_ptr = ffi::allocate(query_bytes.len());
|
|
||||||
|
|
||||||
// Store query in SQLite's memory
|
|
||||||
for (i, byte) in query_bytes.iter().enumerate() {
|
|
||||||
let ptr = query_ptr + i as i32;
|
|
||||||
ffi::store(ptr, *byte);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute the query, and get pointer to the result
|
|
||||||
let result_ptr = ffi::invoke(query_ptr, query_bytes.len());
|
|
||||||
|
|
||||||
// First 4 bytes at result_ptr location encode result size, read that first
|
|
||||||
let mut result_size: usize = 0;
|
|
||||||
for i in 0..3 {
|
|
||||||
let ptr = result_ptr + i as i32;
|
|
||||||
let b = ffi::load(ptr) as usize;
|
|
||||||
result_size = result_size + (b << (8 * i));
|
|
||||||
}
|
|
||||||
// Now we know exact size of the query execution result
|
|
||||||
|
|
||||||
// Read query execution result byte-by-byte
|
|
||||||
let mut result_bytes = vec![0; result_size as usize];
|
|
||||||
for i in 4..(result_size + 4) {
|
|
||||||
let ptr = result_ptr + i as i32;
|
|
||||||
let b = ffi::load(ptr);
|
|
||||||
result_bytes[i as usize - 4] = b;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deallocate query result
|
|
||||||
ffi::deallocate(result_ptr, result_size + 4);
|
|
||||||
|
|
||||||
// Decode query result to a utf8 string
|
|
||||||
let result_str = std::str::from_utf8(result_bytes.as_slice());
|
|
||||||
|
|
||||||
// Log if there's an error
|
|
||||||
if result_str.is_err() {
|
|
||||||
log::error!("unable to decode result from bytes: {:#x?}", result_bytes);
|
|
||||||
}
|
|
||||||
// Wrap error with a better message, and return Result
|
|
||||||
result_str
|
|
||||||
.map_err(|e| {
|
|
||||||
err_msg(&format!(
|
|
||||||
"unable to decode result from bytes {:#x?}: {}",
|
|
||||||
result_bytes, e
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
use std::fmt;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Error(String);
|
|
||||||
|
|
||||||
impl std::error::Error for Error {}
|
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn err_msg(s: &str) -> Box<Error> {
|
|
||||||
Error(s.to_string()).into()
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
// Description of inter-module communication
|
|
||||||
//
|
|
||||||
// Allows fluid module to call methods from sqlite module
|
|
||||||
|
|
||||||
#[link(wasm_import_module = "sqlite")]
|
|
||||||
extern "C" {
|
|
||||||
// Allocate chunk of SQLite memory, and return a pointer to that memory
|
|
||||||
#[link_name = "sqlite_allocate"]
|
|
||||||
pub fn allocate(size: usize) -> i32;
|
|
||||||
|
|
||||||
// Deallocate chunk of memory after it's not used anymore
|
|
||||||
#[link_name = "sqlite_deallocate"]
|
|
||||||
pub fn deallocate(ptr: i32, size: usize);
|
|
||||||
|
|
||||||
// Put 1 byte at ptr location in SQLite memory
|
|
||||||
#[link_name = "sqlite_store"]
|
|
||||||
pub fn store(ptr: i32, byte: u8);
|
|
||||||
|
|
||||||
// Read 1 byte from ptr location of SQLite memory
|
|
||||||
#[link_name = "sqlite_load"]
|
|
||||||
pub fn load(ptr: i32) -> u8;
|
|
||||||
|
|
||||||
// Call SQLite's invocation handler with data specified by pointer and size
|
|
||||||
#[link_name = "sqlite_invoke"]
|
|
||||||
pub fn invoke(ptr: i32, size: usize) -> i32;
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
use fluence::sdk::*;
|
|
||||||
use serde_json::value::RawValue;
|
|
||||||
|
|
||||||
use api::Request;
|
|
||||||
use api::Response;
|
|
||||||
|
|
||||||
use crate::api::AppResult;
|
|
||||||
use crate::errors::err_msg;
|
|
||||||
|
|
||||||
pub mod api;
|
|
||||||
pub mod database;
|
|
||||||
pub mod errors;
|
|
||||||
pub mod ffi;
|
|
||||||
pub mod model;
|
|
||||||
|
|
||||||
fn init() {
|
|
||||||
logger::WasmLogger::init_with_level(log::Level::Info).unwrap();
|
|
||||||
model::create_scheme().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[invocation_handler(init_fn = init)]
|
|
||||||
fn run(arg: String) -> String {
|
|
||||||
// Parse and username JSON request
|
|
||||||
let result = api::parse(arg).and_then(|request| match request {
|
|
||||||
Request::Post { message, username } => add_post(message, username),
|
|
||||||
Request::Fetch { username } => fetch_posts(username),
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = match result {
|
|
||||||
Ok(good) => good,
|
|
||||||
Err(error) => Response::Error {
|
|
||||||
error: error.to_string(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Serialize response to JSON
|
|
||||||
api::serialize(&result)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_post(msg: String, username: String) -> AppResult<Response> {
|
|
||||||
// Store post
|
|
||||||
model::add_post(msg, username)?;
|
|
||||||
// Get total number of posts
|
|
||||||
let count = model::get_posts_count()?;
|
|
||||||
|
|
||||||
Ok(Response::Post { count })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fetch_posts(username: Option<String>) -> AppResult<Response> {
|
|
||||||
let posts_str = match username {
|
|
||||||
// Get all posts if no filter username was passed
|
|
||||||
None => model::get_all_posts()?,
|
|
||||||
// Or get only posts from specified author
|
|
||||||
Some(h) => model::get_posts_by_username(h)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Some Rust-specific detail to play nice with serialization, doesn't matter
|
|
||||||
let raw = RawValue::from_string(posts_str)
|
|
||||||
.map_err(|e| err_msg(&format!("Can't create RawValue: {}", e)))?;
|
|
||||||
|
|
||||||
Ok(Response::Fetch { posts: raw })
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::api::AppResult;
|
|
||||||
use crate::database;
|
|
||||||
use crate::errors::err_msg;
|
|
||||||
|
|
||||||
pub fn create_scheme() -> AppResult<()> {
|
|
||||||
database::query("CREATE TABLE messages(msg text, username text)".to_string())
|
|
||||||
.map_err(|e| err_msg(&format!("Error creating table messages: {}", e)))
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_post(msg: String, username: String) -> AppResult<()> {
|
|
||||||
database::query(format!(
|
|
||||||
r#"INSERT INTO messages VALUES("{}","{}")"#,
|
|
||||||
msg, username
|
|
||||||
))
|
|
||||||
.map_err(|e| {
|
|
||||||
err_msg(&format!(
|
|
||||||
"Error inserting post {} by {}: {}",
|
|
||||||
msg, username, e
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_all_posts() -> AppResult<String> {
|
|
||||||
database::query(
|
|
||||||
"SELECT json_group_array(
|
|
||||||
json_object('msg', msg, 'username', username)
|
|
||||||
) AS json_result FROM (SELECT * FROM messages)"
|
|
||||||
.to_string(),
|
|
||||||
)
|
|
||||||
.map_err(|e| err_msg(&format!("Error retrieving posts: {}", e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_posts_by_username(username: String) -> AppResult<String> {
|
|
||||||
database::query(format!(
|
|
||||||
"SELECT json_group_array(
|
|
||||||
json_object('msg', msg, 'username', username)
|
|
||||||
) AS json_result FROM (SELECT * FROM messages where username = '{}')",
|
|
||||||
username
|
|
||||||
))
|
|
||||||
.map_err(|e| err_msg(&format!("Error retrieving posts: {}", e)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_posts_count() -> AppResult<i32> {
|
|
||||||
let result = database::query("SELECT COUNT(*) from messages".to_string())
|
|
||||||
.map_err(|e| err_msg(&format!("Error retrieving posts count: {}", e)))?;
|
|
||||||
|
|
||||||
i32::from_str(result.as_str()).map_err(|e| {
|
|
||||||
err_msg(&format!(
|
|
||||||
"Can't parse {} to i32 in get_posts_count: {}",
|
|
||||||
result, e
|
|
||||||
))
|
|
||||||
})
|
|
||||||
}
|
|
Reference in New Issue
Block a user