diff --git a/Cargo.lock b/Cargo.lock index a08e351e3..b5766a416 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3456,6 +3456,7 @@ dependencies = [ "url", "urlencoding", "utoipa", + "utoipa-rapidoc", "utoipa-scalar", "uuid", "wiremock", @@ -5866,13 +5867,26 @@ dependencies = [ "quote", "regex", "syn 2.0.60", + "uuid", +] + +[[package]] +name = "utoipa-rapidoc" +version = "4.0.1-beta.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c836d406d590721b89f572cb0379479fcfbe27f31ca65c0775265a1b9026dd34" +dependencies = [ + "actix-web", + "serde", + "serde_json", + "utoipa", ] [[package]] name = "utoipa-scalar" -version = "0.2.0-alpha.0" +version = "0.2.0-beta.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c851bc855ea797c92d61de0cd4d36195c01657bc92e42a02fd1373897d6bfe7" +checksum = "bc86065a210b8540e46d15e0844765d1d14eec7fd6221c2b0de8f6edde990648" dependencies = [ "actix-web", "serde", diff --git a/meilisearch-types/Cargo.toml b/meilisearch-types/Cargo.toml index 7ca86950c..10b8b1bfb 100644 --- a/meilisearch-types/Cargo.toml +++ b/meilisearch-types/Cargo.toml @@ -37,7 +37,7 @@ time = { version = "0.3.36", features = [ "macros", ] } tokio = "1.38" -utoipa = { version = "5.0.0-alpha.1" } +utoipa = { version = "5.0.0-beta.0" } uuid = { version = "1.10.0", features = ["serde", "v4"] } [dev-dependencies] diff --git a/meilisearch-types/src/star_or.rs b/meilisearch-types/src/star_or.rs index cd26a1fb0..d6ef8fefd 100644 --- a/meilisearch-types/src/star_or.rs +++ b/meilisearch-types/src/star_or.rs @@ -6,6 +6,7 @@ use std::str::FromStr; use deserr::{DeserializeError, Deserr, MergeWithError, ValueKind}; use serde::de::Visitor; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use utoipa::{IntoParams, ToSchema}; use crate::deserr::query_params::FromQueryParameter; diff --git a/meilisearch-types/src/tasks.rs b/meilisearch-types/src/tasks.rs index caa1ce4c8..9c324749f 100644 --- a/meilisearch-types/src/tasks.rs +++ b/meilisearch-types/src/tasks.rs @@ -426,7 +426,7 @@ impl std::error::Error for ParseTaskStatusError {} /// The type of the task. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Sequence, ToSchema)] #[serde(rename_all = "camelCase")] -#[schema(rename_all = "camelCase", example = json!(Kind::DocumentAdditionOrUpdate))] +#[schema(rename_all = "camelCase", example = json!(enum_iterator::all::().collect::>()))] pub enum Kind { DocumentAdditionOrUpdate, DocumentEdition, @@ -443,6 +443,10 @@ pub enum Kind { } impl Kind { + pub fn all_variants() -> Vec { + enum_iterator::all::().collect() + } + pub fn related_to_one_index(&self) -> bool { match self { Kind::DocumentAdditionOrUpdate diff --git a/meilisearch/Cargo.toml b/meilisearch/Cargo.toml index 30c98e1ac..46a64ad24 100644 --- a/meilisearch/Cargo.toml +++ b/meilisearch/Cargo.toml @@ -104,8 +104,9 @@ tracing-trace = { version = "0.1.0", path = "../tracing-trace" } tracing-actix-web = "0.7.11" build-info = { version = "1.7.0", path = "../build-info" } roaring = "0.10.2" -utoipa = { version = "5.0.0-beta.0", features = ["actix_extras", "non_strict_integers"] } -utoipa-scalar = { version = "0.2.0-alpha.0", features = ["actix-web"] } +utoipa = { version = "5.0.0-beta.0", features = ["actix_extras", "non_strict_integers", "preserve_order", "uuid", "time", "openapi_extensions"] } +utoipa-scalar = { version = "0.2.0-beta.0", features = ["actix-web"] } +utoipa-rapidoc = { version = "4.0.1-beta.0", features = ["actix-web"] } [dev-dependencies] actix-rt = "2.10.0" diff --git a/meilisearch/src/routes/mod.rs b/meilisearch/src/routes/mod.rs index ed3f767b1..7aadafafd 100644 --- a/meilisearch/src/routes/mod.rs +++ b/meilisearch/src/routes/mod.rs @@ -8,7 +8,6 @@ use actix_web::web::Data; use actix_web::{web, HttpRequest, HttpResponse}; use index_scheduler::IndexScheduler; use meilisearch_auth::AuthController; -use meilisearch_types::deserr::query_params::Param; use meilisearch_types::error::{Code, ResponseError}; use meilisearch_types::settings::{Settings, Unchecked}; use meilisearch_types::tasks::{Kind, Status, Task, TaskId}; @@ -16,6 +15,7 @@ use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use tracing::debug; use utoipa::OpenApi; +use utoipa_rapidoc::RapiDoc; use utoipa_scalar::{Scalar, Servable as ScalarServable}; const PAGINATION_DEFAULT_LIMIT: usize = 20; @@ -27,6 +27,7 @@ pub mod indexes; mod logs; mod metrics; mod multi_search; +mod open_api_utils; mod snapshot; mod swap_indexes; pub mod tasks; @@ -42,6 +43,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) { cfg.service(web::scope("/tasks").configure(tasks::configure)) .service(Scalar::with_url("/scalar", openapi.clone())) + .service(RapiDoc::with_openapi("/api-docs/openapi.json", openapi).path("/rapidoc")) .service(web::resource("/health").route(web::get().to(get_health))) .service(web::scope("/logs").configure(logs::configure)) .service(web::scope("/keys").configure(api_key::configure)) diff --git a/meilisearch/src/routes/tasks.rs b/meilisearch/src/routes/tasks.rs index 89a1df722..bd8852949 100644 --- a/meilisearch/src/routes/tasks.rs +++ b/meilisearch/src/routes/tasks.rs @@ -23,6 +23,7 @@ use time::{Date, Duration, OffsetDateTime, Time}; use tokio::task; use utoipa::{IntoParams, OpenApi, ToSchema}; +use super::open_api_utils::OpenApiAuth; use super::{get_task_id, is_dry_run, SummarizedTaskView}; use crate::analytics::Analytics; use crate::extractors::authentication::policies::*; @@ -34,10 +35,14 @@ const DEFAULT_LIMIT: u32 = 20; #[derive(OpenApi)] #[openapi( - paths(get_tasks, delete_tasks), - info( - title = "The tasks route gives information about the progress of the [asynchronous operations](https://docs.meilisearch.com/learn/advanced/asynchronous_operations.html)." - ), + paths(get_tasks, delete_tasks, cancel_tasks, get_task), + tags(( + name = "Tasks", + description = "The tasks route gives information about the progress of the [asynchronous operations](https://docs.meilisearch.com/learn/advanced/asynchronous_operations.html).", + external_docs(url = "https://www.meilisearch.com/docs/reference/api/tasks"), + + )), + modifiers(&OpenApiAuth), components(schemas(AllTasks, TaskView, Status, DetailsView, ResponseError, Settings, Settings, TypoSettings, MinWordSizeTyposSetting, FacetingSettings, PaginationSettings)) )] pub struct TaskApi; @@ -67,38 +72,38 @@ pub struct TasksFilterQuery { /// Permits to filter tasks by their uid. By default, when the uids query parameter is not set, all task uids are returned. It's possible to specify several uids by separating them with the `,` character. #[deserr(default, error = DeserrQueryParamError)] - #[param(required = false, value_type = Option>, example = json!([231, 423, 598]))] + #[param(required = false, value_type = Option>, example = json!([231, 423, 598, "*"]))] pub uids: OptionStarOrList, #[deserr(default, error = DeserrQueryParamError)] - #[param(required = false, value_type = Option>, example = json!([374]))] + #[param(required = false, value_type = Option>, example = json!([374, "*"]))] pub canceled_by: OptionStarOrList, #[deserr(default, error = DeserrQueryParamError)] - #[param(required = false, value_type = Option>, example = json!([Kind::DocumentDeletion]))] + #[param(required = false, value_type = Option>, example = json!([Kind::DocumentAdditionOrUpdate, "*"]))] pub types: OptionStarOrList, #[deserr(default, error = DeserrQueryParamError)] - #[param(required = false, value_type = Option>, example = json!([Status::Succeeded, Status::Failed, Status::Canceled]))] + #[param(required = false, value_type = Option>, example = json!([Status::Succeeded, Status::Failed, Status::Canceled, Status::Enqueued, Status::Processing, "*"]))] pub statuses: OptionStarOrList, #[deserr(default, error = DeserrQueryParamError)] - #[param(required = false, value_type = Option>, example = json!(["movies", "theater"]))] + #[param(required = false, value_type = Option>, example = json!(["movies", "theater", "*"]))] pub index_uids: OptionStarOrList, #[deserr(default, error = DeserrQueryParamError, try_from(OptionStarOr) = deserialize_date_after -> InvalidTaskDateError)] - #[param(required = false, value_type = Option, example = json!("2024-08-08_16:37:09.971Z"))] + #[param(required = false, value_type = Option, example = json!(["2024-08-08T16:37:09.971Z", "*"]))] pub after_enqueued_at: OptionStarOr, #[deserr(default, error = DeserrQueryParamError, try_from(OptionStarOr) = deserialize_date_before -> InvalidTaskDateError)] - #[param(required = false, value_type = Option, example = json!("2024-08-08_16:37:09.971Z"))] + #[param(required = false, value_type = Option, example = json!(["2024-08-08T16:37:09.971Z", "*"]))] pub before_enqueued_at: OptionStarOr, #[deserr(default, error = DeserrQueryParamError, try_from(OptionStarOr) = deserialize_date_after -> InvalidTaskDateError)] - #[param(required = false, value_type = Option, example = json!("2024-08-08_16:37:09.971Z"))] + #[param(required = false, value_type = Option, example = json!(["2024-08-08T16:37:09.971Z", "*"]))] pub after_started_at: OptionStarOr, #[deserr(default, error = DeserrQueryParamError, try_from(OptionStarOr) = deserialize_date_before -> InvalidTaskDateError)] - #[param(required = false, value_type = Option, example = json!("2024-08-08_16:37:09.971Z"))] + #[param(required = false, value_type = Option, example = json!(["2024-08-08T16:37:09.971Z", "*"]))] pub before_started_at: OptionStarOr, #[deserr(default, error = DeserrQueryParamError, try_from(OptionStarOr) = deserialize_date_after -> InvalidTaskDateError)] - #[param(required = false, value_type = Option, example = json!("2024-08-08_16:37:09.971Z"))] + #[param(required = false, value_type = Option, example = json!(["2024-08-08T16:37:09.971Z", "*"]))] pub after_finished_at: OptionStarOr, #[deserr(default, error = DeserrQueryParamError, try_from(OptionStarOr) = deserialize_date_before -> InvalidTaskDateError)] - #[param(required = false, value_type = Option, example = json!("2024-08-08_16:37:09.971Z"))] + #[param(required = false, value_type = Option, example = json!(["2024-08-08T16:37:09.971Z", "*"]))] pub before_finished_at: OptionStarOr, } @@ -148,38 +153,38 @@ impl TaskDeletionOrCancelationQuery { #[into_params(rename_all = "camelCase", parameter_in = Query)] pub struct TaskDeletionOrCancelationQuery { #[deserr(default, error = DeserrQueryParamError)] - #[param(required = false, value_type = Option>, example = json!([231, 423, 598]))] + #[param(required = false, value_type = Option>, example = json!([231, 423, 598, "*"]))] pub uids: OptionStarOrList, #[deserr(default, error = DeserrQueryParamError)] - #[param(required = false, value_type = Option>, example = json!([374]))] + #[param(required = false, value_type = Option>, example = json!([374, "*"]))] pub canceled_by: OptionStarOrList, #[deserr(default, error = DeserrQueryParamError)] - #[param(required = false, value_type = Option>, example = json!([Kind::DocumentDeletion]))] + #[param(required = false, value_type = Option>, example = json!([Kind::DocumentDeletion, "*"]))] pub types: OptionStarOrList, #[deserr(default, error = DeserrQueryParamError)] - #[param(required = false, value_type = Option>, example = json!([Status::Succeeded, Status::Failed, Status::Canceled]))] + #[param(required = false, value_type = Option>, example = json!([Status::Succeeded, Status::Failed, Status::Canceled, "*"]))] pub statuses: OptionStarOrList, #[deserr(default, error = DeserrQueryParamError)] - #[param(required = false, value_type = Option>, example = json!(["movies", "theater"]))] + #[param(required = false, value_type = Option>, example = json!(["movies", "theater", "*"]))] pub index_uids: OptionStarOrList, #[deserr(default, error = DeserrQueryParamError, try_from(OptionStarOr) = deserialize_date_after -> InvalidTaskDateError)] - #[param(required = false, value_type = Option, example = json!("2024-08-08_16:37:09.971Z"))] + #[param(required = false, value_type = Option, example = json!(["2024-08-08T16:37:09.971Z", "*"]))] pub after_enqueued_at: OptionStarOr, #[deserr(default, error = DeserrQueryParamError, try_from(OptionStarOr) = deserialize_date_before -> InvalidTaskDateError)] - #[param(required = false, value_type = Option, example = json!("2024-08-08_16:37:09.971Z"))] + #[param(required = false, value_type = Option, example = json!(["2024-08-08T16:37:09.971Z", "*"]))] pub before_enqueued_at: OptionStarOr, #[deserr(default, error = DeserrQueryParamError, try_from(OptionStarOr) = deserialize_date_after -> InvalidTaskDateError)] - #[param(required = false, value_type = Option, example = json!("2024-08-08_16:37:09.971Z"))] + #[param(required = false, value_type = Option, example = json!(["2024-08-08T16:37:09.971Z", "*"]))] pub after_started_at: OptionStarOr, #[deserr(default, error = DeserrQueryParamError, try_from(OptionStarOr) = deserialize_date_before -> InvalidTaskDateError)] - #[param(required = false, value_type = Option, example = json!("2024-08-08_16:37:09.971Z"))] + #[param(required = false, value_type = Option, example = json!(["2024-08-08T16:37:09.971Z", "*"]))] pub before_started_at: OptionStarOr, #[deserr(default, error = DeserrQueryParamError, try_from(OptionStarOr) = deserialize_date_after -> InvalidTaskDateError)] - #[param(required = false, value_type = Option, example = json!("2024-08-08_16:37:09.971Z"))] + #[param(required = false, value_type = Option, example = json!(["2024-08-08T16:37:09.971Z", "*"]))] pub after_finished_at: OptionStarOr, #[deserr(default, error = DeserrQueryParamError, try_from(OptionStarOr) = deserialize_date_before -> InvalidTaskDateError)] - #[param(required = false, value_type = Option, example = json!("2024-08-08_16:37:09.971Z"))] + #[param(required = false, value_type = Option, example = json!(["2024-08-08T16:37:09.971Z", "*"]))] pub before_finished_at: OptionStarOr, } @@ -203,6 +208,24 @@ impl TaskDeletionOrCancelationQuery { } } +/// Cancel tasks +/// +/// Cancel enqueued and/or processing [tasks](https://www.meilisearch.com/docs/learn/async/asynchronous_operations) +#[utoipa::path( + post, + path = "/cancel", + tag = "Tasks", + params(TaskDeletionOrCancelationQuery), + responses( + (status = 200, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!({ + "taskUid": 147, + "indexUid": null, + "status": "enqueued", + "type": "taskDeletion", + "enqueuedAt": "2024-08-08T17:05:55.791772Z" + })) + ) +)] async fn cancel_tasks( index_scheduler: GuardedData, Data>, params: AwebQueryParameter, @@ -260,6 +283,7 @@ async fn cancel_tasks( #[utoipa::path( delete, path = "", + tag = "Tasks", params(TaskDeletionOrCancelationQuery), responses( (status = 200, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!({ @@ -333,12 +357,15 @@ pub struct AllTasks { next: Option, } + /// Get all tasks /// /// Get all [tasks](https://docs.meilisearch.com/learn/advanced/asynchronous_operations.html) #[utoipa::path( get, path = "", + tag = "Tasks", + security(("Bearer" = ["tasks.get", "tasks.*", "*"])), params(TasksFilterQuery), responses( (status = 200, description = "Get all tasks", body = AllTasks, content_type = "application/json", example = json!({ @@ -394,6 +421,52 @@ async fn get_tasks( Ok(HttpResponse::Ok().json(tasks)) } +/// Get a task +/// +/// Get a [task](https://www.meilisearch.com/docs/learn/async/asynchronous_operations) +#[utoipa::path( + get, + path = "/{taskUid}", + tag = "Tasks", + security(("Bearer" = ["tasks.get", "tasks.*", "*"])), + params(("taskUid", format = UInt32, example = 0, description = "The task identifier", nullable = false)), + responses( + (status = 200, description = "Task successfully retrieved", body = TaskView, content_type = "application/json", example = json!( + { + "uid": 1, + "indexUid": "movies", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 79000, + "indexedDocuments": 79000 + }, + "error": null, + "duration": "PT1S", + "enqueuedAt": "2021-01-01T09:39:00.000000Z", + "startedAt": "2021-01-01T09:39:01.000000Z", + "finishedAt": "2021-01-01T09:39:02.000000Z" + } + )), + (status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!( + { + "message": "The Authorization header is missing. It must use the bearer authorization method.", + "code": "missing_authorization_header", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#missing_authorization_header" + } + )), + (status = 404, description = "The task uid does not exists", body = ResponseError, content_type = "application/json", example = json!( + { + "message": "Task :taskUid not found.", + "code": "task_not_found", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors/#task_not_found" + } + )) + ) +)] async fn get_task( index_scheduler: GuardedData, Data>, task_uid: web::Path,