This commit is contained in:
Mateusz Gruszczyński
2026-07-20 20:27:05 +02:00
parent 48c92dc382
commit 15ee0ad993
22 changed files with 377 additions and 188 deletions
+101 -108
View File
@@ -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 }));