Add post webhook route

This commit is contained in:
Mubelotix
2025-07-31 12:27:12 +02:00
parent ca27bcaac7
commit 29fb4d5e2a
2 changed files with 60 additions and 12 deletions

View File

@ -423,7 +423,8 @@ InvalidChatCompletionPreQueryPrompt , InvalidRequest , BAD_REQU
InvalidWebhooks , InvalidRequest , BAD_REQUEST ; InvalidWebhooks , InvalidRequest , BAD_REQUEST ;
InvalidWebhooksUrl , InvalidRequest , BAD_REQUEST ; InvalidWebhooksUrl , InvalidRequest , BAD_REQUEST ;
InvalidWebhooksHeaders , InvalidRequest , BAD_REQUEST ; InvalidWebhooksHeaders , InvalidRequest , BAD_REQUEST ;
ReservedWebhook , InvalidRequest , BAD_REQUEST ReservedWebhook , InvalidRequest , BAD_REQUEST ;
WebhookNotFound , InvalidRequest , NOT_FOUND
} }
impl ErrorCode for JoinError { impl ErrorCode for JoinError {

View File

@ -6,9 +6,7 @@ use deserr::actix_web::AwebJson;
use deserr::Deserr; use deserr::Deserr;
use index_scheduler::IndexScheduler; use index_scheduler::IndexScheduler;
use meilisearch_types::deserr::DeserrJsonError; use meilisearch_types::deserr::DeserrJsonError;
use meilisearch_types::error::deserr_codes::{ use meilisearch_types::error::deserr_codes::{InvalidWebhooksHeaders, InvalidWebhooksUrl};
InvalidWebhooks, InvalidWebhooksHeaders, InvalidWebhooksUrl,
};
use meilisearch_types::error::{ErrorCode, ResponseError}; use meilisearch_types::error::{ErrorCode, ResponseError};
use meilisearch_types::keys::actions; use meilisearch_types::keys::actions;
use meilisearch_types::milli::update::Setting; use meilisearch_types::milli::update::Setting;
@ -25,7 +23,7 @@ use crate::extractors::sequential_extractor::SeqHandler;
#[derive(OpenApi)] #[derive(OpenApi)]
#[openapi( #[openapi(
paths(get_webhooks, patch_webhooks, get_webhook), paths(get_webhooks, patch_webhooks, get_webhook, post_webhook),
tags(( tags((
name = "Webhooks", name = "Webhooks",
description = "The `/webhooks` route allows you to register endpoints to be called once tasks are processed.", description = "The `/webhooks` route allows you to register endpoints to be called once tasks are processed.",
@ -38,13 +36,14 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service( cfg.service(
web::resource("") web::resource("")
.route(web::get().to(get_webhooks)) .route(web::get().to(get_webhooks))
.route(web::patch().to(SeqHandler(patch_webhooks))), .route(web::patch().to(SeqHandler(patch_webhooks)))
.route(web::post().to(SeqHandler(post_webhook))),
) )
.service(web::resource("/{uuid}").route(web::get().to(get_webhook))); .service(web::resource("/{uuid}").route(web::get().to(get_webhook)));
} }
#[derive(Debug, Deserr, ToSchema)] #[derive(Debug, Deserr, ToSchema)]
#[deserr(error = DeserrJsonError<InvalidWebhooks>, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")] #[schema(rename_all = "camelCase")]
struct WebhookSettings { struct WebhookSettings {
@ -64,16 +63,17 @@ struct WebhookSettings {
#[schema(rename_all = "camelCase")] #[schema(rename_all = "camelCase")]
struct WebhooksSettings { struct WebhooksSettings {
#[schema(value_type = Option<BTreeMap<String, WebhookSettings>>)] #[schema(value_type = Option<BTreeMap<String, WebhookSettings>>)]
#[deserr(default, error = DeserrJsonError<InvalidWebhooks>)]
#[serde(default)] #[serde(default)]
webhooks: Setting<BTreeMap<Uuid, Setting<WebhookSettings>>>, webhooks: Setting<BTreeMap<Uuid, Setting<WebhookSettings>>>,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
struct WebhookWithMetadata { struct WebhookWithMetadata {
uuid: Uuid, uuid: Uuid,
is_editable: bool, is_editable: bool,
#[schema(value_type = WebhookSettings)]
#[serde(flatten)] #[serde(flatten)]
webhook: Webhook, webhook: Webhook,
} }
@ -138,11 +138,16 @@ async fn get_webhooks(
#[derive(Serialize, Default)] #[derive(Serialize, Default)]
pub struct PatchWebhooksAnalytics { pub struct PatchWebhooksAnalytics {
patch_webhooks_count: usize, patch_webhooks_count: usize,
post_webhook_count: usize,
} }
impl PatchWebhooksAnalytics { impl PatchWebhooksAnalytics {
pub fn patch_webhooks() -> Self { pub fn patch_webhooks() -> Self {
PatchWebhooksAnalytics { patch_webhooks_count: 1 } PatchWebhooksAnalytics { patch_webhooks_count: 1, ..Default::default() }
}
pub fn post_webhook() -> Self {
PatchWebhooksAnalytics { post_webhook_count: 1, ..Default::default() }
} }
} }
@ -154,6 +159,7 @@ impl Aggregate for PatchWebhooksAnalytics {
fn aggregate(self: Box<Self>, new: Box<Self>) -> Box<Self> { fn aggregate(self: Box<Self>, new: Box<Self>) -> Box<Self> {
Box::new(PatchWebhooksAnalytics { Box::new(PatchWebhooksAnalytics {
patch_webhooks_count: self.patch_webhooks_count + new.patch_webhooks_count, patch_webhooks_count: self.patch_webhooks_count + new.patch_webhooks_count,
post_webhook_count: self.post_webhook_count + new.post_webhook_count,
}) })
} }
@ -185,7 +191,7 @@ impl ErrorCode for WebhooksError {
meilisearch_types::error::Code::InvalidWebhooksHeaders meilisearch_types::error::Code::InvalidWebhooksHeaders
} }
WebhooksError::ReservedWebhook(_) => meilisearch_types::error::Code::ReservedWebhook, WebhooksError::ReservedWebhook(_) => meilisearch_types::error::Code::ReservedWebhook,
WebhooksError::WebhookNotFound(_) => meilisearch_types::error::Code::InvalidWebhooks, WebhooksError::WebhookNotFound(_) => meilisearch_types::error::Code::WebhookNotFound,
} }
} }
} }
@ -318,7 +324,7 @@ fn patch_webhooks_inner(
#[utoipa::path( #[utoipa::path(
get, get,
path = "/{name}", path = "/{uuid}",
tag = "Webhooks", tag = "Webhooks",
security(("Bearer" = ["webhooks.get", "*.get", "*"])), security(("Bearer" = ["webhooks.get", "*.get", "*"])),
responses( responses(
@ -353,3 +359,44 @@ async fn get_webhook(
webhook, webhook,
})) }))
} }
#[utoipa::path(
post,
path = "",
tag = "Webhooks",
request_body = WebhookSettings,
security(("Bearer" = ["webhooks.update", "*"])),
responses(
(status = 201, description = "Webhook created successfully", body = WebhookWithMetadata, content_type = "application/json", example = json!({
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://your.site/on-tasks-completed",
"headers": {
"Authorization": "Bearer a-secret-token"
},
"isEditable": true
})),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json"),
(status = 400, description = "Bad request", body = ResponseError, content_type = "application/json"),
)
)]
async fn post_webhook(
index_scheduler: GuardedData<ActionPolicy<{ actions::WEBHOOKS_UPDATE }>, Data<IndexScheduler>>,
webhook_settings: AwebJson<WebhookSettings, DeserrJsonError>,
req: HttpRequest,
analytics: Data<Analytics>,
) -> Result<HttpResponse, ResponseError> {
let uuid = Uuid::new_v4();
let webhooks = patch_webhooks_inner(
&index_scheduler,
WebhooksSettings {
webhooks: Setting::Set(BTreeMap::from([(uuid, Setting::Set(webhook_settings.0))])),
},
)?;
let webhook = webhooks.webhooks.get(&uuid).ok_or(WebhooksError::WebhookNotFound(uuid))?.clone();
analytics.publish(PatchWebhooksAnalytics::post_webhook(), &req);
debug!(returns = ?webhook, "Created webhook {}", uuid);
Ok(HttpResponse::Created().json(WebhookWithMetadata { uuid, is_editable: true, webhook }))
}