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}