diana_aws_lambda/
run_aws_req.rs

1// This file contains serverless logic unique to AWS Lambda and its derivatives (e.g. Netlify)
2
3use async_graphql::{ObjectType, SubscriptionType};
4use aws_lambda_events::encodings::Body;
5use netlify_lambda_http::{Request, Response};
6use std::any::Any;
7
8use diana::{DianaHandler, DianaResponse, Options};
9
10/// A *very* generic error type that the deployment system will accept as a return type.
11pub type AwsError = Box<dyn std::error::Error + Send + Sync + 'static>;
12
13// This allows us to propagate error HTTP responses more easily
14enum AwsReqData {
15    Valid((String, Option<String>)),
16    Invalid(Response<String>), // For some reason
17}
18
19// Gets the stringified body and authentication header from an AWS request
20// We use a generic error type rather than the crate's `error_chain` logic here for AWS' benefit
21fn get_data_from_aws_req(req: Request) -> Result<AwsReqData, AwsError> {
22    // Get the request body (query/mutation) as a string
23    // Any errors are returned gracefully as HTTP responses
24    let body = req.body();
25    let body = match body {
26        Body::Text(body_str) => body_str.to_string(),
27        // Binary bodies are fine as long as we can serialise them into strings
28        Body::Binary(body_binary) => {
29            let body_str = std::str::from_utf8(&body_binary);
30            match body_str {
31                Ok(body_str) => body_str.to_string(),
32                Err(_) => {
33                    let res = Response::builder()
34                        .status(400) // Invalid request
35                        .body(
36                            "Found binary body that couldn't be serialized to string".to_string(),
37                        )?;
38                    return Ok(AwsReqData::Invalid(res));
39                }
40            }
41        }
42        Body::Empty => {
43            let res = Response::builder()
44                .status(400) // Invalid request
45                .body("Found empty body, expected string".to_string())?;
46            return Ok(AwsReqData::Invalid(res));
47        }
48    };
49    // Get the authorisation header as a string
50    // Any errors are returned gracefully as HTTP responses
51    let auth_header = req.headers().get("Authorization");
52    let auth_header = match auth_header {
53        Some(auth_header) => {
54            let header_str = auth_header.to_str();
55            match header_str {
56                Ok(header_str) => Some(header_str.to_string()),
57                Err(_) => {
58                    let res = Response::builder()
59                        .status(400) // Invalid request
60                        .body("Couldn't parse authorization header as string".to_string())?;
61                    return Ok(AwsReqData::Invalid(res));
62                }
63            }
64        }
65        None => None,
66    };
67
68    Ok(AwsReqData::Valid((body, auth_header)))
69}
70
71// Parses the response from `DianaHandler` into HTTP responses that AWS Lambda (or derivatives) can handle
72fn parse_aws_res(res: DianaResponse) -> Result<Response<String>, AwsError> {
73    let res = match res {
74        DianaResponse::Success(gql_res_str) => Response::builder()
75            .status(200) // GraphQL will handle any errors within it through JSON
76            .body(gql_res_str)?,
77        DianaResponse::Blocked => Response::builder()
78            .status(403) // Unauthorised
79            .body("Request blocked due to invalid or insufficient authentication".to_string())?,
80        DianaResponse::Error(_) => Response::builder()
81            .status(500) // Internal server error
82            .body("An internal server error occurred".to_string())?,
83    };
84
85    Ok(res)
86}
87
88/// Runs a request for AWS Lambda or its derivatives (e.g. Netlify).
89/// This just takes the entire Lambda request and does all the processing for you, but it's really just a wrapper around
90/// [`DianaHandler`](diana::DianaHandler).
91/// You should use this function in your Lambda handler as shown in the book.
92pub async fn run_aws_req<C, Q, M, S>(
93    req: Request,
94    opts: Options<C, Q, M, S>,
95) -> Result<Response<String>, AwsError>
96where
97    C: Any + Send + Sync + Clone,
98    Q: Clone + ObjectType + 'static,
99    M: Clone + ObjectType + 'static,
100    S: Clone + SubscriptionType + 'static,
101{
102    // TODO cache the DianaHandler instance
103
104    // Create a new Diana handler (core logic primitive)
105    let diana_handler = DianaHandler::new(opts.clone()).map_err(|err| err.to_string())?;
106    // Process the request data into what's needed
107    let req_data = get_data_from_aws_req(req)?;
108    let (body, auth_header) = match req_data {
109        AwsReqData::Valid(data) => data,
110        AwsReqData::Invalid(http_res) => return Ok(http_res), // Propagate any HTTP responses for errors
111    };
112
113    // Run the serverless request with the extracted data and the user's given options
114    // We convert the Option<String> to Option<&str> with `.as_deref()`
115    // We explicitly state that authentication checks need to be run again
116    let res = diana_handler
117        .run_stateless_without_subscriptions(body, auth_header.as_deref(), None)
118        .await;
119
120    // Convert the result to an appropriate HTTP response
121    let http_res = parse_aws_res(res)?;
122    Ok(http_res)
123}