deleted export
This commit is contained in:
@@ -2008,26 +2008,44 @@ pub async fn delete_assets_bulk(
|
||||
pub async fn export_assets(
|
||||
path: String,
|
||||
format: String,
|
||||
// Both default to false → live-only export (existing behavior).
|
||||
// include_deleted = true → live + deleted rows in one list
|
||||
// only_deleted = true → deleted rows only (implies include)
|
||||
// The UI drives these from the Visibility panel's own toggles so the
|
||||
// export scope always matches what the user is looking at.
|
||||
#[allow(non_snake_case)] include_deleted: Option<bool>,
|
||||
#[allow(non_snake_case)] only_deleted: Option<bool>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<usize, String> {
|
||||
let assets: Vec<Asset> = sqlx::query_as(
|
||||
let include_deleted = include_deleted.unwrap_or(false);
|
||||
let only_deleted = only_deleted.unwrap_or(false);
|
||||
let where_clause = if only_deleted {
|
||||
"WHERE deleted = 1"
|
||||
} else if include_deleted {
|
||||
"WHERE 1 = 1"
|
||||
} else {
|
||||
"WHERE deleted = 0"
|
||||
};
|
||||
let sql = format!(
|
||||
"SELECT id, row_id, asset_type, asset_name, video_name,
|
||||
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
||||
image_path, image_path1, image_path2, image_path3,
|
||||
deleted, modified, merged_into, in_scope, side, lane_side, link_pair_id, link_locked
|
||||
FROM assets WHERE deleted = 0 ORDER BY asset_type, asset_name, id",
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
FROM assets {} ORDER BY asset_type, asset_name, id",
|
||||
where_clause
|
||||
);
|
||||
let assets: Vec<Asset> = sqlx::query_as(&sql)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Resolve link partner ids → row_ids. The raw `link_pair_id` is an
|
||||
// internal autoincrement that changes on every re-import, so a CSV
|
||||
// consumer can't tell who linked to whom by looking at the number.
|
||||
// Look up the partner's row_id once and surface it as a parallel column
|
||||
// ("link_pair_row_id") in every format. Falls back to None when the
|
||||
// partner row is deleted (and thus not in `assets`) — flagged so the
|
||||
// user can see the dangling link.
|
||||
// partner row isn't in the exported set (e.g. deleted while we're
|
||||
// exporting live-only) — flagged so the user can see the dangling link.
|
||||
let row_id_by_id: std::collections::HashMap<i64, String> = assets
|
||||
.iter()
|
||||
.map(|a| (a.id, a.row_id.clone()))
|
||||
@@ -2036,14 +2054,16 @@ pub async fn export_assets(
|
||||
pid.and_then(|p| row_id_by_id.get(&p).cloned())
|
||||
};
|
||||
|
||||
// Pre-fetch all bboxes once so every format branch that wants to surface
|
||||
// them can do so without re-querying. Grouped by asset_id → slot.
|
||||
let bbox_rows_all: Vec<(i64, String, f64, f64, f64, f64)> = sqlx::query_as(
|
||||
// Pre-fetch all bboxes for the rows we're exporting. Match the same
|
||||
// deleted-scope so a deleted row's bboxes ride along when it does.
|
||||
let bbox_sql = format!(
|
||||
"SELECT b.asset_id, b.slot, b.x1, b.y1, b.x2, b.y2
|
||||
FROM asset_bboxes b
|
||||
JOIN assets a ON a.id = b.asset_id
|
||||
WHERE a.deleted = 0",
|
||||
)
|
||||
{}",
|
||||
where_clause.replace("deleted", "a.deleted")
|
||||
);
|
||||
let bbox_rows_all: Vec<(i64, String, f64, f64, f64, f64)> = sqlx::query_as(&bbox_sql)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
@@ -2213,8 +2233,14 @@ pub async fn export_assets(
|
||||
map.insert(format!("{prefix}bbox_y2"), serde_json::json!(bb.3));
|
||||
}
|
||||
};
|
||||
// Bucket into four lists so the envelope can split live from
|
||||
// deleted rows explicitly. Downstream scripts that only care
|
||||
// about live data ignore the `deleted_*` keys; audit tools read
|
||||
// them. Empty lists are omitted from the envelope.
|
||||
let mut fixed_arr: Vec<serde_json::Value> = vec![];
|
||||
let mut range_arr: Vec<serde_json::Value> = vec![];
|
||||
let mut deleted_fixed: Vec<serde_json::Value> = vec![];
|
||||
let mut deleted_range: Vec<serde_json::Value> = vec![];
|
||||
for a in &assets {
|
||||
if a.asset_type == "fixed" {
|
||||
if let (Some(la), Some(ln)) = (a.lat, a.lng) {
|
||||
@@ -2235,7 +2261,11 @@ pub async fn export_assets(
|
||||
inject_bbox(&mut obj, "", bb);
|
||||
}
|
||||
}
|
||||
fixed_arr.push(obj);
|
||||
if a.deleted != 0 {
|
||||
deleted_fixed.push(obj);
|
||||
} else {
|
||||
fixed_arr.push(obj);
|
||||
}
|
||||
}
|
||||
} else if a.asset_type == "range" {
|
||||
if let (Some(la), Some(ln), Some(sla), Some(sln), Some(ela), Some(eln)) =
|
||||
@@ -2271,15 +2301,25 @@ pub async fn export_assets(
|
||||
inject_bbox(&mut obj, "end_", bb);
|
||||
}
|
||||
}
|
||||
range_arr.push(obj);
|
||||
if a.deleted != 0 {
|
||||
deleted_range.push(obj);
|
||||
} else {
|
||||
range_arr.push(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
serde_json::to_string_pretty(&serde_json::json!({
|
||||
"fixed": fixed_arr,
|
||||
"range": range_arr,
|
||||
}))
|
||||
.map_err(|e| e.to_string())?
|
||||
let mut envelope = serde_json::Map::new();
|
||||
envelope.insert("fixed".into(), serde_json::json!(fixed_arr));
|
||||
envelope.insert("range".into(), serde_json::json!(range_arr));
|
||||
if !deleted_fixed.is_empty() {
|
||||
envelope.insert("deleted_fixed".into(), serde_json::json!(deleted_fixed));
|
||||
}
|
||||
if !deleted_range.is_empty() {
|
||||
envelope.insert("deleted_range".into(), serde_json::json!(deleted_range));
|
||||
}
|
||||
serde_json::to_string_pretty(&serde_json::Value::Object(envelope))
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
"csv" => {
|
||||
let mut s = String::new();
|
||||
|
||||
Reference in New Issue
Block a user