use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct GraphQLConfig {
pub enabled: bool,
pub endpoint: String,
pub playground: bool,
pub introspection: bool,
pub schema: SchemaConfig,
pub limits: LimitsConfig,
pub batching: BatchingConfig,
pub caching: CachingConfig,
pub tables: Vec<TableConfig>,
pub relationships: Vec<RelationshipConfig>,
}
impl Default for GraphQLConfig {
fn default() -> Self {
Self {
enabled: true,
endpoint: "/graphql".to_string(),
playground: true,
introspection: true,
schema: SchemaConfig::default(),
limits: LimitsConfig::default(),
batching: BatchingConfig::default(),
caching: CachingConfig::default(),
tables: Vec::new(),
relationships: Vec::new(),
}
}
}
impl GraphQLConfig {
pub fn builder() -> GraphQLConfigBuilder {
GraphQLConfigBuilder::new()
}
pub fn get_table_config(&self, table_name: &str) -> Option<&TableConfig> {
self.tables.iter().find(|t| t.name == table_name)
}
pub fn is_column_excluded(&self, table_name: &str, column_name: &str) -> bool {
self.get_table_config(table_name)
.map(|tc| tc.exclude_columns.contains(&column_name.to_string()))
.unwrap_or(false)
}
pub fn get_graphql_name(&self, table_name: &str) -> String {
self.get_table_config(table_name)
.and_then(|tc| tc.graphql_name.clone())
.unwrap_or_else(|| crate::graphql::to_pascal_case(table_name))
}
}
#[derive(Debug, Default)]
pub struct GraphQLConfigBuilder {
config: GraphQLConfig,
}
impl GraphQLConfigBuilder {
pub fn new() -> Self {
Self {
config: GraphQLConfig::default(),
}
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.config.enabled = enabled;
self
}
pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.config.endpoint = endpoint.into();
self
}
pub fn playground(mut self, enabled: bool) -> Self {
self.config.playground = enabled;
self
}
pub fn introspection(mut self, enabled: bool) -> Self {
self.config.introspection = enabled;
self
}
pub fn auto_generate(mut self, enabled: bool) -> Self {
self.config.schema.auto_generate = enabled;
self
}
pub fn refresh_interval(mut self, interval: Duration) -> Self {
self.config.schema.refresh_interval = interval;
self
}
pub fn max_depth(mut self, depth: u32) -> Self {
self.config.limits.max_depth = depth;
self
}
pub fn max_complexity(mut self, complexity: u32) -> Self {
self.config.limits.max_complexity = complexity;
self
}
pub fn max_aliases(mut self, aliases: u32) -> Self {
self.config.limits.max_aliases = aliases;
self
}
pub fn batching(mut self, enabled: bool) -> Self {
self.config.batching.enabled = enabled;
self
}
pub fn batch_window(mut self, window: Duration) -> Self {
self.config.batching.window = window;
self
}
pub fn max_batch_size(mut self, size: usize) -> Self {
self.config.batching.max_batch_size = size;
self
}
pub fn caching(mut self, enabled: bool) -> Self {
self.config.caching.enabled = enabled;
self
}
pub fn default_ttl(mut self, ttl: Duration) -> Self {
self.config.caching.default_ttl = ttl;
self
}
pub fn table(mut self, table: TableConfig) -> Self {
self.config.tables.push(table);
self
}
pub fn relationship(mut self, relationship: RelationshipConfig) -> Self {
self.config.relationships.push(relationship);
self
}
pub fn build(self) -> GraphQLConfig {
self.config
}
}
#[derive(Debug, Clone)]
pub struct SchemaConfig {
pub auto_generate: bool,
pub refresh_interval: Duration,
pub include_system_tables: bool,
pub schema_prefix: Option<String>,
pub excluded_schemas: Vec<String>,
}
impl Default for SchemaConfig {
fn default() -> Self {
Self {
auto_generate: true,
refresh_interval: Duration::from_secs(300), include_system_tables: false,
schema_prefix: None,
excluded_schemas: vec!["pg_catalog".to_string(), "information_schema".to_string()],
}
}
}
#[derive(Debug, Clone)]
pub struct LimitsConfig {
pub max_depth: u32,
pub max_complexity: u32,
pub max_aliases: u32,
pub max_root_fields: u32,
pub max_batch_size: u32,
pub query_timeout: Duration,
}
impl Default for LimitsConfig {
fn default() -> Self {
Self {
max_depth: 10,
max_complexity: 1000,
max_aliases: 10,
max_root_fields: 20,
max_batch_size: 1000,
query_timeout: Duration::from_secs(30),
}
}
}
#[derive(Debug, Clone)]
pub struct BatchingConfig {
pub enabled: bool,
pub window: Duration,
pub max_batch_size: usize,
pub dedupe: bool,
}
impl Default for BatchingConfig {
fn default() -> Self {
Self {
enabled: true,
window: Duration::from_millis(10),
max_batch_size: 100,
dedupe: true,
}
}
}
#[derive(Debug, Clone)]
pub struct CachingConfig {
pub enabled: bool,
pub default_ttl: Duration,
pub cache_parsed_queries: bool,
pub max_cached_queries: usize,
pub type_ttls: HashMap<String, Duration>,
}
impl Default for CachingConfig {
fn default() -> Self {
Self {
enabled: true,
default_ttl: Duration::from_secs(60),
cache_parsed_queries: true,
max_cached_queries: 10000,
type_ttls: HashMap::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct TableConfig {
pub name: String,
pub graphql_name: Option<String>,
pub exclude_columns: Vec<String>,
pub max_depth: Option<u32>,
pub enable_mutations: bool,
pub primary_key: Option<Vec<String>>,
pub description: Option<String>,
pub authorization: Option<AuthorizationConfig>,
}
impl TableConfig {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
graphql_name: None,
exclude_columns: Vec::new(),
max_depth: None,
enable_mutations: true,
primary_key: None,
description: None,
authorization: None,
}
}
pub fn with_graphql_name(mut self, name: impl Into<String>) -> Self {
self.graphql_name = Some(name.into());
self
}
pub fn exclude(mut self, columns: Vec<String>) -> Self {
self.exclude_columns = columns;
self
}
pub fn with_max_depth(mut self, depth: u32) -> Self {
self.max_depth = Some(depth);
self
}
pub fn mutations(mut self, enabled: bool) -> Self {
self.enable_mutations = enabled;
self
}
pub fn with_primary_key(mut self, columns: Vec<String>) -> Self {
self.primary_key = Some(columns);
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}
#[derive(Debug, Clone)]
pub struct AuthorizationConfig {
pub read_roles: Vec<String>,
pub create_roles: Vec<String>,
pub update_roles: Vec<String>,
pub delete_roles: Vec<String>,
pub row_filter: Option<String>,
}
impl Default for AuthorizationConfig {
fn default() -> Self {
Self {
read_roles: Vec::new(),
create_roles: Vec::new(),
update_roles: Vec::new(),
delete_roles: Vec::new(),
row_filter: None,
}
}
}
#[derive(Debug, Clone)]
pub struct RelationshipConfig {
pub name: String,
pub from_table: String,
pub to_table: String,
pub from_column: String,
pub to_column: String,
pub relation_type: String,
pub description: Option<String>,
}
impl RelationshipConfig {
pub fn new(
name: impl Into<String>,
from_table: impl Into<String>,
to_table: impl Into<String>,
) -> Self {
Self {
name: name.into(),
from_table: from_table.into(),
to_table: to_table.into(),
from_column: "id".to_string(),
to_column: "id".to_string(),
relation_type: "many_to_one".to_string(),
description: None,
}
}
pub fn from_column(mut self, column: impl Into<String>) -> Self {
self.from_column = column.into();
self
}
pub fn to_column(mut self, column: impl Into<String>) -> Self {
self.to_column = column.into();
self
}
pub fn relation_type(mut self, rel_type: impl Into<String>) -> Self {
self.relation_type = rel_type.into();
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_builder() {
let config = GraphQLConfig::builder()
.endpoint("/api/graphql")
.playground(true)
.introspection(true)
.max_depth(5)
.max_complexity(500)
.batching(true)
.batch_window(Duration::from_millis(20))
.build();
assert_eq!(config.endpoint, "/api/graphql");
assert!(config.playground);
assert!(config.introspection);
assert_eq!(config.limits.max_depth, 5);
assert_eq!(config.limits.max_complexity, 500);
assert!(config.batching.enabled);
assert_eq!(config.batching.window, Duration::from_millis(20));
}
#[test]
fn test_table_config() {
let table = TableConfig::new("users")
.with_graphql_name("User")
.exclude(vec!["password_hash".to_string()])
.with_max_depth(3)
.mutations(true);
assert_eq!(table.name, "users");
assert_eq!(table.graphql_name, Some("User".to_string()));
assert!(table.exclude_columns.contains(&"password_hash".to_string()));
assert_eq!(table.max_depth, Some(3));
assert!(table.enable_mutations);
}
#[test]
fn test_relationship_config() {
let rel = RelationshipConfig::new("author", "posts", "users")
.from_column("user_id")
.to_column("id")
.relation_type("many_to_one");
assert_eq!(rel.name, "author");
assert_eq!(rel.from_table, "posts");
assert_eq!(rel.to_table, "users");
assert_eq!(rel.from_column, "user_id");
assert_eq!(rel.to_column, "id");
assert_eq!(rel.relation_type, "many_to_one");
}
#[test]
fn test_get_graphql_name() {
let config = GraphQLConfig::builder()
.table(TableConfig::new("users").with_graphql_name("User"))
.build();
assert_eq!(config.get_graphql_name("users"), "User");
assert_eq!(config.get_graphql_name("blog_posts"), "BlogPosts");
}
#[test]
fn test_is_column_excluded() {
let config = GraphQLConfig::builder()
.table(TableConfig::new("users").exclude(vec!["password".to_string()]))
.build();
assert!(config.is_column_excluded("users", "password"));
assert!(!config.is_column_excluded("users", "email"));
assert!(!config.is_column_excluded("posts", "title"));
}
#[test]
fn test_defaults() {
let config = GraphQLConfig::default();
assert!(config.enabled);
assert_eq!(config.endpoint, "/graphql");
assert!(config.playground);
assert!(config.introspection);
assert!(config.schema.auto_generate);
assert_eq!(config.limits.max_depth, 10);
assert_eq!(config.limits.max_complexity, 1000);
assert!(config.batching.enabled);
assert!(config.caching.enabled);
}
}