use actix_web::web::{self, Data}; use actix_web::{HttpRequest, HttpResponse}; use deserr::actix_web::AwebJson; use deserr::Deserr; use index_scheduler::IndexScheduler; use meilisearch_types::deserr::DeserrJsonError; use meilisearch_types::error::ResponseError; use meilisearch_types::keys::actions; use serde::Serialize; use tracing::debug; use crate::analytics::{Aggregate, Analytics}; use crate::extractors::authentication::policies::ActionPolicy; use crate::extractors::authentication::GuardedData; use crate::extractors::sequential_extractor::SeqHandler; pub fn configure(cfg: &mut web::ServiceConfig) { cfg.service( web::resource("") .route(web::get().to(get_features)) .route(web::patch().to(SeqHandler(patch_features))), ); } async fn get_features( index_scheduler: GuardedData< ActionPolicy<{ actions::EXPERIMENTAL_FEATURES_GET }>, Data, >, ) -> HttpResponse { let features = index_scheduler.features(); let features = features.runtime_features(); debug!(returns = ?features, "Get features"); HttpResponse::Ok().json(features) } #[derive(Debug, Deserr)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct RuntimeTogglableFeatures { #[deserr(default)] pub vector_store: Option, #[deserr(default)] pub metrics: Option, #[deserr(default)] pub logs_route: Option, #[deserr(default)] pub edit_documents_by_function: Option, #[deserr(default)] pub contains_filter: Option, } #[derive(Serialize)] pub struct PatchExperimentalFeatureAnalytics { vector_store: bool, metrics: bool, logs_route: bool, edit_documents_by_function: bool, contains_filter: bool, } impl Aggregate for PatchExperimentalFeatureAnalytics { fn event_name(&self) -> &'static str { "Experimental features Updated" } fn aggregate(self: Box, new: Box) -> Box { Box::new(Self { vector_store: new.vector_store, metrics: new.metrics, logs_route: new.logs_route, edit_documents_by_function: new.edit_documents_by_function, contains_filter: new.contains_filter, }) } fn into_event(self: Box) -> serde_json::Value { serde_json::to_value(*self).unwrap_or_default() } } async fn patch_features( index_scheduler: GuardedData< ActionPolicy<{ actions::EXPERIMENTAL_FEATURES_UPDATE }>, Data, >, new_features: AwebJson, req: HttpRequest, analytics: Data, ) -> Result { let features = index_scheduler.features(); debug!(parameters = ?new_features, "Patch features"); let old_features = features.runtime_features(); let new_features = meilisearch_types::features::RuntimeTogglableFeatures { vector_store: new_features.0.vector_store.unwrap_or(old_features.vector_store), metrics: new_features.0.metrics.unwrap_or(old_features.metrics), logs_route: new_features.0.logs_route.unwrap_or(old_features.logs_route), edit_documents_by_function: new_features .0 .edit_documents_by_function .unwrap_or(old_features.edit_documents_by_function), contains_filter: new_features.0.contains_filter.unwrap_or(old_features.contains_filter), }; // explicitly destructure for analytics rather than using the `Serialize` implementation, because // the it renames to camelCase, which we don't want for analytics. // **Do not** ignore fields with `..` or `_` here, because we want to add them in the future. let meilisearch_types::features::RuntimeTogglableFeatures { vector_store, metrics, logs_route, edit_documents_by_function, contains_filter, } = new_features; analytics.publish( PatchExperimentalFeatureAnalytics { vector_store, metrics, logs_route, edit_documents_by_function, contains_filter, }, &req, ); index_scheduler.put_runtime_features(new_features)?; debug!(returns = ?new_features, "Patch features"); Ok(HttpResponse::Ok().json(new_features)) }