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