refactor: directly pass json body to d1 api in attachment handling to avoid parsing
This commit is contained in:
parent
eb6a1d8a63
commit
b1ea9183bc
3 changed files with 76 additions and 81 deletions
|
|
@ -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<String> = 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<AttachmentKeyRow>) -> Vec<String> {
|
|||
.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<Vec<String>, 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<worker::wasm_bindgen::JsValue> = 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<worker::wasm_bindgen::JsValue> =
|
||||
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<Attach
|
|||
.ok_or_else(|| AppError::NotFound("Attachment not found".to_string()))
|
||||
}
|
||||
|
||||
async fn load_attachment_map(
|
||||
async fn load_attachment_map_json(
|
||||
db: &D1Database,
|
||||
cipher_ids: &[String],
|
||||
json_body: &str,
|
||||
ids_path: &str,
|
||||
) -> Result<HashMap<String, Vec<AttachmentResponse>>, 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<AttachmentDB> = 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)?
|
||||
|
|
|
|||
|
|
@ -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<Cipher> = 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<String>,
|
||||
}
|
||||
|
||||
/// 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<Arc<Env>>,
|
||||
Json(payload): Json<CipherIdsData>,
|
||||
body: String,
|
||||
) -> Result<Json<()>, 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<Arc<Env>>,
|
||||
Json(payload): Json<CipherIdsData>,
|
||||
body: String,
|
||||
) -> Result<Json<()>, 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<Arc<Env>>,
|
||||
Json(payload): Json<CipherIdsData>,
|
||||
body: String,
|
||||
) -> Result<Json<BulkRestoreResponse>, 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<Cipher> = 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::<crate::models::cipher::CipherDBModel>()?
|
||||
|
|
@ -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?;
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ pub async fn get_sync_data(
|
|||
.collect::<Vec<Cipher>>();
|
||||
|
||||
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)?;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue