integrationos_domain/domain/connection/
connection_model_definition.rs

1use super::api_model_config::ApiModelConfig;
2use crate::{
3    id::Id,
4    prelude::{schema::common_model::CommonModel, shared::record_metadata::RecordMetadata},
5};
6use serde::{Deserialize, Serialize};
7use serde_json::{json, Value};
8use strum::{Display, EnumIter};
9
10#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
11#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
12pub enum ParameterLocation {
13    QueryParameter,
14    RequestBody,
15    Header,
16}
17
18#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
19#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
20#[serde(rename_all = "camelCase")]
21pub struct ConnectionModelDefinition {
22    #[serde(rename = "_id")]
23    pub id: Id,
24    pub connection_platform: String,
25    pub connection_definition_id: Id,
26    pub platform_version: String,
27    #[serde(default)]
28    pub key: String,
29    pub title: String,
30    pub name: String,
31    pub model_name: String,
32    #[serde(with = "http_serde_ext::method")]
33    #[cfg_attr(feature = "dummy", dummy(expr = "http::Method::GET"))]
34    pub action: http::Method,
35    pub action_name: CrudAction,
36
37    #[serde(flatten)]
38    pub platform_info: PlatformInfo,
39
40    #[serde(flatten, skip_serializing_if = "Option::is_none")]
41    #[cfg_attr(feature = "dummy", dummy(default))]
42    pub extractor_config: Option<ExtractorConfig>,
43
44    pub test_connection_status: TestConnection,
45    pub test_connection_payload: Option<Value>,
46
47    pub is_default_crud_mapping: Option<bool>,
48    pub mapping: Option<CrudMapping>,
49
50    #[serde(flatten, default)]
51    pub record_metadata: RecordMetadata,
52
53    #[serde(default)]
54    pub supported: bool,
55}
56
57#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
58#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
59#[serde(rename_all = "camelCase")]
60pub struct TestConnection {
61    pub last_tested_at: i64,
62    pub state: TestConnectionState,
63}
64
65impl Default for TestConnection {
66    fn default() -> Self {
67        Self {
68            last_tested_at: 0,
69            state: TestConnectionState::Untested,
70        }
71    }
72}
73
74#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Default)]
75#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
76#[serde(rename_all = "camelCase")]
77pub enum TestConnectionState {
78    Success {
79        #[serde(rename = "requestPayload")]
80        request_payload: String,
81        response: String,
82    },
83    Failure {
84        message: String,
85        #[serde(rename = "requestPayload")]
86        request_payload: String,
87    },
88    #[default]
89    Untested,
90}
91
92pub enum ConnectionModelDefinitionWithState {
93    Populated(ConnectionModelDefinition),
94    Unpopulated(ConnectionModelDefinition),
95}
96
97#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
98#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
99#[serde(untagged)]
100pub enum PlatformInfo {
101    Api(ApiModelConfig),
102}
103
104#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
105#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
106#[serde(rename_all = "camelCase")]
107pub struct ExtractorConfig {
108    pub pull_frequency: i64,
109    pub batch_size: i64,
110    pub cursor: CursorConfig,
111    pub limit: Option<LimitConfig>,
112    pub sleep_after_finish: i64,
113    pub update_config: Option<UpdateConfig>,
114    pub enabled: bool,
115}
116
117#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
118#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
119#[serde(rename_all = "camelCase")]
120pub struct CursorConfig {
121    pub param_name: Option<String>,
122    pub location: Option<ParameterLocation>,
123    pub format: Option<String>,
124    pub cursor_path: String,
125    pub data_path: String,
126    pub js_extractor_function: Option<String>,
127    pub reset_on_end: bool,
128}
129
130#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
131#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
132#[serde(rename_all = "camelCase")]
133pub struct LimitConfig {
134    pub param_name: String,
135    pub location: ParameterLocation,
136}
137
138#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
139#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
140#[serde(rename_all = "camelCase")]
141pub struct UpdateConfig {
142    pub param_name: String,
143    pub location: ParameterLocation,
144    pub format: String,
145}
146
147#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
148#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
149#[serde(rename_all = "camelCase")]
150pub struct CrudMapping {
151    pub action: CrudAction,
152    pub common_model_name: String,
153    pub from_common_model: Option<String>,
154    pub to_common_model: Option<String>,
155}
156
157#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Display, EnumIter)]
158#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
159#[serde(rename_all = "camelCase")]
160#[strum(serialize_all = "camelCase")]
161pub enum CrudAction {
162    GetOne,
163    GetMany,
164    GetCount,
165    Upsert,
166    Update,
167    Create,
168    Delete,
169    Custom,
170}
171
172impl CrudAction {
173    pub fn description(&self) -> &'static str {
174        match self {
175            CrudAction::GetOne => "Get one record",
176            CrudAction::GetMany => "List records",
177            CrudAction::GetCount => "Get count of records",
178            CrudAction::Upsert => "Upsert a record",
179            CrudAction::Update => "Update a record",
180            CrudAction::Create => "Create a record",
181            CrudAction::Delete => "Delete a record",
182            CrudAction::Custom => "Custom action",
183        }
184    }
185
186    pub fn example(&self, common_model: &CommonModel) -> Value {
187        let meta = json!({
188            "timestamp": chrono::offset::Local::now().timestamp_millis(),
189            "latency": 36,
190            "platformRateLimitRemaining": 0,
191            "rateLimitRemaining": 0,
192            "cache": {
193              "hit": false,
194              "ttl": 0,
195              "key": ""
196            },
197            "transactionKey": "tx_ky::7fb4a1ee61454e61adf79f37251affc4",
198            "txn": "727cc0ecc6144f6782ca0d72486d3dea",
199            "platform": "PlatformName",
200            "platformVersion": "v1",
201            "connectionDefinitionKey": "conn_def::7923c71ff5ae4119870bc42182c50cb7",
202            "action": self.to_string(),
203            "commonModel": common_model.name,
204            "commonModelVersion": common_model.record_metadata.version,
205            "connectionKey": "platform::8e09f436-601a-40c4-97cd-f9347e924418",
206            "hash": "3eafdfa35a39411ca4992e7139c69d854f2135ad956c45449a7b6a98288d59a61177f386801d460ca3ee3884635d575b",
207            "heartbeats": [],
208            "totalTransactions": 1
209        });
210
211        match self {
212            CrudAction::Create => {
213                json!({
214                    "status": "success",
215                    "statusCode": 200,
216                    "unified": common_model.sample,
217                    "passthrough": {},
218                    "meta": meta
219                })
220            }
221            CrudAction::GetMany => {
222                json!({
223                    "status": "success",
224                    "statusCode": 200,
225                    "unified": vec![common_model.clone().sample],
226                    "passthrough": {},
227                    "pagination": {
228                        "cursor": "23e6534fa96e810b3",
229                        "limit": 100
230                    },
231                    "meta": meta
232                })
233            }
234            CrudAction::GetOne => {
235                json!({
236                    "status": "success",
237                    "statusCode": 200,
238                    "unified": common_model.sample,
239                    "passthrough": {},
240                    "meta": meta
241                })
242            }
243            CrudAction::GetCount => {
244                json!({
245                    "status": "success",
246                    "statusCode": 200,
247                    "unified": {
248                        "count": 1
249                    },
250                    "passthrough": {},
251                    "meta": meta
252                })
253            }
254            CrudAction::Upsert => {
255                json!({
256                    "status": "success",
257                    "statusCode": 200,
258                    "unified": common_model.sample,
259                    "passthrough": {},
260                    "meta": meta
261                })
262            }
263            CrudAction::Update => {
264                json!({
265                    "status": "success",
266                    "statusCode": 200,
267                    "unified": {},
268                    "passthrough": {},
269                    "meta": meta
270                })
271            }
272            CrudAction::Delete => {
273                json!({
274                    "status": "success",
275                    "statusCode": 200,
276                    "unified": {},
277                    "passthrough": {},
278                    "meta": meta
279                })
280            }
281            CrudAction::Custom => {
282                unimplemented!()
283            }
284        }
285    }
286}
287
288#[cfg(test)]
289mod tests {
290
291    use crate::prelude::connection::api_model_config::AuthMethod;
292
293    use super::*;
294    use serde_json::json;
295
296    #[test]
297    fn test_deserialize_auth_method() {
298        let bearer_token = json!({
299            "type": "BearerToken",
300            "value": "some_token"
301        });
302
303        let api_key = json!({
304            "type": "ApiKey",
305            "key": "X-Api-Key",
306            "value": "some_key"
307        });
308
309        let basic_auth = json!({
310            "type": "BasicAuth",
311            "username": "username",
312            "password": "password"
313        });
314
315        let deserialized_bearer_token: AuthMethod = serde_json::from_value(bearer_token).unwrap();
316        let deserialized_api_key: AuthMethod = serde_json::from_value(api_key).unwrap();
317        let deserialized_basic_auth: AuthMethod = serde_json::from_value(basic_auth).unwrap();
318
319        assert_eq!(
320            deserialized_bearer_token,
321            AuthMethod::BearerToken {
322                value: "some_token".to_string()
323            }
324        );
325        assert_eq!(
326            deserialized_api_key,
327            AuthMethod::ApiKey {
328                key: "X-Api-Key".to_string(),
329                value: "some_key".to_string()
330            }
331        );
332        assert_eq!(
333            deserialized_basic_auth,
334            AuthMethod::BasicAuth {
335                username: "username".to_string(),
336                password: "password".to_string()
337            }
338        );
339    }
340
341    #[test]
342    fn test_deserialize_parameter_location() {
343        let query_parameter = json!("QueryParameter");
344        let request_body = json!("RequestBody");
345        let header = json!("Header");
346
347        let deserialized_query_parameter: ParameterLocation =
348            serde_json::from_value(query_parameter).unwrap();
349        let deserialized_request_body: ParameterLocation =
350            serde_json::from_value(request_body).unwrap();
351        let deserialized_header: ParameterLocation = serde_json::from_value(header).unwrap();
352
353        assert_eq!(
354            deserialized_query_parameter,
355            ParameterLocation::QueryParameter
356        );
357        assert_eq!(deserialized_request_body, ParameterLocation::RequestBody);
358        assert_eq!(deserialized_header, ParameterLocation::Header);
359    }
360
361    #[test]
362    fn test_model_config_deserializing() {
363        let sample_config = json!({
364            "_id" : "conn_mod_def::AAAAAAAAAAA::AAAAAAAAAAAAAAAAAAAAAA",
365            "connectionPlatform" : "stripe",
366            "connectionDefinitionId" : "conn_def::AAAAAAAAAAA::AAAAAAAAAAAAAAAAAAAAAA",
367            "platformVersion" : "v1",
368            "title" : "Get Webhook Endpoints",
369            "name" : "webhook_endpoints",
370            "key" : "api::stripe::v1::Webhook::getOne::webhook_endpoints",
371            "modelName" : "Webhook",
372            "action" : "GET",
373            "actionName": "getOne",
374            "baseUrl" : "https://api.stripe.com/v1",
375            "path" : "webhook_endpoints",
376            "authMethod" : {
377                "type" : "BearerToken",
378                "value" : "stripe_secret_key"
379            },
380            "samples" : {
381                "queryParams": null,
382                "pathParams": null,
383                "body": null
384            },
385            "schemas" : {
386                "headers": null,
387                "queryParams": null,
388                "pathParams": null,
389                "body": null
390            },
391            "paths": null,
392            "responses": [],
393            "headers" : null,
394            "queryParams" : null,
395            "pullFrequency" : 5,
396            "batchSize" : 100,
397            "cursor" : {
398                "paramName" : "starting_after",
399                "location" : "QueryParameter",
400                "format" : "{cursor}",
401                "cursorPath" : "_.body.id",
402                "dataPath" : "_.body.data",
403                "jsExtractorFunction" : null,
404                "resetOnEnd" : true
405            },
406            "limit" : {
407                "paramName" : "limit",
408                "location" : "QueryParameter"
409            },
410            "sleepAfterFinish" : 86400,
411            "updateConfig" : null,
412            "enabled" : true,
413            "_version" : "1.0.0",
414            "testConnectionStatus": {
415                "lastTestedAt": 1697833149,
416                "state" : {
417                    "success" : {
418                        "response" : "{}",
419                        "requestPayload" : "{}"
420                    }
421                }
422            },
423            "createdAt": 1697833149,
424            "updatedAt": 1697833149,
425            "updated": false,
426            "version": "1.0.0",
427            "lastModifiedBy": "system",
428            "deleted": false,
429            "changeLog": {},
430            "tags": [],
431            "active": true,
432            "deprecated": false,
433            "isDefaultCrudMapping": false,
434        });
435
436        let model_config: ConnectionModelDefinition =
437            serde_json::from_value(sample_config).expect("Failed to deserialize ModelConfig");
438
439        assert_eq!(model_config.name, "webhook_endpoints");
440        assert_eq!(model_config.action, http::Method::GET);
441        assert_eq!(model_config.action_name, CrudAction::GetOne);
442        let PlatformInfo::Api(platform_info) = model_config.platform_info;
443        assert_eq!(platform_info.base_url, "https://api.stripe.com/v1");
444        assert_eq!(platform_info.path, "webhook_endpoints");
445        assert_eq!(
446            platform_info.auth_method,
447            AuthMethod::BearerToken {
448                value: "stripe_secret_key".to_string()
449            }
450        );
451        assert_eq!(platform_info.query_params, None);
452        assert_eq!(platform_info.headers, None);
453        if let Some(ExtractorConfig {
454            pull_frequency,
455            batch_size,
456            cursor,
457            limit,
458            sleep_after_finish,
459            update_config,
460            enabled,
461        }) = model_config.extractor_config
462        {
463            assert_eq!(pull_frequency, 5);
464            assert_eq!(batch_size, 100);
465            assert_eq!(sleep_after_finish, 86400);
466            assert_eq!(
467                limit,
468                Some(LimitConfig {
469                    param_name: "limit".to_string(),
470                    location: ParameterLocation::QueryParameter
471                })
472            );
473            assert_eq!(
474                cursor,
475                CursorConfig {
476                    param_name: Some("starting_after".to_string()),
477                    location: Some(ParameterLocation::QueryParameter),
478                    format: Some("{cursor}".to_string()),
479                    cursor_path: "_.body.id".to_string(),
480                    data_path: "_.body.data".to_string(),
481                    reset_on_end: true,
482                    js_extractor_function: None
483                }
484            );
485
486            assert_eq!(update_config, None);
487            assert!(enabled);
488        } else {
489            panic!("Wrong api config type");
490        }
491    }
492}