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 pub api_keys: HashMap<String, Vec<ApiKeyEntry>>,
12 pub channel_namespaces: HashMap<String, Vec<ChannelNamespaceEntry>>,
14 pub types: HashMap<String, Vec<TypeEntry>>,
16 pub schema_statuses: HashMap<String, SchemaStatus>,
18 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 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 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 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 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 Ok(())
286 }
287
288 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, };
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 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 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 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 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}