Skip to main content

heliosdb_proxy/graphql/
config.rs

1//! GraphQL Gateway Configuration
2//!
3//! Configuration types for the GraphQL-to-SQL gateway.
4
5use std::collections::HashMap;
6use std::time::Duration;
7
8/// GraphQL gateway configuration
9#[derive(Debug, Clone)]
10pub struct GraphQLConfig {
11    /// Enable GraphQL gateway
12    pub enabled: bool,
13    /// Endpoint path (default: "/graphql")
14    pub endpoint: String,
15    /// Enable GraphQL Playground
16    pub playground: bool,
17    /// Enable introspection queries
18    pub introspection: bool,
19    /// Schema configuration
20    pub schema: SchemaConfig,
21    /// Complexity limits
22    pub limits: LimitsConfig,
23    /// Batching configuration
24    pub batching: BatchingConfig,
25    /// Caching configuration
26    pub caching: CachingConfig,
27    /// Table-specific configurations
28    pub tables: Vec<TableConfig>,
29    /// Custom relationship configurations
30    pub relationships: Vec<RelationshipConfig>,
31}
32
33impl Default for GraphQLConfig {
34    fn default() -> Self {
35        Self {
36            enabled: true,
37            endpoint: "/graphql".to_string(),
38            playground: true,
39            introspection: true,
40            schema: SchemaConfig::default(),
41            limits: LimitsConfig::default(),
42            batching: BatchingConfig::default(),
43            caching: CachingConfig::default(),
44            tables: Vec::new(),
45            relationships: Vec::new(),
46        }
47    }
48}
49
50impl GraphQLConfig {
51    /// Create a new configuration builder
52    pub fn builder() -> GraphQLConfigBuilder {
53        GraphQLConfigBuilder::new()
54    }
55
56    /// Get table configuration by table name
57    pub fn get_table_config(&self, table_name: &str) -> Option<&TableConfig> {
58        self.tables.iter().find(|t| t.name == table_name)
59    }
60
61    /// Check if a column should be excluded
62    pub fn is_column_excluded(&self, table_name: &str, column_name: &str) -> bool {
63        self.get_table_config(table_name)
64            .map(|tc| tc.exclude_columns.contains(&column_name.to_string()))
65            .unwrap_or(false)
66    }
67
68    /// Get the GraphQL name for a table
69    pub fn get_graphql_name(&self, table_name: &str) -> String {
70        self.get_table_config(table_name)
71            .and_then(|tc| tc.graphql_name.clone())
72            .unwrap_or_else(|| crate::graphql::to_pascal_case(table_name))
73    }
74}
75
76/// Configuration builder for GraphQL gateway
77#[derive(Debug, Default)]
78pub struct GraphQLConfigBuilder {
79    config: GraphQLConfig,
80}
81
82impl GraphQLConfigBuilder {
83    /// Create a new builder
84    pub fn new() -> Self {
85        Self {
86            config: GraphQLConfig::default(),
87        }
88    }
89
90    /// Set enabled status
91    pub fn enabled(mut self, enabled: bool) -> Self {
92        self.config.enabled = enabled;
93        self
94    }
95
96    /// Set endpoint path
97    pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
98        self.config.endpoint = endpoint.into();
99        self
100    }
101
102    /// Enable/disable playground
103    pub fn playground(mut self, enabled: bool) -> Self {
104        self.config.playground = enabled;
105        self
106    }
107
108    /// Enable/disable introspection
109    pub fn introspection(mut self, enabled: bool) -> Self {
110        self.config.introspection = enabled;
111        self
112    }
113
114    /// Set auto-generation of schema
115    pub fn auto_generate(mut self, enabled: bool) -> Self {
116        self.config.schema.auto_generate = enabled;
117        self
118    }
119
120    /// Set schema refresh interval
121    pub fn refresh_interval(mut self, interval: Duration) -> Self {
122        self.config.schema.refresh_interval = interval;
123        self
124    }
125
126    /// Set maximum query depth
127    pub fn max_depth(mut self, depth: u32) -> Self {
128        self.config.limits.max_depth = depth;
129        self
130    }
131
132    /// Set maximum query complexity
133    pub fn max_complexity(mut self, complexity: u32) -> Self {
134        self.config.limits.max_complexity = complexity;
135        self
136    }
137
138    /// Set maximum aliases per query
139    pub fn max_aliases(mut self, aliases: u32) -> Self {
140        self.config.limits.max_aliases = aliases;
141        self
142    }
143
144    /// Enable/disable batching
145    pub fn batching(mut self, enabled: bool) -> Self {
146        self.config.batching.enabled = enabled;
147        self
148    }
149
150    /// Set batch window
151    pub fn batch_window(mut self, window: Duration) -> Self {
152        self.config.batching.window = window;
153        self
154    }
155
156    /// Set maximum batch size
157    pub fn max_batch_size(mut self, size: usize) -> Self {
158        self.config.batching.max_batch_size = size;
159        self
160    }
161
162    /// Enable/disable caching
163    pub fn caching(mut self, enabled: bool) -> Self {
164        self.config.caching.enabled = enabled;
165        self
166    }
167
168    /// Set default cache TTL
169    pub fn default_ttl(mut self, ttl: Duration) -> Self {
170        self.config.caching.default_ttl = ttl;
171        self
172    }
173
174    /// Add a table configuration
175    pub fn table(mut self, table: TableConfig) -> Self {
176        self.config.tables.push(table);
177        self
178    }
179
180    /// Add a relationship configuration
181    pub fn relationship(mut self, relationship: RelationshipConfig) -> Self {
182        self.config.relationships.push(relationship);
183        self
184    }
185
186    /// Build the configuration
187    pub fn build(self) -> GraphQLConfig {
188        self.config
189    }
190}
191
192/// Schema generation configuration
193#[derive(Debug, Clone)]
194pub struct SchemaConfig {
195    /// Automatically generate schema from database
196    pub auto_generate: bool,
197    /// Schema refresh interval
198    pub refresh_interval: Duration,
199    /// Include system tables
200    pub include_system_tables: bool,
201    /// Schema prefix filter (include only tables with this prefix)
202    pub schema_prefix: Option<String>,
203    /// Excluded schemas
204    pub excluded_schemas: Vec<String>,
205}
206
207impl Default for SchemaConfig {
208    fn default() -> Self {
209        Self {
210            auto_generate: true,
211            refresh_interval: Duration::from_secs(300), // 5 minutes
212            include_system_tables: false,
213            schema_prefix: None,
214            excluded_schemas: vec!["pg_catalog".to_string(), "information_schema".to_string()],
215        }
216    }
217}
218
219/// Query complexity limits configuration
220#[derive(Debug, Clone)]
221pub struct LimitsConfig {
222    /// Maximum query depth
223    pub max_depth: u32,
224    /// Maximum query complexity
225    pub max_complexity: u32,
226    /// Maximum number of aliases
227    pub max_aliases: u32,
228    /// Maximum number of root fields
229    pub max_root_fields: u32,
230    /// Maximum batch size for DataLoader
231    pub max_batch_size: u32,
232    /// Query timeout
233    pub query_timeout: Duration,
234}
235
236impl Default for LimitsConfig {
237    fn default() -> Self {
238        Self {
239            max_depth: 10,
240            max_complexity: 1000,
241            max_aliases: 10,
242            max_root_fields: 20,
243            max_batch_size: 1000,
244            query_timeout: Duration::from_secs(30),
245        }
246    }
247}
248
249/// DataLoader batching configuration
250#[derive(Debug, Clone)]
251pub struct BatchingConfig {
252    /// Enable batching
253    pub enabled: bool,
254    /// Batch window duration
255    pub window: Duration,
256    /// Maximum batch size
257    pub max_batch_size: usize,
258    /// Enable request deduplication
259    pub dedupe: bool,
260}
261
262impl Default for BatchingConfig {
263    fn default() -> Self {
264        Self {
265            enabled: true,
266            window: Duration::from_millis(10),
267            max_batch_size: 100,
268            dedupe: true,
269        }
270    }
271}
272
273/// Response caching configuration
274#[derive(Debug, Clone)]
275pub struct CachingConfig {
276    /// Enable caching
277    pub enabled: bool,
278    /// Default TTL for cached responses
279    pub default_ttl: Duration,
280    /// Cache parsed queries
281    pub cache_parsed_queries: bool,
282    /// Maximum number of cached queries
283    pub max_cached_queries: usize,
284    /// Per-type cache TTL overrides
285    pub type_ttls: HashMap<String, Duration>,
286}
287
288impl Default for CachingConfig {
289    fn default() -> Self {
290        Self {
291            enabled: true,
292            default_ttl: Duration::from_secs(60),
293            cache_parsed_queries: true,
294            max_cached_queries: 10000,
295            type_ttls: HashMap::new(),
296        }
297    }
298}
299
300/// Table-specific configuration
301#[derive(Debug, Clone)]
302pub struct TableConfig {
303    /// Database table name
304    pub name: String,
305    /// GraphQL type name override
306    pub graphql_name: Option<String>,
307    /// Columns to exclude from schema
308    pub exclude_columns: Vec<String>,
309    /// Maximum query depth for this table
310    pub max_depth: Option<u32>,
311    /// Enable mutations for this table
312    pub enable_mutations: bool,
313    /// Primary key column(s)
314    pub primary_key: Option<Vec<String>>,
315    /// Custom description
316    pub description: Option<String>,
317    /// Authorization rules
318    pub authorization: Option<AuthorizationConfig>,
319}
320
321impl TableConfig {
322    /// Create a new table configuration
323    pub fn new(name: impl Into<String>) -> Self {
324        Self {
325            name: name.into(),
326            graphql_name: None,
327            exclude_columns: Vec::new(),
328            max_depth: None,
329            enable_mutations: true,
330            primary_key: None,
331            description: None,
332            authorization: None,
333        }
334    }
335
336    /// Set GraphQL type name
337    pub fn with_graphql_name(mut self, name: impl Into<String>) -> Self {
338        self.graphql_name = Some(name.into());
339        self
340    }
341
342    /// Exclude columns
343    pub fn exclude(mut self, columns: Vec<String>) -> Self {
344        self.exclude_columns = columns;
345        self
346    }
347
348    /// Set maximum depth
349    pub fn with_max_depth(mut self, depth: u32) -> Self {
350        self.max_depth = Some(depth);
351        self
352    }
353
354    /// Enable/disable mutations
355    pub fn mutations(mut self, enabled: bool) -> Self {
356        self.enable_mutations = enabled;
357        self
358    }
359
360    /// Set primary key
361    pub fn with_primary_key(mut self, columns: Vec<String>) -> Self {
362        self.primary_key = Some(columns);
363        self
364    }
365
366    /// Set description
367    pub fn with_description(mut self, description: impl Into<String>) -> Self {
368        self.description = Some(description.into());
369        self
370    }
371}
372
373/// Table authorization configuration
374#[derive(Debug, Clone)]
375pub struct AuthorizationConfig {
376    /// Roles that can read
377    pub read_roles: Vec<String>,
378    /// Roles that can create
379    pub create_roles: Vec<String>,
380    /// Roles that can update
381    pub update_roles: Vec<String>,
382    /// Roles that can delete
383    pub delete_roles: Vec<String>,
384    /// Row-level security filter expression
385    pub row_filter: Option<String>,
386}
387
388impl Default for AuthorizationConfig {
389    fn default() -> Self {
390        Self {
391            read_roles: Vec::new(),
392            create_roles: Vec::new(),
393            update_roles: Vec::new(),
394            delete_roles: Vec::new(),
395            row_filter: None,
396        }
397    }
398}
399
400/// Relationship configuration
401#[derive(Debug, Clone)]
402pub struct RelationshipConfig {
403    /// Relationship name (used in GraphQL field)
404    pub name: String,
405    /// Source table
406    pub from_table: String,
407    /// Target table
408    pub to_table: String,
409    /// Source column (foreign key)
410    pub from_column: String,
411    /// Target column (primary key)
412    pub to_column: String,
413    /// Relationship type
414    pub relation_type: String,
415    /// Description
416    pub description: Option<String>,
417}
418
419impl RelationshipConfig {
420    /// Create a new relationship configuration
421    pub fn new(
422        name: impl Into<String>,
423        from_table: impl Into<String>,
424        to_table: impl Into<String>,
425    ) -> Self {
426        Self {
427            name: name.into(),
428            from_table: from_table.into(),
429            to_table: to_table.into(),
430            from_column: "id".to_string(),
431            to_column: "id".to_string(),
432            relation_type: "many_to_one".to_string(),
433            description: None,
434        }
435    }
436
437    /// Set from column
438    pub fn from_column(mut self, column: impl Into<String>) -> Self {
439        self.from_column = column.into();
440        self
441    }
442
443    /// Set to column
444    pub fn to_column(mut self, column: impl Into<String>) -> Self {
445        self.to_column = column.into();
446        self
447    }
448
449    /// Set relation type
450    pub fn relation_type(mut self, rel_type: impl Into<String>) -> Self {
451        self.relation_type = rel_type.into();
452        self
453    }
454
455    /// Set description
456    pub fn with_description(mut self, description: impl Into<String>) -> Self {
457        self.description = Some(description.into());
458        self
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    #[test]
467    fn test_config_builder() {
468        let config = GraphQLConfig::builder()
469            .endpoint("/api/graphql")
470            .playground(true)
471            .introspection(true)
472            .max_depth(5)
473            .max_complexity(500)
474            .batching(true)
475            .batch_window(Duration::from_millis(20))
476            .build();
477
478        assert_eq!(config.endpoint, "/api/graphql");
479        assert!(config.playground);
480        assert!(config.introspection);
481        assert_eq!(config.limits.max_depth, 5);
482        assert_eq!(config.limits.max_complexity, 500);
483        assert!(config.batching.enabled);
484        assert_eq!(config.batching.window, Duration::from_millis(20));
485    }
486
487    #[test]
488    fn test_table_config() {
489        let table = TableConfig::new("users")
490            .with_graphql_name("User")
491            .exclude(vec!["password_hash".to_string()])
492            .with_max_depth(3)
493            .mutations(true);
494
495        assert_eq!(table.name, "users");
496        assert_eq!(table.graphql_name, Some("User".to_string()));
497        assert!(table.exclude_columns.contains(&"password_hash".to_string()));
498        assert_eq!(table.max_depth, Some(3));
499        assert!(table.enable_mutations);
500    }
501
502    #[test]
503    fn test_relationship_config() {
504        let rel = RelationshipConfig::new("author", "posts", "users")
505            .from_column("user_id")
506            .to_column("id")
507            .relation_type("many_to_one");
508
509        assert_eq!(rel.name, "author");
510        assert_eq!(rel.from_table, "posts");
511        assert_eq!(rel.to_table, "users");
512        assert_eq!(rel.from_column, "user_id");
513        assert_eq!(rel.to_column, "id");
514        assert_eq!(rel.relation_type, "many_to_one");
515    }
516
517    #[test]
518    fn test_get_graphql_name() {
519        let config = GraphQLConfig::builder()
520            .table(TableConfig::new("users").with_graphql_name("User"))
521            .build();
522
523        assert_eq!(config.get_graphql_name("users"), "User");
524        assert_eq!(config.get_graphql_name("blog_posts"), "BlogPosts");
525    }
526
527    #[test]
528    fn test_is_column_excluded() {
529        let config = GraphQLConfig::builder()
530            .table(TableConfig::new("users").exclude(vec!["password".to_string()]))
531            .build();
532
533        assert!(config.is_column_excluded("users", "password"));
534        assert!(!config.is_column_excluded("users", "email"));
535        assert!(!config.is_column_excluded("posts", "title"));
536    }
537
538    #[test]
539    fn test_defaults() {
540        let config = GraphQLConfig::default();
541
542        assert!(config.enabled);
543        assert_eq!(config.endpoint, "/graphql");
544        assert!(config.playground);
545        assert!(config.introspection);
546        assert!(config.schema.auto_generate);
547        assert_eq!(config.limits.max_depth, 10);
548        assert_eq!(config.limits.max_complexity, 1000);
549        assert!(config.batching.enabled);
550        assert!(config.caching.enabled);
551    }
552}