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