Refactors realworld example with multi DB support

**General**

* Moves `examples/postgres/realworld` to `examples/realworld`
* The app is now architected to support multiple DBs
* Adds feature flags for `sqlite` and `postgres` to allow user to choose
  which backend to use

    *NOTE* Currently it is not possible to compile with `postgres` and `sqlite`
      enabled as we are using the `query!` and `query_as!` macros and they
      seem to get unhappy.

* Adds CLI flags for picking the DB backend to use at runtime
* Adds schema file and implementation for SQLite for `/api/user` routes
* Adds stub routes and trait for articles and Articles entity

**Changes**

* We now use i32 instead of i64 as the user_id to get around some quirks
  w/ the SQLite driver.
* Reimplements existing route handlers w/ an error handling shim so we can use
  Try inside the biz logic
* *FIX* Adds a `user` key to the register user body to conform w/ realworld's
  API specs

APIs were functionally tested using realworld's API test script
  (https://github.com/gothinkster/realworld/tree/master/api#authentication)
This commit is contained in:
Samani G. Gikandi 2020-03-31 14:35:17 -07:00 committed by Ryan Leckey
parent 2fb38dd0c1
commit 7038dd8ab2
18 changed files with 775 additions and 255 deletions

94
Cargo.lock generated
View file

@ -62,7 +62,7 @@ dependencies = [
"native-tls",
"thiserror",
"tokio 0.2.13",
"url",
"url 2.1.1",
]
[[package]]
@ -123,6 +123,17 @@ dependencies = [
"winapi 0.3.8",
]
[[package]]
name = "async-trait"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "991d0a1a3e790c835fd54ab41742a59251338d8c7577fe7d7f0170c7072be708"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "atty"
version = "0.2.14"
@ -352,6 +363,16 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "cookie"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "888604f00b3db336d2af898ec3c1d5d0ddf5e6d462220f2ededc33a87ac4bbd5"
dependencies = [
"time 0.1.42",
"url 1.7.2",
]
[[package]]
name = "core-foundation"
version = "0.7.0"
@ -830,6 +851,17 @@ dependencies = [
"want",
]
[[package]]
name = "idna"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e"
dependencies = [
"matches",
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "idna"
version = "0.2.0"
@ -1735,7 +1767,7 @@ dependencies = [
"sha2",
"time 0.2.9",
"tokio 0.2.13",
"url",
"url 2.1.1",
"uuid",
]
@ -1760,23 +1792,6 @@ dependencies = [
"sqlx",
]
[[package]]
name = "sqlx-example-postgres-realworld"
version = "0.1.0"
dependencies = [
"anyhow",
"async-std",
"chrono",
"env_logger",
"futures 0.3.4",
"jsonwebtoken",
"rand",
"rust-argon2",
"serde",
"sqlx",
"tide",
]
[[package]]
name = "sqlx-example-postgres-todos"
version = "0.1.0"
@ -1789,6 +1804,27 @@ dependencies = [
"structopt",
]
[[package]]
name = "sqlx-example-realworld"
version = "0.1.0"
dependencies = [
"anyhow",
"async-std",
"async-trait",
"chrono",
"env_logger",
"jsonwebtoken",
"log",
"paw",
"rand",
"rust-argon2",
"serde",
"sqlx",
"structopt",
"thiserror",
"tide",
]
[[package]]
name = "sqlx-example-sqlite-todos"
version = "0.1.0"
@ -1815,7 +1851,7 @@ dependencies = [
"sqlx-core",
"syn",
"tokio 0.2.13",
"url",
"url 2.1.1",
]
[[package]]
@ -2015,11 +2051,12 @@ dependencies = [
[[package]]
name = "tide"
version = "0.5.1"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c99b1991db81e611a2614cd1b07fec89ae33c5f755e1f8eb70826fb5af0eea"
checksum = "e619c99048ae107912703d0efeec4ff4fbff704f064e51d3eee614b28ea7b739"
dependencies = [
"async-std",
"cookie",
"futures 0.3.4",
"http",
"http-service",
@ -2325,13 +2362,24 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cd1f4b4e96b46aeb8d4855db4a7a9bd96eeeb5c6a1ab54593328761642ce2f"
[[package]]
name = "url"
version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a"
dependencies = [
"idna 0.1.5",
"matches",
"percent-encoding 1.0.1",
]
[[package]]
name = "url"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb"
dependencies = [
"idna",
"idna 0.2.0",
"matches",
"percent-encoding 2.1.0",
]

View file

@ -7,9 +7,9 @@ members = [
"cargo-sqlx",
"examples/mysql/todos",
"examples/postgres/listen",
"examples/postgres/realworld",
"examples/postgres/todos",
"examples/sqlite/todos",
"examples/realworld",
]
[package]

View file

@ -1,18 +0,0 @@
[package]
name = "sqlx-example-postgres-realworld"
version = "0.1.0"
edition = "2018"
workspace = "../../../"
[dependencies]
anyhow = "1.0.26"
env_logger = "0.7.1"
async-std = { version = "1.4.0", features = [ "attributes" ] }
tide = "0.5.1"
sqlx = { path = "../../../", features = [ "postgres" ] }
serde = { version = "1.0.104", features = [ "derive" ] }
futures = "0.3.1"
rust-argon2 = "0.6.1"
rand = "0.7.2"
jsonwebtoken = "6.0.1"
chrono = "0.4.10"

View file

@ -1,27 +0,0 @@
# Real World SQLx
## Usage
Declare the database URL.
```
export DATABASE_URL="postgres://postgres@localhost/realworld"
```
Create the database.
```
createdb -U postgres realworld
```
Load the database schema.
```
psql -d "$DATABASE_URL" -f ./schema.sql
```
Run.
```
cargo run
```

View file

@ -1,185 +0,0 @@
use chrono::{Duration, Utc};
use rand::{thread_rng, RngCore};
use sqlx::PgPool;
use std::env;
use tide::{Request, Response};
const SECRET_KEY: &str = "this-is-the-most-secret-key-ever-secreted";
// NOTE: Tide 0.5.x does not handle errors so any fallible methods just [.unwrap] for the moment.
// To be clear, that is not recommended and this should be fixed as soon as Tide fixes its
// error handling.
#[async_std::main]
async fn main() -> anyhow::Result<()> {
let pool = PgPool::new(&env::var("DATABASE_URL")?).await?;
let mut server = tide::with_state(pool);
server.at("/api/users").post(register);
server.at("/api/user").get(get_current_user);
server.listen(("localhost", 8080)).await?;
Ok(())
}
// User
// https://github.com/gothinkster/realworld/tree/master/api#users-for-authentication
#[derive(serde::Serialize)]
struct User {
email: String,
token: String,
username: String,
}
// Registration
// https://github.com/gothinkster/realworld/tree/master/api#registration
// #[post("/api/users")]
async fn register(mut req: Request<PgPool>) -> Response {
#[derive(serde::Deserialize)]
struct RegisterRequestBody {
username: String,
email: String,
password: String,
}
let body: RegisterRequestBody = req.body_json().await.unwrap();
let hash = hash_password(&body.password).unwrap();
// Make a new transaction (for giggles)
let pool = req.state();
let mut tx = pool.begin().await.unwrap();
let rec = sqlx::query!(
r#"
INSERT INTO users ( username, email, password )
VALUES ( $1, $2, $3 )
RETURNING id, username, email
"#,
body.username,
body.email,
hash
)
.fetch_one(&mut tx)
.await
.unwrap();
let token = generate_token(rec.id).unwrap();
// Explicitly commit (otherwise this would rollback on drop)
tx.commit().await.unwrap();
#[derive(serde::Serialize)]
struct RegisterResponseBody {
user: User,
}
Response::new(200)
.body_json(&RegisterResponseBody {
user: User {
username: rec.username,
email: rec.email,
token,
},
})
.unwrap()
}
// Get Current User
// https://github.com/gothinkster/realworld/tree/master/api#get-current-user
// #[get("/api/user")]
async fn get_current_user(req: Request<PgPool>) -> Response {
// TODO: Combine these methods? &Request isn't Sync though
let token = get_token_from_request(&req);
let user_id = authorize(&token).await.unwrap();
let pool = req.state();
let rec = sqlx::query!(
r#"
SELECT username, email
FROM users
WHERE id = $1
"#,
user_id
)
.fetch_one(pool)
.await
.unwrap();
#[derive(serde::Serialize)]
struct GetCurrentUserResponseBody {
user: User,
}
Response::new(200)
.body_json(&GetCurrentUserResponseBody {
user: User {
username: rec.username,
email: rec.email,
token,
},
})
.unwrap()
}
fn get_token_from_request(req: &Request<PgPool>) -> String {
req.header("authorization")
.unwrap_or_default()
.splitn(2, ' ')
.nth(1)
.unwrap_or_default()
.to_owned()
}
async fn authorize(token: &str) -> anyhow::Result<i64> {
let data = jsonwebtoken::decode::<TokenClaims>(
token,
SECRET_KEY.as_ref(),
&jsonwebtoken::Validation::default(),
)?;
Ok(data.claims.sub)
}
// TODO: Does this need to be spawned in async-std ?
fn hash_password(password: &str) -> anyhow::Result<String> {
let salt = generate_random_salt();
let hash = argon2::hash_encoded(password.as_bytes(), &salt, &argon2::Config::default())?;
Ok(hash)
}
fn generate_random_salt() -> [u8; 16] {
let mut salt = [0; 16];
thread_rng().fill_bytes(&mut salt);
salt
}
#[derive(serde::Serialize, serde::Deserialize)]
struct TokenClaims {
sub: i64,
exp: i64,
}
fn generate_token(user_id: i64) -> anyhow::Result<String> {
use jsonwebtoken::Header;
let exp = Utc::now() + Duration::hours(1);
let token = jsonwebtoken::encode(
&Header::default(),
&TokenClaims {
sub: user_id,
exp: exp.timestamp(),
},
SECRET_KEY.as_ref(),
)?;
Ok(token)
}

View file

@ -0,0 +1,30 @@
[package]
name = "sqlx-example-realworld"
version = "0.1.0"
authors = ["Samani G. Gikandi <samani@gojulas.com>"]
edition = "2018"
workspace = "../../"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = []
sqlite = ["sqlx/sqlite"]
postgres = ["sqlx/postgres"]
[dependencies]
anyhow = "1.0.28"
async-std = "1.5.0"
chrono = "0.4.11"
env_logger = "0.7.1"
jsonwebtoken = "6.0"
rand = "0.7.3"
rust-argon2 = "0.6.1"
serde = { version = "1.0.105", features = ["derive"] }
sqlx = { path = "../../" }
tide = "0.6.0"
log = "0.4.8"
async-trait = "0.1.27"
thiserror = "1.0.14"
paw = "1.0"
structopt = { version = "0.3", features = ["paw"] }

View file

@ -0,0 +1,53 @@
# Real World SQLx
An implementation of ["The mother of all demo apps"](https://realworld.io/) using SQLx
This application supports both SQLite and PostgreSQL!
## Usage
1. Pick a DB Backend.
```
export DB_TYPE="postgres"
```
2. Declare the database URL.
```
export DATABASE_URL="postgres://postgres@localhost/realworld"
```
3. Create the database.
```
createdb -U postgres realworld
```
4. Load the database schema from the appropriate file in [schema](./schema) directory.
```
psql -d "${DATABASE_URL}" -f ./schema/postgres.sql
```
5. Run!
```
cargo run --features "${DB_TYPE}" -- --db "${DB_TYPE}
```
6. Send some requests!
```
curl --request POST \
--url http://localhost:8080/api/users \
--header 'content-type: application/json' \
--data '{"user":{"email":"sqlx_user@foo.baz", "password":"not_secure", "username":"sqlx_user"}}'
```
```
curl --request POST \
--url http://localhost:8080/api/users/login \
--header 'content-type: application/json' \
--data '{"user":{"email":"sqlx_user@foo.baz", "password":"not_secure"}}'
```

View file

@ -1,5 +1,5 @@
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ,

View file

@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS users (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
created_at INTEGER NOT NULL DEFAULT (STRFTIME('%s', 'now')),
updated_at INTEGER,
email TEXT UNIQUE NOT NULL,
username TEXT UNIQUE NOT NULL,
password TEXT
);

View file

@ -0,0 +1,39 @@
use crate::db::model::ProvideArticle;
use tide::{Request, Response};
struct Article {
title: String,
description: String,
body: String,
// ...etc...
}
/// List Articles
///
/// https://github.com/gothinkster/realworld/tree/master/api#list-articles
pub async fn list_articles(req: Request<impl ProvideArticle>) -> Response {
unimplemented!()
}
/// Get Article
///
/// https://github.com/gothinkster/realworld/tree/master/api#get-article
pub async fn get_article(req: Request<impl ProvideArticle>) -> Response {
unimplemented!()
}
/// Create Article
///
/// https://github.com/gothinkster/realworld/tree/master/api#create-article
pub async fn create_article(req: Request<impl ProvideArticle>) -> Response {
unimplemented!()
}
/// Delete Article
///
/// https://github.com/gothinkster/realworld/tree/master/api#delete-article
///
/// /api/articles/:slug
pub async fn update_article(req: Request<impl ProvideArticle>) -> Response {
unimplemented!()
}

View file

@ -0,0 +1,36 @@
use log::*;
use tide::{IntoResponse, Response};
/// Route handlers for the /api/articles APIs
pub mod articles;
/// Route handlers for the /user(s) APIs
pub mod users;
/// A shim error that enables ergonomic error handling w/ Tide
#[derive(Debug, thiserror::Error)]
pub enum ApiError {
#[error("Status Code {}", .0.status())]
Api(Response),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
type ApiResult<T> = Result<T, ApiError>;
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
match self {
ApiError::Api(r) => r,
ApiError::Other(e) => {
Response::new(500).body_string(format!("Unexpected error -- {}", e))
}
}
}
}
impl From<Response> for ApiError {
fn from(resp: Response) -> Self {
ApiError::Api(resp)
}
}

View file

@ -0,0 +1,253 @@
use chrono::{Duration, Utc};
use log::*;
use rand::{thread_rng, RngCore};
use tide::{Request, Response, IntoResponse};
use super::{ApiResult, ApiError};
use crate::db::model::{ProvideUser, UserEntity};
use std::default::Default;
const SECRET_KEY: &str = "this-is-the-most-secret-key-ever-secreted";
// User
// https://github.com/gothinkster/realworld/tree/master/api#users-for-authentication
#[derive(Default, serde::Serialize)]
pub struct User {
pub email: String,
pub token: Option<String>,
pub username: String,
pub bio: Option<String>,
pub image: Option<String>,
}
// Registration
// https://github.com/gothinkster/realworld/tree/master/api#registration
// #[post("/api/users")]
pub async fn register(req: Request<impl ProvideUser>) -> Response {
async fn inner(mut req: Request<impl ProvideUser>) -> ApiResult<Response> {
#[derive(serde::Deserialize)]
struct RegisterRequestBody {
user: NewUser
}
#[derive(serde::Deserialize)]
struct NewUser {
username: String,
email: String,
password: String,
}
let RegisterRequestBody {user: NewUser {
username, email, password
}} = req.body_json().await
.map_err(|e| ApiError::Api(Response::new(400).body_string(e.to_string())))?;
let hashed_password = hash_password(&password)?;
let db = req.state();
let id = db.create_user(&username, &email, &hashed_password).await?;
// This is not a hard failure, the user should simply try to login
let token = generate_token(id)
.map_err(|e| {
warn!("Failed to create auth token -- {}", e);
e
})
.ok();
#[derive(serde::Serialize)]
struct RegisterResponseBody {
user: User,
}
let resp = Response::new(200)
.body_json(&RegisterResponseBody {
user: User {
email,
token,
username,
..Default::default()
}
})
.map_err(anyhow::Error::from)?;
Ok(resp)
}
inner(req).await.unwrap_or_else(IntoResponse::into_response)
}
// Get Current User
// https://github.com/gothinkster/realworld/tree/master/api#get-current-user
// #[get("/api/user")]
pub async fn get_current_user(req: Request<impl ProvideUser>) -> Response {
async fn inner(req: Request<impl ProvideUser>) -> ApiResult<Response> {
// FIXME(sgg): Replace this with an auth middleware?
let auth_header = req.header("authorization")
.ok_or_else(|| {
ApiError::Api(Response::new(400).body_string("Missing Authorization header".to_owned()))
})?;
let token = get_token_from_request(auth_header);
let user_id = authorize(&token).await
.map_err(|e| ApiError::Api(Response::new(403).body_string(format!("{}", e))))?;
debug!("Token is authorized to user {}", user_id);
let db = req.state();
let UserEntity { email, username, .. } = db.get_user_by_id(user_id).await?;
#[derive(serde::Serialize)]
struct GetCurrentUserResponseBody {
user: User,
}
let resp = Response::new(200)
.body_json(&GetCurrentUserResponseBody {
user: User {
email,
token: Some(token.to_owned()),
username,
..Default::default()
},
})
.map_err(anyhow::Error::from)?;
Ok(resp)
}
inner(req).await.unwrap_or_else(IntoResponse::into_response)
}
// Login
// https://github.com/gothinkster/realworld/tree/master/api#authentication
pub async fn login(req: Request<impl ProvideUser>) -> Response {
async fn inner(mut req: Request<impl ProvideUser>) -> ApiResult<Response> {
#[derive(serde::Deserialize)]
struct LoginRequestBody {
user: Creds
}
#[derive(serde::Deserialize)]
struct Creds {
email: String,
password: String,
}
let LoginRequestBody {user: Creds { email, password }} = req.
body_json()
.await
.map_err(|_| Response::new(400))?;
debug!("Parsed login request for {}", &email);
debug!("Querying DB for user with email {}", &email);
let db = req.state();
let user = db.get_user_by_email(&email)
.await
.map_err(|e| {
error!("Failed to get user -- {}", e);
e
})?;
debug!("User {} matches email {}", user.id, &email);
let hashed_password = user.password.as_ref()
.ok_or_else(|| Response::new(403))?;
debug!("Authenticating user {}", user.id);
let valid = argon2::verify_encoded(hashed_password, &password.as_bytes())
.map_err(|_| Response::new(403))?;
if ! valid {
debug!("User {} failed authentication", user.id);
Err(Response::new(403))?
}
debug!("Successfully authenticated {}, generating auth token", user.id);
let token = generate_token(user.id)?;
#[derive(serde::Serialize)]
struct LoginResponseBody {
user: User
}
let resp = to_json_response(&LoginResponseBody {
user: User {
email,
token: Some(token),
username: user.username,
..Default::default()
}
})?;
Ok(resp)
}
inner(req).await.unwrap_or_else(IntoResponse::into_response)
}
/// Converts a serializable payload into a JSON response
fn to_json_response<B: serde::Serialize>(body: &B) -> Result<Response, Response> {
Response::new(200)
.body_json(body)
.map_err(|e| {
let error_msg = format!("Failed to serialize response -- {}", e);
warn!("{}", error_msg);
Response::new(500).body_string(error_msg)
})
}
fn get_token_from_request(header: &str) -> String {
header
.splitn(2, ' ')
.nth(1)
.unwrap_or_default()
.to_owned()
}
async fn authorize(token: &str) -> anyhow::Result<i32> {
let data = jsonwebtoken::decode::<TokenClaims>(
token,
SECRET_KEY.as_ref(),
&jsonwebtoken::Validation::default(),
)?;
Ok(data.claims.sub)
}
// TODO: Does this need to be spawned in async-std ?
fn hash_password(password: &str) -> anyhow::Result<String> {
let salt = generate_random_salt();
let hash = argon2::hash_encoded(password.as_bytes(), &salt, &argon2::Config::default())?;
Ok(hash)
}
fn generate_random_salt() -> [u8; 16] {
let mut salt = [0; 16];
thread_rng().fill_bytes(&mut salt);
salt
}
#[derive(serde::Serialize, serde::Deserialize)]
struct TokenClaims {
sub: i32,
exp: i64,
}
fn generate_token(user_id: i32) -> anyhow::Result<String> {
use jsonwebtoken::Header;
let exp = Utc::now() + Duration::hours(1);
let token = jsonwebtoken::encode(
&Header::default(),
&TokenClaims {
sub: user_id,
exp: exp.timestamp(),
},
SECRET_KEY.as_ref(),
)?;
Ok(token)
}

View file

@ -0,0 +1,10 @@
/// Database implementation for PostgreSQL
#[cfg(feature = "postgres")]
pub mod pg;
/// Database implementation for SQLite
#[cfg(feature = "sqlite")]
pub mod sqlite;
/// Database models
pub mod model;

View file

@ -0,0 +1,35 @@
use async_trait::async_trait;
pub struct UserEntity {
pub id: i32,
pub email: String,
pub username: String,
pub password: Option<String>, // FIXME(RFC): Why is this nullable in the DB?
}
/// A type that can provide [`UserEntities`]
#[async_trait]
pub trait ProvideUser {
async fn create_user(&self, username: &str, email: &str, password: &str)
-> anyhow::Result<i32>;
async fn get_user_by_id(&self, user_id: i32) -> anyhow::Result<UserEntity>;
async fn get_user_by_email(&self, email: &str) -> anyhow::Result<UserEntity>;
}
pub struct ArticleEntity {
pub title: String,
pub description: String,
pub body: String,
pub tag_list: Vec<String>,
}
#[async_trait]
pub trait ProvideArticle {
async fn create_article(&self) -> anyhow::Result<ArticleEntity>;
async fn update_article(&self) -> anyhow::Result<ArticleEntity>;
async fn delete_article(&self) -> anyhow::Result<ArticleEntity>;
}

View file

@ -0,0 +1,81 @@
use async_trait::async_trait;
use sqlx::PgPool;
use super::model::*;
use anyhow::Error;
pub async fn connect(db_url: &str) -> anyhow::Result<PgPool> {
let pool = PgPool::new(db_url).await?;
Ok(pool)
}
#[async_trait]
impl ProvideUser for PgPool {
async fn create_user(
&self,
username: &str,
email: &str,
password: &str,
) -> anyhow::Result<i32> {
let rec = sqlx::query!(
r#"
INSERT INTO users ( username, email, password )
VALUES ( $1, $2, $3 )
RETURNING id
"#,
username,
email,
password
)
.fetch_one(self)
.await?;
Ok(rec.id)
}
async fn get_user_by_id(&self, user_id: i32) -> anyhow::Result<UserEntity> {
let rec = sqlx::query_as!(
UserEntity,
r#"
SELECT username, email, id, password
FROM users
WHERE id = $1
"#,
user_id
)
.fetch_one(self)
.await?;
Ok(rec)
}
async fn get_user_by_email(&self, email: &str) -> anyhow::Result<UserEntity> {
let rec = sqlx::query_as!(
UserEntity,
r#"
SELECT username, email, id, password
FROM users
WHERE email = $1
"#,
email
)
.fetch_one(self)
.await?;
Ok(rec)
}
}
#[async_trait]
impl ProvideArticle for PgPool {
async fn create_article(&self) -> Result<ArticleEntity, Error> {
unimplemented!()
}
async fn update_article(&self) -> Result<ArticleEntity, Error> {
unimplemented!()
}
async fn delete_article(&self) -> Result<ArticleEntity, Error> {
unimplemented!()
}
}

View file

@ -0,0 +1,80 @@
use anyhow::{Result, Error};
use async_trait::async_trait;
use sqlx::SqlitePool;
use super::model::*;
pub async fn connect(db_url: &str) -> anyhow::Result<SqlitePool> {
let pool = SqlitePool::new(db_url).await?;
Ok(pool)
}
#[async_trait]
impl ProvideUser for SqlitePool {
async fn create_user(&self, username: &str, email: &str, password: &str) -> Result<i32> {
use sqlx::sqlite::SqliteQueryAs;
// Make a new transaction (for giggles)
let mut tx = self.begin().await?;
let rows_inserted = sqlx::query!(
r#"INSERT INTO users ( username, email, password )
VALUES ( $1, $2, $3 )"#,
username,
email,
password
)
.execute(&mut tx)
.await?;
let (id,) = sqlx::query_as::<_, (i32,)>(r#"SELECT LAST_INSERT_ROWID()"#)
.fetch_one(&mut tx)
.await?;
// FIXME(sgg): Potential bug, when I forget to commit the transaction
// the sqlite locked the table forever for some reason...
// Explicitly commit (otherwise this would rollback on drop)
tx.commit().await?;
Ok(id)
}
async fn get_user_by_id(&self, user_id: i32) -> Result<UserEntity> {
let rec = sqlx::query_as!(
UserEntity,
r#"SELECT id, email, username, password
FROM users
WHERE id = $1"#,
user_id
)
.fetch_one(self)
.await?;
Ok(rec)
}
async fn get_user_by_email(&self, email: &str) -> Result<UserEntity> {
let rec = sqlx::query_as!(
UserEntity,
r#"SELECT id, email, username, password
FROM users
WHERE email = $1"#,
email
)
.fetch_one(self)
.await?;
Ok(rec)
}
}
#[async_trait]
impl ProvideArticle for SqlitePool {
async fn create_article(&self) -> Result<ArticleEntity, Error> {
unimplemented!()
}
async fn update_article(&self) -> Result<ArticleEntity, Error> {
unimplemented!()
}
async fn delete_article(&self) -> Result<ArticleEntity, Error> {
unimplemented!()
}
}

View file

@ -0,0 +1,3 @@
pub mod api;
pub mod db;

View file

@ -0,0 +1,71 @@
use async_std::net::ToSocketAddrs;
use sqlx_example_realworld::db::model::*;
use sqlx_example_realworld::{api, db};
#[derive(structopt::StructOpt)]
struct Args {
#[structopt(long, env = "DATABASE_URL")]
db_url: String,
#[structopt(short, long, default_value = "localhost")]
address: String,
#[structopt(short, long, default_value = "8080")]
port: u16,
#[structopt(long, default_value = "sqlite")]
db: String,
}
async fn run_server<S>(addr: impl ToSocketAddrs, state: S) -> anyhow::Result<()>
where
S: Send + Sync + ProvideUser + ProvideArticle + 'static,
{
let mut server = tide::with_state(state);
server.at("/ping").get(|_| async move { "pong" }); // FIXME(sgg): remove
server.at("/api/users").post(api::users::register);
server.at("/api/users/login").post(api::users::login);
server.at("/api/user").get(api::users::get_current_user);
server.at("/api/articles").get(api::articles::list_articles);
server
.at("/api/articles/:slug")
.get(api::articles::get_article)
.post(api::articles::create_article)
.put(api::articles::update_article);
server.listen(addr).await?;
Ok(())
}
async fn _main(args: Args) -> anyhow::Result<()> {
env_logger::from_env(env_logger::Env::default().default_filter_or("debug")).init();
let Args {
db_url,
address,
port,
db,
} = args;
let addr = (address.as_str(), port);
match db.as_str() {
#[cfg(feature = "sqlite")]
"sqlite" => run_server(addr, db::sqlite::connect(&db_url).await?).await,
#[cfg(feature = "postgres")]
"postgres" => run_server(addr, db::pg::connect(&db_url).await?).await,
other => Err(anyhow::anyhow!(
"Not compiled with support for DB `{}`",
other
)),
}?;
Ok(())
}
#[paw::main]
fn main(args: Args) -> anyhow::Result<()> {
async_std::task::block_on(_main(args))
}