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(&ctx, &body),
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 data = guard
1802 .get(&ctx.account)
1803 .filter(|d| d.source_api_associations.contains_key(assoc_id))
1804 .ok_or_else(|| not_found(&format!("Association {assoc_id} not found.")))?;
1805 let items: Vec<Value> = data
1808 .association_types
1809 .get(assoc_id)
1810 .map(|m| m.values().cloned().collect())
1811 .unwrap_or_default();
1812 Ok(ok(json!({ "types": items })))
1813 }
1814
1815 fn update_source_api_association(
1816 &self,
1817 ctx: &Ctx,
1818 assoc_id: &str,
1819 body: &Value,
1820 ) -> Result<AwsResponse, AwsServiceError> {
1821 require_label(assoc_id, "associationId")?;
1822 let mut guard = self.state.write();
1823 let data = guard.get_or_create(&ctx.account);
1824 let assoc = data
1825 .source_api_associations
1826 .get_mut(assoc_id)
1827 .ok_or_else(|| not_found(&format!("Association {assoc_id} not found.")))?;
1828 if let Some(obj) = assoc.as_object_mut() {
1829 for k in ["description", "sourceApiAssociationConfig"] {
1830 if let Some(v) = body.get(k) {
1831 obj.insert(k.to_string(), v.clone());
1832 }
1833 }
1834 }
1835 Ok(ok(json!({ "sourceApiAssociation": assoc })))
1836 }
1837
1838 fn disassociate_source_api(
1839 &self,
1840 ctx: &Ctx,
1841 assoc_id: &str,
1842 ) -> Result<AwsResponse, AwsServiceError> {
1843 require_label(assoc_id, "associationId")?;
1844 let mut guard = self.state.write();
1845 let data = guard.get_or_create(&ctx.account);
1846 match data.source_api_associations.remove(assoc_id) {
1847 Some(_) => Ok(ok(
1848 json!({ "sourceApiAssociationStatus": "DELETION_IN_PROGRESS" }),
1849 )),
1850 None => Err(not_found(&format!("Association {assoc_id} not found."))),
1851 }
1852 }
1853
1854 fn start_schema_merge(
1855 &self,
1856 ctx: &Ctx,
1857 assoc_id: &str,
1858 ) -> Result<AwsResponse, AwsServiceError> {
1859 require_label(assoc_id, "associationId")?;
1860 let mut guard = self.state.write();
1861 let data = guard.get_or_create(&ctx.account);
1862 let (source_id, merged_id) = {
1864 let assoc = data
1865 .source_api_associations
1866 .get(assoc_id)
1867 .ok_or_else(|| not_found(&format!("Association {assoc_id} not found.")))?;
1868 let source_id = assoc
1869 .get("sourceApiId")
1870 .and_then(Value::as_str)
1871 .unwrap_or_default()
1872 .to_string();
1873 let merged_id = assoc
1874 .get("mergedApiId")
1875 .and_then(Value::as_str)
1876 .unwrap_or_default()
1877 .to_string();
1878 (source_id, merged_id)
1879 };
1880 let source_types: Vec<(String, Value)> = data
1884 .types
1885 .get(&source_id)
1886 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
1887 .unwrap_or_default();
1888 let merged_types = data
1889 .association_types
1890 .entry(assoc_id.to_string())
1891 .or_default();
1892 for (name, ty) in &source_types {
1893 merged_types.insert(name.clone(), ty.clone());
1894 }
1895 if !merged_id.is_empty() {
1896 let dest = data.types.entry(merged_id).or_default();
1897 for (name, ty) in &source_types {
1898 dest.insert(name.clone(), ty.clone());
1899 }
1900 }
1901 if let Some(assoc) = data
1903 .source_api_associations
1904 .get_mut(assoc_id)
1905 .and_then(Value::as_object_mut)
1906 {
1907 assoc.insert("sourceApiAssociationStatus".into(), json!("MERGE_SUCCESS"));
1908 }
1909 Ok(ok(json!({ "sourceApiAssociationStatus": "MERGE_SUCCESS" })))
1910 }
1911
1912 fn start_ds_introspection(
1915 &self,
1916 ctx: &Ctx,
1917 body: &Value,
1918 ) -> Result<AwsResponse, AwsServiceError> {
1919 let id = gen_hex(20);
1920 let rds_config = body.get("rdsDataApiConfig").cloned().unwrap_or(Value::Null);
1926 let record = json!({
1927 "introspectionId": id,
1928 "introspectionStatus": "SUCCESS",
1929 "introspectionStatusDetail": "Introspection completed.",
1930 "introspectionResult": {
1931 "models": [],
1932 },
1933 "rdsDataApiConfig": rds_config,
1935 });
1936 let mut guard = self.state.write();
1937 let data = guard.get_or_create(&ctx.account);
1938 data.introspections.insert(id.clone(), record);
1939 Ok(ok(json!({
1940 "introspectionId": id,
1941 "introspectionStatus": "SUCCESS",
1942 "introspectionStatusDetail": "Introspection completed.",
1943 })))
1944 }
1945
1946 fn get_ds_introspection(
1947 &self,
1948 ctx: &Ctx,
1949 introspection_id: &str,
1950 ) -> Result<AwsResponse, AwsServiceError> {
1951 require_label(introspection_id, "introspectionId")?;
1952 let guard = self.state.read();
1953 let record = guard
1954 .get(&ctx.account)
1955 .and_then(|d| d.introspections.get(introspection_id))
1956 .ok_or_else(|| not_found(&format!("Introspection {introspection_id} not found.")))?;
1957 Ok(ok(json!({
1958 "introspectionId": record.get("introspectionId").cloned().unwrap_or(Value::Null),
1959 "introspectionStatus": record
1960 .get("introspectionStatus")
1961 .cloned()
1962 .unwrap_or(Value::Null),
1963 "introspectionStatusDetail": record
1964 .get("introspectionStatusDetail")
1965 .cloned()
1966 .unwrap_or(Value::Null),
1967 "introspectionResult": record
1968 .get("introspectionResult")
1969 .cloned()
1970 .unwrap_or(json!({ "models": [] })),
1971 })))
1972 }
1973
1974 fn tag_resource(
1977 &self,
1978 ctx: &Ctx,
1979 arn: &str,
1980 body: &Value,
1981 ) -> Result<AwsResponse, AwsServiceError> {
1982 require_resource_arn(arn)?;
1983 let mut guard = self.state.write();
1984 let data = guard.get_or_create(&ctx.account);
1985 let entry = data.tags.entry(arn.to_string()).or_default();
1986 if let Some(tags) = body.get("tags").and_then(Value::as_object) {
1987 for (k, v) in tags {
1988 if let Some(s) = v.as_str() {
1989 entry.insert(k.clone(), s.to_string());
1990 }
1991 }
1992 }
1993 Ok(ok(json!({})))
1994 }
1995
1996 fn untag_resource(
1997 &self,
1998 ctx: &Ctx,
1999 arn: &str,
2000 q: &[(String, String)],
2001 ) -> Result<AwsResponse, AwsServiceError> {
2002 require_resource_arn(arn)?;
2003 let mut guard = self.state.write();
2004 let data = guard.get_or_create(&ctx.account);
2005 if let Some(entry) = data.tags.get_mut(arn) {
2006 for (_, v) in q.iter().filter(|(k, _)| k == "tagKeys") {
2007 entry.remove(v);
2008 }
2009 }
2010 Ok(ok(json!({})))
2011 }
2012
2013 fn list_tags_for_resource(&self, ctx: &Ctx, arn: &str) -> Result<AwsResponse, AwsServiceError> {
2014 require_resource_arn(arn)?;
2015 let guard = self.state.read();
2016 let tags = guard
2017 .get(&ctx.account)
2018 .and_then(|d| d.tags.get(arn))
2019 .cloned()
2020 .unwrap_or_default();
2021 Ok(ok(json!({ "tags": tags })))
2022 }
2023
2024 fn evaluate_code(body: &Value) -> Result<AwsResponse, AwsServiceError> {
2027 let code = body.get("code").and_then(Value::as_str).unwrap_or_default();
2028 let context = body
2029 .get("context")
2030 .and_then(Value::as_str)
2031 .unwrap_or_default();
2032 let ev = crate::evaluate::evaluate(code, context);
2033 Ok(ok(build_evaluation(ev)))
2034 }
2035
2036 fn evaluate_template(body: &Value) -> Result<AwsResponse, AwsServiceError> {
2037 let template = body
2038 .get("template")
2039 .and_then(Value::as_str)
2040 .unwrap_or_default();
2041 let context = body
2042 .get("context")
2043 .and_then(Value::as_str)
2044 .unwrap_or_default();
2045 let ev = crate::evaluate::evaluate(template, context);
2046 Ok(ok(build_evaluation(ev)))
2047 }
2048}
2049
2050fn build_evaluation(ev: crate::evaluate::Evaluation) -> Value {
2053 let mut out = Map::new();
2054 if let Some(r) = ev.result {
2055 out.insert("evaluationResult".into(), json!(r));
2056 }
2057 if let Some(s) = ev.stash {
2058 out.insert("stash".into(), json!(s));
2059 }
2060 if let Some(e) = ev.error {
2061 out.insert("error".into(), json!({ "message": e }));
2062 }
2063 out.insert("logs".into(), json!([]));
2064 Value::Object(out)
2065}
2066
2067fn build_data_source(ctx: &Ctx, api_id: &str, name: &str, body: &Value) -> Value {
2068 let mut ds = Map::new();
2069 ds.insert(
2070 "dataSourceArn".into(),
2071 json!(format!(
2072 "arn:aws:appsync:{}:{}:apis/{api_id}/datasources/{name}",
2073 ctx.region, ctx.account
2074 )),
2075 );
2076 ds.insert("name".into(), json!(name));
2077 copy_keys(
2078 &mut ds,
2079 body,
2080 &[
2081 "description",
2082 "type",
2083 "serviceRoleArn",
2084 "dynamodbConfig",
2085 "lambdaConfig",
2086 "elasticsearchConfig",
2087 "openSearchServiceConfig",
2088 "httpConfig",
2089 "relationalDatabaseConfig",
2090 "eventBridgeConfig",
2091 "metricsConfig",
2092 ],
2093 );
2094 Value::Object(ds)
2095}
2096
2097fn build_resolver(ctx: &Ctx, api_id: &str, type_name: &str, field: &str, body: &Value) -> Value {
2098 let mut r = Map::new();
2099 r.insert("typeName".into(), json!(type_name));
2100 r.insert("fieldName".into(), json!(field));
2101 r.insert(
2102 "resolverArn".into(),
2103 json!(format!(
2104 "arn:aws:appsync:{}:{}:apis/{api_id}/types/{type_name}/resolvers/{field}",
2105 ctx.region, ctx.account
2106 )),
2107 );
2108 r.insert(
2109 "kind".into(),
2110 json!(body.get("kind").and_then(Value::as_str).unwrap_or("UNIT")),
2111 );
2112 copy_keys(
2113 &mut r,
2114 body,
2115 &[
2116 "dataSourceName",
2117 "requestMappingTemplate",
2118 "responseMappingTemplate",
2119 "pipelineConfig",
2120 "syncConfig",
2121 "cachingConfig",
2122 "maxBatchSize",
2123 "runtime",
2124 "code",
2125 "metricsConfig",
2126 ],
2127 );
2128 Value::Object(r)
2129}
2130
2131fn build_function(ctx: &Ctx, api_id: &str, function_id: &str, body: &Value) -> Value {
2132 let mut f = Map::new();
2133 f.insert("functionId".into(), json!(function_id));
2134 f.insert(
2135 "functionArn".into(),
2136 json!(format!(
2137 "arn:aws:appsync:{}:{}:apis/{api_id}/functions/{function_id}",
2138 ctx.region, ctx.account
2139 )),
2140 );
2141 f.insert(
2142 "functionVersion".into(),
2143 json!(body
2144 .get("functionVersion")
2145 .and_then(Value::as_str)
2146 .unwrap_or("2018-05-29")),
2147 );
2148 copy_keys(
2149 &mut f,
2150 body,
2151 &[
2152 "name",
2153 "description",
2154 "dataSourceName",
2155 "requestMappingTemplate",
2156 "responseMappingTemplate",
2157 "syncConfig",
2158 "maxBatchSize",
2159 "runtime",
2160 "code",
2161 ],
2162 );
2163 Value::Object(f)
2164}
2165
2166fn build_type(ctx: &Ctx, api_id: &str, name: &str, definition: &str, format: &str) -> Value {
2167 json!({
2168 "name": name,
2169 "arn": format!("arn:aws:appsync:{}:{}:apis/{api_id}/types/{name}", ctx.region, ctx.account),
2170 "definition": definition,
2171 "format": format,
2172 })
2173}
2174
2175fn build_api_cache(body: &Value) -> Value {
2176 let mut c = Map::new();
2177 c.insert("status".into(), json!("AVAILABLE"));
2178 copy_keys(
2179 &mut c,
2180 body,
2181 &[
2182 "ttl",
2183 "apiCachingBehavior",
2184 "transitEncryptionEnabled",
2185 "atRestEncryptionEnabled",
2186 "type",
2187 "healthMetricsConfig",
2188 ],
2189 );
2190 Value::Object(c)
2191}
2192
2193fn build_domain_name(ctx: &Ctx, domain: &str, body: &Value) -> Value {
2194 let mut c = Map::new();
2195 c.insert("domainName".into(), json!(domain));
2196 c.insert(
2197 "appsyncDomainName".into(),
2198 json!(format!(
2199 "{}.appsync-api.{}.amazonaws.com",
2200 gen_hex(14),
2201 ctx.region
2202 )),
2203 );
2204 c.insert("hostedZoneId".into(), json!("Z2FDTNDATAQYW2"));
2205 c.insert(
2206 "domainNameArn".into(),
2207 json!(format!(
2208 "arn:aws:appsync:{}:{}:domainnames/{domain}",
2209 ctx.region, ctx.account
2210 )),
2211 );
2212 copy_keys(&mut c, body, &["description", "certificateArn", "tags"]);
2213 Value::Object(c)
2214}
2215
2216fn build_event_api(ctx: &Ctx, api_id: &str, body: &Value) -> Value {
2217 let mut api = Map::new();
2218 api.insert("apiId".into(), json!(api_id));
2219 api.insert(
2220 "apiArn".into(),
2221 json!(format!(
2222 "arn:aws:appsync:{}:{}:apis/{api_id}",
2223 ctx.region, ctx.account
2224 )),
2225 );
2226 api.insert("created".into(), json!(now_epoch()));
2227 api.insert("xrayEnabled".into(), json!(false));
2228 api.insert(
2229 "dns".into(),
2230 json!({
2231 "REALTIME": format!("{api_id}.ddpg.appsync-api.{}.amazonaws.com", ctx.region),
2232 "HTTP": format!("{api_id}.appsync-api.{}.amazonaws.com", ctx.region),
2233 }),
2234 );
2235 copy_keys(
2236 &mut api,
2237 body,
2238 &["name", "ownerContact", "tags", "eventConfig"],
2239 );
2240 Value::Object(api)
2241}
2242
2243fn build_channel_namespace(ctx: &Ctx, api_id: &str, name: &str, body: &Value) -> Value {
2244 let mut ns = Map::new();
2245 ns.insert("apiId".into(), json!(api_id));
2246 ns.insert("name".into(), json!(name));
2247 ns.insert(
2248 "channelNamespaceArn".into(),
2249 json!(format!(
2250 "arn:aws:appsync:{}:{}:apis/{api_id}/channelNamespace/{name}",
2251 ctx.region, ctx.account
2252 )),
2253 );
2254 ns.insert("created".into(), json!(now_epoch()));
2255 ns.insert("lastModified".into(), json!(now_epoch()));
2256 copy_keys(
2257 &mut ns,
2258 body,
2259 &[
2260 "subscribeAuthModes",
2261 "publishAuthModes",
2262 "codeHandlers",
2263 "tags",
2264 "handlerConfigs",
2265 ],
2266 );
2267 Value::Object(ns)
2268}
2269
2270fn ok(v: Value) -> AwsResponse {
2273 AwsResponse::json_value(StatusCode::OK, v)
2274}
2275
2276fn parse_body(req: &AwsRequest) -> Result<Value, AwsServiceError> {
2277 if req.body.is_empty() {
2278 return Ok(json!({}));
2279 }
2280 serde_json::from_slice(&req.body).map_err(|e| bad(&format!("Request body is malformed: {e}")))
2281}
2282
2283fn bad(msg: &str) -> AwsServiceError {
2284 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "BadRequestException", msg)
2285}
2286
2287fn not_found(msg: &str) -> AwsServiceError {
2288 AwsServiceError::aws_error(StatusCode::NOT_FOUND, "NotFoundException", msg)
2289}
2290
2291fn require_label(value: &str, field: &str) -> Result<(), AwsServiceError> {
2294 if value.is_empty() {
2295 Err(bad(&format!("{field} is required.")))
2296 } else {
2297 Ok(())
2298 }
2299}
2300
2301fn require_resource_arn(arn: &str) -> Result<(), AwsServiceError> {
2307 require_label(arn, "resourceArn")?;
2308 let len = arn.chars().count();
2309 if !arn.starts_with("arn:") || arn.contains('{') || !(1..=1011).contains(&len) {
2310 return Err(bad(&format!("resourceArn '{arn}' is not a valid ARN.")));
2311 }
2312 Ok(())
2313}
2314
2315fn require_body_str<'a>(body: &'a Value, field: &str) -> Result<&'a str, AwsServiceError> {
2316 body.get(field)
2317 .and_then(Value::as_str)
2318 .filter(|s| !s.is_empty())
2319 .ok_or_else(|| bad(&format!("{field} is required.")))
2320}
2321
2322fn require_query<'a>(q: &'a [(String, String)], key: &str) -> Result<&'a str, AwsServiceError> {
2323 q.iter()
2324 .find(|(k, _)| k == key)
2325 .map(|(_, v)| v.as_str())
2326 .filter(|v| !v.is_empty())
2327 .ok_or_else(|| bad(&format!("{key} query parameter is required.")))
2328}
2329
2330fn parse_query(raw: &str) -> Vec<(String, String)> {
2331 raw.split('&')
2332 .filter(|p| !p.is_empty())
2333 .map(|pair| {
2334 let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
2335 (
2336 percent_decode_str(k).decode_utf8_lossy().into_owned(),
2337 percent_decode_str(v).decode_utf8_lossy().into_owned(),
2338 )
2339 })
2340 .collect()
2341}
2342
2343fn copy_keys(dst: &mut Map<String, Value>, src: &Value, keys: &[&str]) {
2345 for k in keys {
2346 if let Some(v) = src.get(*k) {
2347 if !v.is_null() {
2348 dst.insert((*k).to_string(), v.clone());
2349 }
2350 }
2351 }
2352}
2353
2354fn string_map(obj: &Map<String, Value>) -> std::collections::BTreeMap<String, String> {
2355 obj.iter()
2356 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
2357 .collect()
2358}
2359
2360fn graphql_api_arn(region: &str, account: &str, api_id: &str) -> String {
2361 format!("arn:aws:appsync:{region}:{account}:apis/{api_id}")
2362}
2363
2364fn api_ref_exists(data: Option<&AppSyncData>, ident: &str) -> bool {
2368 let Some(data) = data else {
2369 return false;
2370 };
2371 let id = ident
2372 .rsplit_once("apis/")
2373 .map(|(_, tail)| tail.split('/').next().unwrap_or(tail))
2374 .unwrap_or(ident);
2375 data.graphql_apis.contains_key(id)
2376}
2377
2378fn resolver_key(type_name: &str, field: &str) -> String {
2379 format!("{type_name}::{field}")
2380}
2381
2382fn parse_type_name(definition: &str) -> Option<String> {
2384 for line in definition.lines() {
2385 let t = line.trim();
2386 for kw in [
2387 "type ",
2388 "input ",
2389 "enum ",
2390 "interface ",
2391 "union ",
2392 "scalar ",
2393 ] {
2394 if let Some(rest) = t.strip_prefix(kw) {
2395 if let Some(name) = rest
2396 .split(|c: char| c.is_whitespace() || c == '{' || c == '(' || c == '=')
2397 .next()
2398 {
2399 if !name.is_empty() {
2400 return Some(name.to_string());
2401 }
2402 }
2403 }
2404 }
2405 }
2406 None
2407}
2408
2409fn decode_blob(b64: &str) -> String {
2412 match base64::engine::general_purpose::STANDARD.decode(b64) {
2413 Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
2414 Err(_) => b64.to_string(),
2415 }
2416}
2417
2418fn schema_status_wire(status: &str) -> &str {
2420 match status {
2421 "Processing" => "PROCESSING",
2422 "Success" => "SUCCESS",
2423 "Active" => "ACTIVE",
2424 "Failed" => "FAILED",
2425 "Deleting" => "DELETING",
2426 _ => "NOT_APPLICABLE",
2427 }
2428}
2429
2430fn gen_api_id() -> String {
2431 const CH: &[u8] = b"abcdefghijklmnopqrstuvwxyz234567";
2432 let mut r = rand::thread_rng();
2433 (0..26)
2434 .map(|_| CH[r.gen_range(0..CH.len())] as char)
2435 .collect()
2436}
2437
2438fn gen_hex(n: usize) -> String {
2439 Uuid::new_v4().simple().to_string()[..n.min(32)].to_string()
2440}
2441
2442#[cfg(test)]
2443mod tests {
2444 use super::*;
2445
2446 fn svc() -> AppSyncService {
2447 AppSyncService::new(Arc::new(parking_lot::RwLock::new(
2448 fakecloud_core::multi_account::MultiAccountState::new("000000000000", "us-east-1", ""),
2449 )))
2450 }
2451
2452 fn ctx() -> Ctx {
2453 Ctx {
2454 account: "000000000000".to_string(),
2455 region: "us-east-1".to_string(),
2456 }
2457 }
2458
2459 fn body_json(resp: &AwsResponse) -> Value {
2460 serde_json::from_slice(resp.body.expect_bytes()).expect("json response body")
2461 }
2462
2463 fn err_of(r: Result<AwsResponse, AwsServiceError>) -> AwsServiceError {
2464 r.err().expect("expected error")
2465 }
2466
2467 fn make_api(s: &AppSyncService) -> String {
2468 let created = body_json(
2469 &s.create_graphql_api(
2470 &ctx(),
2471 &json!({ "name": "demo", "authenticationType": "API_KEY" }),
2472 )
2473 .unwrap(),
2474 );
2475 created["graphqlApi"]["apiId"].as_str().unwrap().to_string()
2476 }
2477
2478 #[test]
2479 fn create_get_list_delete_graphql_api() {
2480 let s = svc();
2481 let api_id = make_api(&s);
2482 let got = body_json(&s.get_graphql_api(&ctx(), &api_id).unwrap());
2483 assert_eq!(got["graphqlApi"]["name"], json!("demo"));
2484 assert!(got["graphqlApi"]["uris"]["GRAPHQL"].is_string());
2485 let list = body_json(&s.list_graphql_apis(&ctx()).unwrap());
2486 assert_eq!(list["graphqlApis"].as_array().unwrap().len(), 1);
2487 s.delete_graphql_api(&ctx(), &api_id).unwrap();
2488 let err = err_of(s.get_graphql_api(&ctx(), &api_id));
2489 assert_eq!(err.status(), StatusCode::NOT_FOUND);
2490 }
2491
2492 #[test]
2493 fn missing_api_is_not_found() {
2494 let s = svc();
2495 let err = err_of(s.get_graphql_api(&ctx(), "nope"));
2496 assert_eq!(err.status(), StatusCode::NOT_FOUND);
2497 }
2498
2499 #[test]
2500 fn api_key_lifecycle() {
2501 let s = svc();
2502 let api_id = make_api(&s);
2503 let key = body_json(&s.create_api_key(&ctx(), &api_id, &json!({})).unwrap());
2504 let id = key["apiKey"]["id"].as_str().unwrap().to_string();
2505 assert!(key["apiKey"]["expires"].is_i64());
2506 let list = body_json(&s.list_api_keys(&ctx(), &api_id).unwrap());
2507 assert_eq!(list["apiKeys"].as_array().unwrap().len(), 1);
2508 s.delete_api_key(&ctx(), &api_id, &id).unwrap();
2509 }
2510
2511 #[test]
2512 fn data_source_and_resolver_roundtrip() {
2513 let s = svc();
2514 let api_id = make_api(&s);
2515 let ds = body_json(
2516 &s.create_data_source(&ctx(), &api_id, &json!({ "name": "src", "type": "NONE" }))
2517 .unwrap(),
2518 );
2519 assert_eq!(ds["dataSource"]["type"], json!("NONE"));
2520 let r = body_json(
2521 &s.create_resolver(
2522 &ctx(),
2523 &api_id,
2524 "Query",
2525 &json!({ "fieldName": "hello", "dataSourceName": "src" }),
2526 )
2527 .unwrap(),
2528 );
2529 assert_eq!(r["resolver"]["fieldName"], json!("hello"));
2530 let got = body_json(&s.get_resolver(&ctx(), &api_id, "Query", "hello").unwrap());
2531 assert_eq!(got["resolver"]["typeName"], json!("Query"));
2532 let listed = body_json(&s.list_resolvers(&ctx(), &api_id, "Query").unwrap());
2533 assert_eq!(listed["resolvers"].as_array().unwrap().len(), 1);
2534 }
2535
2536 #[test]
2537 fn resolver_on_missing_api_is_not_found() {
2538 let s = svc();
2539 let err =
2540 err_of(s.create_resolver(&ctx(), "ghost", "Query", &json!({ "fieldName": "hello" })));
2541 assert_eq!(err.status(), StatusCode::NOT_FOUND);
2542 }
2543
2544 #[test]
2545 fn schema_creation_settles_success() {
2546 let s = svc();
2547 let api_id = make_api(&s);
2548 let sdl = base64::engine::general_purpose::STANDARD.encode("type Query { hello: String }");
2549 s.start_schema_creation(&ctx(), &api_id, &json!({ "definition": sdl }))
2550 .unwrap();
2551 let status = body_json(&s.get_schema_creation_status(&ctx(), &api_id).unwrap());
2552 assert_eq!(status["status"], json!("SUCCESS"));
2553 let schema = body_json(
2554 &s.get_introspection_schema(
2555 &ctx(),
2556 &api_id,
2557 &[("format".to_string(), "SDL".to_string())],
2558 )
2559 .unwrap(),
2560 );
2561 assert!(schema["schema"].is_string());
2562 }
2563
2564 #[test]
2565 fn function_lifecycle() {
2566 let s = svc();
2567 let api_id = make_api(&s);
2568 let f = body_json(
2569 &s.create_function(
2570 &ctx(),
2571 &api_id,
2572 &json!({ "name": "fn1", "dataSourceName": "src" }),
2573 )
2574 .unwrap(),
2575 );
2576 let fid = f["functionConfiguration"]["functionId"]
2577 .as_str()
2578 .unwrap()
2579 .to_string();
2580 let got = body_json(&s.get_function(&ctx(), &api_id, &fid).unwrap());
2581 assert_eq!(got["functionConfiguration"]["name"], json!("fn1"));
2582 }
2583
2584 #[test]
2585 fn tagging_roundtrip() {
2586 let s = svc();
2587 let arn = "arn:aws:appsync:us-east-1:000000000000:apis/abc";
2588 s.tag_resource(&ctx(), arn, &json!({ "tags": { "team": "obs" } }))
2589 .unwrap();
2590 let listed = body_json(&s.list_tags_for_resource(&ctx(), arn).unwrap());
2591 assert_eq!(listed["tags"]["team"], json!("obs"));
2592 }
2593
2594 #[test]
2595 fn data_source_introspection_start_then_get_is_retrievable() {
2596 let s = svc();
2600 let started = body_json(
2601 &s.start_ds_introspection(
2602 &ctx(),
2603 &json!({
2604 "rdsDataApiConfig": {
2605 "resourceArn": "arn:aws:rds:us-east-1:000000000000:cluster:db",
2606 "secretArn": "arn:aws:secretsmanager:us-east-1:000000000000:secret:s",
2607 "databaseName": "app",
2608 }
2609 }),
2610 )
2611 .unwrap(),
2612 );
2613 let id = started["introspectionId"].as_str().unwrap().to_string();
2614 assert_eq!(started["introspectionStatus"], json!("SUCCESS"));
2615
2616 let got = body_json(&s.get_ds_introspection(&ctx(), &id).unwrap());
2617 assert_eq!(got["introspectionId"], json!(id));
2618 assert_eq!(got["introspectionStatus"], json!("SUCCESS"));
2619 assert!(got["introspectionResult"]["models"].is_array());
2620
2621 let err = err_of(s.get_ds_introspection(&ctx(), "does-not-exist"));
2623 assert_eq!(err.status(), StatusCode::NOT_FOUND);
2624 }
2625
2626 #[test]
2627 fn schema_merge_populates_types_by_association() {
2628 let s = svc();
2631 let source_id = make_api(&s);
2632 let merged_id = make_api(&s);
2633 s.create_type(
2635 &ctx(),
2636 &source_id,
2637 &json!({ "definition": "type Widget { id: ID! }", "format": "SDL" }),
2638 )
2639 .unwrap();
2640 let assoc = body_json(
2642 &s.associate_source_api(
2643 &ctx(),
2644 &merged_id,
2645 &json!({ "sourceApiIdentifier": source_id }),
2646 )
2647 .unwrap(),
2648 );
2649 let assoc_id = assoc["sourceApiAssociation"]["associationId"]
2650 .as_str()
2651 .unwrap()
2652 .to_string();
2653 let fmt = [("format".to_string(), "SDL".to_string())];
2654 let empty = body_json(
2655 &s.list_types_by_association(&ctx(), &merged_id, &assoc_id, &fmt)
2656 .unwrap(),
2657 );
2658 assert!(empty["types"].as_array().unwrap().is_empty());
2659
2660 let merged = body_json(&s.start_schema_merge(&ctx(), &assoc_id).unwrap());
2662 assert_eq!(merged["sourceApiAssociationStatus"], json!("MERGE_SUCCESS"));
2663 let listed = body_json(
2664 &s.list_types_by_association(&ctx(), &merged_id, &assoc_id, &fmt)
2665 .unwrap(),
2666 );
2667 let types = listed["types"].as_array().unwrap();
2668 assert_eq!(types.len(), 1);
2669 assert_eq!(types[0]["name"], json!("Widget"));
2670 let on_merged = body_json(&s.list_types(&ctx(), &merged_id, &fmt).unwrap());
2672 assert_eq!(on_merged["types"].as_array().unwrap().len(), 1);
2673 }
2674
2675 #[test]
2676 fn evaluate_template_renders() {
2677 let out = body_json(
2678 &AppSyncService::evaluate_template(&json!({
2679 "template": "$util.toJson($ctx.arguments)",
2680 "context": "{\"arguments\":{\"id\":\"1\"}}"
2681 }))
2682 .unwrap(),
2683 );
2684 assert!(out["evaluationResult"].is_string());
2685 }
2686
2687 fn req(method: Method, path: &str) -> AwsRequest {
2688 AwsRequest {
2689 service: "appsync".to_string(),
2690 action: String::new(),
2691 region: "us-east-1".to_string(),
2692 account_id: "000000000000".to_string(),
2693 request_id: "rid".to_string(),
2694 headers: http::HeaderMap::new(),
2695 query_params: std::collections::HashMap::new(),
2696 body: bytes::Bytes::new(),
2697 body_stream: parking_lot::Mutex::new(None),
2698 path_segments: vec![],
2699 raw_path: path.to_string(),
2700 raw_query: String::new(),
2701 method,
2702 is_query_protocol: false,
2703 access_key_id: None,
2704 principal: None,
2705 }
2706 }
2707
2708 #[test]
2709 fn routing_covers_key_ops() {
2710 assert_eq!(
2711 AppSyncService::resolve_action(&req(Method::POST, "/v1/apis"))
2712 .unwrap()
2713 .0,
2714 "CreateGraphqlApi"
2715 );
2716 assert_eq!(
2717 AppSyncService::resolve_action(&req(Method::GET, "/v1/apis/abc"))
2718 .unwrap()
2719 .0,
2720 "GetGraphqlApi"
2721 );
2722 assert_eq!(
2723 AppSyncService::resolve_action(&req(
2724 Method::GET,
2725 "/v1/apis/abc/types/Query/resolvers/hello"
2726 ))
2727 .unwrap()
2728 .0,
2729 "GetResolver"
2730 );
2731 assert_eq!(
2732 AppSyncService::resolve_action(&req(Method::POST, "/v1/apis/abc/ApiCaches/update"))
2733 .unwrap()
2734 .0,
2735 "UpdateApiCache"
2736 );
2737 assert_eq!(
2738 AppSyncService::resolve_action(&req(Method::POST, "/v2/apis"))
2739 .unwrap()
2740 .0,
2741 "CreateApi"
2742 );
2743 assert!(AppSyncService::resolve_action(&req(Method::GET, "/v1/nope")).is_none());
2744 }
2745}