Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions crates/nango/src/integration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
use crate::client::{NangoClient, append_query, check_response, parse_response};
use crate::common_derives;
use crate::connect_session::DataWrapper;

common_derives! {
pub struct Integration {
pub unique_key: String,
pub display_name: String,
pub provider: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub logo: Option<String>,
pub created_at: String,
pub updated_at: String,
}
}

common_derives! {
pub struct IntegrationFull {
pub unique_key: String,
pub display_name: String,
pub provider: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub logo: Option<String>,
pub created_at: String,
pub updated_at: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub webhook_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub credentials: Option<IntegrationCredentials>,
}
}

common_derives! {
#[serde(tag = "type")]
pub enum IntegrationCredentials {
#[serde(rename = "OAUTH1")]
OAuth1 {
client_id: String,
client_secret: String,
#[serde(skip_serializing_if = "Option::is_none")]
scopes: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
webhook_secret: Option<String>,
},
#[serde(rename = "OAUTH2")]
OAuth2 {
client_id: String,
client_secret: String,
#[serde(skip_serializing_if = "Option::is_none")]
scopes: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
webhook_secret: Option<String>,
},
#[serde(rename = "TBA")]
Tba {
client_id: String,
client_secret: String,
#[serde(skip_serializing_if = "Option::is_none")]
scopes: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
webhook_secret: Option<String>,
},
#[serde(rename = "APP")]
App {
app_id: String,
app_link: String,
private_key: String,
},
#[serde(rename = "CUSTOM")]
Custom {
client_id: String,
client_secret: String,
app_id: String,
app_link: String,
private_key: String,
},
}
}

common_derives! {
pub struct CreateIntegrationRequest {
pub unique_key: String,
pub provider: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub credentials: Option<IntegrationCredentials>,
}
}

common_derives! {
#[derive(Default)]
pub struct UpdateIntegrationRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub unique_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub credentials: Option<IntegrationCredentials>,
}
}

impl NangoClient {
pub async fn list_integrations(&self) -> Result<Vec<Integration>, crate::Error> {
let mut url = self.api_base.clone();
url.set_path("/integrations");

let response = self.client.get(url).send().await?;
let wrapper: DataWrapper<Vec<Integration>> = parse_response(response).await?;
Ok(wrapper.data)
}

pub async fn get_integration(
&self,
unique_key: impl std::fmt::Display,
include: &[&str],
) -> Result<IntegrationFull, crate::Error> {
let mut url = self.api_base.clone();
url.set_path(&format!("/integrations/{}", unique_key));

for item in include {
append_query(&mut url, "include", item);
}

let response = self.client.get(url).send().await?;
let wrapper: DataWrapper<IntegrationFull> = parse_response(response).await?;
Ok(wrapper.data)
}

pub async fn create_integration(
&self,
req: CreateIntegrationRequest,
) -> Result<Vec<Integration>, crate::Error> {
let mut url = self.api_base.clone();
url.set_path("/integrations");

let response = self.client.post(url).json(&req).send().await?;
let wrapper: DataWrapper<Vec<Integration>> = parse_response(response).await?;
Ok(wrapper.data)
Comment on lines +133 to +139
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 create_integration deserializes response as Vec<Integration> but API returns a single object

The create_integration method at line 138 deserializes the response as DataWrapper<Vec<Integration>>, expecting the data field to be an array. However, the Nango POST /integrations API endpoint returns a single integration object in the data field ({ data: { ... } }), not an array.

Root Cause

All other create/mutate endpoints in this crate return a single object wrapped in DataWrapper<T> — for example, create_connect_session at crates/nango/src/connect_session.rs:84 uses DataWrapper<ConnectSession>, and update_integration at line 151 of the same file uses DataWrapper<Integration>. The Nango API consistently returns { data: <single_object> } for create operations.

Using DataWrapper<Vec<Integration>> will cause serde deserialization to fail at runtime with a type mismatch error (expecting array, got object), making create_integration completely non-functional.

Impact: Every call to create_integration will fail with a deserialization error, even when the API request itself succeeds.

Suggested change
) -> Result<Vec<Integration>, crate::Error> {
let mut url = self.api_base.clone();
url.set_path("/integrations");
let response = self.client.post(url).json(&req).send().await?;
let wrapper: DataWrapper<Vec<Integration>> = parse_response(response).await?;
Ok(wrapper.data)
) -> Result<Integration, crate::Error> {
let mut url = self.api_base.clone();
url.set_path("/integrations");
let response = self.client.post(url).json(&req).send().await?;
let wrapper: DataWrapper<Integration> = parse_response(response).await?;
Ok(wrapper.data)
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}

pub async fn update_integration(
&self,
unique_key: impl std::fmt::Display,
req: UpdateIntegrationRequest,
) -> Result<Integration, crate::Error> {
let mut url = self.api_base.clone();
url.set_path(&format!("/integrations/{}", unique_key));

let response = self.client.patch(url).json(&req).send().await?;
let wrapper: DataWrapper<Integration> = parse_response(response).await?;
Ok(wrapper.data)
}

pub async fn delete_integration(
&self,
unique_key: impl std::fmt::Display,
) -> Result<(), crate::Error> {
let mut url = self.api_base.clone();
url.set_path(&format!("/integrations/{}", unique_key));

let response = self.client.delete(url).send().await?;
check_response(response).await?;
Ok(())
}
}
2 changes: 2 additions & 0 deletions crates/nango/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ mod client;
mod connect_session;
mod connection;
mod error;
mod integration;
pub mod proxy;
mod types;

pub use client::*;
pub use connect_session::*;
pub use connection::*;
pub use error::*;
pub use integration::*;
pub use proxy::NangoProxyBuilder;
pub use types::*;

Expand Down
Loading