|
| 1 | +Generates the code required to handle AWS AppSync Direct Lambda resolver events based on a GraphQL schema. |
| 2 | + |
| 3 | +This macro takes a path to a GraphQL schema file and generates the complete foundation |
| 4 | +for implementing an AWS AppSync Direct Lambda resolver: |
| 5 | + |
| 6 | +- Rust types for all GraphQL types (enums, inputs, objects) |
| 7 | +- Query/Mutation/Subscription operation enums |
| 8 | +- AWS Lambda runtime setup with logging to handle the AWS AppSync event |
| 9 | +- Optional AWS SDK client initialization |
| 10 | + |
| 11 | +# Schema Path Argument |
| 12 | + |
| 13 | +The first argument to this macro must be a string literal containing the path to your GraphQL schema file. |
| 14 | +The schema path can be: |
| 15 | + |
| 16 | +- An absolute filesystem path (e.g. "/home/user/project/schema.graphql") |
| 17 | +- A relative path, that will be relative to your crate's root directory (e.g. "schema.graphql", "graphql/schema.gql") |
| 18 | +- When in a workspace context, the relative path will be relative to the workspace root directory |
| 19 | + |
| 20 | +# Options |
| 21 | + |
| 22 | +- `batch = bool`: Enable/disable batch request handling (default: true) |
| 23 | +- `hook = fn_name`: Add a custom hook function for request validation/auth |
| 24 | +- `log_init = fn_name`: Use a custom log initialization function instead of the default one |
| 25 | +- (feature: `log`) `event_logging = bool`: If true, the macro will generate code to dump the |
| 26 | + lambda payload JSON as well as parsed `AppsyncEvent<Operation>`s in the logs at debug level (default: `false`) |
| 27 | +- `exclude_lambda_handler = bool`: Skip generation of Lambda handler code |
| 28 | +- `only_lambda_handler = bool`: Only generate Lambda handler code |
| 29 | +- `exclude_appsync_types = bool`: Skip generation of GraphQL type definitions |
| 30 | +- `only_appsync_types = bool`: Only generate GraphQL type definitions |
| 31 | +- `exclude_appsync_operations = bool`: Skip generation of operation enums |
| 32 | +- `only_appsync_operations = bool`: Only generate operation enums |
| 33 | +- `type_override` - see section below for details |
| 34 | +- `name_override` - see section below for details |
| 35 | +- `field_type_override` (Deprecated): Same as `type_override` |
| 36 | + |
| 37 | +## Type Overrides |
| 38 | + |
| 39 | +The `type_override` option allows overriding Rust types affected to various schema elements: |
| 40 | + |
| 41 | +- GraphQL `type` and `input` Field types: `type_override = Type.field: CustomType` |
| 42 | +- Operation return types (Query/Mutation): `type_override = OpType.operation: CustomType` |
| 43 | +- Operation arguments (Query/Mutation/Subscription): `type_override = OpType.operation.arg: CustomType` |
| 44 | + |
| 45 | +These overrides are only for the Rust code and must be compatible for serialization/deserialization purposes, |
| 46 | +i.e. you can use `String` for a GraphQL `ID` but you cannot use a `u32` for a GraphQL `Float`. |
| 47 | + |
| 48 | +## Name Overrides |
| 49 | + |
| 50 | +The `name_override` option supports renaming various schema elements: |
| 51 | + |
| 52 | +- Type/input/enum names: `name_override = TypeName: NewTypeName` |
| 53 | +- Field names: `name_override = Type.field: new_field_name` |
| 54 | +- Enum variants: `name_override = Enum.VARIANT: NewVariant` |
| 55 | + |
| 56 | +These overrides are only for the Rust code and will not change serialization/deserialization, |
| 57 | +i.e. `serde` will rename to the original GraphQL schema name. |
| 58 | + |
| 59 | +# AWS SDK Clients |
| 60 | + |
| 61 | +AWS SDK clients can be initialized by providing function definitions that return a cached SDK client type. |
| 62 | +Each client is initialized only once and stored in a static [OnceLock](std::sync::OnceLock), making subsequent function calls |
| 63 | +essentially free: |
| 64 | + |
| 65 | +- Function name: Any valid Rust identifier that will be used to access the client |
| 66 | +- Return type: Must be a valid AWS SDK client like `aws_sdk_dynamodb::Client` |
| 67 | + |
| 68 | +```no_run |
| 69 | +# mod sub { |
| 70 | +use lambda_appsync::appsync_lambda_main; |
| 71 | +
|
| 72 | +// Single client |
| 73 | +appsync_lambda_main!( |
| 74 | + "schema.graphql", |
| 75 | + dynamodb() -> aws_sdk_dynamodb::Client, |
| 76 | +); |
| 77 | +# } |
| 78 | +# fn main() {} |
| 79 | +``` |
| 80 | +```no_run |
| 81 | +# mod sub { |
| 82 | +# use lambda_appsync::appsync_lambda_main; |
| 83 | +// Multiple clients |
| 84 | +appsync_lambda_main!( |
| 85 | + "schema.graphql", |
| 86 | + dynamodb() -> aws_sdk_dynamodb::Client, |
| 87 | + s3() -> aws_sdk_s3::Client, |
| 88 | +); |
| 89 | +# } |
| 90 | +# fn main() {} |
| 91 | +``` |
| 92 | + |
| 93 | +These client functions can then be called from anywhere in the Lambda crate: |
| 94 | +```no_run |
| 95 | +# fn dynamodb() -> aws_sdk_dynamodb::Client { |
| 96 | +# todo!() |
| 97 | +# } |
| 98 | +# fn s3() -> aws_sdk_s3::Client { |
| 99 | +# todo!() |
| 100 | +# } |
| 101 | +# mod sub { |
| 102 | +use crate::{dynamodb, s3}; |
| 103 | +async fn do_something() { |
| 104 | + let dynamodb_client = dynamodb(); |
| 105 | + let s3_client = s3(); |
| 106 | + // Use clients... |
| 107 | +} |
| 108 | +# } |
| 109 | +# fn main() {} |
| 110 | +``` |
| 111 | + |
| 112 | +# Examples |
| 113 | + |
| 114 | +## Basic usage with authentication hook: |
| 115 | +```no_run |
| 116 | +# mod sub { |
| 117 | +use lambda_appsync::{appsync_lambda_main, AppsyncEvent, AppsyncResponse, AppsyncIdentity}; |
| 118 | +
|
| 119 | +fn is_authorized(identity: &AppsyncIdentity) -> bool { |
| 120 | + todo!() |
| 121 | +} |
| 122 | +
|
| 123 | +// If the function returns Some(AppsyncResponse), the Lambda function will immediately return it. |
| 124 | +// Otherwise, the normal flow of the AppSync operation processing will continue. |
| 125 | +// This is primarily intended for advanced authentication checks that AppSync cannot perform, such as verifying that a user is requesting their own ID. |
| 126 | +async fn auth_hook( |
| 127 | + event: &lambda_appsync::AppsyncEvent<Operation> |
| 128 | +) -> Option<lambda_appsync::AppsyncResponse> { |
| 129 | + // Verify JWT token, check permissions etc |
| 130 | + if !is_authorized(&event.identity) { |
| 131 | + return Some(AppsyncResponse::unauthorized()); |
| 132 | + } |
| 133 | + None |
| 134 | +} |
| 135 | +
|
| 136 | +appsync_lambda_main!( |
| 137 | + "schema.graphql", |
| 138 | + hook = auth_hook, |
| 139 | + dynamodb() -> aws_sdk_dynamodb::Client |
| 140 | +); |
| 141 | +# } |
| 142 | +# fn main() {} |
| 143 | +``` |
| 144 | + |
| 145 | +## Generate only types for lib code generation: |
| 146 | +```no_run |
| 147 | +# mod sub { |
| 148 | +use lambda_appsync::appsync_lambda_main; |
| 149 | +appsync_lambda_main!( |
| 150 | + "schema.graphql", |
| 151 | + only_appsync_types = true |
| 152 | +); |
| 153 | +# } |
| 154 | +# fn main() {} |
| 155 | +``` |
| 156 | + |
| 157 | +## Override field types, operation return type or argument types: |
| 158 | +```no_run |
| 159 | +# mod sub { |
| 160 | +use lambda_appsync::appsync_lambda_main; |
| 161 | +appsync_lambda_main!( |
| 162 | + "schema.graphql", |
| 163 | + // Use String instead of the default lambda_appsync::ID |
| 164 | + // Override Player.id to use String instead of ID |
| 165 | + type_override = Player.id: String, |
| 166 | + // Multiple overrides, here changing another `Player` field type |
| 167 | + type_override = Player.team: String, |
| 168 | + // Return value override |
| 169 | + type_override = Query.gameStatus: String, |
| 170 | + type_override = Mutation.setGameStatus: String, |
| 171 | + // Argument override |
| 172 | + type_override = Query.player.id: String, |
| 173 | + type_override = Mutation.deletePlayer.id: String, |
| 174 | + type_override = Subscription.onDeletePlayer.id: String, |
| 175 | +); |
| 176 | +# } |
| 177 | +# fn main() {} |
| 178 | +``` |
| 179 | + |
| 180 | +## Override type, input, enum, fields or variants names: |
| 181 | +```no_run |
| 182 | +# mod sub { |
| 183 | +use lambda_appsync::appsync_lambda_main; |
| 184 | +appsync_lambda_main!( |
| 185 | + "schema.graphql", |
| 186 | + // Override Player struct name |
| 187 | + name_override = Player: NewPlayer, |
| 188 | + // Override Player struct field name |
| 189 | + name_override = Player.name: email, |
| 190 | + // Override team `PYTHON` to be `Snake` (instead of `Python`) |
| 191 | + name_override = Team.PYTHON: Snake, |
| 192 | + // MUST also override ALL the operations return type !!! |
| 193 | + type_override = Query.players: NewPlayer, |
| 194 | + type_override = Query.player: NewPlayer, |
| 195 | + type_override = Mutation.createPlayer: NewPlayer, |
| 196 | + type_override = Mutation.deletePlayer: NewPlayer, |
| 197 | +); |
| 198 | +# } |
| 199 | +# fn main() {} |
| 200 | +``` |
| 201 | +Note that when using `name_override`, the macro does not automatically change the case: |
| 202 | +you are responsible to provide the appropriate casing or Clippy will complain. |
| 203 | + |
| 204 | +## Use a custom log initialization function: |
| 205 | + |
| 206 | +### Feature `env_logger` (default) |
| 207 | + |
| 208 | +By default, `lambda_appsync` exposes and uses `log` and `env_logger`. You can override the |
| 209 | +initialization code if you wish: |
| 210 | + |
| 211 | +```no_run |
| 212 | +# mod sub { |
| 213 | +// This is in fact equivalent to the default initialization code |
| 214 | +fn log_init_fct() { |
| 215 | + use lambda_appsync::env_logger; |
| 216 | + env_logger::Builder::from_env( |
| 217 | + env_logger::Env::default() |
| 218 | + // Default log level is info, expect tracing::span is warn |
| 219 | + .default_filter_or("info,tracing::span=warn") |
| 220 | + .default_write_style_or("never"), |
| 221 | + ) |
| 222 | + // Format timestamps with microseconds |
| 223 | + .format_timestamp_micros() |
| 224 | + .init(); |
| 225 | +} |
| 226 | +lambda_appsync::appsync_lambda_main!( |
| 227 | + "schema.graphql", |
| 228 | + log_init = log_init_fct |
| 229 | +); |
| 230 | +# } |
| 231 | +# fn main() {} |
| 232 | +``` |
| 233 | + |
| 234 | +### Feature `tracing` |
| 235 | + |
| 236 | +Alternatively, you can use the `tracing` feature so `lambda_appsync` exposes and uses `log`, `tracing` and `tracing-subscriber` |
| 237 | + |
| 238 | +```no_run |
| 239 | +# mod sub { |
| 240 | +// This is in fact equivalent to the default initialization code |
| 241 | +fn tracing_init_fct() { |
| 242 | + use lambda_appsync::{tracing, tracing_subscriber}; |
| 243 | + tracing_subscriber::fmt() |
| 244 | + .json() |
| 245 | + .with_env_filter( |
| 246 | + tracing_subscriber::EnvFilter::from_default_env() |
| 247 | + .add_directive(tracing::Level::INFO.into()), |
| 248 | + ) |
| 249 | + // this needs to be set to remove duplicated information in the log. |
| 250 | + .with_current_span(false) |
| 251 | + // this needs to be set to false, otherwise ANSI color codes will |
| 252 | + // show up in a confusing manner in CloudWatch logs. |
| 253 | + .with_ansi(false) |
| 254 | + // remove the name of the function from every log entry |
| 255 | + .with_target(false) |
| 256 | + .init(); |
| 257 | +} |
| 258 | +lambda_appsync::appsync_lambda_main!( |
| 259 | + "schema.graphql", |
| 260 | + log_init = tracing_init_fct |
| 261 | +); |
| 262 | +# } |
| 263 | +# fn main() {} |
| 264 | +``` |
| 265 | + |
| 266 | +## Disable batch processing: |
| 267 | +```no_run |
| 268 | +# mod sub { |
| 269 | +lambda_appsync::appsync_lambda_main!( |
| 270 | + "schema.graphql", |
| 271 | + batch = false |
| 272 | +); |
| 273 | +# } |
| 274 | +# fn main() {} |
| 275 | +``` |
0 commit comments