Files
windmill/backend/windmill-trigger-mqtt/src/handler.rs
T
0389d9601c chore: upgrade axum 0.7 to 0.8 (#8539)
* chore: upgrade axum 0.7 to 0.8 and related dependencies

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add route reachability tests for ~80 previously untested endpoints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: switch feature-gated trigger handlers from axum::async_trait to async_trait crate

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update new trash routes to axum 0.8 path syntax

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to latest EE commit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: upgrade route tests to assert 2xx responses with proper data setup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: restore npm_proxy and ai_routes tests using local echo servers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: gate workspace fork test behind enterprise feature flag

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add ~40 more endpoint tests (jobs authed, health, favorites, ACLs, reachability)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address review findings from axum 0.8 upgrade

- Use cookie value_trimmed() instead of value() for cookie 0.18 compat
- Update comments still referencing old :workspace_id syntax

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to 61ae055ea31481f1899953e9d5f65566b8c707b1

This commit updates the EE repository reference after PR #486 was merged in windmill-ee-private.

Previous ee-repo-ref: 0059d175a6fdddf52998b183bf91059b224704ac

New ee-repo-ref: 61ae055ea31481f1899953e9d5f65566b8c707b1

Automated by sync-ee-ref workflow.

* test: add test for new get_imports endpoint

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove unused import in raw_apps test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-03-27 09:55:04 +00:00

241 lines
7.5 KiB
Rust

use async_trait::async_trait;
use itertools::Itertools;
use sqlx::{types::Json as SqlxJson, PgConnection};
use windmill_api_auth::ApiAuthed;
use windmill_common::DB;
use windmill_common::{
db::UserDB,
error::{Error, Result},
};
use windmill_git_sync::DeployedObject;
use windmill_store::resources::try_get_resource_from_db_as;
use windmill_trigger::{Trigger, TriggerCrud, TriggerData};
use super::{
MqttClientBuilder, MqttClientVersion, MqttConfig, MqttConfigRequest, MqttResource, MqttTrigger,
MqttV3Config, MqttV5Config, SubscribeTopic, TestMqttConfig,
};
#[async_trait]
impl TriggerCrud for MqttTrigger {
type TriggerConfig = MqttConfig;
type Trigger = Trigger<Self::TriggerConfig>;
type TriggerConfigRequest = MqttConfigRequest;
type TestConnectionConfig = TestMqttConfig;
const TABLE_NAME: &'static str = "mqtt_trigger";
const TRIGGER_TYPE: &'static str = "mqtt";
const SUPPORTS_SERVER_STATE: bool = true;
const SUPPORTS_TEST_CONNECTION: bool = true;
const ROUTE_PREFIX: &'static str = "/mqtt_triggers";
const DEPLOYMENT_NAME: &'static str = "MQTT trigger";
const ADDITIONAL_SELECT_FIELDS: &[&'static str] = &[
"mqtt_resource_path",
"subscribe_topics",
"v3_config",
"v5_config",
"client_id",
"client_version",
];
const IS_ALLOWED_ON_CLOUD: bool = false;
fn get_deployed_object(path: String, parent_path: Option<String>) -> DeployedObject {
DeployedObject::MqttTrigger { path, parent_path }
}
async fn validate_config(
&self,
_db: &DB,
config: &Self::TriggerConfigRequest,
_workspace_id: &str,
) -> Result<()> {
if config.mqtt_resource_path.trim().is_empty() {
return Err(Error::BadRequest(
"MQTT resource path cannot be empty".to_string(),
));
}
if config.subscribe_topics.is_empty() {
return Err(Error::BadRequest(
"At least one subscribe topic must be specified".to_string(),
));
}
Ok(())
}
async fn create_trigger(
&self,
_db: &DB,
tx: &mut PgConnection,
authed: &ApiAuthed,
w_id: &str,
trigger: TriggerData<Self::TriggerConfigRequest>,
) -> Result<()> {
let resolved_edited_by = trigger.base.resolve_edited_by(authed);
let resolved_permissioned_as = trigger.base.resolve_permissioned_as(authed);
let subscribe_topics = trigger
.config
.subscribe_topics
.into_iter()
.map(SqlxJson)
.collect_vec();
let v3_config = trigger.config.v3_config.map(SqlxJson);
let v5_config = trigger.config.v5_config.map(SqlxJson);
sqlx::query!(
r#"
INSERT INTO mqtt_trigger (
mqtt_resource_path,
subscribe_topics,
client_version,
client_id,
v3_config,
v5_config,
workspace_id,
path,
script_path,
is_flow,
permissioned_as,
mode,
edited_by,
error_handler_path,
error_handler_args,
retry
)
VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16
)"#,
trigger.config.mqtt_resource_path,
subscribe_topics.as_slice() as &[SqlxJson<SubscribeTopic>],
trigger.config.client_version as Option<MqttClientVersion>,
trigger.config.client_id,
v3_config as Option<SqlxJson<MqttV3Config>>,
v5_config as Option<SqlxJson<MqttV5Config>>,
w_id,
trigger.base.path,
trigger.base.script_path,
trigger.base.is_flow,
resolved_permissioned_as,
trigger.base.mode() as _,
&resolved_edited_by,
trigger.error_handling.error_handler_path,
trigger.error_handling.error_handler_args as _,
trigger.error_handling.retry as _
)
.execute(tx)
.await?;
Ok(())
}
async fn update_trigger(
&self,
_db: &DB,
tx: &mut PgConnection,
authed: &ApiAuthed,
workspace_id: &str,
path: &str,
trigger: TriggerData<Self::TriggerConfigRequest>,
) -> Result<()> {
let resolved_edited_by = trigger.base.resolve_edited_by(authed);
let resolved_permissioned_as = trigger.base.resolve_permissioned_as(authed);
let subscribe_topics = trigger
.config
.subscribe_topics
.into_iter()
.map(SqlxJson)
.collect_vec();
let v3_config = trigger.config.v3_config.map(SqlxJson);
let v5_config = trigger.config.v5_config.map(SqlxJson);
// Important to set server_id to NULL to stop current mqtt listener
sqlx::query!(
r#"
UPDATE
mqtt_trigger
SET
mqtt_resource_path = $1,
subscribe_topics = $2,
client_version = $3,
client_id = $4,
v3_config = $5,
v5_config = $6,
is_flow = $7,
edited_by = $8,
permissioned_as = $9,
script_path = $10,
path = $11,
edited_at = now(),
error = NULL,
server_id = NULL,
error_handler_path = $14,
error_handler_args = $15,
retry = $16
WHERE
workspace_id = $12 AND
path = $13
"#,
trigger.config.mqtt_resource_path,
subscribe_topics.as_slice() as &[SqlxJson<SubscribeTopic>],
trigger.config.client_version as Option<MqttClientVersion>,
trigger.config.client_id,
v3_config as Option<SqlxJson<MqttV3Config>>,
v5_config as Option<SqlxJson<MqttV5Config>>,
trigger.base.is_flow,
&resolved_edited_by,
resolved_permissioned_as,
trigger.base.script_path,
trigger.base.path,
workspace_id,
path,
trigger.error_handling.error_handler_path,
trigger.error_handling.error_handler_args as _,
trigger.error_handling.retry as _
)
.execute(tx)
.await?;
Ok(())
}
async fn test_connection(
&self,
db: &DB,
authed: &ApiAuthed,
user_db: &UserDB,
workspace_id: &str,
config: Self::TestConnectionConfig,
) -> Result<()> {
let mqtt_resource = try_get_resource_from_db_as::<MqttResource>(
authed,
Some(user_db.clone()),
db,
&config.mqtt_resource_path,
workspace_id,
)
.await?;
let connect_f = async {
let client_builder = MqttClientBuilder::new(
mqtt_resource,
Some(""),
vec![],
config.v3_config.as_ref(),
config.v5_config.as_ref(),
config.client_version.as_ref(),
);
client_builder.build_client().await.map_err(|err| {
Error::BadConfig(format!(
"Error connecting to mqtt broker: {}",
err.to_string()
))
})
};
connect_f.await?;
Ok(())
}
}