From 6fd48b91f3ddb9607a2c219ec8380bcdb6459a2f Mon Sep 17 00:00:00 2001 From: sdairs Date: Fri, 30 Oct 2026 19:41:33 +0100 Subject: [PATCH 1/4] refactor(api): rename convert/service.rs to convert/services.rs (#1173) Match the client/services.rs and models/services.rs module names. Pure rename (100% similarity); only the `mod` line in convert.rs changes. Co-Authored-By: Claude Opus 5.5 --- crates/clickhouse-cloud-api/src/convert.rs | 2 +- .../src/convert/{service.rs => services.rs} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename crates/clickhouse-cloud-api/src/convert/{service.rs => services.rs} (100%) diff --git a/crates/clickhouse-cloud-api/src/convert.rs b/crates/clickhouse-cloud-api/src/convert.rs index 2f2a00594..94d8e7d7e 100644 --- a/crates/clickhouse-cloud-api/src/convert.rs +++ b/crates/clickhouse-cloud-api/src/convert.rs @@ -22,7 +22,7 @@ use std::fmt; mod clickpipes; mod clickstack; mod postgres; -mod service; +mod services; mod shared; /// The response omitted fields that the matching request model requires. diff --git a/crates/clickhouse-cloud-api/src/convert/service.rs b/crates/clickhouse-cloud-api/src/convert/services.rs similarity index 100% rename from crates/clickhouse-cloud-api/src/convert/service.rs rename to crates/clickhouse-cloud-api/src/convert/services.rs From 502a01ede8136ddf7989bd20f0cc0c0d214bfea9 Mon Sep 17 00:00:00 2001 From: sdairs Date: Fri, 30 Oct 2026 19:42:47 +0100 Subject: [PATCH 2/4] refactor(api): split client/organizations.rs by domain (#1173) Move the Client endpoint methods out of client/organizations.rs into private per-domain files that mirror src/models/: - client/byoc.rs: the eight organization_byoc_infrastructure_* methods (create, validate, get, progress_get, private_endpoint_config_get, tags_get, delete, update) - client/invitations.rs: invitation_get_list, invitation_create, invitation_get, invitation_delete - client/members.rs: member_get_list, member_get, member_update, member_delete - client/rbac.rs: organization_roles_get_list, organization_role_post, organization_role_get, organization_role_patch, organization_role_delete organizations.rs keeps balances, organization get/list/update, quotas, notifications, the deprecated private endpoint config list, Prometheus and usage cost. Methods are moved verbatim and keep their relative order; each new file repeats organizations.rs's three `use` lines. No visibility changes. Co-Authored-By: Claude Opus 5.5 --- crates/clickhouse-cloud-api/src/client.rs | 4 + .../clickhouse-cloud-api/src/client/byoc.rs | 212 ++++++++ .../src/client/invitations.rs | 97 ++++ .../src/client/members.rs | 98 ++++ .../src/client/organizations.rs | 509 ------------------ .../clickhouse-cloud-api/src/client/rbac.rs | 122 +++++ 6 files changed, 533 insertions(+), 509 deletions(-) create mode 100644 crates/clickhouse-cloud-api/src/client/byoc.rs create mode 100644 crates/clickhouse-cloud-api/src/client/invitations.rs create mode 100644 crates/clickhouse-cloud-api/src/client/members.rs create mode 100644 crates/clickhouse-cloud-api/src/client/rbac.rs diff --git a/crates/clickhouse-cloud-api/src/client.rs b/crates/clickhouse-cloud-api/src/client.rs index ca6708371..208877278 100644 --- a/crates/clickhouse-cloud-api/src/client.rs +++ b/crates/clickhouse-cloud-api/src/client.rs @@ -5,12 +5,16 @@ use crate::error::Error; mod activity; mod api_keys; mod backups; +mod byoc; mod clickpipes; mod clickstack; +mod invitations; +mod members; mod organizations; mod postgres; mod query; mod query_api_endpoints; +mod rbac; mod saved_queries; mod services; mod udfs; diff --git a/crates/clickhouse-cloud-api/src/client/byoc.rs b/crates/clickhouse-cloud-api/src/client/byoc.rs new file mode 100644 index 000000000..691d1696e --- /dev/null +++ b/crates/clickhouse-cloud-api/src/client/byoc.rs @@ -0,0 +1,212 @@ +use super::Client; +use crate::error::Error; +use crate::models::*; + +impl Client { + /// Create BYOC Infrastructure + pub async fn organization_byoc_infrastructure_create( + &self, + organization_id: &str, + body: &ByocInfrastructurePostRequest, + ) -> Result, Error> { + let path = format!("/v1/organizations/{organization_id}/byocInfrastructure"); + let mut req = self.request(reqwest::Method::POST, &path); + req = req.json(body); + let resp = req.send().await?; + let status = resp.status(); + let body_text = resp.text().await?; + if !status.is_success() { + return Err(Error::Api { + status: status.as_u16(), + message: serde_json::from_str::>(&body_text) + .ok() + .and_then(|r| r.error) + .unwrap_or(body_text.clone()), + }); + } + Ok(serde_json::from_str(&body_text)?) + } + + /// Validate BYOC infrastructure prerequisites + /// + /// Preflight-checks the cloud account's permissions without creating infrastructure. + pub async fn organization_byoc_infrastructure_validate( + &self, + organization_id: &str, + body: &ByocInfrastructureValidatePostRequest, + ) -> Result, Error> { + let path = format!("/v1/organizations/{organization_id}/byocInfrastructure/validate"); + let mut req = self.request(reqwest::Method::POST, &path); + req = req.json(body); + let resp = req.send().await?; + let status = resp.status(); + let body_text = resp.text().await?; + if !status.is_success() { + return Err(Error::Api { + status: status.as_u16(), + message: serde_json::from_str::>(&body_text) + .ok() + .and_then(|r| r.error) + .unwrap_or(body_text.clone()), + }); + } + Ok(serde_json::from_str(&body_text)?) + } + + /// Get BYOC infrastructure details + pub async fn organization_byoc_infrastructure_get( + &self, + organization_id: &str, + byoc_infrastructure_id: &str, + ) -> Result, Error> { + let path = format!( + "/v1/organizations/{organization_id}/byocInfrastructure/{byoc_infrastructure_id}" + ); + let req = self.request(reqwest::Method::GET, &path); + let resp = req.send().await?; + let status = resp.status(); + let body_text = resp.text().await?; + if !status.is_success() { + return Err(Error::Api { + status: status.as_u16(), + message: serde_json::from_str::>(&body_text) + .ok() + .and_then(|r| r.error) + .unwrap_or(body_text.clone()), + }); + } + Ok(serde_json::from_str(&body_text)?) + } + + /// Get BYOC infrastructure provisioning progress + pub async fn organization_byoc_infrastructure_progress_get( + &self, + organization_id: &str, + byoc_infrastructure_id: &str, + ) -> Result, Error> { + let path = format!( + "/v1/organizations/{organization_id}/byocInfrastructure/{byoc_infrastructure_id}/progress" + ); + let req = self.request(reqwest::Method::GET, &path); + let resp = req.send().await?; + let status = resp.status(); + let body_text = resp.text().await?; + if !status.is_success() { + return Err(Error::Api { + status: status.as_u16(), + message: serde_json::from_str::>(&body_text) + .ok() + .and_then(|r| r.error) + .unwrap_or(body_text.clone()), + }); + } + Ok(serde_json::from_str(&body_text)?) + } + + /// Get BYOC infrastructure private endpoint configuration + /// + /// For BYOC organizations this replaces the deprecated organization-level + /// `organization_private_endpoint_config_get_list`. + pub async fn organization_byoc_infrastructure_private_endpoint_config_get( + &self, + organization_id: &str, + byoc_infrastructure_id: &str, + ) -> Result, Error> { + let path = format!( + "/v1/organizations/{organization_id}/byocInfrastructure/{byoc_infrastructure_id}/privateEndpointConfig" + ); + let req = self.request(reqwest::Method::GET, &path); + let resp = req.send().await?; + let status = resp.status(); + let body_text = resp.text().await?; + if !status.is_success() { + return Err(Error::Api { + status: status.as_u16(), + message: serde_json::from_str::>(&body_text) + .ok() + .and_then(|r| r.error) + .unwrap_or(body_text.clone()), + }); + } + Ok(serde_json::from_str(&body_text)?) + } + + /// Get BYOC infrastructure tags + /// + /// Tags are replaced as a whole through `organization_byoc_infrastructure_update`. + pub async fn organization_byoc_infrastructure_tags_get( + &self, + organization_id: &str, + byoc_infrastructure_id: &str, + ) -> Result, Error> { + let path = format!( + "/v1/organizations/{organization_id}/byocInfrastructure/{byoc_infrastructure_id}/tags" + ); + let req = self.request(reqwest::Method::GET, &path); + let resp = req.send().await?; + let status = resp.status(); + let body_text = resp.text().await?; + if !status.is_success() { + return Err(Error::Api { + status: status.as_u16(), + message: serde_json::from_str::>(&body_text) + .ok() + .and_then(|r| r.error) + .unwrap_or(body_text.clone()), + }); + } + Ok(serde_json::from_str(&body_text)?) + } + + /// Remove a BYOC infrastructure + pub async fn organization_byoc_infrastructure_delete( + &self, + organization_id: &str, + byoc_infrastructure_id: &str, + ) -> Result, Error> { + let path = format!( + "/v1/organizations/{organization_id}/byocInfrastructure/{byoc_infrastructure_id}" + ); + let req = self.request(reqwest::Method::DELETE, &path); + let resp = req.send().await?; + let status = resp.status(); + let body_text = resp.text().await?; + if !status.is_success() { + return Err(Error::Api { + status: status.as_u16(), + message: serde_json::from_str::>(&body_text) + .ok() + .and_then(|r| r.error) + .unwrap_or(body_text.clone()), + }); + } + Ok(serde_json::from_str(&body_text)?) + } + + /// Update BYOC Infrastructure + pub async fn organization_byoc_infrastructure_update( + &self, + organization_id: &str, + byoc_infrastructure_id: &str, + body: &ByocInfrastructurePatchRequest, + ) -> Result, Error> { + let path = format!( + "/v1/organizations/{organization_id}/byocInfrastructure/{byoc_infrastructure_id}" + ); + let mut req = self.request(reqwest::Method::PATCH, &path); + req = req.json(body); + let resp = req.send().await?; + let status = resp.status(); + let body_text = resp.text().await?; + if !status.is_success() { + return Err(Error::Api { + status: status.as_u16(), + message: serde_json::from_str::>(&body_text) + .ok() + .and_then(|r| r.error) + .unwrap_or(body_text.clone()), + }); + } + Ok(serde_json::from_str(&body_text)?) + } +} diff --git a/crates/clickhouse-cloud-api/src/client/invitations.rs b/crates/clickhouse-cloud-api/src/client/invitations.rs new file mode 100644 index 000000000..79f7732c1 --- /dev/null +++ b/crates/clickhouse-cloud-api/src/client/invitations.rs @@ -0,0 +1,97 @@ +use super::Client; +use crate::error::Error; +use crate::models::*; + +impl Client { + /// List all invitations + pub async fn invitation_get_list( + &self, + organization_id: &str, + ) -> Result>, Error> { + let path = format!("/v1/organizations/{organization_id}/invitations"); + let req = self.request(reqwest::Method::GET, &path); + let resp = req.send().await?; + let status = resp.status(); + let body_text = resp.text().await?; + if !status.is_success() { + return Err(Error::Api { + status: status.as_u16(), + message: serde_json::from_str::>(&body_text) + .ok() + .and_then(|r| r.error) + .unwrap_or(body_text.clone()), + }); + } + Ok(serde_json::from_str(&body_text)?) + } + + /// Create an invitation + pub async fn invitation_create( + &self, + organization_id: &str, + body: &InvitationPostRequest, + ) -> Result, Error> { + let path = format!("/v1/organizations/{organization_id}/invitations"); + let mut req = self.request(reqwest::Method::POST, &path); + req = req.json(body); + let resp = req.send().await?; + let status = resp.status(); + let body_text = resp.text().await?; + if !status.is_success() { + return Err(Error::Api { + status: status.as_u16(), + message: serde_json::from_str::>(&body_text) + .ok() + .and_then(|r| r.error) + .unwrap_or(body_text.clone()), + }); + } + Ok(serde_json::from_str(&body_text)?) + } + + /// Get invitation details + pub async fn invitation_get( + &self, + organization_id: &str, + invitation_id: &str, + ) -> Result, Error> { + let path = format!("/v1/organizations/{organization_id}/invitations/{invitation_id}"); + let req = self.request(reqwest::Method::GET, &path); + let resp = req.send().await?; + let status = resp.status(); + let body_text = resp.text().await?; + if !status.is_success() { + return Err(Error::Api { + status: status.as_u16(), + message: serde_json::from_str::>(&body_text) + .ok() + .and_then(|r| r.error) + .unwrap_or(body_text.clone()), + }); + } + Ok(serde_json::from_str(&body_text)?) + } + + /// Delete organization invitation + pub async fn invitation_delete( + &self, + organization_id: &str, + invitation_id: &str, + ) -> Result, Error> { + let path = format!("/v1/organizations/{organization_id}/invitations/{invitation_id}"); + let req = self.request(reqwest::Method::DELETE, &path); + let resp = req.send().await?; + let status = resp.status(); + let body_text = resp.text().await?; + if !status.is_success() { + return Err(Error::Api { + status: status.as_u16(), + message: serde_json::from_str::>(&body_text) + .ok() + .and_then(|r| r.error) + .unwrap_or(body_text.clone()), + }); + } + Ok(serde_json::from_str(&body_text)?) + } +} diff --git a/crates/clickhouse-cloud-api/src/client/members.rs b/crates/clickhouse-cloud-api/src/client/members.rs new file mode 100644 index 000000000..20f9120ff --- /dev/null +++ b/crates/clickhouse-cloud-api/src/client/members.rs @@ -0,0 +1,98 @@ +use super::Client; +use crate::error::Error; +use crate::models::*; + +impl Client { + /// List organization members + pub async fn member_get_list( + &self, + organization_id: &str, + ) -> Result>, Error> { + let path = format!("/v1/organizations/{organization_id}/members"); + let req = self.request(reqwest::Method::GET, &path); + let resp = req.send().await?; + let status = resp.status(); + let body_text = resp.text().await?; + if !status.is_success() { + return Err(Error::Api { + status: status.as_u16(), + message: serde_json::from_str::>(&body_text) + .ok() + .and_then(|r| r.error) + .unwrap_or(body_text.clone()), + }); + } + Ok(serde_json::from_str(&body_text)?) + } + + /// Get member details + pub async fn member_get( + &self, + organization_id: &str, + user_id: &str, + ) -> Result, Error> { + let path = format!("/v1/organizations/{organization_id}/members/{user_id}"); + let req = self.request(reqwest::Method::GET, &path); + let resp = req.send().await?; + let status = resp.status(); + let body_text = resp.text().await?; + if !status.is_success() { + return Err(Error::Api { + status: status.as_u16(), + message: serde_json::from_str::>(&body_text) + .ok() + .and_then(|r| r.error) + .unwrap_or(body_text.clone()), + }); + } + Ok(serde_json::from_str(&body_text)?) + } + + /// Update organization member + pub async fn member_update( + &self, + organization_id: &str, + user_id: &str, + body: &MemberPatchRequest, + ) -> Result, Error> { + let path = format!("/v1/organizations/{organization_id}/members/{user_id}"); + let mut req = self.request(reqwest::Method::PATCH, &path); + req = req.json(body); + let resp = req.send().await?; + let status = resp.status(); + let body_text = resp.text().await?; + if !status.is_success() { + return Err(Error::Api { + status: status.as_u16(), + message: serde_json::from_str::>(&body_text) + .ok() + .and_then(|r| r.error) + .unwrap_or(body_text.clone()), + }); + } + Ok(serde_json::from_str(&body_text)?) + } + + /// Remove an organization member + pub async fn member_delete( + &self, + organization_id: &str, + user_id: &str, + ) -> Result, Error> { + let path = format!("/v1/organizations/{organization_id}/members/{user_id}"); + let req = self.request(reqwest::Method::DELETE, &path); + let resp = req.send().await?; + let status = resp.status(); + let body_text = resp.text().await?; + if !status.is_success() { + return Err(Error::Api { + status: status.as_u16(), + message: serde_json::from_str::>(&body_text) + .ok() + .and_then(|r| r.error) + .unwrap_or(body_text.clone()), + }); + } + Ok(serde_json::from_str(&body_text)?) + } +} diff --git a/crates/clickhouse-cloud-api/src/client/organizations.rs b/crates/clickhouse-cloud-api/src/client/organizations.rs index 2405a55b1..4199af53d 100644 --- a/crates/clickhouse-cloud-api/src/client/organizations.rs +++ b/crates/clickhouse-cloud-api/src/client/organizations.rs @@ -178,106 +178,6 @@ impl Client { Ok(serde_json::from_str(&body_text)?) } - /// Create BYOC Infrastructure - pub async fn organization_byoc_infrastructure_create( - &self, - organization_id: &str, - body: &ByocInfrastructurePostRequest, - ) -> Result, Error> { - let path = format!("/v1/organizations/{organization_id}/byocInfrastructure"); - let mut req = self.request(reqwest::Method::POST, &path); - req = req.json(body); - let resp = req.send().await?; - let status = resp.status(); - let body_text = resp.text().await?; - if !status.is_success() { - return Err(Error::Api { - status: status.as_u16(), - message: serde_json::from_str::>(&body_text) - .ok() - .and_then(|r| r.error) - .unwrap_or(body_text.clone()), - }); - } - Ok(serde_json::from_str(&body_text)?) - } - - /// Validate BYOC infrastructure prerequisites - /// - /// Preflight-checks the cloud account's permissions without creating infrastructure. - pub async fn organization_byoc_infrastructure_validate( - &self, - organization_id: &str, - body: &ByocInfrastructureValidatePostRequest, - ) -> Result, Error> { - let path = format!("/v1/organizations/{organization_id}/byocInfrastructure/validate"); - let mut req = self.request(reqwest::Method::POST, &path); - req = req.json(body); - let resp = req.send().await?; - let status = resp.status(); - let body_text = resp.text().await?; - if !status.is_success() { - return Err(Error::Api { - status: status.as_u16(), - message: serde_json::from_str::>(&body_text) - .ok() - .and_then(|r| r.error) - .unwrap_or(body_text.clone()), - }); - } - Ok(serde_json::from_str(&body_text)?) - } - - /// Get BYOC infrastructure details - pub async fn organization_byoc_infrastructure_get( - &self, - organization_id: &str, - byoc_infrastructure_id: &str, - ) -> Result, Error> { - let path = format!( - "/v1/organizations/{organization_id}/byocInfrastructure/{byoc_infrastructure_id}" - ); - let req = self.request(reqwest::Method::GET, &path); - let resp = req.send().await?; - let status = resp.status(); - let body_text = resp.text().await?; - if !status.is_success() { - return Err(Error::Api { - status: status.as_u16(), - message: serde_json::from_str::>(&body_text) - .ok() - .and_then(|r| r.error) - .unwrap_or(body_text.clone()), - }); - } - Ok(serde_json::from_str(&body_text)?) - } - - /// Get BYOC infrastructure provisioning progress - pub async fn organization_byoc_infrastructure_progress_get( - &self, - organization_id: &str, - byoc_infrastructure_id: &str, - ) -> Result, Error> { - let path = format!( - "/v1/organizations/{organization_id}/byocInfrastructure/{byoc_infrastructure_id}/progress" - ); - let req = self.request(reqwest::Method::GET, &path); - let resp = req.send().await?; - let status = resp.status(); - let body_text = resp.text().await?; - if !status.is_success() { - return Err(Error::Api { - status: status.as_u16(), - message: serde_json::from_str::>(&body_text) - .ok() - .and_then(|r| r.error) - .unwrap_or(body_text.clone()), - }); - } - Ok(serde_json::from_str(&body_text)?) - } - /// Get one page of organization notifications, most recent first (Beta). /// /// Pass `next_cursor` unchanged as `cursor` until it is absent. The server @@ -362,415 +262,6 @@ impl Client { Ok(serde_json::from_str(&body_text)?) } - /// Get BYOC infrastructure private endpoint configuration - /// - /// For BYOC organizations this replaces the deprecated organization-level - /// `organization_private_endpoint_config_get_list`. - pub async fn organization_byoc_infrastructure_private_endpoint_config_get( - &self, - organization_id: &str, - byoc_infrastructure_id: &str, - ) -> Result, Error> { - let path = format!( - "/v1/organizations/{organization_id}/byocInfrastructure/{byoc_infrastructure_id}/privateEndpointConfig" - ); - let req = self.request(reqwest::Method::GET, &path); - let resp = req.send().await?; - let status = resp.status(); - let body_text = resp.text().await?; - if !status.is_success() { - return Err(Error::Api { - status: status.as_u16(), - message: serde_json::from_str::>(&body_text) - .ok() - .and_then(|r| r.error) - .unwrap_or(body_text.clone()), - }); - } - Ok(serde_json::from_str(&body_text)?) - } - - /// Get BYOC infrastructure tags - /// - /// Tags are replaced as a whole through `organization_byoc_infrastructure_update`. - pub async fn organization_byoc_infrastructure_tags_get( - &self, - organization_id: &str, - byoc_infrastructure_id: &str, - ) -> Result, Error> { - let path = format!( - "/v1/organizations/{organization_id}/byocInfrastructure/{byoc_infrastructure_id}/tags" - ); - let req = self.request(reqwest::Method::GET, &path); - let resp = req.send().await?; - let status = resp.status(); - let body_text = resp.text().await?; - if !status.is_success() { - return Err(Error::Api { - status: status.as_u16(), - message: serde_json::from_str::>(&body_text) - .ok() - .and_then(|r| r.error) - .unwrap_or(body_text.clone()), - }); - } - Ok(serde_json::from_str(&body_text)?) - } - - /// Remove a BYOC infrastructure - pub async fn organization_byoc_infrastructure_delete( - &self, - organization_id: &str, - byoc_infrastructure_id: &str, - ) -> Result, Error> { - let path = format!( - "/v1/organizations/{organization_id}/byocInfrastructure/{byoc_infrastructure_id}" - ); - let req = self.request(reqwest::Method::DELETE, &path); - let resp = req.send().await?; - let status = resp.status(); - let body_text = resp.text().await?; - if !status.is_success() { - return Err(Error::Api { - status: status.as_u16(), - message: serde_json::from_str::>(&body_text) - .ok() - .and_then(|r| r.error) - .unwrap_or(body_text.clone()), - }); - } - Ok(serde_json::from_str(&body_text)?) - } - - /// Update BYOC Infrastructure - pub async fn organization_byoc_infrastructure_update( - &self, - organization_id: &str, - byoc_infrastructure_id: &str, - body: &ByocInfrastructurePatchRequest, - ) -> Result, Error> { - let path = format!( - "/v1/organizations/{organization_id}/byocInfrastructure/{byoc_infrastructure_id}" - ); - let mut req = self.request(reqwest::Method::PATCH, &path); - req = req.json(body); - let resp = req.send().await?; - let status = resp.status(); - let body_text = resp.text().await?; - if !status.is_success() { - return Err(Error::Api { - status: status.as_u16(), - message: serde_json::from_str::>(&body_text) - .ok() - .and_then(|r| r.error) - .unwrap_or(body_text.clone()), - }); - } - Ok(serde_json::from_str(&body_text)?) - } - - /// List all invitations - pub async fn invitation_get_list( - &self, - organization_id: &str, - ) -> Result>, Error> { - let path = format!("/v1/organizations/{organization_id}/invitations"); - let req = self.request(reqwest::Method::GET, &path); - let resp = req.send().await?; - let status = resp.status(); - let body_text = resp.text().await?; - if !status.is_success() { - return Err(Error::Api { - status: status.as_u16(), - message: serde_json::from_str::>(&body_text) - .ok() - .and_then(|r| r.error) - .unwrap_or(body_text.clone()), - }); - } - Ok(serde_json::from_str(&body_text)?) - } - - /// Create an invitation - pub async fn invitation_create( - &self, - organization_id: &str, - body: &InvitationPostRequest, - ) -> Result, Error> { - let path = format!("/v1/organizations/{organization_id}/invitations"); - let mut req = self.request(reqwest::Method::POST, &path); - req = req.json(body); - let resp = req.send().await?; - let status = resp.status(); - let body_text = resp.text().await?; - if !status.is_success() { - return Err(Error::Api { - status: status.as_u16(), - message: serde_json::from_str::>(&body_text) - .ok() - .and_then(|r| r.error) - .unwrap_or(body_text.clone()), - }); - } - Ok(serde_json::from_str(&body_text)?) - } - - /// Get invitation details - pub async fn invitation_get( - &self, - organization_id: &str, - invitation_id: &str, - ) -> Result, Error> { - let path = format!("/v1/organizations/{organization_id}/invitations/{invitation_id}"); - let req = self.request(reqwest::Method::GET, &path); - let resp = req.send().await?; - let status = resp.status(); - let body_text = resp.text().await?; - if !status.is_success() { - return Err(Error::Api { - status: status.as_u16(), - message: serde_json::from_str::>(&body_text) - .ok() - .and_then(|r| r.error) - .unwrap_or(body_text.clone()), - }); - } - Ok(serde_json::from_str(&body_text)?) - } - - /// Delete organization invitation - pub async fn invitation_delete( - &self, - organization_id: &str, - invitation_id: &str, - ) -> Result, Error> { - let path = format!("/v1/organizations/{organization_id}/invitations/{invitation_id}"); - let req = self.request(reqwest::Method::DELETE, &path); - let resp = req.send().await?; - let status = resp.status(); - let body_text = resp.text().await?; - if !status.is_success() { - return Err(Error::Api { - status: status.as_u16(), - message: serde_json::from_str::>(&body_text) - .ok() - .and_then(|r| r.error) - .unwrap_or(body_text.clone()), - }); - } - Ok(serde_json::from_str(&body_text)?) - } - - /// List organization members - pub async fn member_get_list( - &self, - organization_id: &str, - ) -> Result>, Error> { - let path = format!("/v1/organizations/{organization_id}/members"); - let req = self.request(reqwest::Method::GET, &path); - let resp = req.send().await?; - let status = resp.status(); - let body_text = resp.text().await?; - if !status.is_success() { - return Err(Error::Api { - status: status.as_u16(), - message: serde_json::from_str::>(&body_text) - .ok() - .and_then(|r| r.error) - .unwrap_or(body_text.clone()), - }); - } - Ok(serde_json::from_str(&body_text)?) - } - - /// Get member details - pub async fn member_get( - &self, - organization_id: &str, - user_id: &str, - ) -> Result, Error> { - let path = format!("/v1/organizations/{organization_id}/members/{user_id}"); - let req = self.request(reqwest::Method::GET, &path); - let resp = req.send().await?; - let status = resp.status(); - let body_text = resp.text().await?; - if !status.is_success() { - return Err(Error::Api { - status: status.as_u16(), - message: serde_json::from_str::>(&body_text) - .ok() - .and_then(|r| r.error) - .unwrap_or(body_text.clone()), - }); - } - Ok(serde_json::from_str(&body_text)?) - } - - /// Update organization member - pub async fn member_update( - &self, - organization_id: &str, - user_id: &str, - body: &MemberPatchRequest, - ) -> Result, Error> { - let path = format!("/v1/organizations/{organization_id}/members/{user_id}"); - let mut req = self.request(reqwest::Method::PATCH, &path); - req = req.json(body); - let resp = req.send().await?; - let status = resp.status(); - let body_text = resp.text().await?; - if !status.is_success() { - return Err(Error::Api { - status: status.as_u16(), - message: serde_json::from_str::>(&body_text) - .ok() - .and_then(|r| r.error) - .unwrap_or(body_text.clone()), - }); - } - Ok(serde_json::from_str(&body_text)?) - } - - /// Remove an organization member - pub async fn member_delete( - &self, - organization_id: &str, - user_id: &str, - ) -> Result, Error> { - let path = format!("/v1/organizations/{organization_id}/members/{user_id}"); - let req = self.request(reqwest::Method::DELETE, &path); - let resp = req.send().await?; - let status = resp.status(); - let body_text = resp.text().await?; - if !status.is_success() { - return Err(Error::Api { - status: status.as_u16(), - message: serde_json::from_str::>(&body_text) - .ok() - .and_then(|r| r.error) - .unwrap_or(body_text.clone()), - }); - } - Ok(serde_json::from_str(&body_text)?) - } - - /// List all available roles for an organization - pub async fn organization_roles_get_list( - &self, - organization_id: &str, - ) -> Result>, Error> { - let path = format!("/v1/organizations/{organization_id}/roles"); - let req = self.request(reqwest::Method::GET, &path); - let resp = req.send().await?; - let status = resp.status(); - let body_text = resp.text().await?; - if !status.is_success() { - return Err(Error::Api { - status: status.as_u16(), - message: serde_json::from_str::>(&body_text) - .ok() - .and_then(|r| r.error) - .unwrap_or(body_text.clone()), - }); - } - Ok(serde_json::from_str(&body_text)?) - } - - /// Create a new role - pub async fn organization_role_post( - &self, - organization_id: &str, - body: &RoleCreateRequest, - ) -> Result, Error> { - let path = format!("/v1/organizations/{organization_id}/roles"); - let mut req = self.request(reqwest::Method::POST, &path); - req = req.json(body); - let resp = req.send().await?; - let status = resp.status(); - let body_text = resp.text().await?; - if !status.is_success() { - return Err(Error::Api { - status: status.as_u16(), - message: serde_json::from_str::>(&body_text) - .ok() - .and_then(|r| r.error) - .unwrap_or(body_text.clone()), - }); - } - Ok(serde_json::from_str(&body_text)?) - } - - /// Get role details - pub async fn organization_role_get( - &self, - organization_id: &str, - role_id: &str, - ) -> Result, Error> { - let path = format!("/v1/organizations/{organization_id}/roles/{role_id}"); - let req = self.request(reqwest::Method::GET, &path); - let resp = req.send().await?; - let status = resp.status(); - let body_text = resp.text().await?; - if !status.is_success() { - return Err(Error::Api { - status: status.as_u16(), - message: serde_json::from_str::>(&body_text) - .ok() - .and_then(|r| r.error) - .unwrap_or(body_text.clone()), - }); - } - Ok(serde_json::from_str(&body_text)?) - } - - /// Update a role - pub async fn organization_role_patch( - &self, - organization_id: &str, - role_id: &str, - body: &RoleUpdateRequest, - ) -> Result, Error> { - let path = format!("/v1/organizations/{organization_id}/roles/{role_id}"); - let mut req = self.request(reqwest::Method::PATCH, &path); - req = req.json(body); - let resp = req.send().await?; - let status = resp.status(); - let body_text = resp.text().await?; - if !status.is_success() { - return Err(Error::Api { - status: status.as_u16(), - message: serde_json::from_str::>(&body_text) - .ok() - .and_then(|r| r.error) - .unwrap_or(body_text.clone()), - }); - } - Ok(serde_json::from_str(&body_text)?) - } - - /// Delete a role - pub async fn organization_role_delete( - &self, - organization_id: &str, - role_id: &str, - ) -> Result, Error> { - let path = format!("/v1/organizations/{organization_id}/roles/{role_id}"); - let req = self.request(reqwest::Method::DELETE, &path); - let resp = req.send().await?; - let status = resp.status(); - let body_text = resp.text().await?; - if !status.is_success() { - return Err(Error::Api { - status: status.as_u16(), - message: serde_json::from_str::>(&body_text) - .ok() - .and_then(|r| r.error) - .unwrap_or(body_text.clone()), - }); - } - Ok(serde_json::from_str(&body_text)?) - } - /// Get private endpoint configuration for region within cloud provider for an organization #[deprecated] #[allow(deprecated)] diff --git a/crates/clickhouse-cloud-api/src/client/rbac.rs b/crates/clickhouse-cloud-api/src/client/rbac.rs new file mode 100644 index 000000000..396efe4cd --- /dev/null +++ b/crates/clickhouse-cloud-api/src/client/rbac.rs @@ -0,0 +1,122 @@ +use super::Client; +use crate::error::Error; +use crate::models::*; + +impl Client { + /// List all available roles for an organization + pub async fn organization_roles_get_list( + &self, + organization_id: &str, + ) -> Result>, Error> { + let path = format!("/v1/organizations/{organization_id}/roles"); + let req = self.request(reqwest::Method::GET, &path); + let resp = req.send().await?; + let status = resp.status(); + let body_text = resp.text().await?; + if !status.is_success() { + return Err(Error::Api { + status: status.as_u16(), + message: serde_json::from_str::>(&body_text) + .ok() + .and_then(|r| r.error) + .unwrap_or(body_text.clone()), + }); + } + Ok(serde_json::from_str(&body_text)?) + } + + /// Create a new role + pub async fn organization_role_post( + &self, + organization_id: &str, + body: &RoleCreateRequest, + ) -> Result, Error> { + let path = format!("/v1/organizations/{organization_id}/roles"); + let mut req = self.request(reqwest::Method::POST, &path); + req = req.json(body); + let resp = req.send().await?; + let status = resp.status(); + let body_text = resp.text().await?; + if !status.is_success() { + return Err(Error::Api { + status: status.as_u16(), + message: serde_json::from_str::>(&body_text) + .ok() + .and_then(|r| r.error) + .unwrap_or(body_text.clone()), + }); + } + Ok(serde_json::from_str(&body_text)?) + } + + /// Get role details + pub async fn organization_role_get( + &self, + organization_id: &str, + role_id: &str, + ) -> Result, Error> { + let path = format!("/v1/organizations/{organization_id}/roles/{role_id}"); + let req = self.request(reqwest::Method::GET, &path); + let resp = req.send().await?; + let status = resp.status(); + let body_text = resp.text().await?; + if !status.is_success() { + return Err(Error::Api { + status: status.as_u16(), + message: serde_json::from_str::>(&body_text) + .ok() + .and_then(|r| r.error) + .unwrap_or(body_text.clone()), + }); + } + Ok(serde_json::from_str(&body_text)?) + } + + /// Update a role + pub async fn organization_role_patch( + &self, + organization_id: &str, + role_id: &str, + body: &RoleUpdateRequest, + ) -> Result, Error> { + let path = format!("/v1/organizations/{organization_id}/roles/{role_id}"); + let mut req = self.request(reqwest::Method::PATCH, &path); + req = req.json(body); + let resp = req.send().await?; + let status = resp.status(); + let body_text = resp.text().await?; + if !status.is_success() { + return Err(Error::Api { + status: status.as_u16(), + message: serde_json::from_str::>(&body_text) + .ok() + .and_then(|r| r.error) + .unwrap_or(body_text.clone()), + }); + } + Ok(serde_json::from_str(&body_text)?) + } + + /// Delete a role + pub async fn organization_role_delete( + &self, + organization_id: &str, + role_id: &str, + ) -> Result, Error> { + let path = format!("/v1/organizations/{organization_id}/roles/{role_id}"); + let req = self.request(reqwest::Method::DELETE, &path); + let resp = req.send().await?; + let status = resp.status(); + let body_text = resp.text().await?; + if !status.is_success() { + return Err(Error::Api { + status: status.as_u16(), + message: serde_json::from_str::>(&body_text) + .ok() + .and_then(|r| r.error) + .unwrap_or(body_text.clone()), + }); + } + Ok(serde_json::from_str(&body_text)?) + } +} From 4b3e8552b508e9d912fafd3108c526233b00d99b Mon Sep 17 00:00:00 2001 From: sdairs Date: Fri, 30 Oct 2026 19:43:20 +0100 Subject: [PATCH 3/4] ci: classify the split client files and convert/services.rs (#1173) client/byoc.rs, client/invitations.rs, client/members.rs and client/rbac.rs map to the organization suite only, like their models/invitations.rs, models/members.rs and models/rbac.rs counterparts. Of the live suites, only integration_org_test calls the BYOC, invitation, member and role methods; common/support.rs's cleanup calls invitation_delete, member_update and organization_role_delete only for resources that integration_org_test registers; and the clickhousectl commands the live CLI tests spawn (`cloud service query`, `cloud clickpipe create postgres`) do not reach these methods. organizations.rs keeps service+organization for organization_get, organization_get_list, organization_update and usage_cost_get, which integration_test calls. convert/services.rs keeps convert/service.rs's service+clickpipes mapping; the old path stays mapped so diffs that name it remain classified. Co-Authored-By: Claude Opus 5.5 --- scripts/classify-cloud-integration.py | 7 +++++++ scripts/tests/test_classify_cloud_integration.py | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/classify-cloud-integration.py b/scripts/classify-cloud-integration.py index daf17b75d..c28f4806f 100644 --- a/scripts/classify-cloud-integration.py +++ b/scripts/classify-cloud-integration.py @@ -27,10 +27,13 @@ {"service", "organization"} ), "crates/clickhouse-cloud-api/src/client/backups.rs": frozenset({"service"}), + "crates/clickhouse-cloud-api/src/client/byoc.rs": frozenset({"organization"}), "crates/clickhouse-cloud-api/src/client/clickpipes.rs": frozenset( {"clickpipes"} ), "crates/clickhouse-cloud-api/src/client/clickstack.rs": NO_SUITES, + "crates/clickhouse-cloud-api/src/client/invitations.rs": frozenset({"organization"}), + "crates/clickhouse-cloud-api/src/client/members.rs": frozenset({"organization"}), "crates/clickhouse-cloud-api/src/client/organizations.rs": frozenset( {"service", "organization"} ), @@ -39,6 +42,7 @@ ), "crates/clickhouse-cloud-api/src/client/query.rs": frozenset({"service"}), "crates/clickhouse-cloud-api/src/client/query_api_endpoints.rs": frozenset({"service"}), + "crates/clickhouse-cloud-api/src/client/rbac.rs": frozenset({"organization"}), "crates/clickhouse-cloud-api/src/client/saved_queries.rs": frozenset({"service"}), "crates/clickhouse-cloud-api/src/client/services.rs": frozenset( {"service", "clickpipes"} @@ -56,6 +60,9 @@ "crates/clickhouse-cloud-api/src/convert/service.rs": frozenset( {"service", "clickpipes"} ), + "crates/clickhouse-cloud-api/src/convert/services.rs": frozenset( + {"service", "clickpipes"} + ), "crates/clickhouse-cloud-api/src/convert/shared.rs": ALL_SUITES, "crates/clickhouse-cloud-api/src/error.rs": ALL_SUITES, "crates/clickhouse-cloud-api/src/lib.rs": ALL_SUITES, diff --git a/scripts/tests/test_classify_cloud_integration.py b/scripts/tests/test_classify_cloud_integration.py index 466136364..e839cc574 100644 --- a/scripts/tests/test_classify_cloud_integration.py +++ b/scripts/tests/test_classify_cloud_integration.py @@ -75,7 +75,7 @@ def test_current_source_mapping_values(self): }, frozenset({"service", "clickpipes"}): { "crates/clickhouse-cloud-api/src/client/services.rs", - "crates/clickhouse-cloud-api/src/convert/service.rs", + "crates/clickhouse-cloud-api/src/convert/services.rs", "crates/clickhouse-cloud-api/src/models/services.rs", }, frozenset({"postgres", "clickpipes"}): { @@ -99,6 +99,10 @@ def test_current_source_mapping_values(self): frozenset({"organization"}): { "crates/clickhouse-cloud-api/src/client/activity.rs", "crates/clickhouse-cloud-api/src/models/activity.rs", + "crates/clickhouse-cloud-api/src/client/byoc.rs", + "crates/clickhouse-cloud-api/src/client/invitations.rs", + "crates/clickhouse-cloud-api/src/client/members.rs", + "crates/clickhouse-cloud-api/src/client/rbac.rs", "crates/clickhouse-cloud-api/src/models/invitations.rs", "crates/clickhouse-cloud-api/src/models/members.rs", "crates/clickhouse-cloud-api/src/models/rbac.rs", From d80cd42f1b04881921ac964ef109aa2f55de367a Mon Sep 17 00:00:00 2001 From: sdairs Date: Fri, 30 Oct 2026 19:43:26 +0100 Subject: [PATCH 4/4] chore: ignore the client/organizations.rs split in git blame (#1173) Co-Authored-By: Claude Opus 5.5 --- .git-blame-ignore-revs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 5d25bb4ad..2e1f7bb62 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -16,3 +16,6 @@ e7fcdb1a8f1e48c564717f1ccd0fa654d182aa73 # Move the Query API out of client.rs into client/query.rs (#1173) 8db8f19a0b69ff505c33188414efb85dc50dfd0e + +# Split client/organizations.rs into byoc, invitations, members and rbac (#1173) +9e20dd888a937f5f0b610b22a71aef4ff70b9357