feat: add settings API with key-value store

GET/PUT endpoints for app settings backed by a settings table.
This commit is contained in:
Fam Zheng
2026-03-04 11:46:58 +00:00
parent a3c9fbe8e5
commit c0b681adc3
2 changed files with 105 additions and 0 deletions

96
src/api/settings.rs Normal file
View File

@@ -0,0 +1,96 @@
use std::collections::HashMap;
use std::sync::Arc;
use axum::{
extract::{Path, State},
http::StatusCode,
response::{IntoResponse, Response},
routing::get,
Json, Router,
};
use serde::Deserialize;
use crate::AppState;
use super::{ApiResult, db_err};
#[derive(Deserialize)]
pub struct PutSetting {
pub value: String,
}
pub fn router(state: Arc<AppState>) -> Router {
Router::new()
.route("/settings", get(list_settings))
.route("/settings/{key}", get(get_setting).put(put_setting).delete(delete_setting))
.with_state(state)
}
async fn list_settings(
State(state): State<Arc<AppState>>,
) -> ApiResult<HashMap<String, String>> {
let rows: Vec<(String, String)> = sqlx::query_as(
"SELECT key, value FROM settings ORDER BY key"
)
.fetch_all(&state.db.pool)
.await
.map_err(db_err)?;
let map: HashMap<String, String> = rows.into_iter().collect();
Ok(Json(map))
}
async fn get_setting(
State(state): State<Arc<AppState>>,
Path(key): Path<String>,
) -> ApiResult<HashMap<String, String>> {
let row: Option<(String,)> = sqlx::query_as(
"SELECT value FROM settings WHERE key = ?"
)
.bind(&key)
.fetch_optional(&state.db.pool)
.await
.map_err(db_err)?;
match row {
Some((value,)) => {
let mut map = HashMap::new();
map.insert(key, value);
Ok(Json(map))
}
None => Err((StatusCode::NOT_FOUND, "Setting not found").into_response()),
}
}
async fn put_setting(
State(state): State<Arc<AppState>>,
Path(key): Path<String>,
Json(input): Json<PutSetting>,
) -> ApiResult<HashMap<String, String>> {
sqlx::query(
"INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
)
.bind(&key)
.bind(&input.value)
.execute(&state.db.pool)
.await
.map_err(db_err)?;
let mut map = HashMap::new();
map.insert(key, input.value);
Ok(Json(map))
}
async fn delete_setting(
State(state): State<Arc<AppState>>,
Path(key): Path<String>,
) -> Result<StatusCode, Response> {
let result = sqlx::query("DELETE FROM settings WHERE key = ?")
.bind(&key)
.execute(&state.db.pool)
.await
.map_err(db_err)?;
if result.rows_affected() > 0 {
Ok(StatusCode::NO_CONTENT)
} else {
Err((StatusCode::NOT_FOUND, "Setting not found").into_response())
}
}

View File

@@ -199,6 +199,15 @@ impl Database {
.execute(&self.pool)
.await?;
sqlx::query(
"CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)"
)
.execute(&self.pool)
.await?;
Ok(())
}
}