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:
@@ -2593,7 +2593,10 @@ def list_package_artifacts(
|
|||||||
def list_all_artifacts(
|
def list_all_artifacts(
|
||||||
project: Optional[str] = Query(None, description="Filter by project name"),
|
project: Optional[str] = Query(None, description="Filter by project name"),
|
||||||
package: Optional[str] = Query(None, description="Filter by package 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"),
|
content_type: Optional[str] = Query(None, description="Filter by content type"),
|
||||||
min_size: Optional[int] = Query(None, ge=0, description="Minimum size in bytes"),
|
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"),
|
max_size: Optional[int] = Query(None, ge=0, description="Maximum size in bytes"),
|
||||||
@@ -2628,7 +2631,26 @@ def list_all_artifacts(
|
|||||||
if package:
|
if package:
|
||||||
tag_query = tag_query.filter(Package.name == package)
|
tag_query = tag_query.filter(Package.name == package)
|
||||||
if tag:
|
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()
|
artifact_ids = tag_query.distinct().subquery()
|
||||||
query = query.filter(Artifact.id.in_(artifact_ids))
|
query = query.filter(Artifact.id.in_(artifact_ids))
|
||||||
|
|
||||||
@@ -2722,7 +2744,10 @@ def list_all_artifacts(
|
|||||||
def list_all_tags(
|
def list_all_tags(
|
||||||
project: Optional[str] = Query(None, description="Filter by project name"),
|
project: Optional[str] = Query(None, description="Filter by project name"),
|
||||||
package: Optional[str] = Query(None, description="Filter by package 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(
|
from_date: Optional[datetime] = Query(
|
||||||
None, alias="from", description="Created after"
|
None, alias="from", description="Created after"
|
||||||
),
|
),
|
||||||
@@ -2749,7 +2774,24 @@ def list_all_tags(
|
|||||||
if package:
|
if package:
|
||||||
query = query.filter(Package.name == package)
|
query = query.filter(Package.name == package)
|
||||||
if search:
|
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:
|
if from_date:
|
||||||
query = query.filter(Tag.created_at >= from_date)
|
query = query.filter(Tag.created_at >= from_date)
|
||||||
if to_date:
|
if to_date:
|
||||||
@@ -4011,7 +4053,10 @@ def list_all_uploads(
|
|||||||
None, description="Filter by deduplication status"
|
None, description="Filter by deduplication status"
|
||||||
),
|
),
|
||||||
search: Optional[str] = Query(None, description="Search by original filename"),
|
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(
|
sort: Optional[str] = Query(
|
||||||
None, description="Sort field: uploaded_at, original_name, size"
|
None, description="Sort field: uploaded_at, original_name, size"
|
||||||
),
|
),
|
||||||
@@ -4055,7 +4100,24 @@ def list_all_uploads(
|
|||||||
if search:
|
if search:
|
||||||
query = query.filter(Upload.original_name.ilike(f"%{search}%"))
|
query = query.filter(Upload.original_name.ilike(f"%{search}%"))
|
||||||
if tag:
|
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
|
# Validate and apply sorting
|
||||||
valid_sort_fields = {
|
valid_sort_fields = {
|
||||||
|
|||||||
Reference in New Issue
Block a user