Skip to main content

winterbaume_appsync/
state.rs

1use std::collections::HashMap;
2
3use crate::types::*;
4
5#[derive(Debug, Default)]
6pub struct AppSyncState {
7    pub apis: HashMap<String, GraphqlApi>,
8    pub event_apis: HashMap<String, Api>,
9    pub api_caches: HashMap<String, ApiCacheEntry>,
10    /// api_id -> Vec<ApiKeyEntry>
11    pub api_keys: HashMap<String, Vec<ApiKeyEntry>>,
12    /// api_id -> Vec<ChannelNamespaceEntry>
13    pub channel_namespaces: HashMap<String, Vec<ChannelNamespaceEntry>>,
14    /// api_id -> Vec<TypeEntry>
15    pub types: HashMap<String, Vec<TypeEntry>>,
16    /// api_id -> SchemaStatus
17    pub schema_statuses: HashMap<String, SchemaStatus>,
18    /// resource_arn -> tags
19    pub resource_tags: HashMap<String, HashMap<String, String>>,
20}
21
22#[derive(Debug, thiserror::Error)]
23pub enum AppSyncError {
24    #[error("GraphQL API {api_id} not found.")]
25    GraphqlApiNotFound { api_id: String },
26
27    #[error("Api {api_id} not found.")]
28    ApiNotFound { api_id: String },
29
30    #[error("API cache not found for API {api_id}.")]
31    ApiCacheNotFound { api_id: String },
32
33    #[error("API cache already exists for API {api_id}.")]
34    ApiCacheAlreadyExists { api_id: String },
35
36    #[error("API key {key_id} not found.")]
37    ApiKeyNotFound { key_id: String },
38
39    #[error("Channel namespace {name} not found.")]
40    ChannelNamespaceNotFound { name: String },
41
42    #[error("Schema not found for API {api_id}.")]
43    SchemaNotFound { api_id: String },
44
45    #[error("Type {type_name} not found.")]
46    TypeNotFound { type_name: String },
47}
48
49impl AppSyncState {
50    // ===================== GraphQL API (v1) =====================
51
52    pub fn create_graphql_api(
53        &mut self,
54        name: &str,
55        authentication_type: &str,
56        account_id: &str,
57        region: &str,
58        tags: HashMap<String, String>,
59    ) -> Result<&GraphqlApi, AppSyncError> {
60        let api_id = uuid::Uuid::new_v4().to_string();
61        let arn = format!("arn:aws:appsync:{region}:{account_id}:apis/{api_id}");
62        let graphql_uri = format!("https://{api_id}.appsync-api.{region}.amazonaws.com/graphql");
63        let realtime_uri =
64            format!("wss://{api_id}.appsync-realtime-api.{region}.amazonaws.com/graphql");
65
66        let mut uris = HashMap::new();
67        uris.insert("GRAPHQL".to_string(), graphql_uri);
68        uris.insert("REALTIME".to_string(), realtime_uri);
69
70        let api = GraphqlApi {
71            api_id: api_id.clone(),
72            name: name.to_string(),
73            authentication_type: authentication_type.to_string(),
74            arn: arn.clone(),
75            uris,
76            tags: tags.clone(),
77            xray_enabled: false,
78            additional_authentication_provider: None,
79            lambda_authorizer_config: None,
80            user_pool_config: None,
81            enhanced_metrics_config: None,
82        };
83
84        if !tags.is_empty() {
85            self.resource_tags.insert(arn, tags);
86        }
87
88        self.apis.insert(api_id.clone(), api);
89        Ok(self.apis.get(&api_id).unwrap())
90    }
91
92    pub fn get_graphql_api(&self, api_id: &str) -> Result<&GraphqlApi, AppSyncError> {
93        self.apis
94            .get(api_id)
95            .ok_or_else(|| AppSyncError::GraphqlApiNotFound {
96                api_id: api_id.to_string(),
97            })
98    }
99
100    pub fn delete_graphql_api(&mut self, api_id: &str) -> Result<(), AppSyncError> {
101        if self.apis.remove(api_id).is_none() {
102            return Err(AppSyncError::GraphqlApiNotFound {
103                api_id: api_id.to_string(),
104            });
105        }
106        self.api_caches.remove(api_id);
107        self.api_keys.remove(api_id);
108        self.types.remove(api_id);
109        self.schema_statuses.remove(api_id);
110        Ok(())
111    }
112
113    pub fn list_graphql_apis(&self) -> Vec<&GraphqlApi> {
114        self.apis.values().collect()
115    }
116
117    pub fn update_graphql_api(
118        &mut self,
119        api_id: &str,
120        name: Option<&str>,
121        authentication_type: Option<&str>,
122    ) -> Result<&GraphqlApi, AppSyncError> {
123        let api = self
124            .apis
125            .get_mut(api_id)
126            .ok_or_else(|| AppSyncError::GraphqlApiNotFound {
127                api_id: api_id.to_string(),
128            })?;
129
130        if let Some(n) = name {
131            api.name = n.to_string();
132        }
133        if let Some(at) = authentication_type {
134            api.authentication_type = at.to_string();
135        }
136
137        Ok(api)
138    }
139
140    // ===================== Event API (v2) =====================
141
142    pub fn create_api(
143        &mut self,
144        name: &str,
145        account_id: &str,
146        region: &str,
147        owner_contact: Option<&str>,
148        tags: HashMap<String, String>,
149    ) -> Result<&Api, AppSyncError> {
150        let api_id = uuid::Uuid::new_v4().to_string();
151        let api_arn = format!("arn:aws:appsync:{region}:{account_id}:apis/{api_id}");
152        let now = std::time::SystemTime::now()
153            .duration_since(std::time::UNIX_EPOCH)
154            .unwrap_or_default()
155            .as_secs_f64();
156
157        let api = Api {
158            api_id: api_id.clone(),
159            name: name.to_string(),
160            api_arn: api_arn.clone(),
161            created: now,
162            tags: tags.clone(),
163            owner_contact: owner_contact.map(|s| s.to_string()),
164        };
165
166        if !tags.is_empty() {
167            self.resource_tags.insert(api_arn, tags);
168        }
169
170        self.event_apis.insert(api_id.clone(), api);
171        Ok(self.event_apis.get(&api_id).unwrap())
172    }
173
174    pub fn get_api(&self, api_id: &str) -> Result<&Api, AppSyncError> {
175        self.event_apis
176            .get(api_id)
177            .ok_or_else(|| AppSyncError::ApiNotFound {
178                api_id: api_id.to_string(),
179            })
180    }
181
182    pub fn delete_api(&mut self, api_id: &str) -> Result<(), AppSyncError> {
183        if self.event_apis.remove(api_id).is_none() {
184            return Err(AppSyncError::ApiNotFound {
185                api_id: api_id.to_string(),
186            });
187        }
188        self.channel_namespaces.remove(api_id);
189        Ok(())
190    }
191
192    pub fn list_apis(&self) -> Vec<&Api> {
193        self.event_apis.values().collect()
194    }
195
196    // ===================== API Cache =====================
197
198    pub fn create_api_cache(
199        &mut self,
200        api_id: &str,
201        api_caching_behavior: &str,
202        cache_type: &str,
203        ttl: i64,
204        at_rest_encryption_enabled: bool,
205        transit_encryption_enabled: bool,
206        health_metrics_config: Option<&str>,
207    ) -> Result<&ApiCacheEntry, AppSyncError> {
208        // Verify API exists
209        if !self.apis.contains_key(api_id) {
210            return Err(AppSyncError::GraphqlApiNotFound {
211                api_id: api_id.to_string(),
212            });
213        }
214
215        if self.api_caches.contains_key(api_id) {
216            return Err(AppSyncError::ApiCacheAlreadyExists {
217                api_id: api_id.to_string(),
218            });
219        }
220
221        let entry = ApiCacheEntry {
222            api_id: api_id.to_string(),
223            api_caching_behavior: api_caching_behavior.to_string(),
224            r#type: cache_type.to_string(),
225            ttl,
226            at_rest_encryption_enabled,
227            transit_encryption_enabled,
228            status: "AVAILABLE".to_string(),
229            health_metrics_config: health_metrics_config.map(|s| s.to_string()),
230        };
231
232        self.api_caches.insert(api_id.to_string(), entry);
233        Ok(self.api_caches.get(api_id).unwrap())
234    }
235
236    pub fn get_api_cache(&self, api_id: &str) -> Result<&ApiCacheEntry, AppSyncError> {
237        self.api_caches
238            .get(api_id)
239            .ok_or_else(|| AppSyncError::ApiCacheNotFound {
240                api_id: api_id.to_string(),
241            })
242    }
243
244    pub fn delete_api_cache(&mut self, api_id: &str) -> Result<(), AppSyncError> {
245        if self.api_caches.remove(api_id).is_none() {
246            return Err(AppSyncError::ApiCacheNotFound {
247                api_id: api_id.to_string(),
248            });
249        }
250        Ok(())
251    }
252
253    pub fn update_api_cache(
254        &mut self,
255        api_id: &str,
256        api_caching_behavior: &str,
257        cache_type: &str,
258        ttl: i64,
259        health_metrics_config: Option<&str>,
260    ) -> Result<&ApiCacheEntry, AppSyncError> {
261        let cache =
262            self.api_caches
263                .get_mut(api_id)
264                .ok_or_else(|| AppSyncError::ApiCacheNotFound {
265                    api_id: api_id.to_string(),
266                })?;
267
268        cache.api_caching_behavior = api_caching_behavior.to_string();
269        cache.r#type = cache_type.to_string();
270        cache.ttl = ttl;
271        if let Some(hmc) = health_metrics_config {
272            cache.health_metrics_config = Some(hmc.to_string());
273        }
274
275        Ok(cache)
276    }
277
278    pub fn flush_api_cache(&self, api_id: &str) -> Result<(), AppSyncError> {
279        if !self.api_caches.contains_key(api_id) {
280            return Err(AppSyncError::ApiCacheNotFound {
281                api_id: api_id.to_string(),
282            });
283        }
284        // Flush is a no-op in mock
285        Ok(())
286    }
287
288    // ===================== API Keys =====================
289
290    pub fn create_api_key(
291        &mut self,
292        api_id: &str,
293        description: Option<&str>,
294        expires: i64,
295    ) -> Result<&ApiKeyEntry, AppSyncError> {
296        if !self.apis.contains_key(api_id) {
297            return Err(AppSyncError::GraphqlApiNotFound {
298                api_id: api_id.to_string(),
299            });
300        }
301
302        let id = uuid::Uuid::new_v4().to_string()[..8].to_string();
303        let key = ApiKeyEntry {
304            id: id.clone(),
305            api_id: api_id.to_string(),
306            description: description.map(|s| s.to_string()),
307            expires,
308            deletes: expires + 60 * 60 * 24 * 2, // 2 days after expiry
309        };
310
311        let keys = self.api_keys.entry(api_id.to_string()).or_default();
312        keys.push(key);
313        Ok(keys.last().unwrap())
314    }
315
316    pub fn delete_api_key(&mut self, api_id: &str, key_id: &str) -> Result<(), AppSyncError> {
317        let keys = self
318            .api_keys
319            .get_mut(api_id)
320            .ok_or_else(|| AppSyncError::ApiKeyNotFound {
321                key_id: key_id.to_string(),
322            })?;
323
324        let idx = keys.iter().position(|k| k.id == key_id).ok_or_else(|| {
325            AppSyncError::ApiKeyNotFound {
326                key_id: key_id.to_string(),
327            }
328        })?;
329
330        keys.remove(idx);
331        Ok(())
332    }
333
334    pub fn list_api_keys(&self, api_id: &str) -> Result<Vec<&ApiKeyEntry>, AppSyncError> {
335        if !self.apis.contains_key(api_id) {
336            return Err(AppSyncError::GraphqlApiNotFound {
337                api_id: api_id.to_string(),
338            });
339        }
340        Ok(self
341            .api_keys
342            .get(api_id)
343            .map(|keys| keys.iter().collect())
344            .unwrap_or_default())
345    }
346
347    pub fn update_api_key(
348        &mut self,
349        api_id: &str,
350        key_id: &str,
351        description: Option<&str>,
352        expires: Option<i64>,
353    ) -> Result<&ApiKeyEntry, AppSyncError> {
354        let keys = self
355            .api_keys
356            .get_mut(api_id)
357            .ok_or_else(|| AppSyncError::ApiKeyNotFound {
358                key_id: key_id.to_string(),
359            })?;
360
361        let key = keys.iter_mut().find(|k| k.id == key_id).ok_or_else(|| {
362            AppSyncError::ApiKeyNotFound {
363                key_id: key_id.to_string(),
364            }
365        })?;
366
367        if let Some(d) = description {
368            key.description = Some(d.to_string());
369        }
370        if let Some(e) = expires {
371            key.expires = e;
372            key.deletes = e + 60 * 60 * 24 * 2;
373        }
374
375        Ok(key)
376    }
377
378    // ===================== Channel Namespaces (v2) =====================
379
380    pub fn create_channel_namespace(
381        &mut self,
382        api_id: &str,
383        name: &str,
384        account_id: &str,
385        region: &str,
386        tags: HashMap<String, String>,
387    ) -> Result<&ChannelNamespaceEntry, AppSyncError> {
388        if !self.event_apis.contains_key(api_id) {
389            return Err(AppSyncError::ApiNotFound {
390                api_id: api_id.to_string(),
391            });
392        }
393
394        let arn =
395            format!("arn:aws:appsync:{region}:{account_id}:apis/{api_id}/channelNamespaces/{name}");
396        let now = std::time::SystemTime::now()
397            .duration_since(std::time::UNIX_EPOCH)
398            .unwrap_or_default()
399            .as_secs_f64();
400
401        let entry = ChannelNamespaceEntry {
402            api_id: api_id.to_string(),
403            name: name.to_string(),
404            channel_namespace_arn: arn.clone(),
405            created: now,
406            last_modified: now,
407            tags: tags.clone(),
408        };
409
410        if !tags.is_empty() {
411            self.resource_tags.insert(arn, tags);
412        }
413
414        let namespaces = self
415            .channel_namespaces
416            .entry(api_id.to_string())
417            .or_default();
418        namespaces.push(entry);
419        Ok(namespaces.last().unwrap())
420    }
421
422    pub fn delete_channel_namespace(
423        &mut self,
424        api_id: &str,
425        name: &str,
426    ) -> Result<(), AppSyncError> {
427        let namespaces = self.channel_namespaces.get_mut(api_id).ok_or_else(|| {
428            AppSyncError::ChannelNamespaceNotFound {
429                name: name.to_string(),
430            }
431        })?;
432
433        let idx = namespaces
434            .iter()
435            .position(|ns| ns.name == name)
436            .ok_or_else(|| AppSyncError::ChannelNamespaceNotFound {
437                name: name.to_string(),
438            })?;
439
440        namespaces.remove(idx);
441        Ok(())
442    }
443
444    pub fn list_channel_namespaces(
445        &self,
446        api_id: &str,
447    ) -> Result<Vec<&ChannelNamespaceEntry>, AppSyncError> {
448        if !self.event_apis.contains_key(api_id) {
449            return Err(AppSyncError::ApiNotFound {
450                api_id: api_id.to_string(),
451            });
452        }
453        Ok(self
454            .channel_namespaces
455            .get(api_id)
456            .map(|nss| nss.iter().collect())
457            .unwrap_or_default())
458    }
459
460    // ===================== Schema Creation =====================
461
462    pub fn start_schema_creation(
463        &mut self,
464        api_id: &str,
465        _definition: &[u8],
466    ) -> Result<&SchemaStatus, AppSyncError> {
467        if !self.apis.contains_key(api_id) {
468            return Err(AppSyncError::GraphqlApiNotFound {
469                api_id: api_id.to_string(),
470            });
471        }
472
473        let status = SchemaStatus {
474            status: "SUCCESS".to_string(),
475            details: None,
476        };
477
478        self.schema_statuses.insert(api_id.to_string(), status);
479        Ok(self.schema_statuses.get(api_id).unwrap())
480    }
481
482    pub fn get_schema_creation_status(&self, api_id: &str) -> Result<&SchemaStatus, AppSyncError> {
483        self.schema_statuses
484            .get(api_id)
485            .ok_or_else(|| AppSyncError::SchemaNotFound {
486                api_id: api_id.to_string(),
487            })
488    }
489
490    // ===================== Types =====================
491
492    pub fn get_type(
493        &self,
494        api_id: &str,
495        type_name: &str,
496        _format: &str,
497    ) -> Result<&TypeEntry, AppSyncError> {
498        let types = self
499            .types
500            .get(api_id)
501            .ok_or_else(|| AppSyncError::TypeNotFound {
502                type_name: type_name.to_string(),
503            })?;
504
505        types
506            .iter()
507            .find(|t| t.name == type_name)
508            .ok_or_else(|| AppSyncError::TypeNotFound {
509                type_name: type_name.to_string(),
510            })
511    }
512
513    // ===================== Tag Operations =====================
514
515    pub fn tag_resource(&mut self, arn: &str, tags: HashMap<String, String>) {
516        let entry = self.resource_tags.entry(arn.to_string()).or_default();
517        entry.extend(tags);
518    }
519
520    pub fn untag_resource(&mut self, arn: &str, tag_keys: &[String]) {
521        if let Some(tags) = self.resource_tags.get_mut(arn) {
522            for key in tag_keys {
523                tags.remove(key);
524            }
525        }
526    }
527
528    pub fn list_tags_for_resource(&self, arn: &str) -> HashMap<String, String> {
529        self.resource_tags.get(arn).cloned().unwrap_or_default()
530    }
531}