1use std::sync::Arc;
12
13use async_trait::async_trait;
14use base64::Engine as _;
15use http::{Method, StatusCode};
16use percent_encoding::percent_decode_str;
17use rand::Rng;
18use serde_json::{json, Map, Value};
19use tokio::sync::Mutex as AsyncMutex;
20use uuid::Uuid;
21
22use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
23use fakecloud_persistence::SnapshotStore;
24
25use crate::persistence::save_snapshot;
26use crate::state::{now_epoch, AppSyncData, SchemaState, SharedAppSyncState};
27
28pub const APPSYNC_ACTIONS: &[&str] = &[
30 "AssociateApi",
31 "AssociateMergedGraphqlApi",
32 "AssociateSourceGraphqlApi",
33 "CreateApi",
34 "CreateApiCache",
35 "CreateApiKey",
36 "CreateChannelNamespace",
37 "CreateDataSource",
38 "CreateDomainName",
39 "CreateFunction",
40 "CreateGraphqlApi",
41 "CreateResolver",
42 "CreateType",
43 "DeleteApi",
44 "DeleteApiCache",
45 "DeleteApiKey",
46 "DeleteChannelNamespace",
47 "DeleteDataSource",
48 "DeleteDomainName",
49 "DeleteFunction",
50 "DeleteGraphqlApi",
51 "DeleteResolver",
52 "DeleteType",
53 "DisassociateApi",
54 "DisassociateMergedGraphqlApi",
55 "DisassociateSourceGraphqlApi",
56 "EvaluateCode",
57 "EvaluateMappingTemplate",
58 "FlushApiCache",
59 "GetApi",
60 "GetApiAssociation",
61 "GetApiCache",
62 "GetChannelNamespace",
63 "GetDataSource",
64 "GetDataSourceIntrospection",
65 "GetDomainName",
66 "GetFunction",
67 "GetGraphqlApi",
68 "GetGraphqlApiEnvironmentVariables",
69 "GetIntrospectionSchema",
70 "GetResolver",
71 "GetSchemaCreationStatus",
72 "GetSourceApiAssociation",
73 "GetType",
74 "ListApiKeys",
75 "ListApis",
76 "ListChannelNamespaces",
77 "ListDataSources",
78 "ListDomainNames",
79 "ListFunctions",
80 "ListGraphqlApis",
81 "ListResolvers",
82 "ListResolversByFunction",
83 "ListSourceApiAssociations",
84 "ListTagsForResource",
85 "ListTypes",
86 "ListTypesByAssociation",
87 "PutGraphqlApiEnvironmentVariables",
88 "StartDataSourceIntrospection",
89 "StartSchemaCreation",
90 "StartSchemaMerge",
91 "TagResource",
92 "UntagResource",
93 "UpdateApi",
94 "UpdateApiCache",
95 "UpdateApiKey",
96 "UpdateChannelNamespace",
97 "UpdateDataSource",
98 "UpdateDomainName",
99 "UpdateFunction",
100 "UpdateGraphqlApi",
101 "UpdateResolver",
102 "UpdateSourceApiAssociation",
103 "UpdateType",
104];
105
106const SAVE_AFTER: &[&str] = &[
110 "CreateApi",
111 "CreateApiCache",
112 "CreateApiKey",
113 "CreateChannelNamespace",
114 "CreateDataSource",
115 "CreateDomainName",
116 "CreateFunction",
117 "CreateGraphqlApi",
118 "CreateResolver",
119 "CreateType",
120 "DeleteApi",
121 "DeleteApiCache",
122 "DeleteApiKey",
123 "DeleteChannelNamespace",
124 "DeleteDataSource",
125 "DeleteDomainName",
126 "DeleteFunction",
127 "DeleteGraphqlApi",
128 "DeleteResolver",
129 "DeleteType",
130 "AssociateApi",
131 "AssociateMergedGraphqlApi",
132 "AssociateSourceGraphqlApi",
133 "DisassociateApi",
134 "DisassociateMergedGraphqlApi",
135 "DisassociateSourceGraphqlApi",
136 "FlushApiCache",
137 "PutGraphqlApiEnvironmentVariables",
138 "StartDataSourceIntrospection",
139 "StartSchemaCreation",
140 "StartSchemaMerge",
141 "GetSchemaCreationStatus",
142 "TagResource",
143 "UntagResource",
144 "UpdateApi",
145 "UpdateApiCache",
146 "UpdateApiKey",
147 "UpdateChannelNamespace",
148 "UpdateDataSource",
149 "UpdateDomainName",
150 "UpdateFunction",
151 "UpdateGraphqlApi",
152 "UpdateResolver",
153 "UpdateSourceApiAssociation",
154 "UpdateType",
155];
156
157pub struct AppSyncService {
158 state: SharedAppSyncState,
159 snapshot_store: Option<Arc<dyn SnapshotStore>>,
160 snapshot_lock: Arc<AsyncMutex<()>>,
161}
162
163impl AppSyncService {
164 pub fn new(state: SharedAppSyncState) -> Self {
165 Self {
166 state,
167 snapshot_store: None,
168 snapshot_lock: Arc::new(AsyncMutex::new(())),
169 }
170 }
171
172 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
173 self.snapshot_store = Some(store);
174 self
175 }
176
177 async fn save(&self) {
178 save_snapshot(
179 &self.state,
180 self.snapshot_store.clone(),
181 &self.snapshot_lock,
182 )
183 .await;
184 }
185
186 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
188 let store = self.snapshot_store.clone()?;
189 let state = self.state.clone();
190 let lock = self.snapshot_lock.clone();
191 Some(Arc::new(move || {
192 let state = state.clone();
193 let store = store.clone();
194 let lock = lock.clone();
195 Box::pin(async move {
196 crate::persistence::save_snapshot(&state, Some(store), &lock).await;
197 })
198 }))
199 }
200
201 fn resolve_action(req: &AwsRequest) -> Option<(&'static str, Vec<String>)> {
204 let raw = req.raw_path.split('?').next().unwrap_or(&req.raw_path);
205 let trimmed = raw.strip_prefix('/').unwrap_or(raw);
206 let segs: Vec<String> = if trimmed.is_empty() {
207 Vec::new()
208 } else {
209 trimmed
210 .split('/')
211 .map(|s| percent_decode_str(s).decode_utf8_lossy().into_owned())
212 .collect()
213 };
214 let s: Vec<&str> = segs.iter().map(String::as_str).collect();
215 let m = &req.method;
216 let get = m == Method::GET;
217 let post = m == Method::POST;
218 let put = m == Method::PUT;
219 let del = m == Method::DELETE;
220 let v = |xs: &[&str]| xs.iter().map(|x| x.to_string()).collect::<Vec<_>>();
221 let (action, labels): (&'static str, Vec<String>) = match s.as_slice() {
222 ["v2", "apis"] if post => ("CreateApi", vec![]),
224 ["v2", "apis"] if get => ("ListApis", vec![]),
225 ["v2", "apis", id] if get => ("GetApi", v(&[id])),
226 ["v2", "apis", id] if post => ("UpdateApi", v(&[id])),
227 ["v2", "apis", id] if del => ("DeleteApi", v(&[id])),
228 ["v2", "apis", id, "channelNamespaces"] if post => ("CreateChannelNamespace", v(&[id])),
229 ["v2", "apis", id, "channelNamespaces"] if get => ("ListChannelNamespaces", v(&[id])),
230 ["v2", "apis", id, "channelNamespaces", n] if get => {
231 ("GetChannelNamespace", v(&[id, n]))
232 }
233 ["v2", "apis", id, "channelNamespaces", n] if post => {
234 ("UpdateChannelNamespace", v(&[id, n]))
235 }
236 ["v2", "apis", id, "channelNamespaces", n] if del => {
237 ("DeleteChannelNamespace", v(&[id, n]))
238 }
239
240 ["v1", "apis"] if post => ("CreateGraphqlApi", vec![]),
242 ["v1", "apis"] if get => ("ListGraphqlApis", vec![]),
243
244 ["v1", "apis", id, "ApiCaches", "update"] if post => ("UpdateApiCache", v(&[id])),
246 ["v1", "apis", id, "ApiCaches"] if post => ("CreateApiCache", v(&[id])),
247 ["v1", "apis", id, "ApiCaches"] if get => ("GetApiCache", v(&[id])),
248 ["v1", "apis", id, "ApiCaches"] if del => ("DeleteApiCache", v(&[id])),
249 ["v1", "apis", id, "FlushCache"] if del => ("FlushApiCache", v(&[id])),
250
251 ["v1", "apis", id, "apikeys"] if post => ("CreateApiKey", v(&[id])),
253 ["v1", "apis", id, "apikeys"] if get => ("ListApiKeys", v(&[id])),
254 ["v1", "apis", id, "apikeys", k] if del => ("DeleteApiKey", v(&[id, k])),
255 ["v1", "apis", id, "apikeys", k] if post => ("UpdateApiKey", v(&[id, k])),
256
257 ["v1", "apis", id, "datasources"] if post => ("CreateDataSource", v(&[id])),
259 ["v1", "apis", id, "datasources"] if get => ("ListDataSources", v(&[id])),
260 ["v1", "apis", id, "datasources", n] if get => ("GetDataSource", v(&[id, n])),
261 ["v1", "apis", id, "datasources", n] if post => ("UpdateDataSource", v(&[id, n])),
262 ["v1", "apis", id, "datasources", n] if del => ("DeleteDataSource", v(&[id, n])),
263
264 ["v1", "apis", id, "functions"] if post => ("CreateFunction", v(&[id])),
266 ["v1", "apis", id, "functions"] if get => ("ListFunctions", v(&[id])),
267 ["v1", "apis", id, "functions", fid, "resolvers"] if get => {
268 ("ListResolversByFunction", v(&[id, fid]))
269 }
270 ["v1", "apis", id, "functions", fid] if get => ("GetFunction", v(&[id, fid])),
271 ["v1", "apis", id, "functions", fid] if post => ("UpdateFunction", v(&[id, fid])),
272 ["v1", "apis", id, "functions", fid] if del => ("DeleteFunction", v(&[id, fid])),
273
274 ["v1", "apis", id, "types", tn, "resolvers"] if post => {
276 ("CreateResolver", v(&[id, tn]))
277 }
278 ["v1", "apis", id, "types", tn, "resolvers"] if get => ("ListResolvers", v(&[id, tn])),
279 ["v1", "apis", id, "types", tn, "resolvers", fn_] if get => {
280 ("GetResolver", v(&[id, tn, fn_]))
281 }
282 ["v1", "apis", id, "types", tn, "resolvers", fn_] if post => {
283 ("UpdateResolver", v(&[id, tn, fn_]))
284 }
285 ["v1", "apis", id, "types", tn, "resolvers", fn_] if del => {
286 ("DeleteResolver", v(&[id, tn, fn_]))
287 }
288 ["v1", "apis", id, "types"] if post => ("CreateType", v(&[id])),
289 ["v1", "apis", id, "types"] if get => ("ListTypes", v(&[id])),
290 ["v1", "apis", id, "types", tn] if get => ("GetType", v(&[id, tn])),
291 ["v1", "apis", id, "types", tn] if post => ("UpdateType", v(&[id, tn])),
292 ["v1", "apis", id, "types", tn] if del => ("DeleteType", v(&[id, tn])),
293
294 ["v1", "apis", id, "schema"] if get => ("GetIntrospectionSchema", v(&[id])),
296 ["v1", "apis", id, "schemacreation"] if post => ("StartSchemaCreation", v(&[id])),
297 ["v1", "apis", id, "schemacreation"] if get => ("GetSchemaCreationStatus", v(&[id])),
298
299 ["v1", "apis", id, "environmentVariables"] if get => {
301 ("GetGraphqlApiEnvironmentVariables", v(&[id]))
302 }
303 ["v1", "apis", id, "environmentVariables"] if put => {
304 ("PutGraphqlApiEnvironmentVariables", v(&[id]))
305 }
306
307 ["v1", "apis", id, "sourceApiAssociations"] if get => {
309 ("ListSourceApiAssociations", v(&[id]))
310 }
311
312 ["v1", "apis", id] if get => ("GetGraphqlApi", v(&[id])),
314 ["v1", "apis", id] if post => ("UpdateGraphqlApi", v(&[id])),
315 ["v1", "apis", id] if del => ("DeleteGraphqlApi", v(&[id])),
316
317 ["v1", "domainnames"] if post => ("CreateDomainName", vec![]),
319 ["v1", "domainnames"] if get => ("ListDomainNames", vec![]),
320 ["v1", "domainnames", dn, "apiassociation"] if post => ("AssociateApi", v(&[dn])),
321 ["v1", "domainnames", dn, "apiassociation"] if get => ("GetApiAssociation", v(&[dn])),
322 ["v1", "domainnames", dn, "apiassociation"] if del => ("DisassociateApi", v(&[dn])),
323 ["v1", "domainnames", dn] if get => ("GetDomainName", v(&[dn])),
324 ["v1", "domainnames", dn] if post => ("UpdateDomainName", v(&[dn])),
325 ["v1", "domainnames", dn] if del => ("DeleteDomainName", v(&[dn])),
326
327 ["v1", "sourceApis", sid, "mergedApiAssociations"] if post => {
329 ("AssociateMergedGraphqlApi", v(&[sid]))
330 }
331 ["v1", "sourceApis", sid, "mergedApiAssociations", aid] if del => {
332 ("DisassociateMergedGraphqlApi", v(&[sid, aid]))
333 }
334 ["v1", "mergedApis", mid, "sourceApiAssociations"] if post => {
335 ("AssociateSourceGraphqlApi", v(&[mid]))
336 }
337 ["v1", "mergedApis", mid, "sourceApiAssociations", aid, "types"] if get => {
338 ("ListTypesByAssociation", v(&[mid, aid]))
339 }
340 ["v1", "mergedApis", mid, "sourceApiAssociations", aid, "merge"] if post => {
341 ("StartSchemaMerge", v(&[mid, aid]))
342 }
343 ["v1", "mergedApis", mid, "sourceApiAssociations", aid] if get => {
344 ("GetSourceApiAssociation", v(&[mid, aid]))
345 }
346 ["v1", "mergedApis", mid, "sourceApiAssociations", aid] if post => {
347 ("UpdateSourceApiAssociation", v(&[mid, aid]))
348 }
349 ["v1", "mergedApis", mid, "sourceApiAssociations", aid] if del => {
350 ("DisassociateSourceGraphqlApi", v(&[mid, aid]))
351 }
352
353 ["v1", "datasources", "introspections"] if post => {
355 ("StartDataSourceIntrospection", vec![])
356 }
357 ["v1", "datasources", "introspections", iid] if get => {
358 ("GetDataSourceIntrospection", v(&[iid]))
359 }
360
361 ["v1", "dataplane-evaluatecode"] if post => ("EvaluateCode", vec![]),
363 ["v1", "dataplane-evaluatetemplate"] if post => ("EvaluateMappingTemplate", vec![]),
364
365 ["v1", "tags", arn] if get => ("ListTagsForResource", v(&[arn])),
367 ["v1", "tags", arn] if post => ("TagResource", v(&[arn])),
368 ["v1", "tags", arn] if del => ("UntagResource", v(&[arn])),
369
370 _ => return None,
371 };
372 Some((action, labels))
373 }
374}
375
376#[async_trait]
377impl AwsService for AppSyncService {
378 fn service_name(&self) -> &str {
379 "appsync"
380 }
381
382 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
383 let Some((action, labels)) = Self::resolve_action(&req) else {
384 return Err(AwsServiceError::aws_error(
385 StatusCode::NOT_FOUND,
386 "UnknownOperationException",
387 format!("Unknown operation: {} {}", req.method, req.raw_path),
388 ));
389 };
390 let result = self.dispatch(action, &labels, &req);
391 let success = matches!(result.as_ref(), Ok(resp) if resp.status.is_success());
392 if SAVE_AFTER.contains(&action) && success {
393 self.save().await;
394 }
395 result
396 }
397
398 fn supported_actions(&self) -> &[&str] {
399 APPSYNC_ACTIONS
400 }
401}
402
403struct Ctx {
405 account: String,
406 region: String,
407}
408
409impl AppSyncService {
410 fn dispatch(
411 &self,
412 action: &str,
413 labels: &[String],
414 req: &AwsRequest,
415 ) -> Result<AwsResponse, AwsServiceError> {
416 let body = parse_body(req)?;
417 crate::validate::validate_input(action, &body)?;
418 let ctx = Ctx {
419 account: req.account_id.clone(),
420 region: req.region.clone(),
421 };
422 let q = parse_query(&req.raw_query);
423 crate::validate::validate_query(action, &q)?;
424 let l = |i: usize| labels.get(i).map(String::as_str).unwrap_or_default();
425 match action {
426 "CreateGraphqlApi" => self.create_graphql_api(&ctx, &body),
428 "GetGraphqlApi" => self.get_graphql_api(&ctx, l(0)),
429 "ListGraphqlApis" => self.list_graphql_apis(&ctx),
430 "UpdateGraphqlApi" => self.update_graphql_api(&ctx, l(0), &body),
431 "DeleteGraphqlApi" => self.delete_graphql_api(&ctx, l(0)),
432
433 "CreateApiKey" => self.create_api_key(&ctx, l(0), &body),
435 "ListApiKeys" => self.list_api_keys(&ctx, l(0)),
436 "UpdateApiKey" => self.update_api_key(&ctx, l(0), l(1), &body),
437 "DeleteApiKey" => self.delete_api_key(&ctx, l(0), l(1)),
438
439 "CreateDataSource" => self.create_data_source(&ctx, l(0), &body),
441 "GetDataSource" => self.get_data_source(&ctx, l(0), l(1)),
442 "ListDataSources" => self.list_data_sources(&ctx, l(0)),
443 "UpdateDataSource" => self.update_data_source(&ctx, l(0), l(1), &body),
444 "DeleteDataSource" => self.delete_data_source(&ctx, l(0), l(1)),
445
446 "CreateResolver" => self.create_resolver(&ctx, l(0), l(1), &body),
448 "GetResolver" => self.get_resolver(&ctx, l(0), l(1), l(2)),
449 "ListResolvers" => self.list_resolvers(&ctx, l(0), l(1)),
450 "ListResolversByFunction" => self.list_resolvers_by_function(&ctx, l(0), l(1)),
451 "UpdateResolver" => self.update_resolver(&ctx, l(0), l(1), l(2), &body),
452 "DeleteResolver" => self.delete_resolver(&ctx, l(0), l(1), l(2)),
453
454 "CreateFunction" => self.create_function(&ctx, l(0), &body),
456 "GetFunction" => self.get_function(&ctx, l(0), l(1)),
457 "ListFunctions" => self.list_functions(&ctx, l(0)),
458 "UpdateFunction" => self.update_function(&ctx, l(0), l(1), &body),
459 "DeleteFunction" => self.delete_function(&ctx, l(0), l(1)),
460
461 "CreateType" => self.create_type(&ctx, l(0), &body),
463 "GetType" => self.get_type(&ctx, l(0), l(1), &q),
464 "ListTypes" => self.list_types(&ctx, l(0), &q),
465 "UpdateType" => self.update_type(&ctx, l(0), l(1), &body),
466 "DeleteType" => self.delete_type(&ctx, l(0), l(1)),
467 "StartSchemaCreation" => self.start_schema_creation(&ctx, l(0), &body),
468 "GetSchemaCreationStatus" => self.get_schema_creation_status(&ctx, l(0)),
469 "GetIntrospectionSchema" => self.get_introspection_schema(&ctx, l(0), &q),
470
471 "CreateApiCache" => self.create_api_cache(&ctx, l(0), &body),
473 "UpdateApiCache" => self.update_api_cache(&ctx, l(0), &body),
474 "GetApiCache" => self.get_api_cache(&ctx, l(0)),
475 "DeleteApiCache" => self.delete_api_cache(&ctx, l(0)),
476 "FlushApiCache" => self.flush_api_cache(&ctx, l(0)),
477
478 "PutGraphqlApiEnvironmentVariables" => self.put_env_vars(&ctx, l(0), &body),
480 "GetGraphqlApiEnvironmentVariables" => self.get_env_vars(&ctx, l(0)),
481
482 "CreateDomainName" => self.create_domain_name(&ctx, &body),
484 "GetDomainName" => self.get_domain_name(&ctx, l(0)),
485 "ListDomainNames" => self.list_domain_names(&ctx),
486 "UpdateDomainName" => self.update_domain_name(&ctx, l(0), &body),
487 "DeleteDomainName" => self.delete_domain_name(&ctx, l(0)),
488 "AssociateApi" => self.associate_api(&ctx, l(0), &body),
489 "GetApiAssociation" => self.get_api_association(&ctx, l(0)),
490 "DisassociateApi" => self.disassociate_api(&ctx, l(0)),
491
492 "CreateApi" => self.create_api(&ctx, &body),
494 "GetApi" => self.get_api(&ctx, l(0)),
495 "ListApis" => self.list_apis(&ctx),
496 "UpdateApi" => self.update_api(&ctx, l(0), &body),
497 "DeleteApi" => self.delete_api(&ctx, l(0)),
498 "CreateChannelNamespace" => self.create_channel_namespace(&ctx, l(0), &body),
499 "GetChannelNamespace" => self.get_channel_namespace(&ctx, l(0), l(1)),
500 "ListChannelNamespaces" => self.list_channel_namespaces(&ctx, l(0)),
501 "UpdateChannelNamespace" => self.update_channel_namespace(&ctx, l(0), l(1), &body),
502 "DeleteChannelNamespace" => self.delete_channel_namespace(&ctx, l(0), l(1)),
503
504 "AssociateSourceGraphqlApi" => self.associate_source_api(&ctx, l(0), &body),
506 "AssociateMergedGraphqlApi" => self.associate_merged_api(&ctx, l(0), &body),
507 "GetSourceApiAssociation" => self.get_source_api_association(&ctx, l(1)),
508 "ListSourceApiAssociations" => self.list_source_api_associations(&ctx, l(0)),
509 "UpdateSourceApiAssociation" => self.update_source_api_association(&ctx, l(1), &body),
510 "DisassociateSourceGraphqlApi" => self.disassociate_source_api(&ctx, l(1)),
511 "DisassociateMergedGraphqlApi" => self.disassociate_source_api(&ctx, l(1)),
512 "ListTypesByAssociation" => self.list_types_by_association(&ctx, l(0), l(1), &q),
513 "StartSchemaMerge" => self.start_schema_merge(&ctx, l(1)),
514
515 "StartDataSourceIntrospection" => Self::start_ds_introspection(),
517 "GetDataSourceIntrospection" => self.get_ds_introspection(&ctx, l(0)),
518
519 "TagResource" => self.tag_resource(&ctx, l(0), &body),
521 "UntagResource" => self.untag_resource(&ctx, l(0), &q),
522 "ListTagsForResource" => self.list_tags_for_resource(&ctx, l(0)),
523
524 "EvaluateCode" => Self::evaluate_code(&body),
526 "EvaluateMappingTemplate" => Self::evaluate_template(&body),
527
528 _ => Err(AwsServiceError::action_not_implemented("appsync", action)),
529 }
530 }
531
532 fn create_graphql_api(&self, ctx: &Ctx, body: &Value) -> Result<AwsResponse, AwsServiceError> {
535 let api_id = gen_api_id();
536 let api = self.build_graphql_api(ctx, &api_id, body);
537 let mut guard = self.state.write();
538 let data = guard.get_or_create(&ctx.account);
539 if let Some(tags) = body.get("tags").and_then(Value::as_object) {
540 let arn = graphql_api_arn(&ctx.region, &ctx.account, &api_id);
541 data.tags.insert(api_id.clone(), string_map(tags));
542 data.tags.insert(arn, string_map(tags));
543 }
544 data.graphql_apis.insert(api_id, api.clone());
545 Ok(ok(json!({ "graphqlApi": api })))
546 }
547
548 fn build_graphql_api(&self, ctx: &Ctx, api_id: &str, body: &Value) -> Value {
549 let arn = graphql_api_arn(&ctx.region, &ctx.account, api_id);
550 let mut api = Map::new();
551 api.insert("apiId".into(), json!(api_id));
552 api.insert("arn".into(), json!(arn));
553 api.insert("owner".into(), json!(ctx.account));
554 api.insert(
555 "uris".into(),
556 json!({
557 "GRAPHQL": format!("https://{api_id}.appsync-api.{}.amazonaws.com/graphql", ctx.region),
558 "REALTIME": format!("wss://{api_id}.appsync-realtime-api.{}.amazonaws.com/graphql", ctx.region),
559 }),
560 );
561 api.insert(
562 "dns".into(),
563 json!({
564 "GRAPHQL": format!("{api_id}.appsync-api.{}.amazonaws.com", ctx.region),
565 "REALTIME": format!("{api_id}.appsync-realtime-api.{}.amazonaws.com", ctx.region),
566 }),
567 );
568 api.insert("apiType".into(), json!("GRAPHQL"));
570 api.insert("visibility".into(), json!("GLOBAL"));
571 api.insert("xrayEnabled".into(), json!(false));
572 copy_keys(
573 &mut api,
574 body,
575 &[
576 "name",
577 "authenticationType",
578 "logConfig",
579 "userPoolConfig",
580 "openIDConnectConfig",
581 "additionalAuthenticationProviders",
582 "xrayEnabled",
583 "lambdaAuthorizerConfig",
584 "apiType",
585 "mergedApiExecutionRoleArn",
586 "visibility",
587 "ownerContact",
588 "introspectionConfig",
589 "queryDepthLimit",
590 "resolverCountLimit",
591 "enhancedMetricsConfig",
592 "tags",
593 ],
594 );
595 Value::Object(api)
596 }
597
598 fn get_graphql_api(&self, ctx: &Ctx, api_id: &str) -> Result<AwsResponse, AwsServiceError> {
599 require_label(api_id, "apiId")?;
600 let guard = self.state.read();
601 let api = guard
602 .get(&ctx.account)
603 .and_then(|d| d.graphql_apis.get(api_id))
604 .cloned()
605 .ok_or_else(|| not_found(&format!("GraphQL API {api_id} not found.")))?;
606 Ok(ok(json!({ "graphqlApi": api })))
607 }
608
609 fn list_graphql_apis(&self, ctx: &Ctx) -> Result<AwsResponse, AwsServiceError> {
610 let guard = self.state.read();
611 let apis: Vec<Value> = guard
612 .get(&ctx.account)
613 .map(|d| d.graphql_apis.values().cloned().collect())
614 .unwrap_or_default();
615 Ok(ok(json!({ "graphqlApis": apis })))
616 }
617
618 fn update_graphql_api(
619 &self,
620 ctx: &Ctx,
621 api_id: &str,
622 body: &Value,
623 ) -> Result<AwsResponse, AwsServiceError> {
624 require_label(api_id, "apiId")?;
625 let mut guard = self.state.write();
626 let data = guard.get_or_create(&ctx.account);
627 if !data.graphql_apis.contains_key(api_id) {
628 return Err(not_found(&format!("GraphQL API {api_id} not found.")));
629 }
630 let api = self.build_graphql_api(ctx, api_id, body);
632 data.graphql_apis.insert(api_id.to_string(), api.clone());
633 Ok(ok(json!({ "graphqlApi": api })))
634 }
635
636 fn delete_graphql_api(&self, ctx: &Ctx, api_id: &str) -> Result<AwsResponse, AwsServiceError> {
637 require_label(api_id, "apiId")?;
638 let mut guard = self.state.write();
639 let data = guard.get_or_create(&ctx.account);
640 if data.graphql_apis.remove(api_id).is_none() {
641 return Err(not_found(&format!("GraphQL API {api_id} not found.")));
642 }
643 data.api_keys.remove(api_id);
644 data.data_sources.remove(api_id);
645 data.resolvers.remove(api_id);
646 data.functions.remove(api_id);
647 data.types.remove(api_id);
648 data.api_caches.remove(api_id);
649 data.schemas.remove(api_id);
650 data.env_vars.remove(api_id);
651 Ok(ok(json!({})))
652 }
653
654 fn require_api(&self, data: &AppSyncData, api_id: &str) -> Result<(), AwsServiceError> {
656 require_label(api_id, "apiId")?;
657 if data.graphql_apis.contains_key(api_id) {
658 Ok(())
659 } else {
660 Err(not_found(&format!("GraphQL API {api_id} not found.")))
661 }
662 }
663
664 fn create_api_key(
667 &self,
668 ctx: &Ctx,
669 api_id: &str,
670 body: &Value,
671 ) -> Result<AwsResponse, AwsServiceError> {
672 let mut guard = self.state.write();
673 let data = guard.get_or_create(&ctx.account);
674 self.require_api(data, api_id)?;
675 let id = gen_hex(8);
676 let expires = body
677 .get("expires")
678 .and_then(Value::as_i64)
679 .unwrap_or_else(|| now_epoch() as i64 + 7 * 24 * 3600);
680 let mut key = Map::new();
681 key.insert("id".into(), json!(id));
682 key.insert("expires".into(), json!(expires));
683 key.insert("deletes".into(), json!(expires + 60 * 24 * 3600));
684 copy_keys(&mut key, body, &["description"]);
685 let key = Value::Object(key);
686 data.api_keys
687 .entry(api_id.to_string())
688 .or_default()
689 .insert(id, key.clone());
690 Ok(ok(json!({ "apiKey": key })))
691 }
692
693 fn list_api_keys(&self, ctx: &Ctx, api_id: &str) -> Result<AwsResponse, AwsServiceError> {
694 let guard = self.state.read();
695 let data = guard
696 .get(&ctx.account)
697 .ok_or_else(|| not_found(&format!("GraphQL API {api_id} not found.")))?;
698 self.require_api(data, api_id)?;
699 let keys: Vec<Value> = data
700 .api_keys
701 .get(api_id)
702 .map(|m| m.values().cloned().collect())
703 .unwrap_or_default();
704 Ok(ok(json!({ "apiKeys": keys })))
705 }
706
707 fn update_api_key(
708 &self,
709 ctx: &Ctx,
710 api_id: &str,
711 id: &str,
712 body: &Value,
713 ) -> Result<AwsResponse, AwsServiceError> {
714 require_label(id, "id")?;
715 let mut guard = self.state.write();
716 let data = guard.get_or_create(&ctx.account);
717 self.require_api(data, api_id)?;
718 let key = data
719 .api_keys
720 .get_mut(api_id)
721 .and_then(|m| m.get_mut(id))
722 .ok_or_else(|| not_found(&format!("API key {id} not found.")))?;
723 if let Some(obj) = key.as_object_mut() {
724 if let Some(d) = body.get("description") {
725 obj.insert("description".into(), d.clone());
726 }
727 if let Some(e) = body.get("expires") {
728 obj.insert("expires".into(), e.clone());
729 }
730 }
731 Ok(ok(json!({ "apiKey": key })))
732 }
733
734 fn delete_api_key(
735 &self,
736 ctx: &Ctx,
737 api_id: &str,
738 id: &str,
739 ) -> Result<AwsResponse, AwsServiceError> {
740 require_label(id, "id")?;
741 let mut guard = self.state.write();
742 let data = guard.get_or_create(&ctx.account);
743 self.require_api(data, api_id)?;
744 match data.api_keys.get_mut(api_id).and_then(|m| m.remove(id)) {
745 Some(_) => Ok(ok(json!({}))),
746 None => Err(not_found(&format!("API key {id} not found."))),
747 }
748 }
749
750 fn create_data_source(
753 &self,
754 ctx: &Ctx,
755 api_id: &str,
756 body: &Value,
757 ) -> Result<AwsResponse, AwsServiceError> {
758 let name = require_body_str(body, "name")?.to_string();
759 let mut guard = self.state.write();
760 let data = guard.get_or_create(&ctx.account);
761 self.require_api(data, api_id)?;
762 let ds = build_data_source(ctx, api_id, &name, body);
763 data.data_sources
764 .entry(api_id.to_string())
765 .or_default()
766 .insert(name, ds.clone());
767 Ok(ok(json!({ "dataSource": ds })))
768 }
769
770 fn get_data_source(
771 &self,
772 ctx: &Ctx,
773 api_id: &str,
774 name: &str,
775 ) -> Result<AwsResponse, AwsServiceError> {
776 require_label(name, "name")?;
777 let guard = self.state.read();
778 let ds = guard
779 .get(&ctx.account)
780 .and_then(|d| d.data_sources.get(api_id))
781 .and_then(|m| m.get(name))
782 .cloned()
783 .ok_or_else(|| not_found(&format!("Data source {name} not found.")))?;
784 Ok(ok(json!({ "dataSource": ds })))
785 }
786
787 fn list_data_sources(&self, ctx: &Ctx, api_id: &str) -> Result<AwsResponse, AwsServiceError> {
788 let guard = self.state.read();
789 let data = guard
790 .get(&ctx.account)
791 .ok_or_else(|| not_found(&format!("GraphQL API {api_id} not found.")))?;
792 self.require_api(data, api_id)?;
793 let items: Vec<Value> = data
794 .data_sources
795 .get(api_id)
796 .map(|m| m.values().cloned().collect())
797 .unwrap_or_default();
798 Ok(ok(json!({ "dataSources": items })))
799 }
800
801 fn update_data_source(
802 &self,
803 ctx: &Ctx,
804 api_id: &str,
805 name: &str,
806 body: &Value,
807 ) -> Result<AwsResponse, AwsServiceError> {
808 require_label(name, "name")?;
809 let mut guard = self.state.write();
810 let data = guard.get_or_create(&ctx.account);
811 self.require_api(data, api_id)?;
812 if !data
813 .data_sources
814 .get(api_id)
815 .map(|m| m.contains_key(name))
816 .unwrap_or(false)
817 {
818 return Err(not_found(&format!("Data source {name} not found.")));
819 }
820 let ds = build_data_source(ctx, api_id, name, body);
821 data.data_sources
822 .entry(api_id.to_string())
823 .or_default()
824 .insert(name.to_string(), ds.clone());
825 Ok(ok(json!({ "dataSource": ds })))
826 }
827
828 fn delete_data_source(
829 &self,
830 ctx: &Ctx,
831 api_id: &str,
832 name: &str,
833 ) -> Result<AwsResponse, AwsServiceError> {
834 require_label(name, "name")?;
835 let mut guard = self.state.write();
836 let data = guard.get_or_create(&ctx.account);
837 self.require_api(data, api_id)?;
838 match data
839 .data_sources
840 .get_mut(api_id)
841 .and_then(|m| m.remove(name))
842 {
843 Some(_) => Ok(ok(json!({}))),
844 None => Err(not_found(&format!("Data source {name} not found."))),
845 }
846 }
847
848 fn create_resolver(
851 &self,
852 ctx: &Ctx,
853 api_id: &str,
854 type_name: &str,
855 body: &Value,
856 ) -> Result<AwsResponse, AwsServiceError> {
857 require_label(type_name, "typeName")?;
858 let field = require_body_str(body, "fieldName")?.to_string();
859 let mut guard = self.state.write();
860 let data = guard.get_or_create(&ctx.account);
861 self.require_api(data, api_id)?;
862 let resolver = build_resolver(ctx, api_id, type_name, &field, body);
863 data.resolvers
864 .entry(api_id.to_string())
865 .or_default()
866 .insert(resolver_key(type_name, &field), resolver.clone());
867 Ok(ok(json!({ "resolver": resolver })))
868 }
869
870 fn get_resolver(
871 &self,
872 ctx: &Ctx,
873 api_id: &str,
874 type_name: &str,
875 field: &str,
876 ) -> Result<AwsResponse, AwsServiceError> {
877 require_label(type_name, "typeName")?;
878 require_label(field, "fieldName")?;
879 let guard = self.state.read();
880 let resolver = guard
881 .get(&ctx.account)
882 .and_then(|d| d.resolvers.get(api_id))
883 .and_then(|m| m.get(&resolver_key(type_name, field)))
884 .cloned()
885 .ok_or_else(|| not_found(&format!("Resolver {type_name}.{field} not found.")))?;
886 Ok(ok(json!({ "resolver": resolver })))
887 }
888
889 fn list_resolvers(
890 &self,
891 ctx: &Ctx,
892 api_id: &str,
893 type_name: &str,
894 ) -> Result<AwsResponse, AwsServiceError> {
895 require_label(type_name, "typeName")?;
896 let guard = self.state.read();
897 let data = guard
898 .get(&ctx.account)
899 .ok_or_else(|| not_found(&format!("GraphQL API {api_id} not found.")))?;
900 self.require_api(data, api_id)?;
901 let prefix = format!("{type_name}::");
902 let items: Vec<Value> = data
903 .resolvers
904 .get(api_id)
905 .map(|m| {
906 m.iter()
907 .filter(|(k, _)| k.starts_with(&prefix))
908 .map(|(_, v)| v.clone())
909 .collect()
910 })
911 .unwrap_or_default();
912 Ok(ok(json!({ "resolvers": items })))
913 }
914
915 fn list_resolvers_by_function(
916 &self,
917 ctx: &Ctx,
918 api_id: &str,
919 function_id: &str,
920 ) -> Result<AwsResponse, AwsServiceError> {
921 require_label(function_id, "functionId")?;
922 let guard = self.state.read();
923 let data = guard
924 .get(&ctx.account)
925 .ok_or_else(|| not_found(&format!("GraphQL API {api_id} not found.")))?;
926 self.require_api(data, api_id)?;
927 let items: Vec<Value> = data
928 .resolvers
929 .get(api_id)
930 .map(|m| {
931 m.values()
932 .filter(|r| {
933 r.get("pipelineConfig")
934 .and_then(|p| p.get("functions"))
935 .and_then(Value::as_array)
936 .map(|fs| fs.iter().any(|f| f.as_str() == Some(function_id)))
937 .unwrap_or(false)
938 })
939 .cloned()
940 .collect()
941 })
942 .unwrap_or_default();
943 Ok(ok(json!({ "resolvers": items })))
944 }
945
946 fn update_resolver(
947 &self,
948 ctx: &Ctx,
949 api_id: &str,
950 type_name: &str,
951 field: &str,
952 body: &Value,
953 ) -> Result<AwsResponse, AwsServiceError> {
954 require_label(type_name, "typeName")?;
955 require_label(field, "fieldName")?;
956 let mut guard = self.state.write();
957 let data = guard.get_or_create(&ctx.account);
958 self.require_api(data, api_id)?;
959 let key = resolver_key(type_name, field);
960 if !data
961 .resolvers
962 .get(api_id)
963 .map(|m| m.contains_key(&key))
964 .unwrap_or(false)
965 {
966 return Err(not_found(&format!(
967 "Resolver {type_name}.{field} not found."
968 )));
969 }
970 let resolver = build_resolver(ctx, api_id, type_name, field, body);
971 data.resolvers
972 .entry(api_id.to_string())
973 .or_default()
974 .insert(key, resolver.clone());
975 Ok(ok(json!({ "resolver": resolver })))
976 }
977
978 fn delete_resolver(
979 &self,
980 ctx: &Ctx,
981 api_id: &str,
982 type_name: &str,
983 field: &str,
984 ) -> Result<AwsResponse, AwsServiceError> {
985 require_label(type_name, "typeName")?;
986 require_label(field, "fieldName")?;
987 let mut guard = self.state.write();
988 let data = guard.get_or_create(&ctx.account);
989 self.require_api(data, api_id)?;
990 match data
991 .resolvers
992 .get_mut(api_id)
993 .and_then(|m| m.remove(&resolver_key(type_name, field)))
994 {
995 Some(_) => Ok(ok(json!({}))),
996 None => Err(not_found(&format!(
997 "Resolver {type_name}.{field} not found."
998 ))),
999 }
1000 }
1001
1002 fn create_function(
1005 &self,
1006 ctx: &Ctx,
1007 api_id: &str,
1008 body: &Value,
1009 ) -> Result<AwsResponse, AwsServiceError> {
1010 let mut guard = self.state.write();
1011 let data = guard.get_or_create(&ctx.account);
1012 self.require_api(data, api_id)?;
1013 let function_id = gen_hex(12);
1014 let func = build_function(ctx, api_id, &function_id, body);
1015 data.functions
1016 .entry(api_id.to_string())
1017 .or_default()
1018 .insert(function_id, func.clone());
1019 Ok(ok(json!({ "functionConfiguration": func })))
1020 }
1021
1022 fn get_function(
1023 &self,
1024 ctx: &Ctx,
1025 api_id: &str,
1026 function_id: &str,
1027 ) -> Result<AwsResponse, AwsServiceError> {
1028 require_label(function_id, "functionId")?;
1029 let guard = self.state.read();
1030 let func = guard
1031 .get(&ctx.account)
1032 .and_then(|d| d.functions.get(api_id))
1033 .and_then(|m| m.get(function_id))
1034 .cloned()
1035 .ok_or_else(|| not_found(&format!("Function {function_id} not found.")))?;
1036 Ok(ok(json!({ "functionConfiguration": func })))
1037 }
1038
1039 fn list_functions(&self, ctx: &Ctx, api_id: &str) -> Result<AwsResponse, AwsServiceError> {
1040 let guard = self.state.read();
1041 let data = guard
1042 .get(&ctx.account)
1043 .ok_or_else(|| not_found(&format!("GraphQL API {api_id} not found.")))?;
1044 self.require_api(data, api_id)?;
1045 let items: Vec<Value> = data
1046 .functions
1047 .get(api_id)
1048 .map(|m| m.values().cloned().collect())
1049 .unwrap_or_default();
1050 Ok(ok(json!({ "functions": items })))
1051 }
1052
1053 fn update_function(
1054 &self,
1055 ctx: &Ctx,
1056 api_id: &str,
1057 function_id: &str,
1058 body: &Value,
1059 ) -> Result<AwsResponse, AwsServiceError> {
1060 require_label(function_id, "functionId")?;
1061 let mut guard = self.state.write();
1062 let data = guard.get_or_create(&ctx.account);
1063 self.require_api(data, api_id)?;
1064 if !data
1065 .functions
1066 .get(api_id)
1067 .map(|m| m.contains_key(function_id))
1068 .unwrap_or(false)
1069 {
1070 return Err(not_found(&format!("Function {function_id} not found.")));
1071 }
1072 let func = build_function(ctx, api_id, function_id, body);
1073 data.functions
1074 .entry(api_id.to_string())
1075 .or_default()
1076 .insert(function_id.to_string(), func.clone());
1077 Ok(ok(json!({ "functionConfiguration": func })))
1078 }
1079
1080 fn delete_function(
1081 &self,
1082 ctx: &Ctx,
1083 api_id: &str,
1084 function_id: &str,
1085 ) -> Result<AwsResponse, AwsServiceError> {
1086 require_label(function_id, "functionId")?;
1087 let mut guard = self.state.write();
1088 let data = guard.get_or_create(&ctx.account);
1089 self.require_api(data, api_id)?;
1090 match data
1091 .functions
1092 .get_mut(api_id)
1093 .and_then(|m| m.remove(function_id))
1094 {
1095 Some(_) => Ok(ok(json!({}))),
1096 None => Err(not_found(&format!("Function {function_id} not found."))),
1097 }
1098 }
1099
1100 fn create_type(
1103 &self,
1104 ctx: &Ctx,
1105 api_id: &str,
1106 body: &Value,
1107 ) -> Result<AwsResponse, AwsServiceError> {
1108 let definition = require_body_str(body, "definition")?.to_string();
1109 let format = require_body_str(body, "format")?.to_string();
1110 let mut guard = self.state.write();
1111 let data = guard.get_or_create(&ctx.account);
1112 self.require_api(data, api_id)?;
1113 let name = parse_type_name(&definition).unwrap_or_else(|| gen_hex(6));
1114 let ty = build_type(ctx, api_id, &name, &definition, &format);
1115 data.types
1116 .entry(api_id.to_string())
1117 .or_default()
1118 .insert(name, ty.clone());
1119 Ok(ok(json!({ "type": ty })))
1120 }
1121
1122 fn get_type(
1123 &self,
1124 ctx: &Ctx,
1125 api_id: &str,
1126 type_name: &str,
1127 q: &[(String, String)],
1128 ) -> Result<AwsResponse, AwsServiceError> {
1129 require_label(type_name, "typeName")?;
1130 require_query(q, "format")?;
1131 let guard = self.state.read();
1132 let ty = guard
1133 .get(&ctx.account)
1134 .and_then(|d| d.types.get(api_id))
1135 .and_then(|m| m.get(type_name))
1136 .cloned()
1137 .ok_or_else(|| not_found(&format!("Type {type_name} not found.")))?;
1138 Ok(ok(json!({ "type": ty })))
1139 }
1140
1141 fn list_types(
1142 &self,
1143 ctx: &Ctx,
1144 api_id: &str,
1145 q: &[(String, String)],
1146 ) -> Result<AwsResponse, AwsServiceError> {
1147 require_query(q, "format")?;
1148 let guard = self.state.read();
1149 let data = guard
1150 .get(&ctx.account)
1151 .ok_or_else(|| not_found(&format!("GraphQL API {api_id} not found.")))?;
1152 self.require_api(data, api_id)?;
1153 let items: Vec<Value> = data
1154 .types
1155 .get(api_id)
1156 .map(|m| m.values().cloned().collect())
1157 .unwrap_or_default();
1158 Ok(ok(json!({ "types": items })))
1159 }
1160
1161 fn update_type(
1162 &self,
1163 ctx: &Ctx,
1164 api_id: &str,
1165 type_name: &str,
1166 body: &Value,
1167 ) -> Result<AwsResponse, AwsServiceError> {
1168 require_label(type_name, "typeName")?;
1169 let format = require_body_str(body, "format")?.to_string();
1170 let mut guard = self.state.write();
1171 let data = guard.get_or_create(&ctx.account);
1172 self.require_api(data, api_id)?;
1173 let existing = data
1174 .types
1175 .get(api_id)
1176 .and_then(|m| m.get(type_name))
1177 .cloned()
1178 .ok_or_else(|| not_found(&format!("Type {type_name} not found.")))?;
1179 let definition = body
1180 .get("definition")
1181 .and_then(Value::as_str)
1182 .map(str::to_string)
1183 .or_else(|| {
1184 existing
1185 .get("definition")
1186 .and_then(Value::as_str)
1187 .map(str::to_string)
1188 })
1189 .unwrap_or_default();
1190 let ty = build_type(ctx, api_id, type_name, &definition, &format);
1191 data.types
1192 .entry(api_id.to_string())
1193 .or_default()
1194 .insert(type_name.to_string(), ty.clone());
1195 Ok(ok(json!({ "type": ty })))
1196 }
1197
1198 fn delete_type(
1199 &self,
1200 ctx: &Ctx,
1201 api_id: &str,
1202 type_name: &str,
1203 ) -> Result<AwsResponse, AwsServiceError> {
1204 require_label(type_name, "typeName")?;
1205 let mut guard = self.state.write();
1206 let data = guard.get_or_create(&ctx.account);
1207 self.require_api(data, api_id)?;
1208 match data.types.get_mut(api_id).and_then(|m| m.remove(type_name)) {
1209 Some(_) => Ok(ok(json!({}))),
1210 None => Err(not_found(&format!("Type {type_name} not found."))),
1211 }
1212 }
1213
1214 fn start_schema_creation(
1215 &self,
1216 ctx: &Ctx,
1217 api_id: &str,
1218 body: &Value,
1219 ) -> Result<AwsResponse, AwsServiceError> {
1220 let def_b64 = require_body_str(body, "definition")?;
1221 let sdl = decode_blob(def_b64);
1222 let mut guard = self.state.write();
1223 let data = guard.get_or_create(&ctx.account);
1224 self.require_api(data, api_id)?;
1225 data.schemas.insert(
1226 api_id.to_string(),
1227 SchemaState {
1228 definition: sdl,
1229 status: "Processing".to_string(),
1230 details: "Schema creation in progress.".to_string(),
1231 },
1232 );
1233 Ok(ok(json!({ "status": "PROCESSING" })))
1234 }
1235
1236 fn get_schema_creation_status(
1237 &self,
1238 ctx: &Ctx,
1239 api_id: &str,
1240 ) -> Result<AwsResponse, AwsServiceError> {
1241 require_label(api_id, "apiId")?;
1242 let mut guard = self.state.write();
1243 let data = guard.get_or_create(&ctx.account);
1244 self.require_api(data, api_id)?;
1245 let schema = data.schemas.entry(api_id.to_string()).or_default();
1246 if schema.status.is_empty() || schema.status == "NotApplicable" {
1249 schema.status = "NotApplicable".to_string();
1250 } else {
1251 schema.status = "Success".to_string();
1252 schema.details = "Schema is ready.".to_string();
1253 }
1254 Ok(ok(json!({
1255 "status": schema_status_wire(&schema.status),
1256 "details": schema.details,
1257 })))
1258 }
1259
1260 fn get_introspection_schema(
1261 &self,
1262 ctx: &Ctx,
1263 api_id: &str,
1264 q: &[(String, String)],
1265 ) -> Result<AwsResponse, AwsServiceError> {
1266 require_label(api_id, "apiId")?;
1267 let format = require_query(q, "format")?.to_string();
1268 let guard = self.state.read();
1269 let data = guard
1270 .get(&ctx.account)
1271 .ok_or_else(|| not_found(&format!("GraphQL API {api_id} not found.")))?;
1272 self.require_api(data, api_id)?;
1273 let schema = data.schemas.get(api_id).map(|s| s.definition.clone());
1274 match schema {
1275 Some(sdl) if !sdl.is_empty() => {
1276 let bytes = crate::schema::introspection_bytes(&sdl, &format);
1277 let b64 = base64::engine::general_purpose::STANDARD.encode(bytes);
1278 Ok(ok(json!({ "schema": b64 })))
1279 }
1280 _ => Err(AwsServiceError::aws_error(
1281 StatusCode::BAD_REQUEST,
1282 "GraphQLSchemaException",
1283 "No schema found. Provide a valid schema via StartSchemaCreation first.",
1284 )),
1285 }
1286 }
1287
1288 fn create_api_cache(
1291 &self,
1292 ctx: &Ctx,
1293 api_id: &str,
1294 body: &Value,
1295 ) -> Result<AwsResponse, AwsServiceError> {
1296 let mut guard = self.state.write();
1297 let data = guard.get_or_create(&ctx.account);
1298 self.require_api(data, api_id)?;
1299 if data.api_caches.contains_key(api_id) {
1300 return Err(bad("An API cache already exists for this API."));
1301 }
1302 let cache = build_api_cache(body);
1303 data.api_caches.insert(api_id.to_string(), cache.clone());
1304 Ok(ok(json!({ "apiCache": cache })))
1305 }
1306
1307 fn update_api_cache(
1308 &self,
1309 ctx: &Ctx,
1310 api_id: &str,
1311 body: &Value,
1312 ) -> Result<AwsResponse, AwsServiceError> {
1313 let mut guard = self.state.write();
1314 let data = guard.get_or_create(&ctx.account);
1315 self.require_api(data, api_id)?;
1316 if !data.api_caches.contains_key(api_id) {
1317 return Err(not_found("API cache not found."));
1318 }
1319 let cache = build_api_cache(body);
1320 data.api_caches.insert(api_id.to_string(), cache.clone());
1321 Ok(ok(json!({ "apiCache": cache })))
1322 }
1323
1324 fn get_api_cache(&self, ctx: &Ctx, api_id: &str) -> Result<AwsResponse, AwsServiceError> {
1325 require_label(api_id, "apiId")?;
1326 let guard = self.state.read();
1327 let cache = guard
1328 .get(&ctx.account)
1329 .and_then(|d| d.api_caches.get(api_id))
1330 .cloned()
1331 .ok_or_else(|| not_found("API cache not found."))?;
1332 Ok(ok(json!({ "apiCache": cache })))
1333 }
1334
1335 fn delete_api_cache(&self, ctx: &Ctx, api_id: &str) -> Result<AwsResponse, AwsServiceError> {
1336 require_label(api_id, "apiId")?;
1337 let mut guard = self.state.write();
1338 let data = guard.get_or_create(&ctx.account);
1339 self.require_api(data, api_id)?;
1340 match data.api_caches.remove(api_id) {
1341 Some(_) => Ok(ok(json!({}))),
1342 None => Err(not_found("API cache not found.")),
1343 }
1344 }
1345
1346 fn flush_api_cache(&self, ctx: &Ctx, api_id: &str) -> Result<AwsResponse, AwsServiceError> {
1347 require_label(api_id, "apiId")?;
1348 let guard = self.state.read();
1349 let data = guard
1350 .get(&ctx.account)
1351 .ok_or_else(|| not_found("API cache not found."))?;
1352 if data.api_caches.contains_key(api_id) {
1353 Ok(ok(json!({})))
1354 } else {
1355 Err(not_found("API cache not found."))
1356 }
1357 }
1358
1359 fn put_env_vars(
1362 &self,
1363 ctx: &Ctx,
1364 api_id: &str,
1365 body: &Value,
1366 ) -> Result<AwsResponse, AwsServiceError> {
1367 let mut guard = self.state.write();
1368 let data = guard.get_or_create(&ctx.account);
1369 self.require_api(data, api_id)?;
1370 let vars = body
1371 .get("environmentVariables")
1372 .and_then(Value::as_object)
1373 .map(string_map)
1374 .unwrap_or_default();
1375 data.env_vars.insert(api_id.to_string(), vars.clone());
1376 Ok(ok(json!({ "environmentVariables": vars })))
1377 }
1378
1379 fn get_env_vars(&self, ctx: &Ctx, api_id: &str) -> Result<AwsResponse, AwsServiceError> {
1380 require_label(api_id, "apiId")?;
1381 let guard = self.state.read();
1382 let data = guard
1383 .get(&ctx.account)
1384 .ok_or_else(|| not_found(&format!("GraphQL API {api_id} not found.")))?;
1385 self.require_api(data, api_id)?;
1386 let vars = data.env_vars.get(api_id).cloned().unwrap_or_default();
1387 Ok(ok(json!({ "environmentVariables": vars })))
1388 }
1389
1390 fn create_domain_name(&self, ctx: &Ctx, body: &Value) -> Result<AwsResponse, AwsServiceError> {
1393 let domain = require_body_str(body, "domainName")?.to_string();
1394 let mut guard = self.state.write();
1395 let data = guard.get_or_create(&ctx.account);
1396 let cfg = build_domain_name(ctx, &domain, body);
1397 data.domain_names.insert(domain, cfg.clone());
1398 Ok(ok(json!({ "domainNameConfig": cfg })))
1399 }
1400
1401 fn get_domain_name(&self, ctx: &Ctx, domain: &str) -> Result<AwsResponse, AwsServiceError> {
1402 require_label(domain, "domainName")?;
1403 let guard = self.state.read();
1404 let cfg = guard
1405 .get(&ctx.account)
1406 .and_then(|d| d.domain_names.get(domain))
1407 .cloned()
1408 .ok_or_else(|| not_found(&format!("Domain name {domain} not found.")))?;
1409 Ok(ok(json!({ "domainNameConfig": cfg })))
1410 }
1411
1412 fn list_domain_names(&self, ctx: &Ctx) -> Result<AwsResponse, AwsServiceError> {
1413 let guard = self.state.read();
1414 let items: Vec<Value> = guard
1415 .get(&ctx.account)
1416 .map(|d| d.domain_names.values().cloned().collect())
1417 .unwrap_or_default();
1418 Ok(ok(json!({ "domainNameConfigs": items })))
1419 }
1420
1421 fn update_domain_name(
1422 &self,
1423 ctx: &Ctx,
1424 domain: &str,
1425 body: &Value,
1426 ) -> Result<AwsResponse, AwsServiceError> {
1427 require_label(domain, "domainName")?;
1428 let mut guard = self.state.write();
1429 let data = guard.get_or_create(&ctx.account);
1430 let mut cfg = data
1431 .domain_names
1432 .get(domain)
1433 .cloned()
1434 .ok_or_else(|| not_found(&format!("Domain name {domain} not found.")))?;
1435 if let (Some(obj), Some(desc)) = (cfg.as_object_mut(), body.get("description")) {
1436 obj.insert("description".into(), desc.clone());
1437 }
1438 data.domain_names.insert(domain.to_string(), cfg.clone());
1439 Ok(ok(json!({ "domainNameConfig": cfg })))
1440 }
1441
1442 fn delete_domain_name(&self, ctx: &Ctx, domain: &str) -> Result<AwsResponse, AwsServiceError> {
1443 require_label(domain, "domainName")?;
1444 let mut guard = self.state.write();
1445 let data = guard.get_or_create(&ctx.account);
1446 match data.domain_names.remove(domain) {
1447 Some(_) => {
1448 data.api_associations.remove(domain);
1449 Ok(ok(json!({})))
1450 }
1451 None => Err(not_found(&format!("Domain name {domain} not found."))),
1452 }
1453 }
1454
1455 fn associate_api(
1456 &self,
1457 ctx: &Ctx,
1458 domain: &str,
1459 body: &Value,
1460 ) -> Result<AwsResponse, AwsServiceError> {
1461 require_label(domain, "domainName")?;
1462 let api_id = require_body_str(body, "apiId")?.to_string();
1463 let mut guard = self.state.write();
1464 let data = guard.get_or_create(&ctx.account);
1465 if !data.domain_names.contains_key(domain) {
1466 return Err(not_found(&format!("Domain name {domain} not found.")));
1467 }
1468 let assoc = json!({
1469 "domainName": domain,
1470 "apiId": api_id,
1471 "associationStatus": "SUCCESS",
1472 });
1473 data.api_associations
1474 .insert(domain.to_string(), assoc.clone());
1475 Ok(ok(json!({ "apiAssociation": assoc })))
1476 }
1477
1478 fn get_api_association(&self, ctx: &Ctx, domain: &str) -> Result<AwsResponse, AwsServiceError> {
1479 require_label(domain, "domainName")?;
1480 let guard = self.state.read();
1481 let assoc = guard
1482 .get(&ctx.account)
1483 .and_then(|d| d.api_associations.get(domain))
1484 .cloned()
1485 .ok_or_else(|| not_found(&format!("No API association for {domain}.")))?;
1486 Ok(ok(json!({ "apiAssociation": assoc })))
1487 }
1488
1489 fn disassociate_api(&self, ctx: &Ctx, domain: &str) -> Result<AwsResponse, AwsServiceError> {
1490 require_label(domain, "domainName")?;
1491 let mut guard = self.state.write();
1492 let data = guard.get_or_create(&ctx.account);
1493 match data.api_associations.remove(domain) {
1494 Some(_) => Ok(ok(json!({}))),
1495 None => Err(not_found(&format!("No API association for {domain}."))),
1496 }
1497 }
1498
1499 fn create_api(&self, ctx: &Ctx, body: &Value) -> Result<AwsResponse, AwsServiceError> {
1502 let api_id = gen_api_id();
1503 let api = build_event_api(ctx, &api_id, body);
1504 let mut guard = self.state.write();
1505 let data = guard.get_or_create(&ctx.account);
1506 data.apis.insert(api_id, api.clone());
1507 Ok(ok(json!({ "api": api })))
1508 }
1509
1510 fn get_api(&self, ctx: &Ctx, api_id: &str) -> Result<AwsResponse, AwsServiceError> {
1511 require_label(api_id, "apiId")?;
1512 let guard = self.state.read();
1513 let api = guard
1514 .get(&ctx.account)
1515 .and_then(|d| d.apis.get(api_id))
1516 .cloned()
1517 .ok_or_else(|| not_found(&format!("API {api_id} not found.")))?;
1518 Ok(ok(json!({ "api": api })))
1519 }
1520
1521 fn list_apis(&self, ctx: &Ctx) -> Result<AwsResponse, AwsServiceError> {
1522 let guard = self.state.read();
1523 let items: Vec<Value> = guard
1524 .get(&ctx.account)
1525 .map(|d| d.apis.values().cloned().collect())
1526 .unwrap_or_default();
1527 Ok(ok(json!({ "apis": items })))
1528 }
1529
1530 fn update_api(
1531 &self,
1532 ctx: &Ctx,
1533 api_id: &str,
1534 body: &Value,
1535 ) -> Result<AwsResponse, AwsServiceError> {
1536 require_label(api_id, "apiId")?;
1537 let mut guard = self.state.write();
1538 let data = guard.get_or_create(&ctx.account);
1539 if !data.apis.contains_key(api_id) {
1540 return Err(not_found(&format!("API {api_id} not found.")));
1541 }
1542 let api = build_event_api(ctx, api_id, body);
1543 data.apis.insert(api_id.to_string(), api.clone());
1544 Ok(ok(json!({ "api": api })))
1545 }
1546
1547 fn delete_api(&self, ctx: &Ctx, api_id: &str) -> Result<AwsResponse, AwsServiceError> {
1548 require_label(api_id, "apiId")?;
1549 let mut guard = self.state.write();
1550 let data = guard.get_or_create(&ctx.account);
1551 match data.apis.remove(api_id) {
1552 Some(_) => {
1553 data.channel_namespaces.remove(api_id);
1554 Ok(ok(json!({})))
1555 }
1556 None => Err(not_found(&format!("API {api_id} not found."))),
1557 }
1558 }
1559
1560 fn create_channel_namespace(
1561 &self,
1562 ctx: &Ctx,
1563 api_id: &str,
1564 body: &Value,
1565 ) -> Result<AwsResponse, AwsServiceError> {
1566 let name = require_body_str(body, "name")?.to_string();
1567 let mut guard = self.state.write();
1568 let data = guard.get_or_create(&ctx.account);
1569 if !data.apis.contains_key(api_id) {
1570 return Err(not_found(&format!("API {api_id} not found.")));
1571 }
1572 if data
1573 .channel_namespaces
1574 .get(api_id)
1575 .map(|m| m.contains_key(&name))
1576 .unwrap_or(false)
1577 {
1578 return Err(AwsServiceError::aws_error(
1579 StatusCode::CONFLICT,
1580 "ConflictException",
1581 format!("Channel namespace {name} already exists."),
1582 ));
1583 }
1584 let ns = build_channel_namespace(ctx, api_id, &name, body);
1585 data.channel_namespaces
1586 .entry(api_id.to_string())
1587 .or_default()
1588 .insert(name, ns.clone());
1589 Ok(ok(json!({ "channelNamespace": ns })))
1590 }
1591
1592 fn get_channel_namespace(
1593 &self,
1594 ctx: &Ctx,
1595 api_id: &str,
1596 name: &str,
1597 ) -> Result<AwsResponse, AwsServiceError> {
1598 require_label(name, "name")?;
1599 let guard = self.state.read();
1600 let ns = guard
1601 .get(&ctx.account)
1602 .and_then(|d| d.channel_namespaces.get(api_id))
1603 .and_then(|m| m.get(name))
1604 .cloned()
1605 .ok_or_else(|| not_found(&format!("Channel namespace {name} not found.")))?;
1606 Ok(ok(json!({ "channelNamespace": ns })))
1607 }
1608
1609 fn list_channel_namespaces(
1610 &self,
1611 ctx: &Ctx,
1612 api_id: &str,
1613 ) -> Result<AwsResponse, AwsServiceError> {
1614 require_label(api_id, "apiId")?;
1615 let guard = self.state.read();
1616 let data = guard
1617 .get(&ctx.account)
1618 .ok_or_else(|| not_found(&format!("API {api_id} not found.")))?;
1619 if !data.apis.contains_key(api_id) {
1620 return Err(not_found(&format!("API {api_id} not found.")));
1621 }
1622 let items: Vec<Value> = data
1623 .channel_namespaces
1624 .get(api_id)
1625 .map(|m| m.values().cloned().collect())
1626 .unwrap_or_default();
1627 Ok(ok(json!({ "channelNamespaces": items })))
1628 }
1629
1630 fn update_channel_namespace(
1631 &self,
1632 ctx: &Ctx,
1633 api_id: &str,
1634 name: &str,
1635 body: &Value,
1636 ) -> Result<AwsResponse, AwsServiceError> {
1637 require_label(name, "name")?;
1638 let mut guard = self.state.write();
1639 let data = guard.get_or_create(&ctx.account);
1640 if !data
1641 .channel_namespaces
1642 .get(api_id)
1643 .map(|m| m.contains_key(name))
1644 .unwrap_or(false)
1645 {
1646 return Err(not_found(&format!("Channel namespace {name} not found.")));
1647 }
1648 let ns = build_channel_namespace(ctx, api_id, name, body);
1649 data.channel_namespaces
1650 .entry(api_id.to_string())
1651 .or_default()
1652 .insert(name.to_string(), ns.clone());
1653 Ok(ok(json!({ "channelNamespace": ns })))
1654 }
1655
1656 fn delete_channel_namespace(
1657 &self,
1658 ctx: &Ctx,
1659 api_id: &str,
1660 name: &str,
1661 ) -> Result<AwsResponse, AwsServiceError> {
1662 require_label(name, "name")?;
1663 let mut guard = self.state.write();
1664 let data = guard.get_or_create(&ctx.account);
1665 match data
1666 .channel_namespaces
1667 .get_mut(api_id)
1668 .and_then(|m| m.remove(name))
1669 {
1670 Some(_) => Ok(ok(json!({}))),
1671 None => Err(not_found(&format!("Channel namespace {name} not found."))),
1672 }
1673 }
1674
1675 fn associate_source_api(
1678 &self,
1679 ctx: &Ctx,
1680 merged_id: &str,
1681 body: &Value,
1682 ) -> Result<AwsResponse, AwsServiceError> {
1683 require_label(merged_id, "mergedApiIdentifier")?;
1684 let source_id = require_body_str(body, "sourceApiIdentifier")?.to_string();
1685 self.create_source_assoc(ctx, merged_id, &source_id, body)
1686 }
1687
1688 fn associate_merged_api(
1689 &self,
1690 ctx: &Ctx,
1691 source_id: &str,
1692 body: &Value,
1693 ) -> Result<AwsResponse, AwsServiceError> {
1694 require_label(source_id, "sourceApiIdentifier")?;
1695 let merged_id = require_body_str(body, "mergedApiIdentifier")?.to_string();
1696 self.create_source_assoc(ctx, &merged_id, source_id, body)
1697 }
1698
1699 fn create_source_assoc(
1700 &self,
1701 ctx: &Ctx,
1702 merged_id: &str,
1703 source_id: &str,
1704 body: &Value,
1705 ) -> Result<AwsResponse, AwsServiceError> {
1706 {
1707 let guard = self.state.read();
1711 let data = guard.get(&ctx.account);
1712 if !api_ref_exists(data, merged_id) {
1713 return Err(not_found(&format!("Merged API {merged_id} not found.")));
1714 }
1715 if !api_ref_exists(data, source_id) {
1716 return Err(not_found(&format!("Source API {source_id} not found.")));
1717 }
1718 }
1719 let assoc_id = gen_hex(16);
1720 let mut assoc = Map::new();
1721 assoc.insert("associationId".into(), json!(assoc_id));
1722 assoc.insert(
1723 "associationArn".into(),
1724 json!(format!(
1725 "arn:aws:appsync:{}:{}:apis/{merged_id}/sourceApiAssociations/{assoc_id}",
1726 ctx.region, ctx.account
1727 )),
1728 );
1729 assoc.insert("sourceApiId".into(), json!(source_id));
1730 assoc.insert("mergedApiId".into(), json!(merged_id));
1731 assoc.insert("sourceApiAssociationStatus".into(), json!("MERGE_SUCCESS"));
1732 copy_keys(
1733 &mut assoc,
1734 body,
1735 &["description", "sourceApiAssociationConfig"],
1736 );
1737 let assoc = Value::Object(assoc);
1738 let mut guard = self.state.write();
1739 let data = guard.get_or_create(&ctx.account);
1740 data.source_api_associations.insert(assoc_id, assoc.clone());
1741 Ok(ok(json!({ "sourceApiAssociation": assoc })))
1742 }
1743
1744 fn get_source_api_association(
1745 &self,
1746 ctx: &Ctx,
1747 assoc_id: &str,
1748 ) -> Result<AwsResponse, AwsServiceError> {
1749 require_label(assoc_id, "associationId")?;
1750 let guard = self.state.read();
1751 let assoc = guard
1752 .get(&ctx.account)
1753 .and_then(|d| d.source_api_associations.get(assoc_id))
1754 .cloned()
1755 .ok_or_else(|| not_found(&format!("Association {assoc_id} not found.")))?;
1756 Ok(ok(json!({ "sourceApiAssociation": assoc })))
1757 }
1758
1759 fn list_source_api_associations(
1760 &self,
1761 ctx: &Ctx,
1762 api_id: &str,
1763 ) -> Result<AwsResponse, AwsServiceError> {
1764 require_label(api_id, "apiId")?;
1765 let guard = self.state.read();
1766 let data = guard
1767 .get(&ctx.account)
1768 .ok_or_else(|| not_found(&format!("Merged API {api_id} not found.")))?;
1769 self.require_api(data, api_id)?;
1770 let items: Vec<Value> = data
1771 .source_api_associations
1772 .values()
1773 .filter(|a| {
1774 a.get("mergedApiId").and_then(Value::as_str) == Some(api_id)
1775 || a.get("sourceApiId").and_then(Value::as_str) == Some(api_id)
1776 })
1777 .map(|a| {
1778 json!({
1779 "associationId": a.get("associationId").cloned().unwrap_or(Value::Null),
1780 "associationArn": a.get("associationArn").cloned().unwrap_or(Value::Null),
1781 "sourceApiId": a.get("sourceApiId").cloned().unwrap_or(Value::Null),
1782 "mergedApiId": a.get("mergedApiId").cloned().unwrap_or(Value::Null),
1783 "description": a.get("description").cloned().unwrap_or(Value::Null),
1784 })
1785 })
1786 .collect();
1787 Ok(ok(json!({ "sourceApiAssociationSummaries": items })))
1788 }
1789
1790 fn list_types_by_association(
1791 &self,
1792 ctx: &Ctx,
1793 merged_id: &str,
1794 assoc_id: &str,
1795 q: &[(String, String)],
1796 ) -> Result<AwsResponse, AwsServiceError> {
1797 require_label(merged_id, "mergedApiIdentifier")?;
1798 require_label(assoc_id, "associationId")?;
1799 require_query(q, "format")?;
1800 let guard = self.state.read();
1801 let exists = guard
1802 .get(&ctx.account)
1803 .map(|d| d.source_api_associations.contains_key(assoc_id))
1804 .unwrap_or(false);
1805 if !exists {
1806 return Err(not_found(&format!("Association {assoc_id} not found.")));
1807 }
1808 Ok(ok(json!({ "types": [] })))
1809 }
1810
1811 fn update_source_api_association(
1812 &self,
1813 ctx: &Ctx,
1814 assoc_id: &str,
1815 body: &Value,
1816 ) -> Result<AwsResponse, AwsServiceError> {
1817 require_label(assoc_id, "associationId")?;
1818 let mut guard = self.state.write();
1819 let data = guard.get_or_create(&ctx.account);
1820 let assoc = data
1821 .source_api_associations
1822 .get_mut(assoc_id)
1823 .ok_or_else(|| not_found(&format!("Association {assoc_id} not found.")))?;
1824 if let Some(obj) = assoc.as_object_mut() {
1825 for k in ["description", "sourceApiAssociationConfig"] {
1826 if let Some(v) = body.get(k) {
1827 obj.insert(k.to_string(), v.clone());
1828 }
1829 }
1830 }
1831 Ok(ok(json!({ "sourceApiAssociation": assoc })))
1832 }
1833
1834 fn disassociate_source_api(
1835 &self,
1836 ctx: &Ctx,
1837 assoc_id: &str,
1838 ) -> Result<AwsResponse, AwsServiceError> {
1839 require_label(assoc_id, "associationId")?;
1840 let mut guard = self.state.write();
1841 let data = guard.get_or_create(&ctx.account);
1842 match data.source_api_associations.remove(assoc_id) {
1843 Some(_) => Ok(ok(
1844 json!({ "sourceApiAssociationStatus": "DELETION_IN_PROGRESS" }),
1845 )),
1846 None => Err(not_found(&format!("Association {assoc_id} not found."))),
1847 }
1848 }
1849
1850 fn start_schema_merge(
1851 &self,
1852 ctx: &Ctx,
1853 assoc_id: &str,
1854 ) -> Result<AwsResponse, AwsServiceError> {
1855 require_label(assoc_id, "associationId")?;
1856 let guard = self.state.read();
1857 let exists = guard
1858 .get(&ctx.account)
1859 .map(|d| d.source_api_associations.contains_key(assoc_id))
1860 .unwrap_or(false);
1861 if exists {
1862 Ok(ok(
1863 json!({ "sourceApiAssociationStatus": "MERGE_IN_PROGRESS" }),
1864 ))
1865 } else {
1866 Err(not_found(&format!("Association {assoc_id} not found.")))
1867 }
1868 }
1869
1870 fn start_ds_introspection() -> Result<AwsResponse, AwsServiceError> {
1873 let id = gen_hex(20);
1874 Ok(ok(json!({
1875 "introspectionId": id,
1876 "introspectionStatus": "PROCESSING",
1877 "introspectionStatusDetail": "Introspection started.",
1878 })))
1879 }
1880
1881 fn get_ds_introspection(
1882 &self,
1883 _ctx: &Ctx,
1884 introspection_id: &str,
1885 ) -> Result<AwsResponse, AwsServiceError> {
1886 require_label(introspection_id, "introspectionId")?;
1887 Err(not_found(&format!(
1890 "Introspection {introspection_id} not found."
1891 )))
1892 }
1893
1894 fn tag_resource(
1897 &self,
1898 ctx: &Ctx,
1899 arn: &str,
1900 body: &Value,
1901 ) -> Result<AwsResponse, AwsServiceError> {
1902 require_resource_arn(arn)?;
1903 let mut guard = self.state.write();
1904 let data = guard.get_or_create(&ctx.account);
1905 let entry = data.tags.entry(arn.to_string()).or_default();
1906 if let Some(tags) = body.get("tags").and_then(Value::as_object) {
1907 for (k, v) in tags {
1908 if let Some(s) = v.as_str() {
1909 entry.insert(k.clone(), s.to_string());
1910 }
1911 }
1912 }
1913 Ok(ok(json!({})))
1914 }
1915
1916 fn untag_resource(
1917 &self,
1918 ctx: &Ctx,
1919 arn: &str,
1920 q: &[(String, String)],
1921 ) -> Result<AwsResponse, AwsServiceError> {
1922 require_resource_arn(arn)?;
1923 let mut guard = self.state.write();
1924 let data = guard.get_or_create(&ctx.account);
1925 if let Some(entry) = data.tags.get_mut(arn) {
1926 for (_, v) in q.iter().filter(|(k, _)| k == "tagKeys") {
1927 entry.remove(v);
1928 }
1929 }
1930 Ok(ok(json!({})))
1931 }
1932
1933 fn list_tags_for_resource(&self, ctx: &Ctx, arn: &str) -> Result<AwsResponse, AwsServiceError> {
1934 require_resource_arn(arn)?;
1935 let guard = self.state.read();
1936 let tags = guard
1937 .get(&ctx.account)
1938 .and_then(|d| d.tags.get(arn))
1939 .cloned()
1940 .unwrap_or_default();
1941 Ok(ok(json!({ "tags": tags })))
1942 }
1943
1944 fn evaluate_code(body: &Value) -> Result<AwsResponse, AwsServiceError> {
1947 let code = body.get("code").and_then(Value::as_str).unwrap_or_default();
1948 let context = body
1949 .get("context")
1950 .and_then(Value::as_str)
1951 .unwrap_or_default();
1952 let ev = crate::evaluate::evaluate(code, context);
1953 Ok(ok(build_evaluation(ev)))
1954 }
1955
1956 fn evaluate_template(body: &Value) -> Result<AwsResponse, AwsServiceError> {
1957 let template = body
1958 .get("template")
1959 .and_then(Value::as_str)
1960 .unwrap_or_default();
1961 let context = body
1962 .get("context")
1963 .and_then(Value::as_str)
1964 .unwrap_or_default();
1965 let ev = crate::evaluate::evaluate(template, context);
1966 Ok(ok(build_evaluation(ev)))
1967 }
1968}
1969
1970fn build_evaluation(ev: crate::evaluate::Evaluation) -> Value {
1973 let mut out = Map::new();
1974 if let Some(r) = ev.result {
1975 out.insert("evaluationResult".into(), json!(r));
1976 }
1977 if let Some(s) = ev.stash {
1978 out.insert("stash".into(), json!(s));
1979 }
1980 if let Some(e) = ev.error {
1981 out.insert("error".into(), json!({ "message": e }));
1982 }
1983 out.insert("logs".into(), json!([]));
1984 Value::Object(out)
1985}
1986
1987fn build_data_source(ctx: &Ctx, api_id: &str, name: &str, body: &Value) -> Value {
1988 let mut ds = Map::new();
1989 ds.insert(
1990 "dataSourceArn".into(),
1991 json!(format!(
1992 "arn:aws:appsync:{}:{}:apis/{api_id}/datasources/{name}",
1993 ctx.region, ctx.account
1994 )),
1995 );
1996 ds.insert("name".into(), json!(name));
1997 copy_keys(
1998 &mut ds,
1999 body,
2000 &[
2001 "description",
2002 "type",
2003 "serviceRoleArn",
2004 "dynamodbConfig",
2005 "lambdaConfig",
2006 "elasticsearchConfig",
2007 "openSearchServiceConfig",
2008 "httpConfig",
2009 "relationalDatabaseConfig",
2010 "eventBridgeConfig",
2011 "metricsConfig",
2012 ],
2013 );
2014 Value::Object(ds)
2015}
2016
2017fn build_resolver(ctx: &Ctx, api_id: &str, type_name: &str, field: &str, body: &Value) -> Value {
2018 let mut r = Map::new();
2019 r.insert("typeName".into(), json!(type_name));
2020 r.insert("fieldName".into(), json!(field));
2021 r.insert(
2022 "resolverArn".into(),
2023 json!(format!(
2024 "arn:aws:appsync:{}:{}:apis/{api_id}/types/{type_name}/resolvers/{field}",
2025 ctx.region, ctx.account
2026 )),
2027 );
2028 r.insert(
2029 "kind".into(),
2030 json!(body.get("kind").and_then(Value::as_str).unwrap_or("UNIT")),
2031 );
2032 copy_keys(
2033 &mut r,
2034 body,
2035 &[
2036 "dataSourceName",
2037 "requestMappingTemplate",
2038 "responseMappingTemplate",
2039 "pipelineConfig",
2040 "syncConfig",
2041 "cachingConfig",
2042 "maxBatchSize",
2043 "runtime",
2044 "code",
2045 "metricsConfig",
2046 ],
2047 );
2048 Value::Object(r)
2049}
2050
2051fn build_function(ctx: &Ctx, api_id: &str, function_id: &str, body: &Value) -> Value {
2052 let mut f = Map::new();
2053 f.insert("functionId".into(), json!(function_id));
2054 f.insert(
2055 "functionArn".into(),
2056 json!(format!(
2057 "arn:aws:appsync:{}:{}:apis/{api_id}/functions/{function_id}",
2058 ctx.region, ctx.account
2059 )),
2060 );
2061 f.insert(
2062 "functionVersion".into(),
2063 json!(body
2064 .get("functionVersion")
2065 .and_then(Value::as_str)
2066 .unwrap_or("2018-05-29")),
2067 );
2068 copy_keys(
2069 &mut f,
2070 body,
2071 &[
2072 "name",
2073 "description",
2074 "dataSourceName",
2075 "requestMappingTemplate",
2076 "responseMappingTemplate",
2077 "syncConfig",
2078 "maxBatchSize",
2079 "runtime",
2080 "code",
2081 ],
2082 );
2083 Value::Object(f)
2084}
2085
2086fn build_type(ctx: &Ctx, api_id: &str, name: &str, definition: &str, format: &str) -> Value {
2087 json!({
2088 "name": name,
2089 "arn": format!("arn:aws:appsync:{}:{}:apis/{api_id}/types/{name}", ctx.region, ctx.account),
2090 "definition": definition,
2091 "format": format,
2092 })
2093}
2094
2095fn build_api_cache(body: &Value) -> Value {
2096 let mut c = Map::new();
2097 c.insert("status".into(), json!("AVAILABLE"));
2098 copy_keys(
2099 &mut c,
2100 body,
2101 &[
2102 "ttl",
2103 "apiCachingBehavior",
2104 "transitEncryptionEnabled",
2105 "atRestEncryptionEnabled",
2106 "type",
2107 "healthMetricsConfig",
2108 ],
2109 );
2110 Value::Object(c)
2111}
2112
2113fn build_domain_name(ctx: &Ctx, domain: &str, body: &Value) -> Value {
2114 let mut c = Map::new();
2115 c.insert("domainName".into(), json!(domain));
2116 c.insert(
2117 "appsyncDomainName".into(),
2118 json!(format!(
2119 "{}.appsync-api.{}.amazonaws.com",
2120 gen_hex(14),
2121 ctx.region
2122 )),
2123 );
2124 c.insert("hostedZoneId".into(), json!("Z2FDTNDATAQYW2"));
2125 c.insert(
2126 "domainNameArn".into(),
2127 json!(format!(
2128 "arn:aws:appsync:{}:{}:domainnames/{domain}",
2129 ctx.region, ctx.account
2130 )),
2131 );
2132 copy_keys(&mut c, body, &["description", "certificateArn", "tags"]);
2133 Value::Object(c)
2134}
2135
2136fn build_event_api(ctx: &Ctx, api_id: &str, body: &Value) -> Value {
2137 let mut api = Map::new();
2138 api.insert("apiId".into(), json!(api_id));
2139 api.insert(
2140 "apiArn".into(),
2141 json!(format!(
2142 "arn:aws:appsync:{}:{}:apis/{api_id}",
2143 ctx.region, ctx.account
2144 )),
2145 );
2146 api.insert("created".into(), json!(now_epoch()));
2147 api.insert("xrayEnabled".into(), json!(false));
2148 api.insert(
2149 "dns".into(),
2150 json!({
2151 "REALTIME": format!("{api_id}.ddpg.appsync-api.{}.amazonaws.com", ctx.region),
2152 "HTTP": format!("{api_id}.appsync-api.{}.amazonaws.com", ctx.region),
2153 }),
2154 );
2155 copy_keys(
2156 &mut api,
2157 body,
2158 &["name", "ownerContact", "tags", "eventConfig"],
2159 );
2160 Value::Object(api)
2161}
2162
2163fn build_channel_namespace(ctx: &Ctx, api_id: &str, name: &str, body: &Value) -> Value {
2164 let mut ns = Map::new();
2165 ns.insert("apiId".into(), json!(api_id));
2166 ns.insert("name".into(), json!(name));
2167 ns.insert(
2168 "channelNamespaceArn".into(),
2169 json!(format!(
2170 "arn:aws:appsync:{}:{}:apis/{api_id}/channelNamespace/{name}",
2171 ctx.region, ctx.account
2172 )),
2173 );
2174 ns.insert("created".into(), json!(now_epoch()));
2175 ns.insert("lastModified".into(), json!(now_epoch()));
2176 copy_keys(
2177 &mut ns,
2178 body,
2179 &[
2180 "subscribeAuthModes",
2181 "publishAuthModes",
2182 "codeHandlers",
2183 "tags",
2184 "handlerConfigs",
2185 ],
2186 );
2187 Value::Object(ns)
2188}
2189
2190fn ok(v: Value) -> AwsResponse {
2193 AwsResponse::json_value(StatusCode::OK, v)
2194}
2195
2196fn parse_body(req: &AwsRequest) -> Result<Value, AwsServiceError> {
2197 if req.body.is_empty() {
2198 return Ok(json!({}));
2199 }
2200 serde_json::from_slice(&req.body).map_err(|e| bad(&format!("Request body is malformed: {e}")))
2201}
2202
2203fn bad(msg: &str) -> AwsServiceError {
2204 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "BadRequestException", msg)
2205}
2206
2207fn not_found(msg: &str) -> AwsServiceError {
2208 AwsServiceError::aws_error(StatusCode::NOT_FOUND, "NotFoundException", msg)
2209}
2210
2211fn require_label(value: &str, field: &str) -> Result<(), AwsServiceError> {
2214 if value.is_empty() {
2215 Err(bad(&format!("{field} is required.")))
2216 } else {
2217 Ok(())
2218 }
2219}
2220
2221fn require_resource_arn(arn: &str) -> Result<(), AwsServiceError> {
2227 require_label(arn, "resourceArn")?;
2228 let len = arn.chars().count();
2229 if !arn.starts_with("arn:") || arn.contains('{') || !(1..=1011).contains(&len) {
2230 return Err(bad(&format!("resourceArn '{arn}' is not a valid ARN.")));
2231 }
2232 Ok(())
2233}
2234
2235fn require_body_str<'a>(body: &'a Value, field: &str) -> Result<&'a str, AwsServiceError> {
2236 body.get(field)
2237 .and_then(Value::as_str)
2238 .filter(|s| !s.is_empty())
2239 .ok_or_else(|| bad(&format!("{field} is required.")))
2240}
2241
2242fn require_query<'a>(q: &'a [(String, String)], key: &str) -> Result<&'a str, AwsServiceError> {
2243 q.iter()
2244 .find(|(k, _)| k == key)
2245 .map(|(_, v)| v.as_str())
2246 .filter(|v| !v.is_empty())
2247 .ok_or_else(|| bad(&format!("{key} query parameter is required.")))
2248}
2249
2250fn parse_query(raw: &str) -> Vec<(String, String)> {
2251 raw.split('&')
2252 .filter(|p| !p.is_empty())
2253 .map(|pair| {
2254 let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
2255 (
2256 percent_decode_str(k).decode_utf8_lossy().into_owned(),
2257 percent_decode_str(v).decode_utf8_lossy().into_owned(),
2258 )
2259 })
2260 .collect()
2261}
2262
2263fn copy_keys(dst: &mut Map<String, Value>, src: &Value, keys: &[&str]) {
2265 for k in keys {
2266 if let Some(v) = src.get(*k) {
2267 if !v.is_null() {
2268 dst.insert((*k).to_string(), v.clone());
2269 }
2270 }
2271 }
2272}
2273
2274fn string_map(obj: &Map<String, Value>) -> std::collections::BTreeMap<String, String> {
2275 obj.iter()
2276 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
2277 .collect()
2278}
2279
2280fn graphql_api_arn(region: &str, account: &str, api_id: &str) -> String {
2281 format!("arn:aws:appsync:{region}:{account}:apis/{api_id}")
2282}
2283
2284fn api_ref_exists(data: Option<&AppSyncData>, ident: &str) -> bool {
2288 let Some(data) = data else {
2289 return false;
2290 };
2291 let id = ident
2292 .rsplit_once("apis/")
2293 .map(|(_, tail)| tail.split('/').next().unwrap_or(tail))
2294 .unwrap_or(ident);
2295 data.graphql_apis.contains_key(id)
2296}
2297
2298fn resolver_key(type_name: &str, field: &str) -> String {
2299 format!("{type_name}::{field}")
2300}
2301
2302fn parse_type_name(definition: &str) -> Option<String> {
2304 for line in definition.lines() {
2305 let t = line.trim();
2306 for kw in [
2307 "type ",
2308 "input ",
2309 "enum ",
2310 "interface ",
2311 "union ",
2312 "scalar ",
2313 ] {
2314 if let Some(rest) = t.strip_prefix(kw) {
2315 if let Some(name) = rest
2316 .split(|c: char| c.is_whitespace() || c == '{' || c == '(' || c == '=')
2317 .next()
2318 {
2319 if !name.is_empty() {
2320 return Some(name.to_string());
2321 }
2322 }
2323 }
2324 }
2325 }
2326 None
2327}
2328
2329fn decode_blob(b64: &str) -> String {
2332 match base64::engine::general_purpose::STANDARD.decode(b64) {
2333 Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
2334 Err(_) => b64.to_string(),
2335 }
2336}
2337
2338fn schema_status_wire(status: &str) -> &str {
2340 match status {
2341 "Processing" => "PROCESSING",
2342 "Success" => "SUCCESS",
2343 "Active" => "ACTIVE",
2344 "Failed" => "FAILED",
2345 "Deleting" => "DELETING",
2346 _ => "NOT_APPLICABLE",
2347 }
2348}
2349
2350fn gen_api_id() -> String {
2351 const CH: &[u8] = b"abcdefghijklmnopqrstuvwxyz234567";
2352 let mut r = rand::thread_rng();
2353 (0..26)
2354 .map(|_| CH[r.gen_range(0..CH.len())] as char)
2355 .collect()
2356}
2357
2358fn gen_hex(n: usize) -> String {
2359 Uuid::new_v4().simple().to_string()[..n.min(32)].to_string()
2360}
2361
2362#[cfg(test)]
2363mod tests {
2364 use super::*;
2365
2366 fn svc() -> AppSyncService {
2367 AppSyncService::new(Arc::new(parking_lot::RwLock::new(
2368 fakecloud_core::multi_account::MultiAccountState::new("000000000000", "us-east-1", ""),
2369 )))
2370 }
2371
2372 fn ctx() -> Ctx {
2373 Ctx {
2374 account: "000000000000".to_string(),
2375 region: "us-east-1".to_string(),
2376 }
2377 }
2378
2379 fn body_json(resp: &AwsResponse) -> Value {
2380 serde_json::from_slice(resp.body.expect_bytes()).expect("json response body")
2381 }
2382
2383 fn err_of(r: Result<AwsResponse, AwsServiceError>) -> AwsServiceError {
2384 r.err().expect("expected error")
2385 }
2386
2387 fn make_api(s: &AppSyncService) -> String {
2388 let created = body_json(
2389 &s.create_graphql_api(
2390 &ctx(),
2391 &json!({ "name": "demo", "authenticationType": "API_KEY" }),
2392 )
2393 .unwrap(),
2394 );
2395 created["graphqlApi"]["apiId"].as_str().unwrap().to_string()
2396 }
2397
2398 #[test]
2399 fn create_get_list_delete_graphql_api() {
2400 let s = svc();
2401 let api_id = make_api(&s);
2402 let got = body_json(&s.get_graphql_api(&ctx(), &api_id).unwrap());
2403 assert_eq!(got["graphqlApi"]["name"], json!("demo"));
2404 assert!(got["graphqlApi"]["uris"]["GRAPHQL"].is_string());
2405 let list = body_json(&s.list_graphql_apis(&ctx()).unwrap());
2406 assert_eq!(list["graphqlApis"].as_array().unwrap().len(), 1);
2407 s.delete_graphql_api(&ctx(), &api_id).unwrap();
2408 let err = err_of(s.get_graphql_api(&ctx(), &api_id));
2409 assert_eq!(err.status(), StatusCode::NOT_FOUND);
2410 }
2411
2412 #[test]
2413 fn missing_api_is_not_found() {
2414 let s = svc();
2415 let err = err_of(s.get_graphql_api(&ctx(), "nope"));
2416 assert_eq!(err.status(), StatusCode::NOT_FOUND);
2417 }
2418
2419 #[test]
2420 fn api_key_lifecycle() {
2421 let s = svc();
2422 let api_id = make_api(&s);
2423 let key = body_json(&s.create_api_key(&ctx(), &api_id, &json!({})).unwrap());
2424 let id = key["apiKey"]["id"].as_str().unwrap().to_string();
2425 assert!(key["apiKey"]["expires"].is_i64());
2426 let list = body_json(&s.list_api_keys(&ctx(), &api_id).unwrap());
2427 assert_eq!(list["apiKeys"].as_array().unwrap().len(), 1);
2428 s.delete_api_key(&ctx(), &api_id, &id).unwrap();
2429 }
2430
2431 #[test]
2432 fn data_source_and_resolver_roundtrip() {
2433 let s = svc();
2434 let api_id = make_api(&s);
2435 let ds = body_json(
2436 &s.create_data_source(&ctx(), &api_id, &json!({ "name": "src", "type": "NONE" }))
2437 .unwrap(),
2438 );
2439 assert_eq!(ds["dataSource"]["type"], json!("NONE"));
2440 let r = body_json(
2441 &s.create_resolver(
2442 &ctx(),
2443 &api_id,
2444 "Query",
2445 &json!({ "fieldName": "hello", "dataSourceName": "src" }),
2446 )
2447 .unwrap(),
2448 );
2449 assert_eq!(r["resolver"]["fieldName"], json!("hello"));
2450 let got = body_json(&s.get_resolver(&ctx(), &api_id, "Query", "hello").unwrap());
2451 assert_eq!(got["resolver"]["typeName"], json!("Query"));
2452 let listed = body_json(&s.list_resolvers(&ctx(), &api_id, "Query").unwrap());
2453 assert_eq!(listed["resolvers"].as_array().unwrap().len(), 1);
2454 }
2455
2456 #[test]
2457 fn resolver_on_missing_api_is_not_found() {
2458 let s = svc();
2459 let err =
2460 err_of(s.create_resolver(&ctx(), "ghost", "Query", &json!({ "fieldName": "hello" })));
2461 assert_eq!(err.status(), StatusCode::NOT_FOUND);
2462 }
2463
2464 #[test]
2465 fn schema_creation_settles_success() {
2466 let s = svc();
2467 let api_id = make_api(&s);
2468 let sdl = base64::engine::general_purpose::STANDARD.encode("type Query { hello: String }");
2469 s.start_schema_creation(&ctx(), &api_id, &json!({ "definition": sdl }))
2470 .unwrap();
2471 let status = body_json(&s.get_schema_creation_status(&ctx(), &api_id).unwrap());
2472 assert_eq!(status["status"], json!("SUCCESS"));
2473 let schema = body_json(
2474 &s.get_introspection_schema(
2475 &ctx(),
2476 &api_id,
2477 &[("format".to_string(), "SDL".to_string())],
2478 )
2479 .unwrap(),
2480 );
2481 assert!(schema["schema"].is_string());
2482 }
2483
2484 #[test]
2485 fn function_lifecycle() {
2486 let s = svc();
2487 let api_id = make_api(&s);
2488 let f = body_json(
2489 &s.create_function(
2490 &ctx(),
2491 &api_id,
2492 &json!({ "name": "fn1", "dataSourceName": "src" }),
2493 )
2494 .unwrap(),
2495 );
2496 let fid = f["functionConfiguration"]["functionId"]
2497 .as_str()
2498 .unwrap()
2499 .to_string();
2500 let got = body_json(&s.get_function(&ctx(), &api_id, &fid).unwrap());
2501 assert_eq!(got["functionConfiguration"]["name"], json!("fn1"));
2502 }
2503
2504 #[test]
2505 fn tagging_roundtrip() {
2506 let s = svc();
2507 let arn = "arn:aws:appsync:us-east-1:000000000000:apis/abc";
2508 s.tag_resource(&ctx(), arn, &json!({ "tags": { "team": "obs" } }))
2509 .unwrap();
2510 let listed = body_json(&s.list_tags_for_resource(&ctx(), arn).unwrap());
2511 assert_eq!(listed["tags"]["team"], json!("obs"));
2512 }
2513
2514 #[test]
2515 fn evaluate_template_renders() {
2516 let out = body_json(
2517 &AppSyncService::evaluate_template(&json!({
2518 "template": "$util.toJson($ctx.arguments)",
2519 "context": "{\"arguments\":{\"id\":\"1\"}}"
2520 }))
2521 .unwrap(),
2522 );
2523 assert!(out["evaluationResult"].is_string());
2524 }
2525
2526 fn req(method: Method, path: &str) -> AwsRequest {
2527 AwsRequest {
2528 service: "appsync".to_string(),
2529 action: String::new(),
2530 region: "us-east-1".to_string(),
2531 account_id: "000000000000".to_string(),
2532 request_id: "rid".to_string(),
2533 headers: http::HeaderMap::new(),
2534 query_params: std::collections::HashMap::new(),
2535 body: bytes::Bytes::new(),
2536 body_stream: parking_lot::Mutex::new(None),
2537 path_segments: vec![],
2538 raw_path: path.to_string(),
2539 raw_query: String::new(),
2540 method,
2541 is_query_protocol: false,
2542 access_key_id: None,
2543 principal: None,
2544 }
2545 }
2546
2547 #[test]
2548 fn routing_covers_key_ops() {
2549 assert_eq!(
2550 AppSyncService::resolve_action(&req(Method::POST, "/v1/apis"))
2551 .unwrap()
2552 .0,
2553 "CreateGraphqlApi"
2554 );
2555 assert_eq!(
2556 AppSyncService::resolve_action(&req(Method::GET, "/v1/apis/abc"))
2557 .unwrap()
2558 .0,
2559 "GetGraphqlApi"
2560 );
2561 assert_eq!(
2562 AppSyncService::resolve_action(&req(
2563 Method::GET,
2564 "/v1/apis/abc/types/Query/resolvers/hello"
2565 ))
2566 .unwrap()
2567 .0,
2568 "GetResolver"
2569 );
2570 assert_eq!(
2571 AppSyncService::resolve_action(&req(Method::POST, "/v1/apis/abc/ApiCaches/update"))
2572 .unwrap()
2573 .0,
2574 "UpdateApiCache"
2575 );
2576 assert_eq!(
2577 AppSyncService::resolve_action(&req(Method::POST, "/v2/apis"))
2578 .unwrap()
2579 .0,
2580 "CreateApi"
2581 );
2582 assert!(AppSyncService::resolve_action(&req(Method::GET, "/v1/nope")).is_none());
2583 }
2584}