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
+68 -21
View File
@@ -30,6 +30,7 @@ use sha2::{Digest, Sha256};
use slug::slugify;
use crate::{
collab::{self, AppliedOperation},
db, queries,
state::{NoteUpdate, RoomEvent, SharedState},
};
@@ -301,10 +302,17 @@ pub struct ListPaginationMeta {
total_pages: usize,
}
fn default_list_page() -> usize { 1 }
fn default_list_per_page() -> usize { 25 }
fn default_list_page() -> usize {
1
}
fn default_list_per_page() -> usize {
25
}
fn normalize_list_per_page(value: usize) -> usize {
match value { 25 | 50 | 100 => value, _ => 25 }
match value {
25 | 50 | 100 => value,
_ => 25,
}
}
#[derive(Debug, Serialize)]
@@ -614,7 +622,12 @@ pub async fn open_workspace(
search.is_empty()
|| note.title.to_lowercase().contains(&search)
|| note.slug.to_lowercase().contains(&search)
|| note.created_by.as_deref().unwrap_or_default().to_lowercase().contains(&search)
|| note
.created_by
.as_deref()
.unwrap_or_default()
.to_lowercase()
.contains(&search)
})
.map(|note| {
let stats = stats.get(&note.id);
@@ -658,7 +671,12 @@ pub async fn open_workspace(
Ok(Json(WorkspaceOpenResponse {
workspace: workspace_info_from(&workspace, access_level),
notes,
pagination: ListPaginationMeta { page, per_page, total, total_pages },
pagination: ListPaginationMeta {
page,
per_page,
total,
total_pages,
},
}))
}
@@ -1053,26 +1071,62 @@ pub async fn restore(
.fetch_optional(state.db.pool())
.await?;
let content = content.ok_or_else(ApiError::not_found_revision)?;
let room_key = crate::state::AppState::note_room_key(&workspace_slug, &note_slug);
let channel = state.note_channel(&workspace_slug, &note_slug).await;
let collaboration_snapshot = db::note_collaboration_snapshot(&state.db, note.id).await?;
let collaborative_document = state
.collaborative_document(
&room_key,
collaboration_snapshot.content,
collaboration_snapshot.owner_map,
collaboration_snapshot.revision_id,
)
.await;
let mut document = collaborative_document.lock().await;
let base_revision_id = document.revision_id;
let operation =
collab::replace_operation(document.content.encode_utf16().count(), content, Vec::new());
let (content, owner_map) = collab::apply_operation_to_document(
&document.content,
&document.owner_map,
&operation,
&[],
)
.map_err(|_| ApiError::bad_request("The selected revision could not be restored"))?;
let (revision_id, updated_at) = db::save_revision(
&state.db,
note.id,
workspace.id,
&content,
Some("restore"),
"[]",
&owner_map,
)
.await?;
let update_id = u64::try_from(revision_id).unwrap_or_default().max(1);
let applied = AppliedOperation {
base_revision_id,
revision_id,
client_id: "server_restore".into(),
update_id,
operation: operation.clone(),
owner_replacements: Vec::new(),
};
document.content.clone_from(&content);
document.owner_map.clone_from(&owner_map);
document.revision_id = revision_id;
document.record(applied);
let update = NoteUpdate {
content,
base_revision_id,
revision_id,
updated_at,
author: Some("restore".into()),
owner_map: "[]".into(),
client_id: "server_restore".into(),
update_id,
operation,
owner_replacements: Vec::new(),
};
let _ = state
.note_channel(&workspace_slug, &note_slug)
.await
.send(RoomEvent::Document(update));
let _ = channel.send(RoomEvent::Document(update));
drop(document);
Ok(Json(serde_json::json!({"ok": true})))
}
@@ -1217,15 +1271,8 @@ async fn effective_header_access_level(
is_private: i64,
password_protected: bool,
) -> Result<AccessLevel, ApiError> {
let mut level = request_access_level(
state,
headers,
kind,
slug,
None,
bearer_token(headers),
)
.await?;
let mut level =
request_access_level(state, headers, kind, slug, None, bearer_token(headers)).await?;
if is_private == 0 && !password_protected {
level = std::cmp::max(level, AccessLevel::Write);
}
+194 -8
View File
@@ -465,6 +465,144 @@ pub async fn public_page(
}))
}
async fn commit_public_pad_task_update(
state: &SharedState,
page: &db::PublishedPage,
source_line: usize,
checked: bool,
) -> Result<(), ApiError> {
let pad_id = page.pad_id.ok_or_else(ApiError::not_found_note)?;
let room_key = crate::state::AppState::pad_room_key(&page.resource_slug);
let channel = state.pad_channel(&page.resource_slug).await;
let collaboration_snapshot = db::pad_collaboration_snapshot(&state.db, pad_id).await?;
let collaborative_document = state
.collaborative_document(
&room_key,
collaboration_snapshot.content,
collaboration_snapshot.owner_map,
collaboration_snapshot.revision_id,
)
.await;
let mut document = collaborative_document.lock().await;
let Some(next_content) =
db::updated_public_task_content(&document.content, source_line, checked)
else {
return Ok(());
};
let base_revision_id = document.revision_id;
let operation =
collab::operation_from_edit(&document.content, &next_content, &document.owner_map);
let (content, owner_map) = collab::apply_operation_to_document(
&document.content,
&document.owner_map,
&operation,
&[],
)
.map_err(|_| ApiError::bad_request("The task could not be updated"))?;
let (revision_id, updated_at) =
db::save_pad_revision(&state.db, pad_id, &content, Some("public"), &owner_map).await?;
let update_id = u64::try_from(revision_id).unwrap_or_default().max(1);
document.content.clone_from(&content);
document.owner_map.clone_from(&owner_map);
document.revision_id = revision_id;
document.record(AppliedOperation {
base_revision_id,
revision_id,
client_id: "public_task".into(),
update_id,
operation: operation.clone(),
owner_replacements: Vec::new(),
});
let _ = channel.send(RoomEvent::Document(NoteUpdate {
base_revision_id,
revision_id,
updated_at,
author: Some("public".into()),
client_id: "public_task".into(),
update_id,
operation,
owner_replacements: Vec::new(),
}));
drop(document);
Ok(())
}
async fn commit_public_note_task_update(
state: &SharedState,
page: &db::PublishedPage,
source_line: usize,
checked: bool,
) -> Result<(), ApiError> {
let note_id = page.note_id.ok_or_else(ApiError::not_found_note)?;
let workspace_id = page.workspace_id.ok_or_else(ApiError::not_found_note)?;
let workspace_slug = page
.workspace_slug
.as_deref()
.ok_or_else(ApiError::not_found_note)?;
let room_key = crate::state::AppState::note_room_key(workspace_slug, &page.resource_slug);
let channel = state
.note_channel(workspace_slug, &page.resource_slug)
.await;
let collaboration_snapshot = db::note_collaboration_snapshot(&state.db, note_id).await?;
let collaborative_document = state
.collaborative_document(
&room_key,
collaboration_snapshot.content,
collaboration_snapshot.owner_map,
collaboration_snapshot.revision_id,
)
.await;
let mut document = collaborative_document.lock().await;
let Some(next_content) =
db::updated_public_task_content(&document.content, source_line, checked)
else {
return Ok(());
};
let base_revision_id = document.revision_id;
let operation =
collab::operation_from_edit(&document.content, &next_content, &document.owner_map);
let (content, owner_map) = collab::apply_operation_to_document(
&document.content,
&document.owner_map,
&operation,
&[],
)
.map_err(|_| ApiError::bad_request("The task could not be updated"))?;
let (revision_id, updated_at) = db::save_revision(
&state.db,
note_id,
workspace_id,
&content,
Some("public"),
&owner_map,
)
.await?;
let update_id = u64::try_from(revision_id).unwrap_or_default().max(1);
document.content.clone_from(&content);
document.owner_map.clone_from(&owner_map);
document.revision_id = revision_id;
document.record(AppliedOperation {
base_revision_id,
revision_id,
client_id: "public_task".into(),
update_id,
operation: operation.clone(),
owner_replacements: Vec::new(),
});
let _ = channel.send(RoomEvent::Document(NoteUpdate {
base_revision_id,
revision_id,
updated_at,
author: Some("public".into()),
client_id: "public_task".into(),
update_id,
operation,
owner_replacements: Vec::new(),
}));
drop(document);
Ok(())
}
pub async fn update_public_task(
State(state): State<SharedState>,
headers: HeaderMap,
@@ -480,7 +618,14 @@ pub async fn update_public_task(
"Task updates are disabled for this page",
));
}
let page = db::update_public_task(&state.db, &token, payload.source_line, payload.checked)
if current.pad_id.is_some() {
commit_public_pad_task_update(&state, &current, payload.source_line, payload.checked)
.await?;
} else {
commit_public_note_task_update(&state, &current, payload.source_line, payload.checked)
.await?;
}
let page = db::find_published_page(&state.db, &token)
.await?
.ok_or_else(ApiError::not_found_note)?;
let files =
@@ -564,19 +709,59 @@ pub async fn pad_restore(
.fetch_optional(state.db.pool())
.await?;
let owner_map = owner_map.unwrap_or_else(|| "[]".into());
let room_key = crate::state::AppState::pad_room_key(&slug);
let channel = state.pad_channel(&slug).await;
let collaboration_snapshot = db::pad_collaboration_snapshot(&state.db, pad.id).await?;
let collaborative_document = state
.collaborative_document(
&room_key,
collaboration_snapshot.content,
collaboration_snapshot.owner_map,
collaboration_snapshot.revision_id,
)
.await;
let mut document = collaborative_document.lock().await;
let base_revision_id = document.revision_id;
let restored_owners = collab::owner_spans_from_map(&content, &owner_map);
let operation = collab::replace_operation(
document.content.encode_utf16().count(),
content,
restored_owners,
);
let (content, owner_map) = collab::apply_operation_to_document(
&document.content,
&document.owner_map,
&operation,
&[],
)
.map_err(|_| ApiError::bad_request("The selected revision could not be restored"))?;
let (revision_id, updated_at) =
db::save_pad_revision(&state.db, pad.id, &content, Some("restore"), &owner_map).await?;
let update_id = u64::try_from(revision_id).unwrap_or_default().max(1);
let applied = AppliedOperation {
base_revision_id,
revision_id,
client_id: "server_restore".into(),
update_id,
operation: operation.clone(),
owner_replacements: Vec::new(),
};
document.content.clone_from(&content);
document.owner_map.clone_from(&owner_map);
document.revision_id = revision_id;
document.record(applied);
let update = NoteUpdate {
content,
base_revision_id,
revision_id,
updated_at,
author: Some("restore".into()),
owner_map,
client_id: "server_restore".into(),
update_id,
operation,
owner_replacements: Vec::new(),
};
let _ = state
.pad_channel(&slug)
.await
.send(RoomEvent::Document(update));
let _ = channel.send(RoomEvent::Document(update));
drop(document);
Ok(Json(serde_json::json!({"ok": true})))
}
@@ -591,7 +776,8 @@ pub(super) async fn authorized_pad(
let pad = db::find_pad(&state.db, slug)
.await?
.ok_or_else(ApiError::not_found_note)?;
let token_level = request_access_level(state, headers, "pad", slug, access_token, bearer).await?;
let token_level =
request_access_level(state, headers, "pad", slug, access_token, bearer).await?;
if pad.is_private != 0 && token_level == AccessLevel::None {
return Err(ApiError::not_found_note());
}