fakecloud_appsync/state.rs
1//! Account-partitioned, serializable state for AWS AppSync (`appsync`).
2//!
3//! Every resource is stored as its already-output-valid wire JSON object so
4//! reads echo exactly what writes persisted. All map keys are plain `String`s
5//! (apiId, resource name, ARN, association id), so the snapshot never depends on
6//! the tuple-key serde adapter that has silently broken snapshot serialization
7//! on other services. Sub-resources keyed by a compound `String` (e.g. a
8//! resolver's `typeName::fieldName`) reuse the `::` delimiter, which the
9//! `ResourceName` grammar (`[_A-Za-z][_0-9A-Za-z]*`) can never contain.
10
11use std::collections::BTreeMap;
12use std::sync::Arc;
13
14use parking_lot::RwLock;
15use serde::{Deserialize, Serialize};
16
17use fakecloud_core::multi_account::{AccountState, MultiAccountState};
18use serde_json::Value;
19
20pub const APPSYNC_SNAPSHOT_SCHEMA_VERSION: u32 = 1;
21
22/// A stored GraphQL-schema document + its async creation status.
23#[derive(Debug, Clone, Default, Serialize, Deserialize)]
24pub struct SchemaState {
25 /// The raw SDL schema document ingested by `StartSchemaCreation`.
26 #[serde(default)]
27 pub definition: String,
28 /// Current `SchemaStatus` (`Processing` until the first status read
29 /// settles it to `Success`), matching the async lifecycle of other ops.
30 #[serde(default)]
31 pub status: String,
32 /// Human-readable status details.
33 #[serde(default)]
34 pub details: String,
35}
36
37/// Per-account AWS AppSync state.
38#[derive(Debug, Clone, Default, Serialize, Deserialize)]
39pub struct AppSyncData {
40 /// GraphQL APIs keyed by `apiId`, stored as their `GraphqlApi` wire object.
41 #[serde(default)]
42 pub graphql_apis: BTreeMap<String, Value>,
43 /// Schema state keyed by `apiId`.
44 #[serde(default)]
45 pub schemas: BTreeMap<String, SchemaState>,
46 /// API keys keyed by `apiId` -> `id` -> `ApiKey` wire object.
47 #[serde(default)]
48 pub api_keys: BTreeMap<String, BTreeMap<String, Value>>,
49 /// Data sources keyed by `apiId` -> `name` -> `DataSource` wire object.
50 #[serde(default)]
51 pub data_sources: BTreeMap<String, BTreeMap<String, Value>>,
52 /// Resolvers keyed by `apiId` -> `typeName::fieldName` -> `Resolver`.
53 #[serde(default)]
54 pub resolvers: BTreeMap<String, BTreeMap<String, Value>>,
55 /// Functions keyed by `apiId` -> `functionId` -> `FunctionConfiguration`.
56 #[serde(default)]
57 pub functions: BTreeMap<String, BTreeMap<String, Value>>,
58 /// Schema types keyed by `apiId` -> `typeName` -> `Type` wire object.
59 #[serde(default)]
60 pub types: BTreeMap<String, BTreeMap<String, Value>>,
61 /// API caches keyed by `apiId`, stored as their `ApiCache` wire object.
62 #[serde(default)]
63 pub api_caches: BTreeMap<String, Value>,
64 /// GraphQL-API environment variables keyed by `apiId`.
65 #[serde(default)]
66 pub env_vars: BTreeMap<String, BTreeMap<String, String>>,
67 /// Custom domain names keyed by `domainName` -> `DomainNameConfig`.
68 #[serde(default)]
69 pub domain_names: BTreeMap<String, Value>,
70 /// Domain-name -> `ApiAssociation` links (`AssociateApi`).
71 #[serde(default)]
72 pub api_associations: BTreeMap<String, Value>,
73 /// Event APIs keyed by `apiId`, stored as their `Api` wire object.
74 #[serde(default)]
75 pub apis: BTreeMap<String, Value>,
76 /// Channel namespaces keyed by event-`apiId` -> `name` -> `ChannelNamespace`.
77 #[serde(default)]
78 pub channel_namespaces: BTreeMap<String, BTreeMap<String, Value>>,
79 /// Source-API associations keyed by `associationId` -> `SourceApiAssociation`.
80 #[serde(default)]
81 pub source_api_associations: BTreeMap<String, Value>,
82 /// Types merged into a merged API by `StartSchemaMerge`, keyed by
83 /// `associationId` -> `typeName` -> `Type` wire object. Populated when a
84 /// schema merge copies the source API's types so `ListTypesByAssociation`
85 /// can return them instead of a constant empty list.
86 #[serde(default)]
87 pub association_types: BTreeMap<String, BTreeMap<String, Value>>,
88 /// Data-source introspection jobs keyed by `introspectionId`.
89 #[serde(default)]
90 pub introspections: BTreeMap<String, Value>,
91 /// Tags keyed by resource ARN.
92 #[serde(default)]
93 pub tags: BTreeMap<String, BTreeMap<String, String>>,
94}
95
96/// Current time as an RFC3339 string (AppSync's timestamp members serialise as
97/// epoch-seconds on the restJson1 wire, but stored objects only need to be
98/// self-consistent; epoch floats are used where the model declares a timestamp).
99pub fn now_epoch() -> f64 {
100 chrono::Utc::now().timestamp_millis() as f64 / 1000.0
101}
102
103impl AccountState for AppSyncData {
104 fn new_for_account(_account_id: &str, _region: &str, _endpoint: &str) -> Self {
105 Self::default()
106 }
107}
108
109pub type SharedAppSyncState = Arc<RwLock<MultiAccountState<AppSyncData>>>;
110
111#[derive(Debug, Serialize, Deserialize)]
112pub struct AppSyncSnapshot {
113 pub schema_version: u32,
114 pub accounts: MultiAccountState<AppSyncData>,
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn new_account_is_empty() {
123 let data = AppSyncData::new_for_account("000000000000", "us-east-1", "");
124 assert!(data.graphql_apis.is_empty());
125 assert!(data.tags.is_empty());
126 }
127}