feat: allow --list to accept item IDs for filtering

- Local and client/server modes now support ID-based filtering
- keep -l 1 2 3 lists specific items by ID
- keep -l --ids-only 1 2 3 outputs just those IDs
- Server API adds optional 'ids' query parameter to GET /api/item/
- KeepClient.list_items gains ids parameter
This commit is contained in:
2026-03-17 17:56:35 -03:00
parent 02f0c8d453
commit 074ba64805
7 changed files with 39 additions and 21 deletions

View File

@@ -253,6 +253,7 @@ impl KeepClient {
pub fn list_items(
&self,
ids: &[i64],
tags: &[String],
order: &str,
start: u64,
@@ -268,6 +269,15 @@ impl KeepClient {
params.push(("order".to_string(), order.to_string()));
params.push(("start".to_string(), start.to_string()));
params.push(("count".to_string(), count.to_string()));
if !ids.is_empty() {
params.push((
"ids".to_string(),
ids.iter()
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join(","),
));
}
if !tags.is_empty() {
params.push(("tags".to_string(), tags.join(",")));
}

View File

@@ -81,7 +81,7 @@ fn main() -> Result<(), Error> {
let ids = &mut Vec::new();
let tags = &mut Vec::new();
// For --info, --get, and --export modes, treat numeric strings as IDs
// For --info, --get, --export, and --list modes, treat numeric strings as IDs
for v in args.ids_or_tags.iter() {
debug!("MAIN: Parsed value: {v:?}");
match v.clone() {
@@ -90,15 +90,15 @@ fn main() -> Result<(), Error> {
ids.push(num)
}
NumberOrString::Str(str) => {
// For --info, --get, and --export, try to parse strings as numbers to treat them as IDs
if (args.mode.info || args.mode.get || args.mode.export)
// For --info, --get, --export, and --list, try to parse strings as numbers to treat them as IDs
if (args.mode.info || args.mode.get || args.mode.export || args.mode.list)
&& let Ok(num) = str.parse::<i64>()
{
debug!("MAIN: Adding parsed string to ids: {num}");
ids.push(num);
continue;
}
// If not a number, or not using --info/--get/--export, treat as tag
// If not a number, or not using --info/--get/--export/--list, treat as tag
debug!("MAIN: Adding to tags: {str}");
tags.push(str)
}
@@ -256,7 +256,7 @@ fn main() -> Result<(), Error> {
filter_chain,
),
KeepModes::List => {
keep::modes::client::list::mode(&client, &mut cmd, &settings, tags)
keep::modes::client::list::mode(&client, &mut cmd, &settings, ids, tags)
}
KeepModes::Delete => {
keep::modes::client::delete::mode(&client, &mut cmd, &settings, ids)

View File

@@ -10,6 +10,7 @@ pub fn mode(
client: &KeepClient,
_cmd: &mut Command,
settings: &crate::config::Settings,
ids: &[i64],
tags: &[String],
) -> Result<(), anyhow::Error> {
debug!("CLIENT_LIST: Listing items via remote server");
@@ -19,7 +20,7 @@ pub fn mode(
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let items = client.list_items(tags, "newest", 0, 100, &meta_filter)?;
let items = client.list_items(ids, tags, "newest", 0, 100, &meta_filter)?;
if settings.ids_only {
for item in &items {

View File

@@ -673,13 +673,13 @@ pub fn resolve_item_id(
if !ids.is_empty() {
Ok(ids[0])
} else if !tags.is_empty() {
let items = client.list_items(tags, "newest", 0, 1, &HashMap::new())?;
let items = client.list_items(&[], tags, "newest", 0, 1, &HashMap::new())?;
if items.is_empty() {
return Err(anyhow!("No items found matching tags: {:?}", tags));
}
Ok(items[0].id)
} else {
let items = client.list_items(&[], "newest", 0, 1, &HashMap::new())?;
let items = client.list_items(&[], &[], "newest", 0, 1, &HashMap::new())?;
if items.is_empty() {
return Err(anyhow!("No items found"));
}
@@ -696,13 +696,13 @@ pub fn resolve_item_ids(
if !ids.is_empty() {
Ok(ids.to_vec())
} else if !tags.is_empty() {
let items = client.list_items(tags, "newest", 0, 0, &HashMap::new())?;
let items = client.list_items(&[], tags, "newest", 0, 0, &HashMap::new())?;
if items.is_empty() {
return Err(anyhow!("No items found matching tags: {:?}", tags));
}
Ok(items.into_iter().map(|i| i.id).collect())
} else {
let items = client.list_items(&[], "newest", 0, 1, &HashMap::new())?;
let items = client.list_items(&[], &[], "newest", 0, 1, &HashMap::new())?;
if items.is_empty() {
return Err(anyhow!("No items found"));
}

View File

@@ -81,28 +81,20 @@ struct ListItem {
///
/// * `Result<()>` - Success or error if listing fails.
pub fn mode_list(
cmd: &mut clap::Command,
_cmd: &mut clap::Command,
settings: &config::Settings,
ids: &mut [i64],
tags: &[String],
conn: &mut rusqlite::Connection,
data_path: std::path::PathBuf,
) -> Result<()> {
if !ids.is_empty() {
cmd.error(
clap::error::ErrorKind::InvalidValue,
"ID given, you can only supply tags when using --list",
)
.exit();
}
let item_service = ItemService::new(data_path.clone());
let meta_filter: std::collections::HashMap<String, Option<String>> = settings
.meta
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let items_with_meta = item_service.list_items(conn, tags, &meta_filter)?;
let items_with_meta = item_service.get_items(conn, ids, tags, &meta_filter)?;
if settings.ids_only {
for item_with_meta in &items_with_meta {

View File

@@ -114,6 +114,17 @@ pub async fn handle_list_items(
State(state): State<AppState>,
Query(params): Query<ListItemsQuery>,
) -> Result<Response, StatusCode> {
// Parse IDs from query parameter
let ids: Vec<i64> = params
.ids
.as_ref()
.map(|s| {
s.split(',')
.filter_map(|id| id.trim().parse::<i64>().ok())
.collect()
})
.unwrap_or_default();
let tags: Vec<String> = params
.tags
.as_ref()
@@ -136,7 +147,7 @@ pub async fn handle_list_items(
let mut items_with_meta = task::spawn_blocking(move || {
let conn = db.blocking_lock();
let item_service = ItemService::new(data_dir);
item_service.list_items(&conn, &tags, &meta_filter)
item_service.get_items(&conn, &ids, &tags, &meta_filter)
})
.await
.map_err(|e| {

View File

@@ -459,6 +459,10 @@ pub struct TagsQuery {
/// ```
#[derive(Debug, Deserialize)]
pub struct ListItemsQuery {
/// Optional comma-separated item IDs for filtering.
///
/// String containing numeric IDs to filter the item list.
pub ids: Option<String>,
/// Optional comma-separated tags for filtering.
///
/// String containing tags to filter the item list.