lambda_appsync_proc/
lib.rs

1//! Crate not intended for direct use.
2
3mod appsync_lambda_main;
4mod appsync_operation;
5mod common;
6
7use proc_macro::TokenStream;
8
9/// Generates the code required to handle AWS AppSync Direct Lambda resolver events based on a GraphQL schema.
10///
11/// This macro takes a path to a GraphQL schema file and generates the complete foundation
12/// for implementing an AWS AppSync Direct Lambda resolver:
13///
14/// - Rust types for all GraphQL types (enums, inputs, objects)
15/// - Query/Mutation/Subscription operation enums
16/// - AWS Lambda runtime setup with logging to handle the AWS AppSync event
17/// - Optional AWS SDK client initialization
18///
19/// # Schema Path Argument
20///
21/// The first argument to this macro must be a string literal containing the path to your GraphQL schema file.
22/// The schema path can be:
23///
24/// - An absolute filesystem path (e.g. "/home/user/project/schema.graphql")
25/// - A relative path, that will be relative to your crate's root directory (e.g. "schema.graphql", "graphql/schema.gql")
26/// - When in a workspace context, the relative path will be relative to the workspace root directory
27///
28/// # Options
29///
30/// - `batch = bool`: Enable/disable batch request handling (default: true)
31/// - `hook = fn_name`: Add a custom hook function for request validation/auth
32/// - `exclude_lambda_handler = bool`: Skip generation of Lambda handler code
33/// - `only_lambda_handler = bool`: Only generate Lambda handler code
34/// - `exclude_appsync_types = bool`: Skip generation of GraphQL type definitions
35/// - `only_appsync_types = bool`: Only generate GraphQL type definitions
36/// - `exclude_appsync_operations = bool`: Skip generation of operation enums
37/// - `only_appsync_operations = bool`: Only generate operation enums
38/// - `field_type_override = Type.field: CustomType`: Override type of a specific field
39/// - `field_type_override = Type.field.arg: CustomType`: Override type of a field argument
40///
41/// # AWS SDK Clients
42///
43/// AWS SDK clients can be initialized by providing function definitions that return a cached SDK client type.
44/// Each client is initialized only once and stored in a static [OnceLock](std::sync::OnceLock), making subsequent function calls
45/// essentially free:
46///
47/// - Function name: Any valid Rust identifier that will be used to access the client
48/// - Return type: Must be a valid AWS SDK client like `aws_sdk_dynamodb::Client`
49///
50/// ```no_run
51/// use lambda_appsync::appsync_lambda_main;
52///
53/// // Single client
54/// appsync_lambda_main!(
55///     "schema.graphql",
56///     dynamodb() -> aws_sdk_dynamodb::Client,
57/// );
58/// ```
59/// ```no_run
60/// # use lambda_appsync::appsync_lambda_main;
61/// // Multiple clients
62/// appsync_lambda_main!(
63///     "schema.graphql",
64///     dynamodb() -> aws_sdk_dynamodb::Client,
65///     s3() -> aws_sdk_s3::Client,
66/// );
67/// ```
68///
69/// These client functions can then be called from anywhere in the Lambda crate:
70/// ```no_run
71/// # fn dynamodb() -> aws_sdk_dynamodb::Client {
72/// #  todo!()
73/// # }
74/// # fn s3() -> aws_sdk_s3::Client {
75/// #   todo!()
76/// # }
77/// # mod sub {
78/// use crate::{dynamodb, s3};
79/// async fn do_something() {
80///     let dynamodb_client = dynamodb();
81///     let s3_client = s3();
82///     // Use clients...
83/// }
84/// # }
85/// # fn main() {}
86/// ```
87///
88/// # Examples
89///
90/// Basic usage with authentication hook:
91/// ```no_run
92/// use lambda_appsync::{appsync_lambda_main, AppsyncEvent, AppsyncResponse, AppsyncIdentity};
93///
94/// fn is_authorized(identity: &AppsyncIdentity) -> bool {
95///     todo!()
96/// }
97///
98/// // If the function returns Some(AppsyncResponse), the Lambda function will immediately return it.
99/// // Otherwise, the normal flow of the AppSync operation processing will continue.
100/// // This is primarily intended for advanced authentication checks that AppSync cannot perform, such as verifying that a user is requesting their own ID.
101/// async fn auth_hook(
102///     event: &lambda_appsync::AppsyncEvent<Operation>
103/// ) -> Option<lambda_appsync::AppsyncResponse> {
104///     // Verify JWT token, check permissions etc
105///     if !is_authorized(&event.identity) {
106///         return Some(AppsyncResponse::unauthorized());
107///     }
108///     None
109/// }
110///
111/// appsync_lambda_main!(
112///     "schema.graphql",
113///     hook = auth_hook,
114///     dynamodb() -> aws_sdk_dynamodb::Client
115/// );
116/// ```
117///
118/// Generate only types for lib code generation:
119/// ```no_run
120/// use lambda_appsync::appsync_lambda_main;
121/// appsync_lambda_main!(
122///     "schema.graphql",
123///     only_appsync_types = true
124/// );
125/// ```
126///
127/// Override field types, operation return type or argument types:
128/// ```no_run
129/// use lambda_appsync::appsync_lambda_main;
130/// appsync_lambda_main!(
131///     "schema.graphql",
132///     // Use String instead of the default lambda_appsync::ID
133///     // Override Player.id to use String instead of ID
134///     field_type_override = Player.id: String,
135///     // Multiple overrides, here changing another `Player` field type
136///     field_type_override = Player.team: String,
137///     // Return value override
138///     field_type_override = Query.gameStatus: String,
139///     field_type_override = Mutation.setGameStatus: String,
140///     // Argument override
141///     field_type_override = Query.player.id: String,
142///     field_type_override = Mutation.deletePlayer.id: String,
143///     field_type_override = Subscription.onDeletePlayer.id: String,
144/// );
145/// ```
146///
147/// Disable batch processing:
148/// ```no_run
149/// lambda_appsync::appsync_lambda_main!(
150///     "schema.graphql",
151///     batch = false
152/// );
153/// ```
154#[proc_macro]
155pub fn appsync_lambda_main(input: TokenStream) -> TokenStream {
156    appsync_lambda_main::appsync_lambda_main_impl(input)
157}
158
159/// Marks an async function as an AWS AppSync resolver operation, binding it to a specific Query,
160/// Mutation or Subscription operation defined in the GraphQL schema.
161///
162/// The marked function must match the signature of the GraphQL operation, with parameters and return
163/// type matching what is defined in the schema. The function will be wired up to handle requests
164/// for that operation through the AWS AppSync Direct Lambda resolver.
165///
166/// # Important
167/// This macro can only be used in a crate where the [appsync_lambda_main!] macro has been used at the
168/// root level (typically in `main.rs`). The code generated by this macro depends on types and
169/// implementations that are created by [appsync_lambda_main!].
170///
171/// # Example Usage
172///
173/// ```no_run
174/// # lambda_appsync::appsync_lambda_main!(
175/// #    "schema.graphql",
176/// #     exclude_lambda_handler = true,
177/// # );
178/// # mod sub {
179/// # async fn dynamodb_get_players() -> Result<Vec<Player>, AppsyncError> {
180/// #    todo!()
181/// # }
182/// # async fn dynamodb_create_player(name: String) -> Result<Player, AppsyncError> {
183/// #    todo!()
184/// # }
185/// use lambda_appsync::{appsync_operation, AppsyncError};
186///
187/// // Your types are declared at the crate level by the appsync_lambda_main! macro
188/// use crate::Player;
189///
190/// // Execute when a 'players' query is received
191/// #[appsync_operation(query(players))]
192/// async fn get_players() -> Result<Vec<Player>, AppsyncError> {
193///     // Implement resolver logic
194///     Ok(dynamodb_get_players().await?)
195/// }
196///
197/// // Handle a 'createPlayer' mutation
198/// #[appsync_operation(mutation(createPlayer))]
199/// async fn create_player(name: String) -> Result<Player, AppsyncError> {
200///     Ok(dynamodb_create_player(name).await?)
201/// }
202/// # }
203/// # fn main() {}
204/// ```
205///
206/// ## Using the AppSync event
207///
208/// You may need to explore the [AppsyncEvent](struct.AppsyncEvent.html) received by the lambda
209/// in your operation handler. You can make it available by adding the `with_appsync_event` flag and
210/// adding a reference to it in your operation handler signature (must be the last argument), like so:
211/// ```no_run
212/// # lambda_appsync::appsync_lambda_main!(
213/// #    "schema.graphql",
214/// #     exclude_lambda_handler = true,
215/// # );
216/// # mod sub {
217/// # async fn dynamodb_create_player(name: String) -> Result<Player, AppsyncError> {
218/// #    todo!()
219/// # }
220/// use lambda_appsync::{appsync_operation, AppsyncError, AppsyncEvent, AppsyncIdentity};
221///
222/// // Your types are declared at the crate level by the appsync_lambda_main! macro
223/// use crate::{Operation, Player};
224///
225/// // Use the AppsyncEvent
226/// #[appsync_operation(mutation(createPlayer), with_appsync_event)]
227/// async fn create_player(name: String, event: &AppsyncEvent<Operation>) -> Result<Player, AppsyncError> {
228///     // Example: extract the cognito user ID
229///     let user_id = if let AppsyncIdentity::Cognito(cognito_id) = &event.identity {
230///         cognito_id.sub.clone()
231///     } else {
232///         return Err(AppsyncError::new("Unauthorized", "Must be Cognito authenticated"))
233///     };
234///     Ok(dynamodb_create_player(name).await?)
235/// }
236/// # }
237/// # fn main() {}
238/// ```
239///
240/// Note that the `args` field of the [AppsyncEvent](struct.AppsyncEvent.html) will always contain
241/// [Null](https://docs.rs/serde_json/latest/serde_json/enum.Value.html#variant.Null) at this stage because its initial content is taken to extract
242/// the argument values for the operation.
243///
244/// ## Preserve original function name
245///
246/// By default the [macro@appsync_operation] macro will discard your function's name but
247/// you can also keep it available by adding the `keep_original_function_name` flag:
248/// ```no_run
249/// # lambda_appsync::appsync_lambda_main!(
250/// #    "schema.graphql",
251/// #     exclude_lambda_handler = true,
252/// # );
253/// # mod sub {
254/// use lambda_appsync::{appsync_operation, AppsyncError};
255///
256/// // Your types are declared at the crate level by the appsync_lambda_main! macro
257/// use crate::Player;
258///
259/// # async fn dynamodb_get_players() -> Result<Vec<Player>, AppsyncError> {
260/// #    todo!()
261/// # }
262/// // Keep the original function name available separately
263/// #[appsync_operation(query(players), keep_original_function_name)]
264/// async fn fetch_players() -> Result<Vec<Player>, AppsyncError> {
265///     Ok(dynamodb_get_players().await?)
266/// }
267/// async fn other_stuff() {
268///     // Can still call fetch_players() directly
269///     fetch_players().await;
270/// }
271/// # }
272/// # fn main() {}
273/// ```
274///
275/// ## Using enhanced subscription filters
276///
277/// ```no_run
278/// # lambda_appsync::appsync_lambda_main!(
279/// #    "schema.graphql",
280/// #     exclude_lambda_handler = true,
281/// # );
282/// // (Optional) Use an enhanced subscription filter for onCreatePlayer
283/// use lambda_appsync::{appsync_operation, AppsyncError};
284/// use lambda_appsync::subscription_filters::{FilterGroup, Filter, FieldPath};
285///
286/// #[appsync_operation(subscription(onCreatePlayer))]
287/// async fn on_create_player(name: String) -> Result<Option<FilterGroup>, AppsyncError> {
288///     Ok(Some(FilterGroup::from([
289///         Filter::from([
290///             FieldPath::new("name")?.contains(name)
291///         ])
292///     ])))
293/// }
294/// # fn main() {}
295/// ```
296///
297/// When using a single [FieldPath](subscription_filters/struct.FieldPath.html) you can turn it directly into a [FilterGroup](subscription_filters/struct.FilterGroup.html).
298/// The following code is equivalent to the one above:
299/// ```no_run
300/// # lambda_appsync::appsync_lambda_main!(
301/// #    "schema.graphql",
302/// #     exclude_lambda_handler = true,
303/// # );
304/// # use lambda_appsync::{appsync_operation, AppsyncError};
305/// # use lambda_appsync::subscription_filters::{FilterGroup, Filter, FieldPath};
306/// #[appsync_operation(subscription(onCreatePlayer))]
307/// async fn on_create_player(name: String) -> Result<Option<FilterGroup>, AppsyncError> {
308///     Ok(Some(FieldPath::new("name")?.contains(name).into()))
309/// }
310/// # fn main() {}
311/// ```
312///
313/// ### Important Note
314///
315/// When using enhanced subscription filters (i.e., returning a [FilterGroup](subscription_filters/struct.FilterGroup.html)
316/// from Subscribe operation handlers), you need to modify your ***Response*** mapping in AWS AppSync.
317/// It must contain the following:
318///
319/// ```vtl
320/// #if($context.result.data)
321/// $extensions.setSubscriptionFilter($context.result.data)
322/// #end
323/// null
324/// ```
325#[proc_macro_attribute]
326pub fn appsync_operation(args: TokenStream, input: TokenStream) -> TokenStream {
327    appsync_operation::appsync_operation_impl(args, input)
328}