Skip to content

Commit

Permalink
Merge pull request #1 from hasura:header-forwarding
Browse files Browse the repository at this point in the history
Header-forwarding
  • Loading branch information
BenoitRanque authored Jun 28, 2024
2 parents c53d9a3 + 36412bf commit 7d057c4
Show file tree
Hide file tree
Showing 16 changed files with 1,413 additions and 638 deletions.
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ version.workspace = true
edition.workspace = true

[dependencies]
glob-match = "0.2.1"
graphql_client = "0.14.0"
graphql-parser = "0.4.0"
reqwest = { version = "0.12.3", features = [
"json",
"rustls-tls",
Expand Down
30 changes: 24 additions & 6 deletions crates/common/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::config::ConnectionConfig;
use glob_match::glob_match;
use serde::Serialize;
use std::{collections::BTreeMap, error::Error, fmt::Debug};

Expand All @@ -13,28 +14,45 @@ pub fn get_http_client(
pub async fn execute_graphql<T: serde::de::DeserializeOwned>(
query: &str,
variables: BTreeMap<String, serde_json::Value>,
endpoint: &str,
headers: &BTreeMap<String, String>,
client: &reqwest::Client,
connection_config: &ConnectionConfig,
) -> Result<graphql_client::Response<T>, Box<dyn Error>> {
let mut request = client.post(&connection_config.endpoint);
return_headers: &Vec<String>,
) -> Result<(BTreeMap<String, String>, graphql_client::Response<T>), Box<dyn Error>> {
let mut request = client.post(endpoint);

for (header_name, header_value) in &connection_config.headers {
request = request.header(header_name, &header_value.value);
for (header_name, header_value) in headers {
request = request.header(header_name, header_value);
}

let request_body = GraphQLRequest::new(query, &variables);

let request = request.json(&request_body);

let response = request.send().await?;
let headers = response
.headers()
.iter()
.filter_map(|(name, value)| {
for pattern in return_headers {
if glob_match(&pattern.to_lowercase(), &name.as_str().to_lowercase()) {
return Some((
name.to_string(),
value.to_str().unwrap_or_default().to_string(),
));
}
}
None
})
.collect();

if response.error_for_status_ref().is_err() {
return Err(response.text().await?.into());
}

let response: graphql_client::Response<T> = response.json().await?;

Ok(response)
Ok((headers, response))
}

#[derive(Debug, Serialize)]
Expand Down
27 changes: 24 additions & 3 deletions crates/common/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,35 @@
use crate::config_file::Header;
use std::collections::BTreeMap;

use crate::{
config_file::{RequestConfig, ResponseConfig},
schema::SchemaDefinition,
};

#[derive(Debug, Clone)]
pub struct ServerConfig {
pub connection: ConnectionConfig,
pub schema_string: String,
pub request: RequestConfig<String>,
pub response: ResponseConfig<String>,
pub schema: SchemaDefinition,
}

#[derive(Debug, Clone)]
pub struct ConnectionConfig {
pub endpoint: String,
pub headers: BTreeMap<String, Header<String>>,
pub headers: BTreeMap<String, String>,
}

impl ResponseConfig<String> {
pub fn query_response_type_name(&self, query: &str) -> String {
format!(
"{}{}Query{}",
self.type_name_prefix, query, self.type_name_suffix
)
}
pub fn mutation_response_type_name(&self, mutation: &str) -> String {
format!(
"{}{}Mutation{}",
self.type_name_prefix, mutation, self.type_name_suffix
)
}
}
171 changes: 156 additions & 15 deletions crates/common/src/config_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,24 @@ pub const CONFIG_SCHEMA_FILE_NAME: &str = "configuration.schema.json";
pub struct ServerConfigFile {
#[serde(rename = "$schema")]
pub json_schema: String,
pub connection: ConnectionConfigFile,
/// Connection configuration for query execution
pub execution: ConnectionConfigFile,
/// Optional Connection Configuration for introspection
pub introspection: ConnectionConfigFile,
/// Optional configuration for requests
pub request: RequestConfig<Option<String>>,
/// Optional configuration for responses
pub response: ResponseConfig<Option<String>>,
}

impl Default for ServerConfigFile {
fn default() -> Self {
Self {
json_schema: CONFIG_SCHEMA_FILE_NAME.to_owned(),
connection: ConnectionConfigFile {
endpoint: ConfigValue::Value("".to_string()),
headers: BTreeMap::from_iter(vec![(
"Authorization".to_owned(),
Header {
value: ConfigValue::ValueFromEnv(
"GRAPHQL_ENDPOINT_AUTHORIZATION".to_string(),
),
},
)]),
},
execution: ConnectionConfigFile::default(),
introspection: ConnectionConfigFile::default(),
request: RequestConfig::default(),
response: ResponseConfig::default(),
}
}
}
Expand All @@ -36,12 +36,64 @@ impl Default for ServerConfigFile {
pub struct ConnectionConfigFile {
pub endpoint: ConfigValue,
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub headers: BTreeMap<String, Header<ConfigValue>>,
pub headers: BTreeMap<String, ConfigValue>,
}

impl Default for ConnectionConfigFile {
fn default() -> Self {
Self {
endpoint: ConfigValue::Value("".to_string()),
headers: BTreeMap::from_iter(vec![
(
"Content-Type".to_owned(),
ConfigValue::Value("application/json".to_string()),
),
(
"Authorization".to_owned(),
ConfigValue::ValueFromEnv("GRAPHQL_ENDPOINT_AUTHORIZATION".to_string()),
),
]),
}
}
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Header<T> {
pub value: T,
#[serde(rename_all = "camelCase")]
pub struct RequestConfig<T> {
/// Name of the headers argument
/// Must not conflict with any arguments of root fields in the target schema
/// Defaults to "_headers", set to a different value if there is a conflict
pub headers_argument: T,
/// Name of the headers argument type
/// Must not conflict with other types in the target schema
/// Defaults to "_HeaderMap", set to a different value if there is a conflict
pub headers_type_name: T,
/// List of headers to from the request
/// Defaults to ["*"], AKA all headers
/// Supports glob patterns eg. "X-Hasura-*"
pub forward_headers: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ResponseConfig<T> {
/// Name of the headers field in the response type
/// Defaults to "headers"
pub headers_field: T,
/// Name of the response field in the response type
/// Defaults to "response"
pub response_field: T,
/// Prefix for response type names
/// Defaults to "_"
/// Generated response type names must be unique once prefix and suffix are applied
pub type_name_prefix: T,
/// Suffix for response type names
/// Defaults to "Response"
/// Generated response type names must be unique once prefix and suffix are applied
pub type_name_suffix: T,
/// List of headers to from the response
/// Defaults to ["*"], AKA all headers
/// Supports glob patterns eg. "X-Hasura-*"
pub forward_headers: Option<Vec<String>>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
Expand All @@ -51,3 +103,92 @@ pub enum ConfigValue {
#[serde(rename = "valueFromEnv")]
ValueFromEnv(String),
}

impl Default for RequestConfig<String> {
fn default() -> Self {
Self {
headers_argument: "_headers".to_owned(),
headers_type_name: "_HeaderMap".to_owned(),
forward_headers: Some(vec!["*".to_owned()]),
}
}
}

impl Default for RequestConfig<Option<String>> {
fn default() -> Self {
Self {
headers_argument: None,
headers_type_name: None,
forward_headers: Some(vec!["*".to_owned()]),
}
}
}

impl Default for ResponseConfig<String> {
fn default() -> Self {
Self {
headers_field: "headers".to_owned(),
response_field: "response".to_owned(),
type_name_prefix: "_".to_owned(),
type_name_suffix: "Response".to_owned(),
forward_headers: Some(vec!["*".to_owned()]),
}
}
}

impl Default for ResponseConfig<Option<String>> {
fn default() -> Self {
Self {
headers_field: None,
response_field: None,
type_name_prefix: None,
type_name_suffix: None,
forward_headers: Some(vec!["*".to_owned()]),
}
}
}

impl From<RequestConfig<Option<String>>> for RequestConfig<String> {
fn from(value: RequestConfig<Option<String>>) -> Self {
RequestConfig {
headers_argument: value
.headers_argument
.unwrap_or_else(|| Self::default().headers_argument),
headers_type_name: value
.headers_type_name
.unwrap_or_else(|| Self::default().headers_type_name),
forward_headers: value.forward_headers.and_then(|forward_headers| {
if forward_headers.is_empty() {
None
} else {
Some(forward_headers)
}
}),
}
}
}
impl From<ResponseConfig<Option<String>>> for ResponseConfig<String> {
fn from(value: ResponseConfig<Option<String>>) -> Self {
ResponseConfig {
headers_field: value
.headers_field
.unwrap_or_else(|| Self::default().headers_field),
response_field: value
.response_field
.unwrap_or_else(|| Self::default().response_field),
type_name_prefix: value
.type_name_prefix
.unwrap_or_else(|| Self::default().type_name_prefix),
type_name_suffix: value
.type_name_suffix
.unwrap_or_else(|| Self::default().type_name_suffix),
forward_headers: value.forward_headers.and_then(|forward_headers| {
if forward_headers.is_empty() {
None
} else {
Some(forward_headers)
}
}),
}
}
}
1 change: 1 addition & 0 deletions crates/common/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod client;
pub mod config;
pub mod config_file;
pub mod schema;
Loading

0 comments on commit 7d057c4

Please sign in to comment.