1use std::collections::HashMap;
6use std::time::Duration;
7
8#[derive(Debug, Clone)]
10pub struct GraphQLConfig {
11 pub enabled: bool,
13 pub endpoint: String,
15 pub playground: bool,
17 pub introspection: bool,
19 pub schema: SchemaConfig,
21 pub limits: LimitsConfig,
23 pub batching: BatchingConfig,
25 pub caching: CachingConfig,
27 pub tables: Vec<TableConfig>,
29 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 pub fn builder() -> GraphQLConfigBuilder {
53 GraphQLConfigBuilder::new()
54 }
55
56 pub fn get_table_config(&self, table_name: &str) -> Option<&TableConfig> {
58 self.tables.iter().find(|t| t.name == table_name)
59 }
60
61 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 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#[derive(Debug, Default)]
78pub struct GraphQLConfigBuilder {
79 config: GraphQLConfig,
80}
81
82impl GraphQLConfigBuilder {
83 pub fn new() -> Self {
85 Self {
86 config: GraphQLConfig::default(),
87 }
88 }
89
90 pub fn enabled(mut self, enabled: bool) -> Self {
92 self.config.enabled = enabled;
93 self
94 }
95
96 pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
98 self.config.endpoint = endpoint.into();
99 self
100 }
101
102 pub fn playground(mut self, enabled: bool) -> Self {
104 self.config.playground = enabled;
105 self
106 }
107
108 pub fn introspection(mut self, enabled: bool) -> Self {
110 self.config.introspection = enabled;
111 self
112 }
113
114 pub fn auto_generate(mut self, enabled: bool) -> Self {
116 self.config.schema.auto_generate = enabled;
117 self
118 }
119
120 pub fn refresh_interval(mut self, interval: Duration) -> Self {
122 self.config.schema.refresh_interval = interval;
123 self
124 }
125
126 pub fn max_depth(mut self, depth: u32) -> Self {
128 self.config.limits.max_depth = depth;
129 self
130 }
131
132 pub fn max_complexity(mut self, complexity: u32) -> Self {
134 self.config.limits.max_complexity = complexity;
135 self
136 }
137
138 pub fn max_aliases(mut self, aliases: u32) -> Self {
140 self.config.limits.max_aliases = aliases;
141 self
142 }
143
144 pub fn batching(mut self, enabled: bool) -> Self {
146 self.config.batching.enabled = enabled;
147 self
148 }
149
150 pub fn batch_window(mut self, window: Duration) -> Self {
152 self.config.batching.window = window;
153 self
154 }
155
156 pub fn max_batch_size(mut self, size: usize) -> Self {
158 self.config.batching.max_batch_size = size;
159 self
160 }
161
162 pub fn caching(mut self, enabled: bool) -> Self {
164 self.config.caching.enabled = enabled;
165 self
166 }
167
168 pub fn default_ttl(mut self, ttl: Duration) -> Self {
170 self.config.caching.default_ttl = ttl;
171 self
172 }
173
174 pub fn table(mut self, table: TableConfig) -> Self {
176 self.config.tables.push(table);
177 self
178 }
179
180 pub fn relationship(mut self, relationship: RelationshipConfig) -> Self {
182 self.config.relationships.push(relationship);
183 self
184 }
185
186 pub fn build(self) -> GraphQLConfig {
188 self.config
189 }
190}
191
192#[derive(Debug, Clone)]
194pub struct SchemaConfig {
195 pub auto_generate: bool,
197 pub refresh_interval: Duration,
199 pub include_system_tables: bool,
201 pub schema_prefix: Option<String>,
203 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), 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#[derive(Debug, Clone)]
221pub struct LimitsConfig {
222 pub max_depth: u32,
224 pub max_complexity: u32,
226 pub max_aliases: u32,
228 pub max_root_fields: u32,
230 pub max_batch_size: u32,
232 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#[derive(Debug, Clone)]
251pub struct BatchingConfig {
252 pub enabled: bool,
254 pub window: Duration,
256 pub max_batch_size: usize,
258 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#[derive(Debug, Clone)]
275pub struct CachingConfig {
276 pub enabled: bool,
278 pub default_ttl: Duration,
280 pub cache_parsed_queries: bool,
282 pub max_cached_queries: usize,
284 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#[derive(Debug, Clone)]
302pub struct TableConfig {
303 pub name: String,
305 pub graphql_name: Option<String>,
307 pub exclude_columns: Vec<String>,
309 pub max_depth: Option<u32>,
311 pub enable_mutations: bool,
313 pub primary_key: Option<Vec<String>>,
315 pub description: Option<String>,
317 pub authorization: Option<AuthorizationConfig>,
319}
320
321impl TableConfig {
322 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 pub fn with_graphql_name(mut self, name: impl Into<String>) -> Self {
338 self.graphql_name = Some(name.into());
339 self
340 }
341
342 pub fn exclude(mut self, columns: Vec<String>) -> Self {
344 self.exclude_columns = columns;
345 self
346 }
347
348 pub fn with_max_depth(mut self, depth: u32) -> Self {
350 self.max_depth = Some(depth);
351 self
352 }
353
354 pub fn mutations(mut self, enabled: bool) -> Self {
356 self.enable_mutations = enabled;
357 self
358 }
359
360 pub fn with_primary_key(mut self, columns: Vec<String>) -> Self {
362 self.primary_key = Some(columns);
363 self
364 }
365
366 pub fn with_description(mut self, description: impl Into<String>) -> Self {
368 self.description = Some(description.into());
369 self
370 }
371}
372
373#[derive(Debug, Clone)]
375pub struct AuthorizationConfig {
376 pub read_roles: Vec<String>,
378 pub create_roles: Vec<String>,
380 pub update_roles: Vec<String>,
382 pub delete_roles: Vec<String>,
384 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#[derive(Debug, Clone)]
402pub struct RelationshipConfig {
403 pub name: String,
405 pub from_table: String,
407 pub to_table: String,
409 pub from_column: String,
411 pub to_column: String,
413 pub relation_type: String,
415 pub description: Option<String>,
417}
418
419impl RelationshipConfig {
420 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 pub fn from_column(mut self, column: impl Into<String>) -> Self {
439 self.from_column = column.into();
440 self
441 }
442
443 pub fn to_column(mut self, column: impl Into<String>) -> Self {
445 self.to_column = column.into();
446 self
447 }
448
449 pub fn relation_type(mut self, rel_type: impl Into<String>) -> Self {
451 self.relation_type = rel_type.into();
452 self
453 }
454
455 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}