From 4859a36cb6fc4b57417c7c72f053f6665cb3f691 Mon Sep 17 00:00:00 2001 From: Tamo Date: Thu, 8 Aug 2024 19:14:19 +0200 Subject: [PATCH] Implements the get and delete tasks route --- Cargo.lock | 39 +++++++ meilisearch-types/Cargo.toml | 1 + meilisearch-types/src/deserr/query_params.rs | 7 ++ meilisearch-types/src/error.rs | 4 +- meilisearch-types/src/settings.rs | 59 ++++++++-- meilisearch-types/src/task_view.rs | 38 ++++++- meilisearch-types/src/tasks.rs | 9 +- meilisearch/Cargo.toml | 2 + meilisearch/src/main.rs | 1 + meilisearch/src/routes/mod.rs | 23 +++- meilisearch/src/routes/tasks.rs | 111 ++++++++++++++++++- 11 files changed, 271 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bcca35173..fdfb8c738 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3455,6 +3455,8 @@ dependencies = [ "tracing-trace", "url", "urlencoding", + "utoipa", + "utoipa-scalar", "uuid", "wiremock", "yaup", @@ -3507,6 +3509,7 @@ dependencies = [ "thiserror", "time", "tokio", + "utoipa", "uuid", ] @@ -5841,6 +5844,42 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "utoipa" +version = "5.0.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c082de846a4d434a9dcfe3358dbe4a0aa5d4f826c3af29cdbd97404e1ffe71f4" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "5.0.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a71c23e17df16027cc552b5b249a2a5e6a1ea36ab37363a1ac29b69ab36035" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.60", +] + +[[package]] +name = "utoipa-scalar" +version = "0.2.0-alpha.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c851bc855ea797c92d61de0cd4d36195c01657bc92e42a02fd1373897d6bfe7" +dependencies = [ + "actix-web", + "serde", + "serde_json", + "utoipa", +] + [[package]] name = "uuid" version = "1.10.0" diff --git a/meilisearch-types/Cargo.toml b/meilisearch-types/Cargo.toml index cb4937e57..7ca86950c 100644 --- a/meilisearch-types/Cargo.toml +++ b/meilisearch-types/Cargo.toml @@ -37,6 +37,7 @@ time = { version = "0.3.36", features = [ "macros", ] } tokio = "1.38" +utoipa = { version = "5.0.0-alpha.1" } uuid = { version = "1.10.0", features = ["serde", "v4"] } [dev-dependencies] diff --git a/meilisearch-types/src/deserr/query_params.rs b/meilisearch-types/src/deserr/query_params.rs index dded0ea5c..d3ee25efa 100644 --- a/meilisearch-types/src/deserr/query_params.rs +++ b/meilisearch-types/src/deserr/query_params.rs @@ -16,6 +16,7 @@ use std::ops::Deref; use std::str::FromStr; use deserr::{DeserializeError, Deserr, MergeWithError, ValueKind}; +use utoipa::ToSchema; use super::{DeserrParseBoolError, DeserrParseIntError}; use crate::index_uid::IndexUid; @@ -29,6 +30,12 @@ use crate::tasks::{Kind, Status}; #[derive(Default, Debug, Clone, Copy)] pub struct Param(pub T); +impl<'a, T: ToSchema<'a>> ToSchema<'a> for Param { + fn schema() -> (&'a str, utoipa::openapi::RefOr) { + T::schema() + } +} + impl Deref for Param { type Target = T; diff --git a/meilisearch-types/src/error.rs b/meilisearch-types/src/error.rs index 514ed18c3..b8651a39d 100644 --- a/meilisearch-types/src/error.rs +++ b/meilisearch-types/src/error.rs @@ -7,9 +7,11 @@ use aweb::rt::task::JoinError; use convert_case::Casing; use milli::heed::{Error as HeedError, MdbError}; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)] #[serde(rename_all = "camelCase")] +#[schema(rename_all = "camelCase")] pub struct ResponseError { #[serde(skip)] pub code: StatusCode, diff --git a/meilisearch-types/src/settings.rs b/meilisearch-types/src/settings.rs index 9e7a2bc15..2123a8e27 100644 --- a/meilisearch-types/src/settings.rs +++ b/meilisearch-types/src/settings.rs @@ -13,6 +13,7 @@ use milli::proximity::ProximityPrecision; use milli::update::Setting; use milli::{Criterion, CriterionError, Index, DEFAULT_VALUES_PER_FACET}; use serde::{Deserialize, Serialize, Serializer}; +use utoipa::ToSchema; use crate::deserr::DeserrJsonError; use crate::error::deserr_codes::*; @@ -69,54 +70,63 @@ fn validate_min_word_size_for_typo_setting( Ok(s) } -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr, ToSchema)] #[serde(deny_unknown_fields, rename_all = "camelCase")] #[deserr(deny_unknown_fields, rename_all = camelCase, validate = validate_min_word_size_for_typo_setting -> DeserrJsonError)] pub struct MinWordSizeTyposSetting { #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default)] + #[schema(value_type = Option, example = json!(5))] pub one_typo: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default)] + #[schema(value_type = Option, example = json!(9))] pub two_typos: Setting, } -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr, ToSchema)] #[serde(deny_unknown_fields, rename_all = "camelCase")] #[deserr(deny_unknown_fields, rename_all = camelCase, where_predicate = __Deserr_E: deserr::MergeWithError>)] pub struct TypoSettings { #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default)] + #[schema(value_type = Option, example = json!(true))] pub enabled: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default, error = DeserrJsonError)] + #[schema(value_type = Option, example = json!({ "oneTypo": 5, "twoTypo": 9 }))] pub min_word_size_for_typos: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default)] + #[schema(value_type = Option>, example = json!(["iPhone", "phone"]))] pub disable_on_words: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default)] + #[schema(value_type = Option>, example = json!(["uuid", "url"]))] pub disable_on_attributes: Setting>, } -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr, ToSchema)] #[serde(deny_unknown_fields, rename_all = "camelCase")] #[deserr(rename_all = camelCase, deny_unknown_fields)] pub struct FacetingSettings { #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default)] + #[schema(value_type = Option, example = json!(10))] pub max_values_per_facet: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default)] + #[schema(value_type = Option>, example = json!({ "genre": FacetValuesSort::Count }))] pub sort_facet_values_by: Setting>, } -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr, ToSchema)] #[serde(deny_unknown_fields, rename_all = "camelCase")] #[deserr(rename_all = camelCase, deny_unknown_fields)] pub struct PaginationSettings { #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default)] + #[schema(value_type = Option, example = json!(250))] pub max_total_hits: Setting, } @@ -137,67 +147,100 @@ impl MergeWithError for DeserrJsonError` from a `Settings`. -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr, ToSchema)] #[serde( deny_unknown_fields, rename_all = "camelCase", bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'static>") )] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] +#[schema(rename_all = "camelCase")] pub struct Settings { + /// Fields displayed in the returned documents. #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default, error = DeserrJsonError)] + #[schema(value_type = Option>, example = json!(["id", "title", "description", "url"]))] pub displayed_attributes: WildcardSetting, - + /// Fields in which to search for matching query words sorted by order of importance. #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default, error = DeserrJsonError)] + #[schema(value_type = Option>, example = json!(["title", "description"]))] pub searchable_attributes: WildcardSetting, - + /// Attributes to use for faceting and filtering. See [Filtering and Faceted Search](https://www.meilisearch.com/docs/learn/filtering_and_sorting/search_with_facet_filters). #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default, error = DeserrJsonError)] + #[schema(value_type = Option>, example = json!(["release_date", "genre"]))] pub filterable_attributes: Setting>, + /// Attributes to use when sorting search results. #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default, error = DeserrJsonError)] + #[schema(value_type = Option>, example = json!(["release_date"]))] pub sortable_attributes: Setting>, + /// List of ranking rules sorted by order of importance. The order is customizable. + /// [A list of ordered built-in ranking rules](https://www.meilisearch.com/docs/learn/relevancy/relevancy). #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default, error = DeserrJsonError)] + #[schema(value_type = Option>, example = json!([RankingRuleView::Words, RankingRuleView::Typo, RankingRuleView::Proximity, RankingRuleView::Attribute, RankingRuleView::Exactness]))] pub ranking_rules: Setting>, + /// List of words ignored when present in search queries. #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default, error = DeserrJsonError)] + #[schema(value_type = Option>, example = json!(["the", "a", "them", "their"]))] pub stop_words: Setting>, + /// List of characters not delimiting where one term begins and ends. #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default, error = DeserrJsonError)] + #[schema(value_type = Option>, example = json!([" ", "\n"]))] pub non_separator_tokens: Setting>, + /// List of characters delimiting where one term begins and ends. #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default, error = DeserrJsonError)] + #[schema(value_type = Option>, example = json!(["S"]))] pub separator_tokens: Setting>, + /// List of strings Meilisearch should parse as a single term. #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default, error = DeserrJsonError)] + #[schema(value_type = Option>, example = json!(["iPhone pro"]))] pub dictionary: Setting>, + /// List of associated words treated similarly. A word associated to an array of word as synonyms. #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default, error = DeserrJsonError)] + #[schema(value_type = Option>>, example = json!({ "he": ["she", "they", "them"], "phone": ["iPhone", "android"]}))] pub synonyms: Setting>>, + /// Search returns documents with distinct (different) values of the given field. #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default, error = DeserrJsonError)] + #[schema(value_type = Option, example = json!("sku"))] pub distinct_attribute: Setting, + /// Precision level when calculating the proximity ranking rule. #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default, error = DeserrJsonError)] + #[schema(value_type = Option, example = json!(ProximityPrecisionView::ByAttribute))] pub proximity_precision: Setting, + /// Customize typo tolerance feature. #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default, error = DeserrJsonError)] + #[schema(value_type = Option, example = json!({ "enabled": true, "disableOnAttributes": ["title"]}))] pub typo_tolerance: Setting, + /// Faceting settings. #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default, error = DeserrJsonError)] + #[schema(value_type = Option, example = json!({ "maxValuesPerFacet": 10, "sortFacetValuesBy": { "genre": FacetValuesSort::Count }}))] pub faceting: Setting, + /// Pagination settings. #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default, error = DeserrJsonError)] + #[schema(value_type = Option, example = json!({ "maxValuesPerFacet": 10, "sortFacetValuesBy": { "genre": FacetValuesSort::Count }}))] pub pagination: Setting, + /// Embedder required for performing meaning-based search queries. #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default, error = DeserrJsonError)] pub embedders: Setting>>, + /// Maximum duration of a search query. #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default, error = DeserrJsonError)] + #[schema(value_type = Option, example = json!(50))] pub search_cutoff_ms: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default, error = DeserrJsonError)] @@ -865,7 +908,7 @@ impl From for ProximityPrecision { } } -#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq, ToSchema)] pub struct WildcardSetting(Setting>); impl From>> for WildcardSetting { diff --git a/meilisearch-types/src/task_view.rs b/meilisearch-types/src/task_view.rs index 3075fa899..c60436360 100644 --- a/meilisearch-types/src/task_view.rs +++ b/meilisearch-types/src/task_view.rs @@ -1,30 +1,47 @@ use milli::Object; use serde::Serialize; use time::{Duration, OffsetDateTime}; +use utoipa::ToSchema; use crate::error::ResponseError; use crate::settings::{Settings, Unchecked}; use crate::tasks::{serialize_duration, Details, IndexSwap, Kind, Status, Task, TaskId}; -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] +#[schema(rename_all = "camelCase")] pub struct TaskView { + /// The unique sequential identifier of the task. + #[schema(value_type = u32, example = 4312)] pub uid: TaskId, + /// The unique identifier of the index where this task is operated. + #[schema(example = json!("movies"))] #[serde(default)] pub index_uid: Option, pub status: Status, + /// The type of the task. #[serde(rename = "type")] pub kind: Kind, + /// The uid of the task that performed the taskCancelation if the task has been canceled. + #[schema(value_type = Option, example = json!(4326))] pub canceled_by: Option, #[serde(skip_serializing_if = "Option::is_none")] pub details: Option, pub error: Option, + /// Total elasped time the engine was in processing state expressed as a `ISO-8601` duration format. + #[schema(value_type = Option, example = json!(null))] #[serde(serialize_with = "serialize_duration", default)] pub duration: Option, + /// An `RFC 3339` format for date/time/duration. + #[schema(value_type = String, example = json!("2024-08-08_14:12:09.393Z"))] #[serde(with = "time::serde::rfc3339")] pub enqueued_at: OffsetDateTime, + /// An `RFC 3339` format for date/time/duration. + #[schema(value_type = String, example = json!("2024-08-08_14:12:09.393Z"))] #[serde(with = "time::serde::rfc3339::option", default)] pub started_at: Option, + /// An `RFC 3339` format for date/time/duration. + #[schema(value_type = String, example = json!("2024-08-08_14:12:09.393Z"))] #[serde(with = "time::serde::rfc3339::option", default)] pub finished_at: Option, } @@ -47,37 +64,52 @@ impl TaskView { } } -#[derive(Default, Debug, PartialEq, Eq, Clone, Serialize)] +/// Details information of the task payload. +#[derive(Default, Debug, PartialEq, Eq, Clone, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] +#[schema(rename_all = "camelCase")] pub struct DetailsView { + /// Number of documents received for documentAdditionOrUpdate task. #[serde(skip_serializing_if = "Option::is_none")] pub received_documents: Option, + /// Number of documents finally indexed for documentAdditionOrUpdate task or a documentAdditionOrUpdate batch of tasks. #[serde(skip_serializing_if = "Option::is_none")] pub indexed_documents: Option>, + /// Number of documents edited for editDocumentByFunction task. #[serde(skip_serializing_if = "Option::is_none")] pub edited_documents: Option>, + /// Value for the primaryKey field encountered if any for indexCreation or indexUpdate task. #[serde(skip_serializing_if = "Option::is_none")] pub primary_key: Option>, + /// Number of provided document ids for the documentDeletion task. #[serde(skip_serializing_if = "Option::is_none")] pub provided_ids: Option, + /// Number of documents finally deleted for documentDeletion and indexDeletion tasks. #[serde(skip_serializing_if = "Option::is_none")] pub deleted_documents: Option>, + /// Number of tasks that match the request for taskCancelation or taskDeletion tasks. #[serde(skip_serializing_if = "Option::is_none")] pub matched_tasks: Option, + /// Number of tasks canceled for taskCancelation. #[serde(skip_serializing_if = "Option::is_none")] pub canceled_tasks: Option>, + /// Number of tasks deleted for taskDeletion. #[serde(skip_serializing_if = "Option::is_none")] pub deleted_tasks: Option>, + /// Original filter query for taskCancelation or taskDeletion tasks. #[serde(skip_serializing_if = "Option::is_none")] pub original_filter: Option>, + /// Identifier generated for the dump for dumpCreation task. #[serde(skip_serializing_if = "Option::is_none")] pub dump_uid: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub context: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub function: Option, + /// [Learn more about the settings in this guide](https://www.meilisearch.com/docs/reference/api/settings). #[serde(skip_serializing_if = "Option::is_none")] - #[serde(flatten)] + // #[serde(flatten)] + #[schema(value_type = Option>)] pub settings: Option>>, #[serde(skip_serializing_if = "Option::is_none")] pub swaps: Option>, diff --git a/meilisearch-types/src/tasks.rs b/meilisearch-types/src/tasks.rs index 1dd6d3fbf..caa1ce4c8 100644 --- a/meilisearch-types/src/tasks.rs +++ b/meilisearch-types/src/tasks.rs @@ -9,6 +9,7 @@ use milli::Object; use roaring::RoaringBitmap; use serde::{Deserialize, Serialize, Serializer}; use time::{Duration, OffsetDateTime}; +use utoipa::{IntoParams, ToSchema}; use uuid::Uuid; use crate::error::ResponseError; @@ -361,7 +362,9 @@ impl From<&KindWithContent> for Option
{ } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Sequence)] +/// The status of a task. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Sequence, ToSchema)] +#[schema(example = json!(Status::Processing))] #[serde(rename_all = "camelCase")] pub enum Status { Enqueued, @@ -420,8 +423,10 @@ impl fmt::Display for ParseTaskStatusError { } impl std::error::Error for ParseTaskStatusError {} -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Sequence)] +/// 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))] pub enum Kind { DocumentAdditionOrUpdate, DocumentEdition, diff --git a/meilisearch/Cargo.toml b/meilisearch/Cargo.toml index 2a16e1017..00eeaacbc 100644 --- a/meilisearch/Cargo.toml +++ b/meilisearch/Cargo.toml @@ -104,6 +104,8 @@ 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-alpha.1", features = ["actix_extras", "non_strict_integers"] } +utoipa-scalar = { version = "0.2.0-alpha.0", features = ["actix-web"] } [dev-dependencies] actix-rt = "2.10.0" diff --git a/meilisearch/src/main.rs b/meilisearch/src/main.rs index b66bfc5b8..1b4023f7c 100644 --- a/meilisearch/src/main.rs +++ b/meilisearch/src/main.rs @@ -24,6 +24,7 @@ use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; use tracing::level_filters::LevelFilter; use tracing_subscriber::layer::SubscriberExt as _; use tracing_subscriber::Layer; +use utoipa::OpenApi; #[global_allocator] static ALLOC: MiMalloc = MiMalloc; diff --git a/meilisearch/src/routes/mod.rs b/meilisearch/src/routes/mod.rs index c25aeee70..9efb19bc4 100644 --- a/meilisearch/src/routes/mod.rs +++ b/meilisearch/src/routes/mod.rs @@ -1,20 +1,22 @@ use std::collections::BTreeMap; +use crate::extractors::authentication::policies::*; +use crate::extractors::authentication::GuardedData; +use crate::search_queue::SearchQueue; +use crate::Opt; 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}; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use tracing::debug; - -use crate::extractors::authentication::policies::*; -use crate::extractors::authentication::GuardedData; -use crate::search_queue::SearchQueue; -use crate::Opt; +use utoipa::OpenApi; +use utoipa_scalar::{Scalar, Servable as ScalarServable}; const PAGINATION_DEFAULT_LIMIT: usize = 20; @@ -29,8 +31,19 @@ mod snapshot; mod swap_indexes; pub mod tasks; +#[derive(OpenApi)] +#[openapi( + nest((path = "/tasks", api = tasks::TaskApi) ), + // paths(get_todos, create_todo, delete_todo, get_todo_by_id, update_todo, search_todos), + // components(schemas(TaskId)) +)] +pub struct MeilisearchApi; + pub fn configure(cfg: &mut web::ServiceConfig) { + let openapi = MeilisearchApi::openapi(); + cfg.service(web::scope("/tasks").configure(tasks::configure)) + .service(Scalar::with_url("/scalar", openapi.clone())) .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 02f009ff7..ffd25aa75 100644 --- a/meilisearch/src/routes/tasks.rs +++ b/meilisearch/src/routes/tasks.rs @@ -8,8 +8,12 @@ use meilisearch_types::deserr::DeserrQueryParamError; use meilisearch_types::error::deserr_codes::*; use meilisearch_types::error::{InvalidTaskDateError, ResponseError}; use meilisearch_types::index_uid::IndexUid; +use meilisearch_types::settings::{ + Checked, FacetingSettings, MinWordSizeTyposSetting, PaginationSettings, Settings, TypoSettings, + Unchecked, +}; use meilisearch_types::star_or::{OptionStarOr, OptionStarOrList}; -use meilisearch_types::task_view::TaskView; +use meilisearch_types::task_view::{DetailsView, TaskView}; use meilisearch_types::tasks::{Kind, KindWithContent, Status}; use serde::Serialize; use serde_json::json; @@ -17,6 +21,7 @@ use time::format_description::well_known::Rfc3339; use time::macros::format_description; use time::{Date, Duration, OffsetDateTime, Time}; use tokio::task; +use utoipa::{IntoParams, OpenApi, ToSchema}; use super::{get_task_id, is_dry_run, SummarizedTaskView}; use crate::analytics::Analytics; @@ -27,6 +32,16 @@ use crate::Opt; 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)." + ), + components(schemas(AllTasks, TaskView, Status, DetailsView, ResponseError, Settings, Settings, TypoSettings, MinWordSizeTyposSetting, FacetingSettings, PaginationSettings)) +)] +pub struct TaskApi; + pub fn configure(cfg: &mut web::ServiceConfig) { cfg.service( web::resource("") @@ -36,36 +51,54 @@ pub fn configure(cfg: &mut web::ServiceConfig) { .service(web::resource("/cancel").route(web::post().to(SeqHandler(cancel_tasks)))) .service(web::resource("/{task_id}").route(web::get().to(SeqHandler(get_task)))); } -#[derive(Debug, Deserr)] + +#[derive(Debug, Deserr, IntoParams)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] +#[into_params(rename_all = "camelCase", parameter_in = Query)] pub struct TasksFilterQuery { + /// Maximum number of results to return. #[deserr(default = Param(DEFAULT_LIMIT), error = DeserrQueryParamError)] + #[param(required = false, value_type = u32, example = 12, default = json!(DEFAULT_LIMIT))] pub limit: Param, + /// Fetch the next set of results from the given uid. #[deserr(default, error = DeserrQueryParamError)] + #[param(required = false, value_type = Option, example = 12421)] pub from: Option>, + /// 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]))] pub uids: OptionStarOrList, #[deserr(default, error = DeserrQueryParamError)] + #[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]))] pub types: OptionStarOrList, #[deserr(default, error = DeserrQueryParamError)] + #[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"]))] 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"))] 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"))] 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"))] 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"))] 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"))] 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"))] pub before_finished_at: OptionStarOr, } @@ -110,31 +143,43 @@ impl TaskDeletionOrCancelationQuery { } } -#[derive(Debug, Deserr)] +#[derive(Debug, Deserr, IntoParams)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] +#[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]))] pub uids: OptionStarOrList, #[deserr(default, error = DeserrQueryParamError)] + #[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]))] pub types: OptionStarOrList, #[deserr(default, error = DeserrQueryParamError)] + #[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"]))] 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"))] 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"))] 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"))] 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"))] 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"))] 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"))] pub before_finished_at: OptionStarOr, } @@ -209,6 +254,23 @@ async fn cancel_tasks( Ok(HttpResponse::Ok().json(task)) } +/// Delete tasks. +/// +/// Delete [tasks](https://docs.meilisearch.com/learn/advanced/asynchronous_operations.html) on filter +#[utoipa::path( + delete, + path = "", + 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 delete_tasks( index_scheduler: GuardedData, Data>, params: AwebQueryParameter, @@ -258,15 +320,56 @@ async fn delete_tasks( Ok(HttpResponse::Ok().json(task)) } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, ToSchema)] pub struct AllTasks { results: Vec, + /// Total number of browsable results using offset/limit parameters for the given resource. total: u64, + /// Limit given for the query. If limit is not provided as a query parameter, this parameter displays the default limit value. limit: u32, + /// The first task uid returned. from: Option, + /// Represents the value to send in from to fetch the next slice of the results. The first item for the next slice starts at this exact number. When the returned value is null, it means that all the data have been browsed in the given order. next: Option, } +/// Get all tasks +/// +/// Get all [tasks](https://docs.meilisearch.com/learn/advanced/asynchronous_operations.html) +#[utoipa::path( + get, + path = "", + params(TasksFilterQuery), + responses( + (status = 200, description = "Get all tasks", body = AllTasks, content_type = "application/json", example = json!({ + "results": [ + { + "uid": 144, + "indexUid": "mieli", + "status": "succeeded", + "type": "settingsUpdate", + "canceledBy": null, + "details": { + "settings": { + "filterableAttributes": [ + "play_count" + ] + } + }, + "error": null, + "duration": "PT0.009330S", + "enqueuedAt": "2024-08-08T09:01:13.348471Z", + "startedAt": "2024-08-08T09:01:13.349442Z", + "finishedAt": "2024-08-08T09:01:13.358772Z" + } + ], + "total": 1, + "limit": 1, + "from": 144, + "next": null + })) + ) +)] async fn get_tasks( index_scheduler: GuardedData, Data>, params: AwebQueryParameter,