Skip to main content

fakecloud_appsync/
service.rs

1//! AWS AppSync (`appsync`) restJson1 dispatch + operation handlers.
2//!
3//! Every AppSync operation is a RESTful `<METHOD> /v1|/v2/...` route with path
4//! labels, so requests route on HTTP method + `@http` URI template (segment
5//! matching with captured labels). State is account-partitioned and persisted.
6//! Sub-resource operations validate their parent GraphQL API and return the
7//! declared `NotFoundException` when the parent -- or the addressed resource --
8//! is absent, so synthetic conformance ids land on a declared error and real
9//! round-trips against a created API succeed.
10
11use 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
28/// Every operation name in the AWS AppSync Smithy model (74 operations).
29pub 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
106/// Operations that mutate persisted state on success (so a snapshot is taken).
107/// `GetSchemaCreationStatus` is included because reading it settles the async
108/// schema-creation lifecycle from `Processing` to `Success`.
109const 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    /// Persist hook for the CloudFormation provisioner; `None` in memory mode.
187    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    /// Route a request to an operation name + captured path labels by HTTP
202    /// method + `@http` URI template. Labels are captured in path order.
203    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 Event API surface ----
223            ["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 GraphQL API surface ----
241            ["v1", "apis"] if post => ("CreateGraphqlApi", vec![]),
242            ["v1", "apis"] if get => ("ListGraphqlApis", vec![]),
243
244            // API caches (before the generic {apiId} arms; distinct trailing literal)
245            ["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            // API keys
252            ["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            // Data sources
258            ["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            // Functions
265            ["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            // Types + resolvers (resolvers nest under types)
275            ["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            // Schema
295            ["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            // Environment variables
300            ["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            // Source-API associations listed on a graphql api
308            ["v1", "apis", id, "sourceApiAssociations"] if get => {
309                ("ListSourceApiAssociations", v(&[id]))
310            }
311
312            // Generic graphql api by id (must come after the sub-resource arms)
313            ["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            // Domain names + api associations
318            ["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            // Merged / source API associations
328            ["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            // Data-source introspection
354            ["v1", "datasources", "introspections"] if post => {
355                ("StartDataSourceIntrospection", vec![])
356            }
357            ["v1", "datasources", "introspections", iid] if get => {
358                ("GetDataSourceIntrospection", v(&[iid]))
359            }
360
361            // Evaluation data plane
362            ["v1", "dataplane-evaluatecode"] if post => ("EvaluateCode", vec![]),
363            ["v1", "dataplane-evaluatetemplate"] if post => ("EvaluateMappingTemplate", vec![]),
364
365            // Tags
366            ["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
403/// Per-request account + region context.
404struct 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            // ---- GraphQL APIs ----
427            "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            // ---- API keys ----
434            "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            // ---- Data sources ----
440            "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            // ---- Resolvers ----
447            "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            // ---- Functions ----
455            "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            // ---- Types + schema ----
462            "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            // ---- API caches ----
472            "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            // ---- Environment variables ----
479            "PutGraphqlApiEnvironmentVariables" => self.put_env_vars(&ctx, l(0), &body),
480            "GetGraphqlApiEnvironmentVariables" => self.get_env_vars(&ctx, l(0)),
481
482            // ---- Domain names + api associations ----
483            "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            // ---- Event APIs + channel namespaces ----
493            "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            // ---- Source API associations ----
505            "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            // ---- Data-source introspection ----
516            "StartDataSourceIntrospection" => self.start_ds_introspection(&ctx, &body),
517            "GetDataSourceIntrospection" => self.get_ds_introspection(&ctx, l(0)),
518
519            // ---- Tags ----
520            "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            // ---- Evaluation data plane ----
525            "EvaluateCode" => Self::evaluate_code(&body),
526            "EvaluateMappingTemplate" => Self::evaluate_template(&body),
527
528            _ => Err(AwsServiceError::action_not_implemented("appsync", action)),
529        }
530    }
531
532    // ===================== GraphQL APIs =====================
533
534    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        // Defaults for members AWS always populates.
569        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        // Rebuild preserving apiId/arn/uris identity, applying the update body.
631        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    /// Return `NotFoundException` unless the GraphQL API exists in this account.
655    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    // ===================== API keys =====================
665
666    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    // ===================== Data sources =====================
751
752    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    // ===================== Resolvers =====================
849
850    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    // ===================== Functions =====================
1003
1004    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    // ===================== Types + schema =====================
1101
1102    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        // Settle the async lifecycle: first status read after creation flips
1247        // Processing -> Success, mirroring other async ops.
1248        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    // ===================== API caches =====================
1289
1290    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    // ===================== Environment variables =====================
1360
1361    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    // ===================== Domain names =====================
1391
1392    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    // ===================== Event APIs =====================
1500
1501    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    // ===================== Source API associations =====================
1676
1677    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            // Both the merged and source APIs must exist. Synthetic ids (and an
1708            // unsubstituted `{...}` label) resolve to nothing -> NotFoundException,
1709            // which every association op declares.
1710            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        // Types merged into the merged API by StartSchemaMerge. Empty until a
1806        // merge has run for this association.
1807        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        // Resolve the source + merged APIs from the association before mutating.
1863        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        // Copy the source API's types into both the association's merged-type set
1881        // (returned by ListTypesByAssociation) and the merged API's own type
1882        // store, so the merge is observable rather than a no-op.
1883        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        // Reflect the completed synchronous merge on the association record.
1902        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    // ===================== Data-source introspection =====================
1913
1914    fn start_ds_introspection(
1915        &self,
1916        ctx: &Ctx,
1917        body: &Value,
1918    ) -> Result<AwsResponse, AwsServiceError> {
1919        let id = gen_hex(20);
1920        // The emulator has no live RDS instance to introspect, so the discovered
1921        // schema is an empty-but-well-formed IntrospectionResult. The record is
1922        // persisted so the Start -> Get poll flow retrieves it instead of a
1923        // permanent 404. The RDS config the caller supplied is echoed back on the
1924        // stored record so a reader can see what was requested.
1925        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            // Retained for reads/introspection; not part of the Get output shape.
1934            "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    // ===================== Tags =====================
1975
1976    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    // ===================== Evaluation data plane =====================
2025
2026    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
2050// ===================== builders =====================
2051
2052fn 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
2270// ===================== helpers =====================
2271
2272fn 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
2291/// Reject an empty path label (a too-short probe URI) with `BadRequestException`
2292/// before resource lookup.
2293fn 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
2301/// Validate an AppSync `resourceArn` path label: it must be a well-formed ARN
2302/// (`arn:...:appsync:...`) within the declared length bounds. AppSync's
2303/// `ResourceArn` shape constrains it to `^arn:aws:appsync:...` (length 70..=75);
2304/// rejecting a non-ARN (or an unsubstituted `{resourceArn}` placeholder) with
2305/// `BadRequestException` matches the service's own label validation.
2306fn 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
2343/// Copy each present, non-null key from `src` into `dst`.
2344fn 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
2364/// Whether an API reference (a bare `apiId` or an `...:apis/<id>` ARN) names a
2365/// GraphQL API that exists in this account. Used by the association ops, which
2366/// reference their source/merged APIs by identifier.
2367fn 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
2382/// Extract the top-level type name declared by an SDL `type X { ... }` snippet.
2383fn 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
2409/// Decode a base64 restJson1 blob to a UTF-8 string, tolerating raw (already
2410/// decoded) input for robustness.
2411fn 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
2418/// Map internal schema status to the `SchemaStatus` wire enum.
2419fn 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        // Regression: StartDataSourceIntrospection persisted nothing and
2597        // GetDataSourceIntrospection unconditionally 404'd, so the Start -> Get
2598        // poll flow could never retrieve the discovered schema.
2599        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        // Unknown id is still a declared NotFoundException.
2622        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        // Regression: StartSchemaMerge never merged source types and
2629        // ListTypesByAssociation always returned an empty list.
2630        let s = svc();
2631        let source_id = make_api(&s);
2632        let merged_id = make_api(&s);
2633        // Define a type on the source API.
2634        s.create_type(
2635            &ctx(),
2636            &source_id,
2637            &json!({ "definition": "type Widget { id: ID! }", "format": "SDL" }),
2638        )
2639        .unwrap();
2640        // Before a merge there is nothing to list.
2641        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        // After the merge the source type is associated with the merged API.
2661        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        // The merged API's own type store now carries the merged type too.
2671        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}