use crate::{ database::{Database, DatabaseKind}, queries, }; use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, password_hash::SaltString}; use chrono::{DateTime, NaiveDateTime, Utc}; use rand_core::{OsRng, RngCore}; use serde::Serialize; use sqlx::FromRow; use sqlx::{Any, Row, Transaction, any::AnyRow}; fn any_text(row: &AnyRow, column: &str) -> Result { match row.try_get::(column) { Ok(value) => Ok(value), Err(string_error) => match row.try_get::, _>(column) { Ok(value) => { String::from_utf8(value).map_err(|error| sqlx::Error::Decode(Box::new(error))) } Err(_) => Err(string_error), }, } } fn any_optional_text(row: &AnyRow, column: &str) -> Result, sqlx::Error> { match row.try_get::, _>(column) { Ok(value) => Ok(value), Err(string_error) => match row.try_get::>, _>(column) { Ok(Some(value)) => String::from_utf8(value) .map(Some) .map_err(|error| sqlx::Error::Decode(Box::new(error))), Ok(None) => Ok(None), Err(_) => Err(string_error), }, } } async fn inserted_id( kind: DatabaseKind, tx: &mut Transaction<'_, Any>, table: &str, ) -> Result { let query = match kind { DatabaseKind::Sqlite => queries::SQLITE_LAST_INSERT_ID, DatabaseKind::MySql => queries::MYSQL_LAST_INSERT_ID, DatabaseKind::Postgres => match table { "note_revisions" => queries::POSTGRES_NOTE_REVISION_LAST_INSERT_ID, "revisions" => queries::POSTGRES_PAD_REVISION_LAST_INSERT_ID, _ => unreachable!("unsupported identity table"), }, }; sqlx::query_scalar(query).fetch_one(&mut **tx).await } #[derive(Debug, Clone, FromRow)] pub struct Workspace { pub id: i64, pub slug: String, pub title: String, pub password_hash: Option, pub created_at: String, pub updated_at: String, pub is_private: i64, } fn workspace_from_any_row(row: AnyRow) -> Result { Ok(Workspace { id: row.try_get("id")?, slug: any_text(&row, "slug")?, title: any_text(&row, "title")?, password_hash: any_optional_text(&row, "password_hash")?, created_at: any_text(&row, "created_at")?, updated_at: any_text(&row, "updated_at")?, is_private: row.try_get("is_private")?, }) } #[derive(Debug, Clone, Serialize, FromRow)] pub struct Note { pub id: i64, #[serde(skip_serializing)] #[sqlx(rename = "workspace_id")] pub _workspace_id: i64, pub slug: String, pub title: String, #[serde(skip_serializing)] pub content: String, pub created_at: String, pub updated_at: String, pub owner_map: String, pub protected: bool, pub created_by: Option, } #[derive(Debug, Clone, FromRow)] struct SqliteNote { id: i64, workspace_id: i64, slug: String, title: String, content: String, created_at: String, updated_at: String, owner_map: String, protected: i64, created_by: Option, } impl From for Note { fn from(value: SqliteNote) -> Self { Self { id: value.id, _workspace_id: value.workspace_id, slug: value.slug, title: value.title, content: value.content, created_at: value.created_at, updated_at: value.updated_at, owner_map: value.owner_map, protected: value.protected != 0, created_by: value.created_by, } } } fn note_from_any_row(row: AnyRow) -> Result { Ok(Note { id: row.try_get("id")?, _workspace_id: row.try_get("workspace_id")?, slug: any_text(&row, "slug")?, title: any_text(&row, "title")?, content: any_text(&row, "content")?, created_at: any_text(&row, "created_at")?, updated_at: any_text(&row, "updated_at")?, owner_map: any_text(&row, "owner_map")?, protected: row.try_get("protected")?, created_by: any_optional_text(&row, "created_by")?, }) } #[derive(Debug, Serialize, FromRow)] pub struct Revision { pub id: i64, pub content: String, pub created_at: String, pub author: Option, pub owner_map: String, } fn revision_from_any_row(row: AnyRow) -> Result { Ok(Revision { id: row.try_get("id")?, content: any_text(&row, "content")?, created_at: any_text(&row, "created_at")?, author: any_optional_text(&row, "author")?, owner_map: any_text(&row, "owner_map")?, }) } pub async fn find_workspace(pool: &Database, slug: &str) -> Result, sqlx::Error> { if pool.kind() == DatabaseKind::MySql { return sqlx::query(queries::get(pool.kind(), queries::Q001)) .bind(slug) .fetch_optional(pool.pool()) .await? .map(workspace_from_any_row) .transpose(); } sqlx::query_as::<_, Workspace>(queries::get(pool.kind(), queries::Q001)) .bind(slug) .fetch_optional(pool.pool()) .await } pub async fn create_workspace( pool: &Database, slug: &str, title: &str, password: Option<&str>, ) -> Result { let password_hash = password .filter(|value| !value.is_empty()) .map(hash_password); sqlx::query(queries::get(pool.kind(), queries::Q002)) .bind(slug) .bind(title) .bind(password_hash) .execute(pool.pool()) .await?; if pool.kind() == DatabaseKind::MySql { return workspace_from_any_row( sqlx::query(queries::get(pool.kind(), queries::Q001)) .bind(slug) .fetch_one(pool.pool()) .await?, ); } sqlx::query_as::<_, Workspace>(queries::get(pool.kind(), queries::Q001)) .bind(slug) .fetch_one(pool.pool()) .await } pub fn verify_workspace_password(workspace: &Workspace, password: Option<&str>) -> bool { match ( &workspace.password_hash, password.filter(|value| !value.is_empty()), ) { (None, _) => true, (Some(hash), Some(password)) => PasswordHash::new(hash) .ok() .and_then(|parsed| { Argon2::default() .verify_password(password.as_bytes(), &parsed) .ok() }) .is_some(), (Some(_), None) => false, } } pub async fn list_notes(pool: &Database, workspace_id: i64) -> Result, sqlx::Error> { if pool.kind() == DatabaseKind::Sqlite { return Ok(sqlx::query_as::<_, SqliteNote>(queries::Q003) .bind(workspace_id) .fetch_all(pool.pool()) .await? .into_iter() .map(Note::from) .collect()); } if pool.kind() == DatabaseKind::MySql { return sqlx::query(queries::get(pool.kind(), queries::Q003)) .bind(workspace_id) .fetch_all(pool.pool()) .await? .into_iter() .map(note_from_any_row) .collect(); } sqlx::query_as::<_, Note>(queries::get(pool.kind(), queries::Q003)) .bind(workspace_id) .fetch_all(pool.pool()) .await } pub async fn find_note( pool: &Database, workspace_id: i64, slug: &str, ) -> Result, sqlx::Error> { if pool.kind() == DatabaseKind::Sqlite { return Ok(sqlx::query_as::<_, SqliteNote>(queries::Q004) .bind(workspace_id) .bind(slug) .fetch_optional(pool.pool()) .await? .map(Note::from)); } if pool.kind() == DatabaseKind::MySql { return sqlx::query(queries::get(pool.kind(), queries::Q004)) .bind(workspace_id) .bind(slug) .fetch_optional(pool.pool()) .await? .map(note_from_any_row) .transpose(); } sqlx::query_as::<_, Note>(queries::get(pool.kind(), queries::Q004)) .bind(workspace_id) .bind(slug) .fetch_optional(pool.pool()) .await } pub async fn create_note( pool: &Database, workspace_id: i64, slug: &str, title: &str, protected: bool, created_by: Option<&str>, ) -> Result { sqlx::query(queries::get(pool.kind(), queries::Q005)) .bind(workspace_id) .bind(slug) .bind(title) .bind(protected) .bind(created_by) .execute(pool.pool()) .await?; find_note(pool, workspace_id, slug) .await? .ok_or(sqlx::Error::RowNotFound) } pub async fn save_revision( 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.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(queries::get(pool.kind(), queries::Q007)) .bind(workspace_id) .execute(&mut *tx) .await?; sqlx::query(queries::get(pool.kind(), queries::Q008)) .bind(note_id) .bind(content) .bind(author) .bind(owner_map) .execute(&mut *tx) .await?; 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((revision_id, updated_at)) } pub async fn list_revisions(pool: &Database, note_id: i64) -> Result, sqlx::Error> { if pool.kind() == DatabaseKind::MySql { return sqlx::query(queries::get(pool.kind(), queries::Q010)) .bind(note_id) .fetch_all(pool.pool()) .await? .into_iter() .map(revision_from_any_row) .collect(); } sqlx::query_as::<_, Revision>(queries::get(pool.kind(), queries::Q010)) .bind(note_id) .fetch_all(pool.pool()) .await } pub fn random_suffix(length: usize) -> String { const ALPHABET: &[u8] = b"abcdefghjkmnpqrstuvwxyz23456789"; let mut bytes = vec![0_u8; length]; let mut rng = OsRng; rng.fill_bytes(&mut bytes); bytes .into_iter() .map(|value| ALPHABET[(value as usize) % ALPHABET.len()] as char) .collect() } fn hash_password(password: &str) -> String { let salt = SaltString::generate(&mut OsRng); Argon2::default() .hash_password(password.as_bytes(), &salt) .expect("Argon2 hashing should succeed") .to_string() } pub fn normalize_timestamp(value: &str) -> String { let value = value.trim(); if let Ok(timestamp) = DateTime::parse_from_rfc3339(value) { return timestamp.with_timezone(&Utc).to_rfc3339(); } // PostgreSQL renders TEXT timestamps as e.g. `2026-07-20 14:32:10.123456+00`. // RFC 3339 requires `T` and a colon in the numeric offset. let mut postgres = value.replacen(' ', "T", 1); if postgres.len() >= 3 { let offset_start = postgres.len() - 3; let offset = &postgres[offset_start..]; if (offset.starts_with('+') || offset.starts_with('-')) && offset[1..] .chars() .all(|character| character.is_ascii_digit()) { postgres.push_str(":00"); } } if let Ok(timestamp) = DateTime::parse_from_rfc3339(&postgres) { return timestamp.with_timezone(&Utc).to_rfc3339(); } for format in ["%Y-%m-%d %H:%M:%S%.f%:z", "%Y-%m-%dT%H:%M:%S%.f%:z"] { if let Ok(timestamp) = DateTime::parse_from_str(value, format) { return timestamp.with_timezone(&Utc).to_rfc3339(); } } for format in ["%Y-%m-%d %H:%M:%S%.f", "%Y-%m-%dT%H:%M:%S%.f"] { if let Ok(timestamp) = NaiveDateTime::parse_from_str(value, format) { return timestamp.and_utc().to_rfc3339(); } } // Existing rows should still be readable even if they were stored without a zone. value.to_string() } #[derive(Debug, Clone, FromRow)] pub struct Pad { pub id: i64, pub slug: String, pub title: String, pub content: String, pub password_hash: Option, pub created_at: String, pub updated_at: String, pub owner_map: String, pub is_private: i64, } fn pad_from_any_row(row: AnyRow) -> Result { Ok(Pad { id: row.try_get("id")?, slug: any_text(&row, "slug")?, title: any_text(&row, "title")?, content: any_text(&row, "content")?, password_hash: any_optional_text(&row, "password_hash")?, created_at: any_text(&row, "created_at")?, updated_at: any_text(&row, "updated_at")?, owner_map: any_text(&row, "owner_map")?, is_private: row.try_get("is_private")?, }) } pub async fn find_pad(pool: &Database, slug: &str) -> Result, sqlx::Error> { if pool.kind() == DatabaseKind::MySql { return sqlx::query(queries::get(pool.kind(), queries::Q011)) .bind(slug) .fetch_optional(pool.pool()) .await? .map(pad_from_any_row) .transpose(); } sqlx::query_as::<_, Pad>(queries::get(pool.kind(), queries::Q011)) .bind(slug) .fetch_optional(pool.pool()) .await } pub async fn create_pad( pool: &Database, slug: &str, title: &str, password: Option<&str>, ) -> Result { let password_hash = password .filter(|value| !value.is_empty()) .map(hash_password); sqlx::query(queries::get(pool.kind(), queries::Q012)) .bind(slug) .bind(title) .bind(password_hash) .execute(pool.pool()) .await?; if pool.kind() == DatabaseKind::MySql { return pad_from_any_row( sqlx::query(queries::get(pool.kind(), queries::Q011)) .bind(slug) .fetch_one(pool.pool()) .await?, ); } sqlx::query_as::<_, Pad>(queries::get(pool.kind(), queries::Q011)) .bind(slug) .fetch_one(pool.pool()) .await } pub fn verify_pad_password(pad: &Pad, password: Option<&str>) -> bool { match ( &pad.password_hash, password.filter(|value| !value.is_empty()), ) { (None, _) => true, (Some(hash), Some(password)) => PasswordHash::new(hash) .ok() .and_then(|parsed| { Argon2::default() .verify_password(password.as_bytes(), &parsed) .ok() }) .is_some(), (Some(_), None) => false, } } pub async fn save_pad_revision( pool: &Database, pad_id: i64, content: &str, author: Option<&str>, owner_map: &str, ) -> Result<(i64, String), sqlx::Error> { 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?; sqlx::query(queries::get(pool.kind(), queries::Q014)) .bind(pad_id) .bind(content) .bind(author) .bind(owner_map) .execute(&mut *tx) .await?; 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((revision_id, updated_at)) } pub async fn list_pad_revisions( pool: &Database, pad_id: i64, ) -> Result, sqlx::Error> { if pool.kind() == DatabaseKind::MySql { return sqlx::query(queries::get(pool.kind(), queries::Q016)) .bind(pad_id) .fetch_all(pool.pool()) .await? .into_iter() .map(revision_from_any_row) .collect(); } sqlx::query_as::<_, Revision>(queries::get(pool.kind(), queries::Q016)) .bind(pad_id) .fetch_all(pool.pool()) .await } #[derive(Debug, Clone, Serialize)] pub struct PublishedPage { pub token: String, pub pad_id: Option, pub note_id: Option, pub allow_task_updates: bool, pub title: String, pub content: String, pub updated_at: String, } #[derive(Debug, Clone, FromRow)] struct PublishedPageRow { token: String, pad_id: Option, note_id: Option, allow_task_updates: i64, title: String, content: String, updated_at: String, } #[derive(Debug, Clone, FromRow)] struct PostgresPublishedPageRow { token: String, pad_id: Option, note_id: Option, allow_task_updates: bool, title: String, content: String, updated_at: String, } impl From for PublishedPage { fn from(value: PostgresPublishedPageRow) -> Self { Self { token: value.token, pad_id: value.pad_id, note_id: value.note_id, allow_task_updates: value.allow_task_updates, title: value.title, content: value.content, updated_at: value.updated_at, } } } impl From for PublishedPage { fn from(value: PublishedPageRow) -> Self { Self { token: value.token, pad_id: value.pad_id, note_id: value.note_id, allow_task_updates: value.allow_task_updates != 0, title: value.title, content: value.content, updated_at: value.updated_at, } } } pub async fn publish_pad(pool: &Database, pad_id: i64) -> Result { if let Some(token) = sqlx::query_scalar::<_, String>(queries::get(pool.kind(), queries::Q017)) .bind(pad_id) .fetch_optional(pool.pool()) .await? { return Ok(token); } let token = random_suffix(18); sqlx::query(queries::get(pool.kind(), queries::Q018)) .bind(&token) .bind(pad_id) .execute(pool.pool()) .await?; Ok(token) } pub async fn publish_note(pool: &Database, note_id: i64) -> Result { if let Some(token) = sqlx::query_scalar::<_, String>(queries::get(pool.kind(), queries::Q019)) .bind(note_id) .fetch_optional(pool.pool()) .await? { return Ok(token); } let token = random_suffix(18); sqlx::query(queries::get(pool.kind(), queries::Q020)) .bind(&token) .bind(note_id) .execute(pool.pool()) .await?; Ok(token) } pub async fn find_published_page( pool: &Database, token: &str, ) -> Result, sqlx::Error> { if pool.kind() == DatabaseKind::Postgres { return Ok( sqlx::query_as::<_, PostgresPublishedPageRow>(queries::Q021_POSTGRES) .bind(token) .fetch_optional(pool.pool()) .await? .map(Into::into), ); } Ok( sqlx::query_as::<_, PublishedPageRow>(queries::get(pool.kind(), queries::Q021)) .bind(token) .fetch_optional(pool.pool()) .await? .map(Into::into), ) } pub async fn pad_public_task_updates(pool: &Database, pad_id: i64) -> Result { if pool.kind() == DatabaseKind::Postgres { return Ok(sqlx::query_scalar::<_, bool>(queries::Q044_POSTGRES) .bind(pad_id) .fetch_optional(pool.pool()) .await? .unwrap_or(false)); } let value = sqlx::query_scalar::<_, i64>(queries::get(pool.kind(), queries::Q044)) .bind(pad_id) .fetch_optional(pool.pool()) .await? .unwrap_or(0); Ok(value != 0) } pub async fn note_public_task_updates(pool: &Database, note_id: i64) -> Result { if pool.kind() == DatabaseKind::Postgres { return Ok(sqlx::query_scalar::<_, bool>(queries::Q045_POSTGRES) .bind(note_id) .fetch_optional(pool.pool()) .await? .unwrap_or(false)); } let value = sqlx::query_scalar::<_, i64>(queries::get(pool.kind(), queries::Q045)) .bind(note_id) .fetch_optional(pool.pool()) .await? .unwrap_or(0); Ok(value != 0) } pub async fn set_pad_public_task_updates( pool: &Database, pad_id: i64, allow: bool, ) -> Result<(), sqlx::Error> { publish_pad(pool, pad_id).await?; let mut query = sqlx::query(queries::get(pool.kind(), queries::Q040)); query = if pool.kind() == DatabaseKind::Postgres { query.bind(allow) } else { query.bind(if allow { 1i64 } else { 0i64 }) }; query.bind(pad_id).execute(pool.pool()).await?; Ok(()) } pub async fn set_note_public_task_updates( pool: &Database, note_id: i64, allow: bool, ) -> Result<(), sqlx::Error> { publish_note(pool, note_id).await?; let mut query = sqlx::query(queries::get(pool.kind(), queries::Q041)); query = if pool.kind() == DatabaseKind::Postgres { query.bind(allow) } else { query.bind(if allow { 1i64 } else { 0i64 }) }; query.bind(note_id).execute(pool.pool()).await?; Ok(()) } pub async fn update_public_task( pool: &Database, token: &str, source_line: usize, checked: bool, ) -> Result, sqlx::Error> { let Some(mut page) = find_published_page(pool, token).await? else { return Ok(None); }; if !page.allow_task_updates || source_line == 0 { return Ok(Some(page)); } let mut lines: Vec = page.content.split('\n').map(str::to_owned).collect(); let Some(line) = lines.get_mut(source_line - 1) else { return Ok(Some(page)); }; let bytes = line.as_bytes(); let mut i = 0usize; while i < bytes.len() && bytes[i].is_ascii_whitespace() { i += 1; } if i >= bytes.len() || !matches!(bytes[i], b'-' | b'*' | b'+') { return Ok(Some(page)); } i += 1; while i < bytes.len() && bytes[i].is_ascii_whitespace() { i += 1; } if i + 2 >= bytes.len() || bytes[i] != b'[' || !matches!(bytes[i + 1], b' ' | b'x' | b'X') || bytes[i + 2] != b']' { return Ok(Some(page)); } line.replace_range(i + 1..i + 2, if checked { "x" } else { " " }); page.content = lines.join("\n"); if let Some(id) = page.pad_id { sqlx::query(queries::get(pool.kind(), queries::Q042)) .bind(&page.content) .bind(id) .execute(pool.pool()) .await?; } else if let Some(id) = page.note_id { sqlx::query(queries::get(pool.kind(), queries::Q043)) .bind(&page.content) .bind(id) .execute(pool.pool()) .await?; } find_published_page(pool, token).await } pub async fn pad_file_token(pool: &Database, pad_id: i64) -> Result { if let Some(token) = sqlx::query_scalar::<_, Option>(queries::get(pool.kind(), queries::Q022)) .bind(pad_id) .fetch_one(pool.pool()) .await? { return Ok(token); } let token = format!("p_{}", random_suffix(24)); sqlx::query(queries::get(pool.kind(), queries::Q023)) .bind(&token) .bind(pad_id) .execute(pool.pool()) .await?; sqlx::query_scalar::<_, String>(queries::get(pool.kind(), queries::Q022)) .bind(pad_id) .fetch_one(pool.pool()) .await } pub async fn note_file_token(pool: &Database, note_id: i64) -> Result { if let Some(token) = sqlx::query_scalar::<_, Option>(queries::get(pool.kind(), queries::Q024)) .bind(note_id) .fetch_one(pool.pool()) .await? { return Ok(token); } let token = format!("n_{}", random_suffix(24)); sqlx::query(queries::get(pool.kind(), queries::Q025)) .bind(&token) .bind(note_id) .execute(pool.pool()) .await?; sqlx::query_scalar::<_, String>(queries::get(pool.kind(), queries::Q024)) .bind(note_id) .fetch_one(pool.pool()) .await } #[derive(Debug, Clone, Copy)] pub enum FileOwnerKind { Pad, Note, } #[derive(Debug, Clone, Copy)] pub struct FileOwner { pub kind: FileOwnerKind, pub id: i64, } pub async fn find_file_owner( pool: &Database, token: &str, ) -> Result, sqlx::Error> { if let Some(id) = sqlx::query_scalar::<_, i64>(queries::get(pool.kind(), queries::Q026)) .bind(token) .fetch_optional(pool.pool()) .await? { return Ok(Some(FileOwner { kind: FileOwnerKind::Pad, id, })); } if let Some(id) = sqlx::query_scalar::<_, i64>(queries::get(pool.kind(), queries::Q027)) .bind(token) .fetch_optional(pool.pool()) .await? { return Ok(Some(FileOwner { kind: FileOwnerKind::Note, id, })); } Ok(None) } #[derive(Debug, Clone, Serialize, FromRow)] pub struct NoteFile { pub id: i64, pub filename: String, pub url: String, pub mime_type: String, pub size_bytes: i64, pub created_at: String, pub is_attached: bool, pub detached_at: Option, } #[derive(Debug, Clone, FromRow)] struct SqliteNoteFile { id: i64, filename: String, url: String, mime_type: String, size_bytes: i64, created_at: String, is_attached: i64, detached_at: Option, } impl From for NoteFile { fn from(value: SqliteNoteFile) -> Self { Self { id: value.id, filename: value.filename, url: value.url, mime_type: value.mime_type, size_bytes: value.size_bytes, created_at: value.created_at, is_attached: value.is_attached != 0, detached_at: value.detached_at, } } } pub async fn delete_note(pool: &Database, note_id: i64) -> Result<(), sqlx::Error> { sqlx::query(queries::get(pool.kind(), queries::Q031)) .bind(note_id) .execute(pool.pool()) .await?; Ok(()) } pub async fn register_note_file( pool: &Database, note_id: i64, filename: &str, url: &str, mime_type: &str, size_bytes: i64, ) -> Result<(), sqlx::Error> { sqlx::query(queries::get(pool.kind(), queries::Q032)) .bind(note_id) .bind(filename) .bind(url) .bind(mime_type) .bind(size_bytes) .execute(pool.pool()) .await?; Ok(()) } pub async fn list_note_files(pool: &Database, note_id: i64) -> Result, sqlx::Error> { list_files(pool, queries::Q033, note_id).await } async fn list_files( pool: &Database, query: &'static str, owner_id: i64, ) -> Result, sqlx::Error> { if pool.kind() == DatabaseKind::Sqlite { return Ok(sqlx::query_as::<_, SqliteNoteFile>(query) .bind(owner_id) .fetch_all(pool.pool()) .await? .into_iter() .map(NoteFile::from) .collect()); } sqlx::query_as::<_, NoteFile>(queries::get(pool.kind(), query)) .bind(owner_id) .fetch_all(pool.pool()) .await } pub async fn set_note_file_attached( pool: &Database, file_id: i64, attached: bool, ) -> Result<(), sqlx::Error> { let detached_at: Option = if attached { None } else { Some(chrono::Utc::now().to_rfc3339()) }; sqlx::query(queries::get(pool.kind(), queries::Q034)) .bind(attached) .bind(detached_at) .bind(file_id) .execute(pool.pool()) .await?; Ok(()) } pub async fn register_pad_file( pool: &Database, pad_id: i64, filename: &str, url: &str, mime_type: &str, size_bytes: i64, ) -> Result<(), sqlx::Error> { sqlx::query(queries::get(pool.kind(), queries::Q035)) .bind(pad_id) .bind(filename) .bind(url) .bind(mime_type) .bind(size_bytes) .execute(pool.pool()) .await?; Ok(()) } pub async fn list_pad_files(pool: &Database, pad_id: i64) -> Result, sqlx::Error> { list_files(pool, queries::Q036, pad_id).await } pub async fn set_pad_file_attached( pool: &Database, file_id: i64, attached: bool, ) -> Result<(), sqlx::Error> { let detached_at: Option = if attached { None } else { Some(chrono::Utc::now().to_rfc3339()) }; sqlx::query(queries::get(pool.kind(), queries::Q037)) .bind(attached) .bind(detached_at) .bind(file_id) .execute(pool.pool()) .await?; Ok(()) } pub async fn find_note_file( pool: &Database, note_id: i64, file_id: i64, ) -> Result, sqlx::Error> { if pool.kind() == DatabaseKind::Sqlite { return Ok(sqlx::query_as::<_, SqliteNoteFile>(queries::Q038) .bind(file_id) .bind(note_id) .fetch_optional(pool.pool()) .await? .map(NoteFile::from)); } sqlx::query_as::<_, NoteFile>(queries::get(pool.kind(), queries::Q038)) .bind(file_id) .bind(note_id) .fetch_optional(pool.pool()) .await } pub async fn delete_note_file( pool: &Database, note_id: i64, file_id: i64, ) -> Result<(), sqlx::Error> { sqlx::query(queries::get(pool.kind(), queries::Q039)) .bind(file_id) .bind(note_id) .execute(pool.pool()) .await?; Ok(()) } pub async fn find_pad_file( pool: &Database, pad_id: i64, file_id: i64, ) -> Result, sqlx::Error> { if pool.kind() == DatabaseKind::Sqlite { return Ok(sqlx::query_as::<_, SqliteNoteFile>(queries::Q046) .bind(file_id) .bind(pad_id) .fetch_optional(pool.pool()) .await? .map(NoteFile::from)); } sqlx::query_as::<_, NoteFile>(queries::get(pool.kind(), queries::Q046)) .bind(file_id) .bind(pad_id) .fetch_optional(pool.pool()) .await } pub async fn delete_pad_file( pool: &Database, pad_id: i64, file_id: i64, ) -> Result<(), sqlx::Error> { sqlx::query(queries::get(pool.kind(), queries::Q047)) .bind(file_id) .bind(pad_id) .execute(pool.pool()) .await?; Ok(()) }