This commit is contained in:
Mateusz Gruszczyński committed 2026-09-25 15:31:55 +02:00
1 parent 90e3c399ea
commit 57ad26878b
12 files changed
+477 -31

No files matched your search

+63
View File
@@ -452,6 +452,17 @@ pub struct NoteListItem {
revision_count: i64,
}
#[derive(Debug, Serialize)]
pub struct ArchivedNoteListItem {
slug: String,
title: String,
created_at: String,
updated_at: String,
deleted_at: String,
protected: bool,
created_by: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct NoteInfo {
workspace_slug: String,
@@ -788,6 +799,58 @@ pub async fn set_workspace_settings(
)))
}
pub async fn archived_notes(
State(state): State<SharedState>,
headers: HeaderMap,
Path(workspace_slug): Path<String>,
) -> Result<Json<Vec<ArchivedNoteListItem>>, ApiError> {
let workspace = db::find_workspace(&state.db, &workspace_slug)
.await?
.ok_or_else(ApiError::not_found_workspace)?;
if !can_manage_workspace_settings(&state, &headers, &workspace).await {
return Err(ApiError::forbidden(
"Only the workspace owner can view archived notes.",
));
}
let notes = db::list_archived_notes(&state.db, workspace.id)
.await?
.into_iter()
.map(|note| ArchivedNoteListItem {
slug: note.slug,
title: note.title,
created_at: db::normalize_timestamp(&note.created_at),
updated_at: db::normalize_timestamp(&note.updated_at),
deleted_at: db::normalize_timestamp(&note.deleted_at),
protected: note.protected,
created_by: note.created_by,
})
.collect();
Ok(Json(notes))
}
pub async fn restore_archived_note(
State(state): State<SharedState>,
headers: HeaderMap,
Path((workspace_slug, note_slug)): Path<(String, String)>,
) -> Result<Json<serde_json::Value>, ApiError> {
let workspace = db::find_workspace(&state.db, &workspace_slug)
.await?
.ok_or_else(ApiError::not_found_workspace)?;
if !can_manage_workspace_settings(&state, &headers, &workspace).await {
return Err(ApiError::forbidden(
"Only the workspace owner can restore archived notes.",
));
}
if !db::restore_archived_note(&state.db, workspace.id, &note_slug).await? {
return Err(ApiError::not_found_note());
}
Ok(Json(serde_json::json!({
"ok": true,
"slug": note_slug,
"url": format!("/w/{workspace_slug}/n/{note_slug}")
})))
}
pub async fn set_workspace_password(
State(state): State<SharedState>,
headers: HeaderMap,
+8
View File
@@ -187,6 +187,14 @@ pub fn router(
"/api/workspaces/{workspace_slug}/settings",
post(api::set_workspace_settings),
)
.route(
"/api/workspaces/{workspace_slug}/archived-notes",
get(api::archived_notes),
)
.route(
"/api/workspaces/{workspace_slug}/archived-notes/{note_slug}/restore",
post(api::restore_archived_note),
)
.route(
"/api/workspaces/{workspace_slug}/open",
post(api::open_workspace),
+96
View File
@@ -74,6 +74,19 @@ pub struct Note {
pub created_by_guest_id: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ArchivedNote {
#[serde(skip_serializing)]
pub id: i64,
pub slug: String,
pub title: String,
pub created_at: String,
pub updated_at: String,
pub deleted_at: String,
pub protected: bool,
pub created_by: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CollaborationSnapshot {
pub content: String,
@@ -325,6 +338,89 @@ pub async fn archive_note(pool: &Database, note_id: i64) -> Result<(), sqlx::Err
Ok(())
}
/// Return notes that were soft-deleted from a workspace, newest first.
pub async fn list_archived_notes(
pool: &Database,
workspace_id: i64,
) -> Result<Vec<ArchivedNote>, sqlx::Error> {
let query = match pool.kind() {
DatabaseKind::Sqlite => {
r#"SELECT id, slug, title, created_at, updated_at, deleted_at, protected, created_by
FROM notes
WHERE workspace_id = ? AND deleted_at IS NOT NULL
ORDER BY deleted_at DESC, id DESC"#
}
DatabaseKind::Postgres => {
r#"SELECT id, slug, title, created_at, updated_at, deleted_at,
CAST(CASE WHEN protected THEN 1 ELSE 0 END AS BIGINT) AS protected, created_by
FROM notes
WHERE workspace_id = $1 AND deleted_at IS NOT NULL
ORDER BY deleted_at DESC, id DESC"#
}
DatabaseKind::MySql => {
r#"SELECT id, CAST(slug AS CHAR CHARACTER SET utf8mb4) AS slug,
CAST(title AS CHAR CHARACTER SET utf8mb4) AS title, created_at, updated_at, deleted_at,
CAST(CASE WHEN protected THEN 1 ELSE 0 END AS SIGNED) AS protected,
CAST(created_by AS CHAR CHARACTER SET utf8mb4) AS created_by
FROM notes
WHERE workspace_id = ? AND deleted_at IS NOT NULL
ORDER BY deleted_at DESC, id DESC"#
}
};
let rows = sqlx::query(query)
.bind(workspace_id)
.fetch_all(pool.pool())
.await?;
rows.into_iter()
.map(|row| {
Ok(ArchivedNote {
id: row.try_get("id")?,
slug: row.try_get("slug")?,
title: row.try_get("title")?,
created_at: row.try_get("created_at")?,
updated_at: row.try_get("updated_at")?,
deleted_at: row.try_get("deleted_at")?,
protected: row.try_get::<i64, _>("protected")? != 0,
created_by: row.try_get("created_by")?,
})
})
.collect()
}
/// Restore a soft-deleted note. Returns false when the archived note no longer exists.
pub async fn restore_archived_note(
pool: &Database,
workspace_id: i64,
slug: &str,
) -> Result<bool, sqlx::Error> {
let query = match pool.kind() {
DatabaseKind::Postgres => {
r#"UPDATE notes SET deleted_at = NULL, updated_at = (CURRENT_TIMESTAMP::text)
WHERE workspace_id = $1 AND slug = $2 AND deleted_at IS NOT NULL"#
}
DatabaseKind::Sqlite | DatabaseKind::MySql => {
r#"UPDATE notes SET deleted_at = NULL, updated_at = CURRENT_TIMESTAMP
WHERE workspace_id = ? AND slug = ? AND deleted_at IS NOT NULL"#
}
};
let mut tx = pool.pool().begin().await?;
let result = sqlx::query(query)
.bind(workspace_id)
.bind(slug)
.execute(&mut *tx)
.await?;
if result.rows_affected() == 0 {
tx.rollback().await?;
return Ok(false);
}
sqlx::query(queries::get(pool.kind(), queries::Q007))
.bind(workspace_id)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(true)
}
pub async fn create_note(
pool: &Database,
workspace_id: i64,