multidb
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
use sqlx::{any::AnyPoolOptions, AnyPool};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DatabaseKind {
|
||||
Sqlite,
|
||||
Postgres,
|
||||
MySql,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Database {
|
||||
pool: AnyPool,
|
||||
kind: DatabaseKind,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub async fn connect(url: &str, max_connections: u32) -> Result<Self, sqlx::Error> {
|
||||
sqlx::any::install_default_drivers();
|
||||
let kind = DatabaseKind::from_url(url)?;
|
||||
let pool = AnyPoolOptions::new()
|
||||
.max_connections(max_connections)
|
||||
.connect(url)
|
||||
.await?;
|
||||
if kind == DatabaseKind::Sqlite {
|
||||
sqlx::query("PRAGMA foreign_keys = ON").execute(&pool).await?;
|
||||
sqlx::query("PRAGMA journal_mode = WAL").execute(&pool).await?;
|
||||
sqlx::query("PRAGMA busy_timeout = 5000").execute(&pool).await?;
|
||||
}
|
||||
Ok(Self { pool, kind })
|
||||
}
|
||||
|
||||
pub fn pool(&self) -> &AnyPool { &self.pool }
|
||||
pub fn kind(&self) -> DatabaseKind { self.kind }
|
||||
}
|
||||
|
||||
impl DatabaseKind {
|
||||
fn from_url(url: &str) -> Result<Self, sqlx::Error> {
|
||||
if url.starts_with("sqlite:") { Ok(Self::Sqlite) }
|
||||
else if url.starts_with("postgres:") || url.starts_with("postgresql:") { Ok(Self::Postgres) }
|
||||
else if url.starts_with("mysql:") { Ok(Self::MySql) }
|
||||
else { Err(sqlx::Error::Configuration("DATABASE_URL must use sqlite://, postgres:// or mysql://".into())) }
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,23 @@ use argon2::{
|
||||
use chrono::{DateTime, Utc};
|
||||
use rand_core::{OsRng, RngCore};
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
use sqlx::FromRow;
|
||||
use crate::{database::{Database, DatabaseKind}, queries};
|
||||
use sqlx::{Any, Transaction};
|
||||
|
||||
|
||||
async fn inserted_id(kind: DatabaseKind, tx: &mut Transaction<'_, Any>, table: &str) -> Result<i64, sqlx::Error> {
|
||||
let query = match kind {
|
||||
DatabaseKind::Sqlite => "SELECT last_insert_rowid()",
|
||||
DatabaseKind::MySql => "SELECT LAST_INSERT_ID()",
|
||||
DatabaseKind::Postgres => match table {
|
||||
"note_revisions" => "SELECT currval(pg_get_serial_sequence('note_revisions', 'id'))",
|
||||
"revisions" => "SELECT currval(pg_get_serial_sequence('revisions', 'id'))",
|
||||
_ => unreachable!("unsupported identity table"),
|
||||
},
|
||||
};
|
||||
sqlx::query_scalar(query).fetch_one(&mut **tx).await
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct Workspace {
|
||||
@@ -39,36 +55,30 @@ pub struct Revision {
|
||||
pub owner_map: String,
|
||||
}
|
||||
|
||||
pub async fn find_workspace(pool: &SqlitePool, slug: &str) -> Result<Option<Workspace>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Workspace>(
|
||||
"SELECT id, slug, title, password_hash, created_at, updated_at FROM workspaces WHERE slug = ?",
|
||||
)
|
||||
pub async fn find_workspace(pool: &Database, slug: &str) -> Result<Option<Workspace>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Workspace>(queries::get(pool.kind(), queries::Q001))
|
||||
.bind(slug)
|
||||
.fetch_optional(pool)
|
||||
.fetch_optional(pool.pool())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_workspace(
|
||||
pool: &SqlitePool,
|
||||
pool: &Database,
|
||||
slug: &str,
|
||||
title: &str,
|
||||
password: Option<&str>,
|
||||
) -> Result<Workspace, sqlx::Error> {
|
||||
let password_hash = password.filter(|value| !value.is_empty()).map(hash_password);
|
||||
let result = sqlx::query(
|
||||
"INSERT INTO workspaces (slug, title, password_hash) VALUES (?, ?, ?)",
|
||||
)
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q002))
|
||||
.bind(slug)
|
||||
.bind(title)
|
||||
.bind(password_hash)
|
||||
.execute(pool)
|
||||
.execute(pool.pool())
|
||||
.await?;
|
||||
|
||||
sqlx::query_as::<_, Workspace>(
|
||||
"SELECT id, slug, title, password_hash, created_at, updated_at FROM workspaces WHERE id = ?",
|
||||
)
|
||||
.bind(result.last_insert_rowid())
|
||||
.fetch_one(pool)
|
||||
sqlx::query_as::<_, Workspace>(queries::get(pool.kind(), queries::Q001))
|
||||
.bind(slug)
|
||||
.fetch_one(pool.pool())
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -87,92 +97,84 @@ pub fn verify_workspace_password(workspace: &Workspace, password: Option<&str>)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_notes(pool: &SqlitePool, workspace_id: i64) -> Result<Vec<Note>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Note>(
|
||||
"SELECT id, workspace_id, slug, title, content, created_at, updated_at, owner_map FROM notes WHERE workspace_id = ? ORDER BY updated_at DESC, id DESC",
|
||||
)
|
||||
pub async fn list_notes(pool: &Database, workspace_id: i64) -> Result<Vec<Note>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Note>(queries::get(pool.kind(), queries::Q003))
|
||||
.bind(workspace_id)
|
||||
.fetch_all(pool)
|
||||
.fetch_all(pool.pool())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_note(
|
||||
pool: &SqlitePool,
|
||||
pool: &Database,
|
||||
workspace_id: i64,
|
||||
slug: &str,
|
||||
) -> Result<Option<Note>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Note>(
|
||||
"SELECT id, workspace_id, slug, title, content, created_at, updated_at, owner_map FROM notes WHERE workspace_id = ? AND slug = ?",
|
||||
)
|
||||
sqlx::query_as::<_, Note>(queries::get(pool.kind(), queries::Q004))
|
||||
.bind(workspace_id)
|
||||
.bind(slug)
|
||||
.fetch_optional(pool)
|
||||
.fetch_optional(pool.pool())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_note(
|
||||
pool: &SqlitePool,
|
||||
pool: &Database,
|
||||
workspace_id: i64,
|
||||
slug: &str,
|
||||
title: &str,
|
||||
) -> Result<Note, sqlx::Error> {
|
||||
let result = sqlx::query(
|
||||
"INSERT INTO notes (workspace_id, slug, title) VALUES (?, ?, ?)",
|
||||
)
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q005))
|
||||
.bind(workspace_id)
|
||||
.bind(slug)
|
||||
.bind(title)
|
||||
.execute(pool)
|
||||
.execute(pool.pool())
|
||||
.await?;
|
||||
|
||||
sqlx::query_as::<_, Note>(
|
||||
"SELECT id, workspace_id, slug, title, content, created_at, updated_at, owner_map FROM notes WHERE id = ?",
|
||||
)
|
||||
.bind(result.last_insert_rowid())
|
||||
.fetch_one(pool)
|
||||
sqlx::query_as::<_, Note>(queries::get(pool.kind(), queries::Q004))
|
||||
.bind(workspace_id)
|
||||
.bind(slug)
|
||||
.fetch_one(pool.pool())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn save_revision(
|
||||
pool: &SqlitePool,
|
||||
pool: &Database,
|
||||
note_id: i64,
|
||||
workspace_id: i64,
|
||||
content: &str,
|
||||
author: Option<&str>,
|
||||
owner_map: &str,
|
||||
) -> Result<(i64, String), sqlx::Error> {
|
||||
let mut tx = pool.begin().await?;
|
||||
sqlx::query("UPDATE notes SET content = ?, owner_map = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?")
|
||||
let mut tx = pool.pool().begin().await?;
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q006))
|
||||
.bind(content)
|
||||
.bind(owner_map)
|
||||
.bind(note_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query("UPDATE workspaces SET updated_at = CURRENT_TIMESTAMP WHERE id = ?")
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q007))
|
||||
.bind(workspace_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
let result = sqlx::query("INSERT INTO note_revisions (note_id, content, author, owner_map) VALUES (?, ?, ?, ?)")
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q008))
|
||||
.bind(note_id)
|
||||
.bind(content)
|
||||
.bind(author)
|
||||
.bind(owner_map)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
let updated_at: String = sqlx::query_scalar("SELECT updated_at FROM notes WHERE id = ?")
|
||||
let revision_id = inserted_id(pool.kind(), &mut tx, "note_revisions").await?;
|
||||
let updated_at: String = sqlx::query_scalar(queries::get(pool.kind(), queries::Q009))
|
||||
.bind(note_id)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok((result.last_insert_rowid(), updated_at))
|
||||
Ok((revision_id, updated_at))
|
||||
}
|
||||
|
||||
pub async fn list_revisions(pool: &SqlitePool, note_id: i64) -> Result<Vec<Revision>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Revision>(
|
||||
"SELECT id, content, created_at, author, owner_map FROM note_revisions WHERE note_id = ? ORDER BY id DESC LIMIT 100",
|
||||
)
|
||||
pub async fn list_revisions(pool: &Database, note_id: i64) -> Result<Vec<Revision>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Revision>(queries::get(pool.kind(), queries::Q010))
|
||||
.bind(note_id)
|
||||
.fetch_all(pool)
|
||||
.fetch_all(pool.pool())
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -213,36 +215,30 @@ pub struct Pad {
|
||||
pub owner_map: String,
|
||||
}
|
||||
|
||||
pub async fn find_pad(pool: &SqlitePool, slug: &str) -> Result<Option<Pad>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Pad>(
|
||||
"SELECT id, slug, title, content, password_hash, created_at, updated_at, owner_map FROM pads WHERE slug = ?",
|
||||
)
|
||||
pub async fn find_pad(pool: &Database, slug: &str) -> Result<Option<Pad>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Pad>(queries::get(pool.kind(), queries::Q011))
|
||||
.bind(slug)
|
||||
.fetch_optional(pool)
|
||||
.fetch_optional(pool.pool())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_pad(
|
||||
pool: &SqlitePool,
|
||||
pool: &Database,
|
||||
slug: &str,
|
||||
title: &str,
|
||||
password: Option<&str>,
|
||||
) -> Result<Pad, sqlx::Error> {
|
||||
let password_hash = password.filter(|value| !value.is_empty()).map(hash_password);
|
||||
let result = sqlx::query(
|
||||
"INSERT INTO pads (slug, title, password_hash) VALUES (?, ?, ?)",
|
||||
)
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q012))
|
||||
.bind(slug)
|
||||
.bind(title)
|
||||
.bind(password_hash)
|
||||
.execute(pool)
|
||||
.execute(pool.pool())
|
||||
.await?;
|
||||
|
||||
sqlx::query_as::<_, Pad>(
|
||||
"SELECT id, slug, title, content, password_hash, created_at, updated_at, owner_map FROM pads WHERE id = ?",
|
||||
)
|
||||
.bind(result.last_insert_rowid())
|
||||
.fetch_one(pool)
|
||||
sqlx::query_as::<_, Pad>(queries::get(pool.kind(), queries::Q011))
|
||||
.bind(slug)
|
||||
.fetch_one(pool.pool())
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -262,43 +258,42 @@ pub fn verify_pad_password(pad: &Pad, password: Option<&str>) -> bool {
|
||||
}
|
||||
|
||||
pub async fn save_pad_revision(
|
||||
pool: &SqlitePool,
|
||||
pool: &Database,
|
||||
pad_id: i64,
|
||||
content: &str,
|
||||
author: Option<&str>,
|
||||
owner_map: &str,
|
||||
) -> Result<(i64, String), sqlx::Error> {
|
||||
let mut tx = pool.begin().await?;
|
||||
sqlx::query("UPDATE pads SET content = ?, owner_map = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?")
|
||||
let mut tx = pool.pool().begin().await?;
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q013))
|
||||
.bind(content)
|
||||
.bind(owner_map)
|
||||
.bind(pad_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
let result = sqlx::query("INSERT INTO revisions (pad_id, content, author, owner_map) VALUES (?, ?, ?, ?)")
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q014))
|
||||
.bind(pad_id)
|
||||
.bind(content)
|
||||
.bind(author)
|
||||
.bind(owner_map)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
let updated_at: String = sqlx::query_scalar("SELECT updated_at FROM pads WHERE id = ?")
|
||||
let revision_id = inserted_id(pool.kind(), &mut tx, "revisions").await?;
|
||||
let updated_at: String = sqlx::query_scalar(queries::get(pool.kind(), queries::Q015))
|
||||
.bind(pad_id)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok((result.last_insert_rowid(), updated_at))
|
||||
Ok((revision_id, updated_at))
|
||||
}
|
||||
|
||||
pub async fn list_pad_revisions(
|
||||
pool: &SqlitePool,
|
||||
pool: &Database,
|
||||
pad_id: i64,
|
||||
) -> Result<Vec<Revision>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Revision>(
|
||||
"SELECT id, content, created_at, author, owner_map FROM revisions WHERE pad_id = ? ORDER BY id DESC LIMIT 100",
|
||||
)
|
||||
sqlx::query_as::<_, Revision>(queries::get(pool.kind(), queries::Q016))
|
||||
.bind(pad_id)
|
||||
.fetch_all(pool)
|
||||
.fetch_all(pool.pool())
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -310,90 +305,88 @@ pub struct PublishedPage {
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
pub async fn publish_pad(pool: &SqlitePool, pad_id: i64) -> Result<String, sqlx::Error> {
|
||||
if let Some(token) = sqlx::query_scalar::<_, String>("SELECT token FROM published_pages WHERE pad_id = ?")
|
||||
pub async fn publish_pad(pool: &Database, pad_id: i64) -> Result<String, sqlx::Error> {
|
||||
if let Some(token) = sqlx::query_scalar::<_, String>(queries::get(pool.kind(), queries::Q017))
|
||||
.bind(pad_id)
|
||||
.fetch_optional(pool)
|
||||
.fetch_optional(pool.pool())
|
||||
.await?
|
||||
{
|
||||
return Ok(token);
|
||||
}
|
||||
let token = random_suffix(18);
|
||||
sqlx::query("INSERT INTO published_pages (token, pad_id) VALUES (?, ?)")
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q018))
|
||||
.bind(&token)
|
||||
.bind(pad_id)
|
||||
.execute(pool)
|
||||
.execute(pool.pool())
|
||||
.await?;
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
pub async fn publish_note(pool: &SqlitePool, note_id: i64) -> Result<String, sqlx::Error> {
|
||||
if let Some(token) = sqlx::query_scalar::<_, String>("SELECT token FROM published_pages WHERE note_id = ?")
|
||||
pub async fn publish_note(pool: &Database, note_id: i64) -> Result<String, sqlx::Error> {
|
||||
if let Some(token) = sqlx::query_scalar::<_, String>(queries::get(pool.kind(), queries::Q019))
|
||||
.bind(note_id)
|
||||
.fetch_optional(pool)
|
||||
.fetch_optional(pool.pool())
|
||||
.await?
|
||||
{
|
||||
return Ok(token);
|
||||
}
|
||||
let token = random_suffix(18);
|
||||
sqlx::query("INSERT INTO published_pages (token, note_id) VALUES (?, ?)")
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q020))
|
||||
.bind(&token)
|
||||
.bind(note_id)
|
||||
.execute(pool)
|
||||
.execute(pool.pool())
|
||||
.await?;
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
pub async fn find_published_page(pool: &SqlitePool, token: &str) -> Result<Option<PublishedPage>, sqlx::Error> {
|
||||
sqlx::query_as::<_, PublishedPage>(
|
||||
"SELECT pp.token, COALESCE(p.title, n.title) AS title, COALESCE(p.content, n.content) AS content, COALESCE(p.updated_at, n.updated_at) AS updated_at FROM published_pages pp LEFT JOIN pads p ON p.id = pp.pad_id LEFT JOIN notes n ON n.id = pp.note_id WHERE pp.token = ?",
|
||||
)
|
||||
pub async fn find_published_page(pool: &Database, token: &str) -> Result<Option<PublishedPage>, sqlx::Error> {
|
||||
sqlx::query_as::<_, PublishedPage>(queries::get(pool.kind(), queries::Q021))
|
||||
.bind(token)
|
||||
.fetch_optional(pool)
|
||||
.fetch_optional(pool.pool())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn pad_file_token(pool: &SqlitePool, pad_id: i64) -> Result<String, sqlx::Error> {
|
||||
if let Some(token) = sqlx::query_scalar::<_, Option<String>>("SELECT file_token FROM pads WHERE id = ?")
|
||||
pub async fn pad_file_token(pool: &Database, pad_id: i64) -> Result<String, sqlx::Error> {
|
||||
if let Some(token) = sqlx::query_scalar::<_, Option<String>>(queries::get(pool.kind(), queries::Q022))
|
||||
.bind(pad_id)
|
||||
.fetch_one(pool)
|
||||
.fetch_one(pool.pool())
|
||||
.await?
|
||||
{
|
||||
return Ok(token);
|
||||
}
|
||||
|
||||
let token = format!("p_{}", random_suffix(24));
|
||||
sqlx::query("UPDATE pads SET file_token = ? WHERE id = ? AND file_token IS NULL")
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q023))
|
||||
.bind(&token)
|
||||
.bind(pad_id)
|
||||
.execute(pool)
|
||||
.execute(pool.pool())
|
||||
.await?;
|
||||
|
||||
sqlx::query_scalar::<_, String>("SELECT file_token FROM pads WHERE id = ?")
|
||||
sqlx::query_scalar::<_, String>(queries::get(pool.kind(), queries::Q022))
|
||||
.bind(pad_id)
|
||||
.fetch_one(pool)
|
||||
.fetch_one(pool.pool())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn note_file_token(pool: &SqlitePool, note_id: i64) -> Result<String, sqlx::Error> {
|
||||
if let Some(token) = sqlx::query_scalar::<_, Option<String>>("SELECT file_token FROM notes WHERE id = ?")
|
||||
pub async fn note_file_token(pool: &Database, note_id: i64) -> Result<String, sqlx::Error> {
|
||||
if let Some(token) = sqlx::query_scalar::<_, Option<String>>(queries::get(pool.kind(), queries::Q024))
|
||||
.bind(note_id)
|
||||
.fetch_one(pool)
|
||||
.fetch_one(pool.pool())
|
||||
.await?
|
||||
{
|
||||
return Ok(token);
|
||||
}
|
||||
|
||||
let token = format!("n_{}", random_suffix(24));
|
||||
sqlx::query("UPDATE notes SET file_token = ? WHERE id = ? AND file_token IS NULL")
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q025))
|
||||
.bind(&token)
|
||||
.bind(note_id)
|
||||
.execute(pool)
|
||||
.execute(pool.pool())
|
||||
.await?;
|
||||
|
||||
sqlx::query_scalar::<_, String>("SELECT file_token FROM notes WHERE id = ?")
|
||||
sqlx::query_scalar::<_, String>(queries::get(pool.kind(), queries::Q024))
|
||||
.bind(note_id)
|
||||
.fetch_one(pool)
|
||||
.fetch_one(pool.pool())
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -410,17 +403,17 @@ pub struct FileOwner {
|
||||
pub id: i64,
|
||||
}
|
||||
|
||||
pub async fn find_file_owner(pool: &SqlitePool, token: &str) -> Result<Option<FileOwner>, sqlx::Error> {
|
||||
if let Some(id) = sqlx::query_scalar::<_, i64>("SELECT id FROM pads WHERE file_token = ?")
|
||||
pub async fn find_file_owner(pool: &Database, token: &str) -> Result<Option<FileOwner>, sqlx::Error> {
|
||||
if let Some(id) = sqlx::query_scalar::<_, i64>(queries::get(pool.kind(), queries::Q026))
|
||||
.bind(token)
|
||||
.fetch_optional(pool)
|
||||
.fetch_optional(pool.pool())
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(FileOwner { kind: FileOwnerKind::Pad, id }));
|
||||
}
|
||||
if let Some(id) = sqlx::query_scalar::<_, i64>("SELECT id FROM notes WHERE file_token = ?")
|
||||
if let Some(id) = sqlx::query_scalar::<_, i64>(queries::get(pool.kind(), queries::Q027))
|
||||
.bind(token)
|
||||
.fetch_optional(pool)
|
||||
.fetch_optional(pool.pool())
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(FileOwner { kind: FileOwnerKind::Note, id }));
|
||||
|
||||
+14
-6
@@ -1,14 +1,16 @@
|
||||
mod api;
|
||||
mod app;
|
||||
mod config;
|
||||
mod database;
|
||||
mod db;
|
||||
mod queries;
|
||||
mod state;
|
||||
mod websocket;
|
||||
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use config::Config;
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
use database::{Database, DatabaseKind};
|
||||
use state::AppState;
|
||||
use tokio::net::TcpListener;
|
||||
use tracing::info;
|
||||
@@ -23,11 +25,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if let Some(path) = config.database_url.strip_prefix("sqlite://").and_then(|v| v.split('?').next()) {
|
||||
if let Some(parent) = std::path::Path::new(path).parent() { std::fs::create_dir_all(parent)?; }
|
||||
}
|
||||
let db = SqlitePoolOptions::new()
|
||||
.max_connections(config.database_max_connections)
|
||||
.connect(&config.database_url)
|
||||
.await?;
|
||||
sqlx::migrate!().run(&db).await?;
|
||||
let db = Database::connect(&config.database_url, config.database_max_connections).await?;
|
||||
run_migrations(&db).await?;
|
||||
|
||||
std::fs::create_dir_all(&config.files_dir)?;
|
||||
let state = Arc::new(AppState::new(
|
||||
@@ -78,3 +77,12 @@ async fn shutdown_signal() {
|
||||
let terminate = std::future::pending::<()>();
|
||||
tokio::select! { () = ctrl_c => {}, () = terminate => {} }
|
||||
}
|
||||
|
||||
async fn run_migrations(db: &Database) -> Result<(), sqlx::migrate::MigrateError> {
|
||||
let path = match db.kind() {
|
||||
DatabaseKind::Sqlite => std::path::Path::new("migrations/sqlite"),
|
||||
DatabaseKind::Postgres => std::path::Path::new("migrations/postgres"),
|
||||
DatabaseKind::MySql => std::path::Path::new("migrations/mysql"),
|
||||
};
|
||||
sqlx::migrate::Migrator::new(path).await?.run(db.pool()).await
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
use std::{collections::HashMap, sync::{Mutex, OnceLock}};
|
||||
use crate::database::DatabaseKind;
|
||||
|
||||
pub const Q001: &str = "SELECT id, slug, title, password_hash, created_at, updated_at FROM workspaces WHERE slug = ?";
|
||||
pub const Q002: &str = "INSERT INTO workspaces (slug, title, password_hash) VALUES (?, ?, ?)";
|
||||
pub const Q003: &str = "SELECT id, workspace_id, slug, title, content, created_at, updated_at, owner_map FROM notes WHERE workspace_id = ? ORDER BY updated_at DESC, id DESC";
|
||||
pub const Q004: &str = "SELECT id, workspace_id, slug, title, content, created_at, updated_at, owner_map FROM notes WHERE workspace_id = ? AND slug = ?";
|
||||
pub const Q005: &str = "INSERT INTO notes (workspace_id, slug, title) VALUES (?, ?, ?)";
|
||||
pub const Q006: &str = "UPDATE notes SET content = ?, owner_map = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?";
|
||||
pub const Q007: &str = "UPDATE workspaces SET updated_at = CURRENT_TIMESTAMP WHERE id = ?";
|
||||
pub const Q008: &str = "INSERT INTO note_revisions (note_id, content, author, owner_map) VALUES (?, ?, ?, ?)";
|
||||
pub const Q009: &str = "SELECT updated_at FROM notes WHERE id = ?";
|
||||
pub const Q010: &str = "SELECT id, content, created_at, author, owner_map FROM note_revisions WHERE note_id = ? ORDER BY id DESC LIMIT 100";
|
||||
pub const Q011: &str = "SELECT id, slug, title, content, password_hash, created_at, updated_at, owner_map FROM pads WHERE slug = ?";
|
||||
pub const Q012: &str = "INSERT INTO pads (slug, title, password_hash) VALUES (?, ?, ?)";
|
||||
pub const Q013: &str = "UPDATE pads SET content = ?, owner_map = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?";
|
||||
pub const Q014: &str = "INSERT INTO revisions (pad_id, content, author, owner_map) VALUES (?, ?, ?, ?)";
|
||||
pub const Q015: &str = "SELECT updated_at FROM pads WHERE id = ?";
|
||||
pub const Q016: &str = "SELECT id, content, created_at, author, owner_map FROM revisions WHERE pad_id = ? ORDER BY id DESC LIMIT 100";
|
||||
pub const Q017: &str = "SELECT token FROM published_pages WHERE pad_id = ?";
|
||||
pub const Q018: &str = "INSERT INTO published_pages (token, pad_id) VALUES (?, ?)";
|
||||
pub const Q019: &str = "SELECT token FROM published_pages WHERE note_id = ?";
|
||||
pub const Q020: &str = "INSERT INTO published_pages (token, note_id) VALUES (?, ?)";
|
||||
pub const Q021: &str = "SELECT pp.token, COALESCE(p.title, n.title) AS title, COALESCE(p.content, n.content) AS content, COALESCE(p.updated_at, n.updated_at) AS updated_at FROM published_pages pp LEFT JOIN pads p ON p.id = pp.pad_id LEFT JOIN notes n ON n.id = pp.note_id WHERE pp.token = ?";
|
||||
pub const Q022: &str = "SELECT file_token FROM pads WHERE id = ?";
|
||||
pub const Q023: &str = "UPDATE pads SET file_token = ? WHERE id = ? AND file_token IS NULL";
|
||||
pub const Q024: &str = "SELECT file_token FROM notes WHERE id = ?";
|
||||
pub const Q025: &str = "UPDATE notes SET file_token = ? WHERE id = ? AND file_token IS NULL";
|
||||
pub const Q026: &str = "SELECT id FROM pads WHERE file_token = ?";
|
||||
pub const Q027: &str = "SELECT id FROM notes WHERE file_token = ?";
|
||||
|
||||
static POSTGRES_QUERIES: OnceLock<Mutex<HashMap<&'static str, &'static str>>> = OnceLock::new();
|
||||
|
||||
pub fn get(kind: DatabaseKind, query: &'static str) -> &'static str {
|
||||
if kind != DatabaseKind::Postgres { return query; }
|
||||
let cache = POSTGRES_QUERIES.get_or_init(|| Mutex::new(HashMap::new()));
|
||||
let mut cache = cache.lock().expect("query cache lock poisoned");
|
||||
if let Some(value) = cache.get(query) { return value; }
|
||||
let cache_key = query;
|
||||
let query = query.replace("CURRENT_TIMESTAMP", "(CURRENT_TIMESTAMP::text)");
|
||||
let mut index = 0;
|
||||
let mut converted = String::with_capacity(query.len() + 8);
|
||||
for ch in query.chars() {
|
||||
if ch == '?' {
|
||||
index += 1;
|
||||
converted.push('$');
|
||||
converted.push_str(&index.to_string());
|
||||
} else {
|
||||
converted.push(ch);
|
||||
}
|
||||
}
|
||||
let converted = Box::leak(converted.into_boxed_str());
|
||||
cache.insert(cache_key, converted);
|
||||
converted
|
||||
}
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use sqlx::SqlitePool;
|
||||
use crate::database::Database;
|
||||
use tokio::sync::{broadcast, RwLock};
|
||||
|
||||
const CHANNEL_CAPACITY: usize = 256;
|
||||
@@ -15,7 +15,7 @@ pub struct NoteUpdate {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AppState {
|
||||
pub db: SqlitePool,
|
||||
pub db: Database,
|
||||
pub asset_version: String,
|
||||
pub files_dir: String,
|
||||
pub upload_max_size_bytes: usize,
|
||||
@@ -23,7 +23,7 @@ pub struct AppState {
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(db: SqlitePool, asset_version: String, files_dir: String, upload_max_size_bytes: usize) -> Self {
|
||||
pub fn new(db: Database, asset_version: String, files_dir: String, upload_max_size_bytes: usize) -> Self {
|
||||
Self { db, asset_version, files_dir, upload_max_size_bytes, channels: RwLock::new(HashMap::new()) }
|
||||
}
|
||||
async fn channel_for_key(&self, key: String) -> broadcast::Sender<NoteUpdate> {
|
||||
|
||||
Reference in New Issue
Block a user