Add wildcard and multi-value support for tag/search filters (#18)

- Tag filters now support wildcards (*) converted to SQL LIKE (%)
- Tag filters now support comma-separated multiple values
- Applied to /api/v1/artifacts, /api/v1/tags, and /api/v1/uploads endpoints
This commit is contained in:
Mondo Diaz
2026-01-06 16:02:44 -06:00
parent 55517220cd
commit 737f6fc379

View File

@@ -2593,7 +2593,10 @@ def list_package_artifacts(
def list_all_artifacts(
project: Optional[str] = Query(None, description="Filter by project name"),
package: Optional[str] = Query(None, description="Filter by package name"),
tag: Optional[str] = Query(None, description="Filter by tag name"),
tag: Optional[str] = Query(
None,
description="Filter by tag name. Supports wildcards (*) and comma-separated values",
),
content_type: Optional[str] = Query(None, description="Filter by content type"),
min_size: Optional[int] = Query(None, ge=0, description="Minimum size in bytes"),
max_size: Optional[int] = Query(None, ge=0, description="Maximum size in bytes"),
@@ -2628,7 +2631,26 @@ def list_all_artifacts(
if package:
tag_query = tag_query.filter(Package.name == package)
if tag:
tag_query = tag_query.filter(Tag.name == tag)
# Support multiple values (comma-separated) and wildcards (*)
tag_values = [t.strip() for t in tag.split(",") if t.strip()]
if len(tag_values) == 1:
tag_val = tag_values[0]
if "*" in tag_val:
# Wildcard: convert * to SQL LIKE %
tag_query = tag_query.filter(
Tag.name.ilike(tag_val.replace("*", "%"))
)
else:
tag_query = tag_query.filter(Tag.name == tag_val)
else:
# Multiple values: check if any match (with wildcard support)
tag_conditions = []
for tag_val in tag_values:
if "*" in tag_val:
tag_conditions.append(Tag.name.ilike(tag_val.replace("*", "%")))
else:
tag_conditions.append(Tag.name == tag_val)
tag_query = tag_query.filter(or_(*tag_conditions))
artifact_ids = tag_query.distinct().subquery()
query = query.filter(Artifact.id.in_(artifact_ids))
@@ -2722,7 +2744,10 @@ def list_all_artifacts(
def list_all_tags(
project: Optional[str] = Query(None, description="Filter by project name"),
package: Optional[str] = Query(None, description="Filter by package name"),
search: Optional[str] = Query(None, description="Search by tag name"),
search: Optional[str] = Query(
None,
description="Search by tag name. Supports wildcards (*) and comma-separated values",
),
from_date: Optional[datetime] = Query(
None, alias="from", description="Created after"
),
@@ -2749,7 +2774,24 @@ def list_all_tags(
if package:
query = query.filter(Package.name == package)
if search:
query = query.filter(Tag.name.ilike(f"%{search}%"))
# Support multiple values (comma-separated) and wildcards (*)
search_values = [s.strip() for s in search.split(",") if s.strip()]
if len(search_values) == 1:
search_val = search_values[0]
if "*" in search_val:
query = query.filter(Tag.name.ilike(search_val.replace("*", "%")))
else:
query = query.filter(Tag.name.ilike(f"%{search_val}%"))
else:
search_conditions = []
for search_val in search_values:
if "*" in search_val:
search_conditions.append(
Tag.name.ilike(search_val.replace("*", "%"))
)
else:
search_conditions.append(Tag.name.ilike(f"%{search_val}%"))
query = query.filter(or_(*search_conditions))
if from_date:
query = query.filter(Tag.created_at >= from_date)
if to_date:
@@ -4011,7 +4053,10 @@ def list_all_uploads(
None, description="Filter by deduplication status"
),
search: Optional[str] = Query(None, description="Search by original filename"),
tag: Optional[str] = Query(None, description="Filter by tag name"),
tag: Optional[str] = Query(
None,
description="Filter by tag name. Supports wildcards (*) and comma-separated values",
),
sort: Optional[str] = Query(
None, description="Sort field: uploaded_at, original_name, size"
),
@@ -4055,7 +4100,24 @@ def list_all_uploads(
if search:
query = query.filter(Upload.original_name.ilike(f"%{search}%"))
if tag:
query = query.filter(Upload.tag_name == tag)
# Support multiple values (comma-separated) and wildcards (*)
tag_values = [t.strip() for t in tag.split(",") if t.strip()]
if len(tag_values) == 1:
tag_val = tag_values[0]
if "*" in tag_val:
query = query.filter(Upload.tag_name.ilike(tag_val.replace("*", "%")))
else:
query = query.filter(Upload.tag_name == tag_val)
else:
tag_conditions = []
for tag_val in tag_values:
if "*" in tag_val:
tag_conditions.append(
Upload.tag_name.ilike(tag_val.replace("*", "%"))
)
else:
tag_conditions.append(Upload.tag_name == tag_val)
query = query.filter(or_(*tag_conditions))
# Validate and apply sorting
valid_sort_fields = {