feat: frontend-ready backend — pagination, auto-derivatives, list endpoints, bulk ops, OpenAPI

Pagination: count_by_owner + count_search on AssetRepository,
timeline/search return real total count (not page len).

Auto-derivatives: worker enqueues GenerateDerivative when
ExtractMetadata job completes, closing the upload→thumbnail gap.

List endpoints: GET /albums, GET /stacks with user scoping.
ListAlbumsHandler, ListStacksHandler, find_by_owner on AssetStackRepository.

Tag filtering: tag_name field on AssetFilters, JOIN asset_tags+tags
in postgres search/count queries.

Bulk operations: POST /assets/bulk-delete, POST /assets/bulk-tag.

Album update: PUT /albums/{id} with UpdateAlbumHandler (title, description).

OpenAPI: utoipa annotations on all 47 endpoints + all request/response
schemas registered. Scalar UI at /scalar covers full API.
This commit is contained in:
2026-05-31 23:06:25 +02:00
parent bcaf49cc81
commit 7b5bb66b37
33 changed files with 1048 additions and 72 deletions

View File

@@ -40,6 +40,19 @@ pub struct ListJobsParams {
pub offset: Option<u32>,
}
#[utoipa::path(
get, path = "/api/v1/jobs",
security(("bearer_token" = [])),
params(
("status" = Option<String>, Query, description = "Status filter"),
("limit" = Option<u32>, Query, description = "Page size"),
("offset" = Option<u32>, Query, description = "Page offset")
),
responses(
(status = 200, description = "Job list", body = JobListResponse),
(status = 401, description = "Unauthorized")
)
)]
pub async fn list_jobs(
State(state): State<AppState>,
claims: JwtClaims,
@@ -59,6 +72,15 @@ pub async fn list_jobs(
}))
}
#[utoipa::path(
post, path = "/api/v1/jobs",
request_body = EnqueueJobRequest,
security(("bearer_token" = [])),
responses(
(status = 201, description = "Job enqueued", body = JobResponse),
(status = 401, description = "Unauthorized")
)
)]
pub async fn enqueue_job(
State(state): State<AppState>,
claims: JwtClaims,
@@ -82,6 +104,15 @@ pub async fn enqueue_job(
Ok((StatusCode::CREATED, Json(JobResponse::from_domain(&job))))
}
#[utoipa::path(
post, path = "/api/v1/jobs/{id}/start",
security(("bearer_token" = [])),
params(("id" = uuid::Uuid, Path, description = "Job ID")),
responses(
(status = 200, description = "Job started", body = JobResponse),
(status = 404, description = "Not found")
)
)]
pub async fn start_job(
State(state): State<AppState>,
claims: JwtClaims,
@@ -95,6 +126,16 @@ pub async fn start_job(
Ok(Json(JobResponse::from_domain(&job)))
}
#[utoipa::path(
post, path = "/api/v1/jobs/{id}/complete",
request_body = CompleteJobRequest,
security(("bearer_token" = [])),
params(("id" = uuid::Uuid, Path, description = "Job ID")),
responses(
(status = 200, description = "Job completed", body = JobResponse),
(status = 404, description = "Not found")
)
)]
pub async fn complete_job(
State(state): State<AppState>,
claims: JwtClaims,
@@ -110,6 +151,16 @@ pub async fn complete_job(
Ok(Json(JobResponse::from_domain(&job)))
}
#[utoipa::path(
post, path = "/api/v1/jobs/{id}/fail",
request_body = FailJobRequest,
security(("bearer_token" = [])),
params(("id" = uuid::Uuid, Path, description = "Job ID")),
responses(
(status = 200, description = "Job failed", body = JobResponse),
(status = 404, description = "Not found")
)
)]
pub async fn fail_job(
State(state): State<AppState>,
claims: JwtClaims,
@@ -125,6 +176,15 @@ pub async fn fail_job(
Ok(Json(JobResponse::from_domain(&job)))
}
#[utoipa::path(
get, path = "/api/v1/jobs/batches/{id}",
security(("bearer_token" = [])),
params(("id" = uuid::Uuid, Path, description = "Batch ID")),
responses(
(status = 200, description = "Batch progress", body = BatchProgressResponse),
(status = 404, description = "Not found")
)
)]
pub async fn batch_progress(
State(state): State<AppState>,
claims: JwtClaims,
@@ -138,6 +198,15 @@ pub async fn batch_progress(
Ok(Json(BatchProgressResponse::from_domain(&progress)))
}
#[utoipa::path(
post, path = "/api/v1/plugins",
request_body = ManagePluginRequest,
security(("bearer_token" = [])),
responses(
(status = 201, description = "Plugin managed", body = PluginResponse),
(status = 401, description = "Unauthorized")
)
)]
pub async fn manage_plugin(
State(state): State<AppState>,
claims: JwtClaims,
@@ -182,6 +251,15 @@ pub async fn manage_plugin(
))
}
#[utoipa::path(
post, path = "/api/v1/pipelines",
request_body = ConfigurePipelineRequest,
security(("bearer_token" = [])),
responses(
(status = 201, description = "Pipeline configured", body = PipelineResponse),
(status = 401, description = "Unauthorized")
)
)]
pub async fn configure_pipeline(
State(state): State<AppState>,
claims: JwtClaims,