···11-if nix flake show &> /dev/null; then
22- use flake
11+if nix flake show &>/dev/null; then
22+ use flake
33fi
44+export DATABASE_URL="postgres://lumina:lumina_pw@localhost:5432/lumina_config"
···5555[doc("Just runs the Podman image for a Redis and Postgres server for local development run to connect to.")]
5656[group("local-devel")]
5757local-devel-prep: create-data-dirs
5858- podman run --replace --name lumina-redis -p 6379:6379 -v ./data/redis:/data -d docker.io/redis/redis-stack:7.2.0-v18
5959- podman run --replace -d -p 5432:5432 --name luminadb -e POSTGRES_USER=lumina -e POSTGRES_PASSWORD=lumina_pw -e POSTGRES_DB=lumina_config -v ./data/postgres:/var/lib/postgresql/data:Z docker.io/library/postgres:17-alpine3.22
5858+ # The redis container can be replaced if it already exists, but the postgres container needs to be checked, due to
5959+ # sqlx needing to connect to it to create the database and run the migrations, so if it is restarted, it may not be
6060+ # ready by the time sqlx tries to connect.
6161+ podman run --replace --name lumina-redis -p 6379:6379 -v ./data/redis:/data -d docker.io/redis/redis-stack:7.2.0-v18
6262+ @podman inspect -f '{{{{.State.Running}}}}' luminadb 2>/dev/null | grep -q 'true' \
6363+ && echo "luminadb is already running." \
6464+ || podman run -d -p 5432:5432 \
6565+ --name luminadb \
6666+ -e POSTGRES_USER=lumina \
6767+ -e POSTGRES_PASSWORD=lumina_pw \
6868+ -e POSTGRES_DB=lumina_config \
6969+ -v ./data/postgres:/var/lib/postgresql/data:Z \
7070+ docker.io/library/postgres:17-alpine3.22
7171+ sqlx db create
7272+ sqlx migrate run
7373+ echo "Postgres database created and migrations ran"
7474+60756176[doc("Run the server in development mode")]
6277[group("local-devel")]
-4
SQL/create_pg.sql
migrations/0001_luminadb.sql
···1616 * along with this program. If not, see <https://www.gnu.org/licenses/>.
1717 */
18181919-/*
2020- This file is also auto-included by server/src/database.rs, which uses it to create the database on first launch.
2121- */
2222-2319-- Create logs table
2420CREATE TABLE IF NOT EXISTS logs
2521(
+36-21
flake.nix
···11{
22 description = "Lumina Development Environment";
33-43 inputs = {
54 nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
65 utils.url = "github:numtide/flake-utils";
···2120 system:
2221 let
2322 pkgs = import nixpkgs { inherit system; };
2424- # Define the Rust toolchain using Fenix
2523 rustToolchain = fenix.packages.${system}.stable.withComponents [
2624 "cargo"
2725 "rustc"
···3028 "rust-analyzer"
3129 "rust-src"
3230 ];
3131+ # Define libraries in one place to avoid repetition
3232+ libraries = with pkgs; [
3333+ stdenv.cc.cc
3434+ glib
3535+ dbus
3636+ curl
3737+ openssl
3838+ ];
3939+4040+ packages = with pkgs; [
4141+4242+ # Language tool chains: Rust, Gleam
4343+ rustToolchain
4444+ gleam
4545+ bun
4646+ # For tidying and typing
4747+ # nodePackages.prettier
4848+ sqlx-cli
4949+5050+ # Helpers on OS level
5151+ pkg-config
5252+5353+ # Podman
5454+ podman
5555+5656+ # Runners
5757+ watchexec
5858+ just
5959+ ];
3360 in
3461 {
3562 devShells.default = pkgs.mkShell {
3636- buildInputs = with pkgs; [
3737- # Language tool chains: Rust, Gleam
3838- rustToolchain
3939- gleam
4040- bun
4141- # For tidying and typing
4242- # nodePackages.prettier
4343-4444- # Helpers on OS level
4545- pkg-config
4646- dbus
4747-4848- # Podman
4949- podman
6363+ # Tools go here
6464+ nativeBuildInputs = [ pkgs.pkg-config ];
50655151- # Runners
5252- watchexec
5353- just
5454- ];
6666+ # Libraries go here
6767+ buildInputs = packages ++ libraries;
55685669 shellHook = ''
5757- export LD_LIBRARY_PATH="${pkgs.stdenv.cc.cc.lib}/lib:$LD_LIBRARY_PATH"
7070+ export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath libraries}:$LD_LIBRARY_PATH"
7171+5872 bun i --cwd=client/
5973 echo "❄️ dev environment loaded"
6074 just --list
···6579 };
6680 }
6781 );
8282+6883}
+7-1
server/Cargo.toml
···1616ws = { package = "rocket_ws", version = "0.1.1" }
1717serde_json = "1.0.140"
1818uuid = { version = "1.16.0", features = ["v4", "serde"] }
1919+sqlx = { version = "0.8.6", features = [
2020+ "postgres",
2121+ "runtime-tokio",
2222+ "tls-native-tls",
2323+ "uuid",
2424+] }
2525+anyhow = "1.0.101"
1926cynthia_con = { version = "0.1.4" }
2027dotenv = "0.15.0"
2121-tokio-postgres = { version = "0.7.13", features = ["with-uuid-1"] }
2228bcrypt = "0.17.0"
2329bb8 = "0.8"
2430bb8-postgres = "0.8"
+88-37
server/src/database.rs
···2222use crate::EnvVar::*;
2323use crate::errors::LuminaError::{self};
2424use crate::helpers::events::EventLogger;
2525-use crate::postgres;
2625use crate::timeline;
2726use crate::{info_elog, success_elog, warn_elog};
2827use bb8::Pool;
2929-use bb8_postgres::PostgresConnectionManager;
3028use bb8_redis::RedisConnectionManager;
3129use cynthia_con::{CynthiaColors, CynthiaStyles};
3030+use sqlx::postgres::PgPool;
3231use std::time::Duration;
3333-use tokio_postgres::NoTls;
3232+3333+struct DatabaseConfig {
3434+ postgres_username: String,
3535+ postgres_password: Option<String>,
3636+ postgres_host: String,
3737+ postgres_port: u16,
3838+ postgres_dbname: String,
3939+ redis_url: String,
4040+}
34413542pub(crate) async fn setup() -> Result<PgConn, LuminaError> {
3643 let ev_log = EventLogger::new(&None);
···5663 };
57645865 {
5959- let pg_config: tokio_postgres::Config = {
6666+ let pg_config: DatabaseConfig = {
6067 let mut uuu = (
6168 "unspecified database".to_string(),
6269 "unspecified host".to_string(),
6370 "unknown port".to_string(),
6471 );
6565- let mut pg_config = postgres::Config::new();
6666- pg_config.user(&{
6767- std::env::var("LUMINA_POSTGRES_USERNAME").unwrap_or("lumina".to_string())
6868- });
7272+ let mut pg_config = DatabaseConfig {
7373+ postgres_username: std::env::var("LUMINA_POSTGRES_USERNAME")
7474+ .unwrap_or("lumina".to_string()),
7575+ postgres_password: std::env::var("LUMINA_POSTGRES_PASSWORD").ok(),
7676+ postgres_host: std::env::var("LUMINA_POSTGRES_HOST")
7777+ .unwrap_or("localhost".to_string()),
7878+ postgres_port: std::env::var("LUMINA_POSTGRES_PORT")
7979+ .ok()
8080+ .and_then(|p| p.parse::<u16>().ok())
8181+ .unwrap_or(5432),
8282+ postgres_dbname: std::env::var("LUMINA_POSTGRES_DATABASE")
8383+ .unwrap_or("lumina_config".to_string()),
8484+ redis_url,
8585+ };
8686+ pg_config.postgres_username =
8787+ std::env::var("LUMINA_POSTGRES_USERNAME").unwrap_or("lumina".to_string());
6988 let dbname =
7089 std::env::var("LUMINA_POSTGRES_DATABASE").unwrap_or("lumina_config".to_string());
7190 uuu.0 = dbname.clone();
7272- pg_config.dbname(&dbname);
9191+ pg_config.postgres_dbname = dbname;
7392 let port = match std::env::var("LUMINA_POSTGRES_PORT") {
7493 Err(..) => {
7594 warn_elog!(
···82101 };
83102 uuu.2 = port.clone();
84103 // Parse the port as u16, if it fails, return an error
8585- pg_config.port(
8686- port.parse::<u16>()
8787- .map_err(|_| LuminaError::ConfInvalid(LUMINA_POSTGRES_PORT))?,
8888- );
104104+ pg_config.postgres_port = port
105105+ .parse::<u16>()
106106+ .map_err(|_| LuminaError::ConfInvalid(LUMINA_POSTGRES_PORT))?;
89107 match std::env::var("LUMINA_POSTGRES_HOST") {
90108 Ok(val) => {
91109 uuu.1 = val.clone();
9292- pg_config.host(&val);
110110+ pg_config.postgres_host = val;
93111 }
94112 Err(_) => {
95113 warn_elog!(
···98116 );
99117 // Default to localhost if not set
100118 uuu.1 = "localhost".to_string();
101101- pg_config.host("localhost");
119119+ pg_config.postgres_host = "localhost".to_string();
102120 }
103121 };
104122 match std::env::var("LUMINA_POSTGRES_PASSWORD") {
105123 Ok(val) => {
106106- pg_config.password(&val);
124124+ pg_config.postgres_password = Some(val);
107125 }
108126 Err(_) => {
109127 warn_elog!(
···123141 };
124142125143 // Create Postgres connection pool
126126- let pg_manager = PostgresConnectionManager::new(pg_config.clone(), NoTls);
127127- let pg_pool = Pool::builder().build(pg_manager).await?;
144144+ let uri = format!(
145145+ "postgres://{}{}@{}:{}/{}",
146146+ pg_config.postgres_username,
147147+ pg_config
148148+ .postgres_password
149149+ .as_deref()
150150+ .map(|a| format!(":{}", a))
151151+ .unwrap_or_default(),
152152+ pg_config.postgres_host,
153153+ pg_config.postgres_port,
154154+ pg_config.postgres_dbname
155155+ );
156156+ let pg_pool = PgPool::connect(uri.as_str()).await?;
128157 {
129129- let pg_conn = pg_pool.get().await?;
130130- pg_conn
131131- .batch_execute(include_str!("../../SQL/create_pg.sql"))
132132- .await?;
158158+ // This is where previously the database schema was created if it did not exist, but now
159159+ // we use sqlx and let it do that :)
160160+ // pg_conn
161161+ // .batch_execute(include_str!("../../SQL/create_pg.sql"))
162162+ // .await?;
133163 // Populate bloom filters
134164 let mut redis_conn = redis_pool.get().await?;
135165 let email_key = "bloom:email";
136166 let username_key = "bloom:username";
137167138138- let rows = pg_conn
139139- .query("SELECT email, username FROM users", &[])
140140- .await?;
141141- for row in rows {
142142- let email: String = row.get(0);
143143- let username: String = row.get(1);
168168+ // (email, username)
169169+ let users_and_emails = operations::list_users_and_emails(&pg_pool).await?;
170170+ for (email, username) in users_and_emails {
144171 let _: () = redis::cmd("BF.ADD")
145172 .arg(email_key)
146173 .arg(email)
···174201#[derive()]
175202pub enum DbConn {
176203 /// The main database is a Postgres database in this variant.
177177- PgsqlConnection(
178178- Pool<PostgresConnectionManager<NoTls>>,
179179- Pool<RedisConnectionManager>,
180180- ),
204204+ PgsqlConnection(PgPool, Pool<RedisConnectionManager>),
181205}
182206183207pub(crate) trait DatabaseConnections {
···189213190214 /// Get a reference to the Postgres pool
191215 /// This returns a clone of the pool without recreating it entirely, so it is cheap to call
192192- fn get_postgres_pool(&self) -> Pool<PostgresConnectionManager<NoTls>>;
216216+ fn get_postgres_pool(&self) -> PgPool;
193217194218 /// Recreate the database connection.
195219 async fn recreate(&self) -> PgConn
···213237 DbConn::PgsqlConnection(_, redis_pool) => redis_pool.clone(),
214238 }
215239 }
216216- fn get_postgres_pool(&self) -> Pool<PostgresConnectionManager<NoTls>> {
240240+ fn get_postgres_pool(&self) -> PgPool {
217241 match self {
218218- DbConn::PgsqlConnection(pg_pool, _) => pg_pool.clone(),
242242+ DbConn::PgsqlConnection(pg_pool, _) => return pg_pool.clone(),
219243 }
220244 }
221245}
···225249 self.redis_pool.clone()
226250 }
227251228228- fn get_postgres_pool(&self) -> Pool<PostgresConnectionManager<NoTls>> {
252252+ fn get_postgres_pool(&self) -> PgPool {
229253 self.postgres_pool.clone()
230254 }
231255···239263/// Simplified type only accounting for the Postgres struct, since the enum adds some future flexibility, but also a lot of overhead.
240264/// If all goes well, this PgConn type will have replaced DbConn entirely after a few iterations of improvement over the years.
241265pub struct PgConn {
242242- pub(crate) postgres_pool: Pool<PostgresConnectionManager<NoTls>>,
266266+ pub(crate) postgres_pool: PgPool,
243267 pub(crate) redis_pool: Pool<RedisConnectionManager>,
244268}
245269···409433410434 Ok(())
411435}
436436+437437+mod operations {
438438+ use super::*;
439439+ use anyhow::Result;
440440+ /// List all users and their emails from the database, used for populating bloom filters on
441441+ ///startup
442442+ ///
443443+ /// Returns a vector of tuples containing the email and username of each user in the database:
444444+ /// ```rust
445445+ /// Vec<(String, String)> // (email, username)
446446+ /// ```
447447+ pub async fn list_users_and_emails(pool: &PgPool) -> Result<Vec<(String, String)>> {
448448+ let recs = sqlx::query!(
449449+ r#"
450450+SELECT email, username
451451+FROM users
452452+"#
453453+ )
454454+ .fetch_all(pool)
455455+ .await?;
456456+ let mut res = vec![];
457457+ for rec in recs {
458458+ res.push((rec.email, rec.username));
459459+ }
460460+ return Ok(res);
461461+ }
462462+}