From b1ea9183bc87e45c146cc8b51e5419b2bb14d197 Mon Sep 17 00:00:00 2001 From: qaz741wsd856 Date: Tue, 9 Dec 2025 16:18:13 +0000 Subject: [PATCH] refactor: directly pass json body to d1 api in attachment handling to avoid parsing --- src/handlers/attachments.rs | 60 +++++++++++++---------- src/handlers/ciphers.rs | 95 ++++++++++++++++--------------------- src/handlers/sync.rs | 2 +- 3 files changed, 76 insertions(+), 81 deletions(-) diff --git a/src/handlers/attachments.rs b/src/handlers/attachments.rs index 303b5f8..1a49a2b 100644 --- a/src/handlers/attachments.rs +++ b/src/handlers/attachments.rs @@ -406,7 +406,8 @@ pub async fn hydrate_cipher_attachments( return Ok(()); } - let mut map = load_attachment_map(db, &[cipher.id.clone()]).await?; + let ids_json = serde_json::to_string(&[&cipher.id]).map_err(|_| AppError::Internal)?; + let mut map = load_attachment_map_json(db, &ids_json, "$").await?; if let Some(list) = map.remove(&cipher.id) { if !list.is_empty() { cipher.attachments = Some(list); @@ -415,11 +416,15 @@ pub async fn hydrate_cipher_attachments( Ok(()) } -/// Batch attach attachments to multiple Ciphers +/// Batch attach attachments to multiple Ciphers. +/// - If `json_body` and `ids_path` are provided, use them directly (e.g. body + "$.ids"). +/// - Otherwise extract ids from ciphers and use "$" as path. pub async fn hydrate_ciphers_attachments( db: &D1Database, env: &Env, ciphers: &mut [Cipher], + json_body: Option<&str>, + ids_path: Option<&str>, ) -> Result<(), AppError> { if !attachments_enabled(env) { for cipher in ciphers.iter_mut() { @@ -428,8 +433,17 @@ pub async fn hydrate_ciphers_attachments( return Ok(()); } - let ids: Vec = ciphers.iter().map(|c| c.id.clone()).collect(); - let mut map = load_attachment_map(db, &ids).await?; + let owned_json: String; + let (body, path) = match (json_body, ids_path) { + (Some(b), Some(p)) => (b, p), + _ => { + let ids: Vec<&str> = ciphers.iter().map(|c| c.id.as_str()).collect(); + owned_json = serde_json::to_string(&ids).map_err(|_| AppError::Internal)?; + (owned_json.as_str(), "$") + } + }; + + let mut map = load_attachment_map_json(db, body, path).await?; for cipher in ciphers.iter_mut() { if let Some(list) = map.remove(&cipher.id) { @@ -473,22 +487,21 @@ fn map_rows_to_keys(rows: Vec) -> Vec { .collect() } -pub(crate) async fn list_attachment_keys_for_cipher_ids( +/// List attachment keys for given cipher IDs. +/// - `json_body`: JSON text containing the ids array +/// - `ids_path`: path to ids array within json_body (e.g. "$.ids" or "$" if top-level) +pub(crate) async fn list_attachment_keys_for_cipher_ids_json( db: &D1Database, - cipher_ids: &[String], + json_body: &str, + ids_path: &str, user_id: Option<&str>, ) -> Result, AppError> { - if cipher_ids.is_empty() { - return Ok(Vec::new()); - } - - let ids_json = serde_json::to_string(cipher_ids).map_err(|_| AppError::Internal)?; - - let mut sql = "SELECT a.cipher_id, a.id FROM attachments a JOIN ciphers c ON a.cipher_id = c.id WHERE c.id IN (SELECT value FROM json_each(?1))".to_string(); - let mut params: Vec = vec![ids_json.into()]; + let mut sql = "SELECT a.cipher_id, a.id FROM attachments a JOIN ciphers c ON a.cipher_id = c.id WHERE c.id IN (SELECT value FROM json_each(?1, ?2))".to_string(); + let mut params: Vec = + vec![json_body.to_owned().into(), ids_path.to_owned().into()]; if let Some(uid) = user_id { - sql.push_str(" AND c.user_id = ?2"); + sql.push_str(" AND c.user_id = ?3"); params.push(uid.into()); } @@ -598,19 +611,16 @@ async fn fetch_attachment(db: &D1Database, attachment_id: &str) -> Result Result>, AppError> { - if cipher_ids.is_empty() { - return Ok(HashMap::new()); - } - - let ids_json = serde_json::to_string(cipher_ids).map_err(|_| AppError::Internal)?; - let attachments: Vec = db - .prepare("SELECT * FROM attachments WHERE cipher_id IN (SELECT value FROM json_each(?1))") - .bind(&[ids_json.into()])? + .prepare( + "SELECT * FROM attachments WHERE cipher_id IN (SELECT value FROM json_each(?1, ?2))", + ) + .bind(&[json_body.to_owned().into(), ids_path.to_owned().into()])? .all() .await .map_err(|_| AppError::Database)? diff --git a/src/handlers/ciphers.rs b/src/handlers/ciphers.rs index d8482b9..bbda465 100644 --- a/src/handlers/ciphers.rs +++ b/src/handlers/ciphers.rs @@ -1,12 +1,10 @@ -use super::get_batch_size; use axum::{extract::State, Extension, Json}; use chrono::{DateTime, Utc}; use log; // Used for warning logs on parse failures -use serde::Deserialize; use serde_json::Value; use std::sync::Arc; use uuid::Uuid; -use worker::{query, D1PreparedStatement, Env}; +use worker::{query, Env}; use crate::auth::Claims; use crate::db; @@ -235,7 +233,7 @@ pub async fn list_ciphers( let mut ciphers: Vec = ciphers_db.into_iter().map(|c| c.into()).collect(); - attachments::hydrate_ciphers_attachments(&db, env.as_ref(), &mut ciphers).await?; + attachments::hydrate_ciphers_attachments(&db, env.as_ref(), &mut ciphers, None, None).await?; Ok(Json(CipherListResponse { data: ciphers, @@ -324,13 +322,6 @@ pub async fn update_cipher_partial( Ok(Json(cipher)) } -/// Request body for bulk cipher operations -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CipherIdsData { - pub ids: Vec, -} - /// Soft delete a single cipher (PUT /api/ciphers/{id}/delete) /// Sets deleted_at to current timestamp #[worker::send] @@ -359,28 +350,22 @@ pub async fn soft_delete_cipher( } /// Soft delete multiple ciphers (PUT /api/ciphers/delete) +/// Accepts raw JSON body and uses json_each with path to extract ids directly. #[worker::send] pub async fn soft_delete_ciphers_bulk( claims: Claims, State(env): State>, - Json(payload): Json, + body: String, ) -> Result, AppError> { let db = db::get_db(&env)?; let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(); - let ids = payload.ids; - - if ids.is_empty() { - return Ok(Json(())); - } - - let ids_json = serde_json::to_string(&ids).map_err(|_| AppError::Internal)?; query!( &db, - "UPDATE ciphers SET deleted_at = ?1, updated_at = ?1 WHERE user_id = ?2 AND id IN (SELECT value FROM json_each(?3))", + "UPDATE ciphers SET deleted_at = ?1, updated_at = ?1 WHERE user_id = ?2 AND id IN (SELECT value FROM json_each(?3, '$.ids'))", now, claims.sub, - ids_json + body ) .map_err(|_| AppError::Database)? .run() @@ -403,9 +388,14 @@ pub async fn hard_delete_cipher( if attachments::attachments_enabled(env.as_ref()) { let bucket = attachments::require_bucket(env.as_ref())?; - let keys = - attachments::list_attachment_keys_for_cipher_ids(&db, &[id.clone()], Some(&claims.sub)) - .await?; + let id_json = serde_json::to_string(&[&id]).map_err(|_| AppError::Internal)?; + let keys = attachments::list_attachment_keys_for_cipher_ids_json( + &db, + &id_json, + "$", + Some(&claims.sub), + ) + .await?; attachments::delete_r2_objects(&bucket, &keys).await?; } @@ -425,33 +415,32 @@ pub async fn hard_delete_cipher( } /// Hard delete multiple ciphers (DELETE /api/ciphers or POST /api/ciphers/delete) +/// Accepts raw JSON body and uses json_each with path to extract ids directly. #[worker::send] pub async fn hard_delete_ciphers_bulk( claims: Claims, State(env): State>, - Json(payload): Json, + body: String, ) -> Result, AppError> { let db = db::get_db(&env)?; - let ids = payload.ids; - - if ids.is_empty() { - return Ok(Json(())); - } - - let ids_json = serde_json::to_string(&ids).map_err(|_| AppError::Internal)?; if attachments::attachments_enabled(env.as_ref()) { let bucket = attachments::require_bucket(env.as_ref())?; - let keys = - attachments::list_attachment_keys_for_cipher_ids(&db, &ids, Some(&claims.sub)).await?; + let keys = attachments::list_attachment_keys_for_cipher_ids_json( + &db, + &body, + "$.ids", + Some(&claims.sub), + ) + .await?; attachments::delete_r2_objects(&bucket, &keys).await?; } query!( &db, - "DELETE FROM ciphers WHERE user_id = ?1 AND id IN (SELECT value FROM json_each(?2))", + "DELETE FROM ciphers WHERE user_id = ?1 AND id IN (SELECT value FROM json_each(?2, '$.ids'))", claims.sub, - ids_json + body ) .map_err(|_| AppError::Database)? .run() @@ -515,34 +504,23 @@ pub struct BulkRestoreResponse { } /// Restore multiple ciphers (PUT /api/ciphers/restore) +/// Accepts raw JSON body and uses json_each with path to extract ids directly. #[worker::send] pub async fn restore_ciphers_bulk( claims: Claims, State(env): State>, - Json(payload): Json, + body: String, ) -> Result, AppError> { let db = db::get_db(&env)?; let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(); - let ids = payload.ids; - if ids.is_empty() { - return Ok(Json(BulkRestoreResponse { - data: vec![], - object: "list".to_string(), - continuation_token: None, - })); - } - - // Precompute JSON array for ids, used by both UPDATE and SELECT - let ids_json = serde_json::to_string(&ids).map_err(|_| AppError::Internal)?; - - // Single bulk UPDATE using json_each() + // Single bulk UPDATE using json_each() with path query!( &db, - "UPDATE ciphers SET deleted_at = NULL, updated_at = ?1 WHERE user_id = ?2 AND id IN (SELECT value FROM json_each(?3))", + "UPDATE ciphers SET deleted_at = NULL, updated_at = ?1 WHERE user_id = ?2 AND id IN (SELECT value FROM json_each(?3, '$.ids'))", now, claims.sub, - ids_json.clone() + body ) .map_err(|_| AppError::Database)? .run() @@ -550,9 +528,9 @@ pub async fn restore_ciphers_bulk( let mut restored_ciphers: Vec = db .prepare( - "SELECT * FROM ciphers WHERE user_id = ?1 AND id IN (SELECT value FROM json_each(?2))", + "SELECT * FROM ciphers WHERE user_id = ?1 AND id IN (SELECT value FROM json_each(?2, '$.ids'))", ) - .bind(&[claims.sub.clone().into(), ids_json.into()])? + .bind(&[claims.sub.clone().into(), body.clone().into()])? .all() .await? .results::()? @@ -560,7 +538,14 @@ pub async fn restore_ciphers_bulk( .map(|cipher| cipher.into()) .collect(); - attachments::hydrate_ciphers_attachments(&db, env.as_ref(), &mut restored_ciphers).await?; + attachments::hydrate_ciphers_attachments( + &db, + env.as_ref(), + &mut restored_ciphers, + Some(&body), + Some("$.ids"), + ) + .await?; db::touch_user_updated_at(&db, &claims.sub).await?; diff --git a/src/handlers/sync.rs b/src/handlers/sync.rs index 60c5607..27fc07c 100644 --- a/src/handlers/sync.rs +++ b/src/handlers/sync.rs @@ -65,7 +65,7 @@ pub async fn get_sync_data( .collect::>(); let mut ciphers = ciphers; - attachments::hydrate_ciphers_attachments(&db, env.as_ref(), &mut ciphers).await?; + attachments::hydrate_ciphers_attachments(&db, env.as_ref(), &mut ciphers, None, None).await?; let profile = Profile::from_user(user)?;