v0.2.85
This commit is contained in:
1 parent
90e3c399ea
commit
57ad26878b
12 files changed
+477
-31
No files matched your search
@@ -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(¬e.created_at),
|
||||
updated_at: db::normalize_timestamp(¬e.updated_at),
|
||||
deleted_at: db::normalize_timestamp(¬e.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, ¬e_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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user