fix in db and collaborate

This commit is contained in:
Mateusz Gruszczyński
2026-08-04 14:22:26 +02:00
parent 1a77ccd1bf
commit 8d58549d11
26 changed files with 3009 additions and 298 deletions
+16 -22
View File
@@ -78,14 +78,11 @@ pub async fn load_editor_preferences(
user_id: i64,
resource: EditorPreferenceResource,
) -> Result<Option<EditorPreferences>, sqlx::Error> {
let Some(row) = sqlx::query(queries::get(
pool.kind(),
preference_select_query(resource),
))
.bind(user_id)
.bind(resource_id(resource))
.fetch_optional(pool.pool())
.await?
let Some(row) = sqlx::query(queries::get(pool.kind(), preference_select_query(resource)))
.bind(user_id)
.bind(resource_id(resource))
.fetch_optional(pool.pool())
.await?
else {
return Ok(None);
};
@@ -113,20 +110,17 @@ pub async fn save_editor_configuration(
let user_id = user_id.ok_or_else(|| {
sqlx::Error::Protocol("user id is required for personal editor preferences".into())
})?;
sqlx::query(queries::get(
pool.kind(),
preference_upsert_query(resource),
))
.bind(user_id)
.bind(resource_id(resource))
.bind(preferences.compact_view)
.bind(preferences.editor_line_numbers)
.bind(preferences.preview_line_numbers)
.bind(preferences.line_links)
.bind(&preferences.font_family)
.bind(preferences.font_size)
.execute(&mut *tx)
.await?;
sqlx::query(queries::get(pool.kind(), preference_upsert_query(resource)))
.bind(user_id)
.bind(resource_id(resource))
.bind(preferences.compact_view)
.bind(preferences.editor_line_numbers)
.bind(preferences.preview_line_numbers)
.bind(preferences.line_links)
.bind(&preferences.font_family)
.bind(preferences.font_size)
.execute(&mut *tx)
.await?;
}
if let Some((resource_kind, resource_slug, settings)) = resource_settings {
+140 -4
View File
@@ -72,6 +72,13 @@ pub struct Note {
pub created_by_guest_id: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CollaborationSnapshot {
pub content: String,
pub owner_map: String,
pub revision_id: i64,
}
#[derive(Debug, Clone, FromRow)]
struct SqliteNote {
id: i64,
@@ -316,6 +323,68 @@ pub async fn save_revision(
Ok((revision_id, updated_at))
}
pub async fn save_collaborative_revision(
pool: &Database,
note_id: i64,
workspace_id: i64,
content: &str,
author: Option<&str>,
owner_map: &str,
collaboration_client_id: &str,
collaboration_update_id: i64,
) -> 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::Q054))
.bind(note_id)
.bind(content)
.bind(author)
.bind(owner_map)
.bind(collaboration_client_id)
.bind(collaboration_update_id)
.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 latest_note_collaboration_update_id(
pool: &Database,
note_id: i64,
collaboration_client_id: &str,
) -> Result<Option<u64>, sqlx::Error> {
let update_id = sqlx::query_scalar::<_, Option<i64>>(queries::get(pool.kind(), queries::Q056))
.bind(note_id)
.bind(collaboration_client_id)
.fetch_one(pool.pool())
.await?;
Ok(update_id.and_then(|value| u64::try_from(value).ok()))
}
pub async fn note_collaboration_snapshot(
pool: &Database,
note_id: i64,
) -> Result<CollaborationSnapshot, sqlx::Error> {
sqlx::query_as::<_, CollaborationSnapshot>(queries::get(pool.kind(), queries::Q058))
.bind(note_id)
.fetch_one(pool.pool())
.await
}
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)
@@ -392,7 +461,6 @@ pub struct Pad {
pub password_hash: Option<String>,
pub created_at: String,
pub updated_at: String,
pub owner_map: String,
pub is_private: i64,
pub created_by_guest_id: Option<String>,
}
@@ -476,6 +544,63 @@ pub async fn save_pad_revision(
Ok((revision_id, updated_at))
}
pub async fn save_collaborative_pad_revision(
pool: &Database,
pad_id: i64,
content: &str,
author: Option<&str>,
owner_map: &str,
collaboration_client_id: &str,
collaboration_update_id: i64,
) -> 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::Q055))
.bind(pad_id)
.bind(content)
.bind(author)
.bind(owner_map)
.bind(collaboration_client_id)
.bind(collaboration_update_id)
.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 latest_pad_collaboration_update_id(
pool: &Database,
pad_id: i64,
collaboration_client_id: &str,
) -> Result<Option<u64>, sqlx::Error> {
let update_id = sqlx::query_scalar::<_, Option<i64>>(queries::get(pool.kind(), queries::Q057))
.bind(pad_id)
.bind(collaboration_client_id)
.fetch_one(pool.pool())
.await?;
Ok(update_id.and_then(|value| u64::try_from(value).ok()))
}
pub async fn pad_collaboration_snapshot(
pool: &Database,
pad_id: i64,
) -> Result<CollaborationSnapshot, sqlx::Error> {
sqlx::query_as::<_, CollaborationSnapshot>(queries::get(pool.kind(), queries::Q059))
.bind(pad_id)
.fetch_one(pool.pool())
.await
}
pub async fn list_pad_revisions(
pool: &Database,
pad_id: i64,
@@ -486,6 +611,16 @@ pub async fn list_pad_revisions(
.await
}
impl<'r> sqlx::FromRow<'r, AnyRow> for CollaborationSnapshot {
fn from_row(row: &'r AnyRow) -> Result<Self, sqlx::Error> {
Ok(Self {
content: crate::row_decode::text(row, "content")?,
owner_map: crate::row_decode::text(row, "owner_map")?,
revision_id: row.try_get("revision_id")?,
})
}
}
impl<'r> sqlx::FromRow<'r, AnyRow> for Workspace {
fn from_row(row: &'r AnyRow) -> Result<Self, sqlx::Error> {
Ok(Self {
@@ -538,7 +673,6 @@ impl<'r> sqlx::FromRow<'r, AnyRow> for Pad {
password_hash: crate::row_decode::optional_text(row, "password_hash")?,
created_at: crate::row_decode::text(row, "created_at")?,
updated_at: crate::row_decode::text(row, "updated_at")?,
owner_map: crate::row_decode::text(row, "owner_map")?,
is_private: row.try_get("is_private")?,
created_by_guest_id: crate::row_decode::optional_text(row, "created_by_guest_id")?,
})
@@ -570,7 +704,6 @@ mod password_verification_tests {
password_hash,
created_at: String::new(),
updated_at: String::new(),
owner_map: "[]".into(),
is_private: 1,
created_by_guest_id: None,
}
@@ -579,7 +712,10 @@ mod password_verification_tests {
#[test]
fn missing_password_does_not_grant_password_access() {
assert!(!verify_workspace_password(&workspace(None), None));
assert!(!verify_workspace_password(&workspace(None), Some("anything")));
assert!(!verify_workspace_password(
&workspace(None),
Some("anything")
));
assert!(!verify_pad_password(&pad(None), None));
assert!(!verify_pad_password(&pad(None), Some("anything")));
}
+44 -42
View File
@@ -15,6 +15,10 @@ pub struct PublishedPage {
pub pad_id: Option<i64>,
pub note_id: Option<i64>,
pub allow_task_updates: bool,
pub resource_slug: String,
pub workspace_id: Option<i64>,
pub workspace_slug: Option<String>,
pub owner_map: String,
pub title: String,
pub content: String,
pub updated_at: String,
@@ -26,6 +30,10 @@ struct PublishedPageRow {
pad_id: Option<i64>,
note_id: Option<i64>,
allow_task_updates: i64,
resource_slug: String,
workspace_id: Option<i64>,
workspace_slug: Option<String>,
owner_map: String,
title: String,
content: String,
updated_at: String,
@@ -37,6 +45,10 @@ struct PostgresPublishedPageRow {
pad_id: Option<i64>,
note_id: Option<i64>,
allow_task_updates: bool,
resource_slug: String,
workspace_id: Option<i64>,
workspace_slug: Option<String>,
owner_map: String,
title: String,
content: String,
updated_at: String,
@@ -49,6 +61,10 @@ impl From<PostgresPublishedPageRow> for PublishedPage {
pad_id: value.pad_id,
note_id: value.note_id,
allow_task_updates: value.allow_task_updates,
resource_slug: value.resource_slug,
workspace_id: value.workspace_id,
workspace_slug: value.workspace_slug,
owner_map: value.owner_map,
title: value.title,
content: value.content,
updated_at: value.updated_at,
@@ -62,6 +78,10 @@ impl From<PublishedPageRow> for PublishedPage {
pad_id: value.pad_id,
note_id: value.note_id,
allow_task_updates: value.allow_task_updates != 0,
resource_slug: value.resource_slug,
workspace_id: value.workspace_id,
workspace_slug: value.workspace_slug,
owner_map: value.owner_map,
title: value.title,
content: value.content,
updated_at: value.updated_at,
@@ -310,29 +330,23 @@ pub async fn set_note_public_page_unprotected(
Ok(())
}
pub async fn update_public_task(
pool: &Database,
token: &str,
pub fn updated_public_task_content(
content: &str,
source_line: usize,
checked: bool,
) -> Result<Option<PublishedPage>, 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));
) -> Option<String> {
if source_line == 0 {
return None;
}
let mut lines: Vec<String> = page.content.split('\n').map(str::to_owned).collect();
let Some(line) = lines.get_mut(source_line - 1) else {
return Ok(Some(page));
};
let mut lines: Vec<String> = content.split('\n').map(str::to_owned).collect();
let line = lines.get_mut(source_line - 1)?;
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));
return None;
}
i += 1;
while i < bytes.len() && bytes[i].is_ascii_whitespace() {
@@ -343,24 +357,10 @@ pub async fn update_public_task(
|| !matches!(bytes[i + 1], b' ' | b'x' | b'X')
|| bytes[i + 2] != b']'
{
return Ok(Some(page));
return None;
}
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
Some(lines.join("\n"))
}
pub async fn pad_file_token(pool: &Database, pad_id: i64) -> Result<String, sqlx::Error> {
@@ -416,6 +416,10 @@ impl<'r> sqlx::FromRow<'r, AnyRow> for PublishedPageRow {
pad_id: row.try_get("pad_id")?,
note_id: row.try_get("note_id")?,
allow_task_updates: row.try_get("allow_task_updates")?,
resource_slug: crate::row_decode::text(row, "resource_slug")?,
workspace_id: row.try_get("workspace_id")?,
workspace_slug: crate::row_decode::optional_text(row, "workspace_slug")?,
owner_map: crate::row_decode::text(row, "owner_map")?,
title: crate::row_decode::text(row, "title")?,
content: crate::row_decode::text(row, "content")?,
updated_at: crate::row_decode::text(row, "updated_at")?,
@@ -429,6 +433,10 @@ impl<'r> sqlx::FromRow<'r, AnyRow> for PostgresPublishedPageRow {
pad_id: row.try_get("pad_id")?,
note_id: row.try_get("note_id")?,
allow_task_updates: row.try_get("allow_task_updates")?,
resource_slug: crate::row_decode::text(row, "resource_slug")?,
workspace_id: row.try_get("workspace_id")?,
workspace_slug: crate::row_decode::optional_text(row, "workspace_slug")?,
owner_map: crate::row_decode::text(row, "owner_map")?,
title: crate::row_decode::text(row, "title")?,
content: crate::row_decode::text(row, "content")?,
updated_at: crate::row_decode::text(row, "updated_at")?,
@@ -437,17 +445,14 @@ impl<'r> sqlx::FromRow<'r, AnyRow> for PostgresPublishedPageRow {
}
pub async fn pad_public_page_disabled(pool: &Database, pad_id: i64) -> Result<bool, sqlx::Error> {
let sql = match pool.kind() {
DatabaseKind::Postgres => "SELECT public_page_disabled FROM pads WHERE id = $1",
_ => "SELECT public_page_disabled FROM pads WHERE id = ?",
};
let query = queries::get(pool.kind(), queries::PAD_PUBLIC_PAGE_DISABLED);
if pool.kind() == DatabaseKind::Postgres {
return Ok(sqlx::query_scalar::<_, bool>(sql)
return Ok(sqlx::query_scalar::<_, bool>(query)
.bind(pad_id)
.fetch_one(pool.pool())
.await?);
}
Ok(sqlx::query_scalar::<_, i64>(sql)
Ok(sqlx::query_scalar::<_, i64>(query)
.bind(pad_id)
.fetch_one(pool.pool())
.await?
@@ -455,17 +460,14 @@ pub async fn pad_public_page_disabled(pool: &Database, pad_id: i64) -> Result<bo
}
pub async fn note_public_page_disabled(pool: &Database, note_id: i64) -> Result<bool, sqlx::Error> {
let sql = match pool.kind() {
DatabaseKind::Postgres => "SELECT public_page_disabled FROM notes WHERE id = $1",
_ => "SELECT public_page_disabled FROM notes WHERE id = ?",
};
let query = queries::get(pool.kind(), queries::NOTE_PUBLIC_PAGE_DISABLED);
if pool.kind() == DatabaseKind::Postgres {
return Ok(sqlx::query_scalar::<_, bool>(sql)
return Ok(sqlx::query_scalar::<_, bool>(query)
.bind(note_id)
.fetch_one(pool.pool())
.await?);
}
Ok(sqlx::query_scalar::<_, i64>(sql)
Ok(sqlx::query_scalar::<_, i64>(query)
.bind(note_id)
.fetch_one(pool.pool())
.await?