| 
 | 1 | +use super::IdempotencyKey;  | 
 | 2 | +use actix_web::body::to_bytes;  | 
 | 3 | +use actix_web::http::{self, StatusCode};  | 
 | 4 | +use actix_web::HttpResponse;  | 
 | 5 | +use sqlx::postgres::PgHasArrayType;  | 
 | 6 | +use sqlx::PgPool;  | 
 | 7 | +use uuid::Uuid;  | 
 | 8 | + | 
 | 9 | +#[derive(Debug, sqlx::Type)]  | 
 | 10 | +#[sqlx(type_name = "header_pair")]  | 
 | 11 | +struct HeaderPairRecord {  | 
 | 12 | +    name: String,  | 
 | 13 | +    value: Vec<u8>,  | 
 | 14 | +}  | 
 | 15 | + | 
 | 16 | +impl PgHasArrayType for HeaderPairRecord {  | 
 | 17 | +    fn array_type_info() -> sqlx::postgres::PgTypeInfo {  | 
 | 18 | +        sqlx::postgres::PgTypeInfo::with_name("_header_pair")  | 
 | 19 | +    }  | 
 | 20 | +}  | 
 | 21 | + | 
 | 22 | +pub async fn get_saved_response(  | 
 | 23 | +    pool: &PgPool,  | 
 | 24 | +    idempotency_key: &IdempotencyKey,  | 
 | 25 | +    user_id: Uuid,  | 
 | 26 | +) -> Result<Option<HttpResponse>, anyhow::Error> {  | 
 | 27 | +    let saved_response = sqlx::query!(  | 
 | 28 | +        r#"  | 
 | 29 | +            SELECT  | 
 | 30 | +            response_status_code,  | 
 | 31 | +            response_headers as "response_headers: Vec<HeaderPairRecord>",  | 
 | 32 | +            response_body  | 
 | 33 | +            FROM idempotency  | 
 | 34 | +            WHERE  | 
 | 35 | +            user_id = $1 AND  | 
 | 36 | +            idempotency_key = $2  | 
 | 37 | +        "#,  | 
 | 38 | +        user_id,  | 
 | 39 | +        idempotency_key.as_ref()  | 
 | 40 | +    )  | 
 | 41 | +    .fetch_optional(pool)  | 
 | 42 | +    .await?;  | 
 | 43 | +    if let Some(r) = saved_response {  | 
 | 44 | +        let status_code = StatusCode::from_u16(r.response_status_code.try_into()?)?;  | 
 | 45 | +        let mut response = HttpResponse::build(status_code);  | 
 | 46 | +        for HeaderPairRecord { name, value } in r.response_headers {  | 
 | 47 | +            response.append_header((name, value));  | 
 | 48 | +        }  | 
 | 49 | +        Ok(Some(response.body(r.response_body)))  | 
 | 50 | +    } else {  | 
 | 51 | +        Ok(None)  | 
 | 52 | +    }  | 
 | 53 | +}  | 
 | 54 | + | 
 | 55 | +pub async fn save_response(  | 
 | 56 | +    pool: &PgPool,  | 
 | 57 | +    idempotency_key: &IdempotencyKey,  | 
 | 58 | +    user_id: Uuid,  | 
 | 59 | +    http_response: HttpResponse,  | 
 | 60 | +) -> Result<HttpResponse, anyhow::Error> {  | 
 | 61 | +    let (response_head, body) = http_response.into_parts();  | 
 | 62 | +    let body = to_bytes(body).await.map_err(|e| anyhow::anyhow!("{}", e))?;  | 
 | 63 | +    let status_code = response_head.status().as_u16() as i16;  | 
 | 64 | +    let headers = {  | 
 | 65 | +        let mut h = Vec::with_capacity(response_head.headers().len());  | 
 | 66 | +        for (name, value) in response_head.headers().iter() {  | 
 | 67 | +            let name = name.as_str().to_owned();  | 
 | 68 | +            let value = value.as_bytes().to_owned();  | 
 | 69 | +            h.push(HeaderPairRecord { name, value });  | 
 | 70 | +        }  | 
 | 71 | +        h  | 
 | 72 | +    };  | 
 | 73 | +    sqlx::query_unchecked!(  | 
 | 74 | +        r#"  | 
 | 75 | +        INSERT INTO idempotency (  | 
 | 76 | +            user_id,  | 
 | 77 | +            idempotency_key,  | 
 | 78 | +            response_status_code,  | 
 | 79 | +            response_headers,  | 
 | 80 | +            response_body,  | 
 | 81 | +            created_at  | 
 | 82 | +            )  | 
 | 83 | +        VALUES ($1, $2, $3, $4, $5, now())  | 
 | 84 | +        "#,  | 
 | 85 | +        user_id,  | 
 | 86 | +        idempotency_key.as_ref(),  | 
 | 87 | +        status_code,  | 
 | 88 | +        headers,  | 
 | 89 | +        body.as_ref(),  | 
 | 90 | +    )  | 
 | 91 | +    .execute(pool)  | 
 | 92 | +    .await?;  | 
 | 93 | + | 
 | 94 | +    let http_response = response_head.set_body(body).map_into_boxed_body();  | 
 | 95 | +    Ok(http_response)  | 
 | 96 | +}  | 
0 commit comments