mirror of
https://github.com/LeNei/axum-sqlx-template.git
synced 2026-02-13 14:54:40 +00:00
first commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
.env
|
||||||
2082
Cargo.lock
generated
Normal file
2082
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
Cargo.toml
Normal file
33
Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
[package]
|
||||||
|
name = "axum-sqlx-template"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["LeNei <leonmarc.neisskenwirth@gmail.com>"]
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
[lib]
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
path = "src/main.rs"
|
||||||
|
name = "api"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
sqlx = { version = "0.6", default-features = false, features = ["runtime-tokio-rustls", "macros", "postgres", "uuid", "chrono", "migrate", "offline"] }
|
||||||
|
chrono = { version = "0.4.22", features = ["serde"] }
|
||||||
|
secrecy = { version = "0.8", features = ["serde"] }
|
||||||
|
config = { version = "0.13", default-features = false, features = ["yaml"] }
|
||||||
|
serde-aux = "4.1.2"
|
||||||
|
log = "0.4"
|
||||||
|
tracing = "0.1.19"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] }
|
||||||
|
tracing-bunyan-formatter = "0.3"
|
||||||
|
tracing-log = "0.1.1"
|
||||||
|
axum = { version = "0.6.14", features = ["tracing"] }
|
||||||
|
tower-http = { version = "0.4.0", features = ["trace", "cors"] }
|
||||||
|
http = "0.2"
|
||||||
|
hyper = { version = "0.14", features = ["full"] }
|
||||||
|
anyhow = "1.0"
|
||||||
10
configuration/base.yaml
Normal file
10
configuration/base.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
application:
|
||||||
|
port: 8080
|
||||||
|
host: 0.0.0.0
|
||||||
|
database:
|
||||||
|
host: "127.0.0.1"
|
||||||
|
port: 5432
|
||||||
|
username: "postgres"
|
||||||
|
password: "postgres"
|
||||||
|
database_name: "postgres"
|
||||||
|
require_ssl: false
|
||||||
5
configuration/local.yaml
Normal file
5
configuration/local.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
application:
|
||||||
|
host: "127.0.0.1"
|
||||||
|
base_url: "http://127.0.0.1"
|
||||||
|
database:
|
||||||
|
require_ssl: false
|
||||||
4
configuration/production.yaml
Normal file
4
configuration/production.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
application:
|
||||||
|
host: "0.0.0.0"
|
||||||
|
database:
|
||||||
|
require_ssl: true
|
||||||
4
configuration/staging.yaml
Normal file
4
configuration/staging.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
application:
|
||||||
|
host: "0.0.0.0"
|
||||||
|
database:
|
||||||
|
require_ssl: true
|
||||||
5
migrations/20230507154934_initial_setup.down.sql
Normal file
5
migrations/20230507154934_initial_setup.down.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
-- remove function for updated_at
|
||||||
|
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
|
||||||
|
|
||||||
|
DROP FUNCTION IF EXISTS diesel_set_updated_at();
|
||||||
|
|
||||||
20
migrations/20230507154934_initial_setup.up.sql
Normal file
20
migrations/20230507154934_initial_setup.up.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
-- Create updated_at function and trigger
|
||||||
|
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
|
||||||
|
BEGIN
|
||||||
|
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
|
||||||
|
BEGIN
|
||||||
|
IF (
|
||||||
|
NEW IS DISTINCT FROM OLD AND
|
||||||
|
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
|
||||||
|
) THEN
|
||||||
|
NEW.updated_at := current_timestamp;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
3
sqlx-data.json
Normal file
3
sqlx-data.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"db": "PostgreSQL"
|
||||||
|
}
|
||||||
10
src/config/app.rs
Normal file
10
src/config/app.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
use serde_aux::field_attributes::deserialize_number_from_string;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub struct ApplicationSettings {
|
||||||
|
#[serde(deserialize_with = "deserialize_number_from_string")]
|
||||||
|
pub port: u16,
|
||||||
|
pub host: String,
|
||||||
|
pub base_url: String,
|
||||||
|
}
|
||||||
47
src/config/database.rs
Normal file
47
src/config/database.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use secrecy::{ExposeSecret, Secret};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_aux::field_attributes::deserialize_number_from_string;
|
||||||
|
use sqlx::postgres::PgPoolOptions;
|
||||||
|
use sqlx::postgres::{PgConnectOptions, PgSslMode};
|
||||||
|
use sqlx::ConnectOptions;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use tracing;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub struct DatabaseSettings {
|
||||||
|
pub username: String,
|
||||||
|
pub password: Secret<String>,
|
||||||
|
#[serde(deserialize_with = "deserialize_number_from_string")]
|
||||||
|
pub port: u16,
|
||||||
|
pub host: String,
|
||||||
|
pub database_name: String,
|
||||||
|
pub require_ssl: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DatabaseSettings {
|
||||||
|
pub fn without_db(&self) -> PgConnectOptions {
|
||||||
|
let ssl_mode = if self.require_ssl {
|
||||||
|
PgSslMode::Require
|
||||||
|
} else {
|
||||||
|
PgSslMode::Prefer
|
||||||
|
};
|
||||||
|
PgConnectOptions::new()
|
||||||
|
.host(&self.host)
|
||||||
|
.username(&self.username)
|
||||||
|
.password(self.password.expose_secret())
|
||||||
|
.port(self.port)
|
||||||
|
.ssl_mode(ssl_mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_db(&self) -> PgConnectOptions {
|
||||||
|
let mut options = self.without_db().database(&self.database_name);
|
||||||
|
options.log_statements(tracing::log::LevelFilter::Trace);
|
||||||
|
options
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_connection_pool(&self) -> PgPool {
|
||||||
|
PgPoolOptions::new()
|
||||||
|
.acquire_timeout(std::time::Duration::from_secs(2))
|
||||||
|
.connect_lazy_with(self.with_db())
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/config/logging.rs
Normal file
37
src/config/logging.rs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
use tracing::subscriber::set_global_default;
|
||||||
|
use tracing::Subscriber;
|
||||||
|
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
|
||||||
|
use tracing_log::LogTracer;
|
||||||
|
use tracing_subscriber::fmt::MakeWriter;
|
||||||
|
use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry};
|
||||||
|
|
||||||
|
/// Compose multiple layers into a `tracing`'s subscriber.
|
||||||
|
///
|
||||||
|
/// # Implementation Notes
|
||||||
|
///
|
||||||
|
/// We are using `impl Subscriber` as return type to avoid having to spell out the actual
|
||||||
|
/// type of the returned subscriber, which is indeed quite complex.
|
||||||
|
pub fn get_subscriber<Sink>(
|
||||||
|
name: String,
|
||||||
|
env_filter: String,
|
||||||
|
sink: Sink,
|
||||||
|
) -> impl Subscriber + Sync + Send
|
||||||
|
where
|
||||||
|
Sink: for<'a> MakeWriter<'a> + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let env_filter =
|
||||||
|
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(env_filter));
|
||||||
|
let formatting_layer = BunyanFormattingLayer::new(name, sink);
|
||||||
|
Registry::default()
|
||||||
|
.with(env_filter)
|
||||||
|
.with(JsonStorageLayer)
|
||||||
|
.with(formatting_layer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a subscriber as global default to process span data.
|
||||||
|
///
|
||||||
|
/// It should only be called once!
|
||||||
|
pub fn init_subscriber(subscriber: impl Subscriber + Sync + Send) {
|
||||||
|
LogTracer::init().expect("Failed to set logger");
|
||||||
|
set_global_default(subscriber).expect("Failed to set subscriber");
|
||||||
|
}
|
||||||
76
src/config/mod.rs
Normal file
76
src/config/mod.rs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
pub mod app;
|
||||||
|
pub mod database;
|
||||||
|
pub mod logging;
|
||||||
|
|
||||||
|
use app::ApplicationSettings;
|
||||||
|
use database::DatabaseSettings;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub struct Settings {
|
||||||
|
pub database: DatabaseSettings,
|
||||||
|
pub application: ApplicationSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_configuration() -> Result<Settings, config::ConfigError> {
|
||||||
|
let base_path = std::env::current_dir().expect("Failed to determine the current directory");
|
||||||
|
let configuration_directory = base_path.join("configuration");
|
||||||
|
|
||||||
|
// Detect the running environment.
|
||||||
|
// Default to `local` if unspecified.
|
||||||
|
let environment: Environment = std::env::var("APP_ENVIRONMENT")
|
||||||
|
.unwrap_or_else(|_| "local".into())
|
||||||
|
.try_into()
|
||||||
|
.expect("Failed to parse APP_ENVIRONMENT.");
|
||||||
|
let environment_filename = format!("{}.yaml", environment.as_str());
|
||||||
|
let settings = config::Config::builder()
|
||||||
|
.add_source(config::File::from(
|
||||||
|
configuration_directory.join("base.yaml"),
|
||||||
|
))
|
||||||
|
.add_source(config::File::from(
|
||||||
|
configuration_directory.join(environment_filename),
|
||||||
|
))
|
||||||
|
// Add in settings from environment variables (with a prefix of APP and '__' as separator)
|
||||||
|
// E.g. `APP_APPLICATION__PORT=5001 would set `Settings.application.port`
|
||||||
|
.add_source(
|
||||||
|
config::Environment::with_prefix("APP")
|
||||||
|
.prefix_separator("_")
|
||||||
|
.separator("__"),
|
||||||
|
)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
settings.try_deserialize::<Settings>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The possible runtime environment for our application.
|
||||||
|
pub enum Environment {
|
||||||
|
Local,
|
||||||
|
Staging,
|
||||||
|
Production,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Environment {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Environment::Local => "local",
|
||||||
|
Environment::Staging => "staging",
|
||||||
|
Environment::Production => "production",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<String> for Environment {
|
||||||
|
type Error = String;
|
||||||
|
|
||||||
|
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"local" => Ok(Self::Local),
|
||||||
|
"staging" => Ok(Self::Staging),
|
||||||
|
"production" => Ok(Self::Production),
|
||||||
|
other => Err(format!(
|
||||||
|
"{} is not a supported environment. Use either `local` or `production`.",
|
||||||
|
other
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/lib.rs
Normal file
3
src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod config;
|
||||||
|
pub mod routes;
|
||||||
|
pub mod startup;
|
||||||
12
src/main.rs
Normal file
12
src/main.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
use axum_sqlx_template::config::get_configuration;
|
||||||
|
use axum_sqlx_template::config::logging::{get_subscriber, init_subscriber};
|
||||||
|
use axum_sqlx_template::startup::build;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
let subscriber = get_subscriber("api".into(), "info".into(), std::io::stdout);
|
||||||
|
init_subscriber(subscriber);
|
||||||
|
|
||||||
|
let configuration = get_configuration().expect("Failed to read configuration.");
|
||||||
|
build(configuration.clone()).await
|
||||||
|
}
|
||||||
29
src/routes/mod.rs
Normal file
29
src/routes/mod.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
use axum::{routing::get, Router};
|
||||||
|
use http::Method;
|
||||||
|
use hyper::StatusCode;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use tower_http::cors::{Any, CorsLayer};
|
||||||
|
use tower_http::trace::TraceLayer;
|
||||||
|
|
||||||
|
#[tracing::instrument(name = "Ping")]
|
||||||
|
async fn ping() -> StatusCode {
|
||||||
|
StatusCode::OK
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ApiContext {
|
||||||
|
pub db: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_routes(api_context: ApiContext) -> Router {
|
||||||
|
let cors = CorsLayer::new()
|
||||||
|
// allow `GET` and `POST` when accessing the resource
|
||||||
|
.allow_methods([Method::GET, Method::POST])
|
||||||
|
// allow requests from any origin
|
||||||
|
.allow_origin(Any);
|
||||||
|
Router::new()
|
||||||
|
.route("/ping", get(ping))
|
||||||
|
.layer(TraceLayer::new_for_http())
|
||||||
|
.layer(cors)
|
||||||
|
.with_state(api_context)
|
||||||
|
}
|
||||||
25
src/startup.rs
Normal file
25
src/startup.rs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
use crate::config::Settings;
|
||||||
|
use crate::routes::{build_routes, ApiContext};
|
||||||
|
use anyhow::Context;
|
||||||
|
use axum::Router;
|
||||||
|
use std::net::TcpListener;
|
||||||
|
|
||||||
|
pub async fn build(settings: Settings) -> anyhow::Result<()> {
|
||||||
|
let api_context = ApiContext {
|
||||||
|
db: settings.database.get_connection_pool(),
|
||||||
|
};
|
||||||
|
let api_router = build_routes(api_context);
|
||||||
|
let address = format!(
|
||||||
|
"{}:{}",
|
||||||
|
settings.application.host, settings.application.port
|
||||||
|
);
|
||||||
|
let listener = TcpListener::bind(address).context("Failed to bind to port")?;
|
||||||
|
|
||||||
|
run(api_router, listener).await
|
||||||
|
}
|
||||||
|
async fn run(router: Router, listener: TcpListener) -> anyhow::Result<()> {
|
||||||
|
axum::Server::from_tcp(listener)?
|
||||||
|
.serve(router.into_make_service())
|
||||||
|
.await
|
||||||
|
.context("Failed to start server")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user