1pub mod config;
41pub mod dataloader;
42pub mod engine;
43pub mod introspector;
44pub mod metrics;
45pub mod resolver;
46pub mod sql_generator;
47pub mod validation;
48
49pub use config::{GraphQLConfig, GraphQLConfigBuilder, RelationshipConfig, TableConfig};
50pub use dataloader::{BatchResult, DataLoader, DataLoaderConfig};
51pub use engine::{GraphQLEngine, GraphQLError, GraphQLRequest, GraphQLResponse};
52pub use introspector::{GraphQLField, GraphQLSchema, GraphQLType, SchemaIntrospector};
53pub use metrics::{GraphQLMetrics, OperationMetrics, QueryStats};
54pub use resolver::{FieldResolver, ResolverContext, ResolverResult};
55pub use sql_generator::{Filter, QueryPlan, Selection, SqlGenerator, SqlQuery};
56pub use validation::{ComplexityResult, QueryValidator, ValidationError};
57
58use std::collections::HashMap;
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub enum OperationType {
63 Query,
65 Mutation,
67 Subscription,
69}
70
71impl std::fmt::Display for OperationType {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 match self {
74 OperationType::Query => write!(f, "query"),
75 OperationType::Mutation => write!(f, "mutation"),
76 OperationType::Subscription => write!(f, "subscription"),
77 }
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
83pub enum RelationType {
84 OneToOne,
86 OneToMany,
88 ManyToOne,
90 ManyToMany,
92}
93
94impl RelationType {
95 #[allow(clippy::should_implement_trait)]
97 pub fn from_str(s: &str) -> Option<Self> {
98 match s.to_lowercase().as_str() {
99 "one_to_one" | "onetoone" | "1:1" => Some(RelationType::OneToOne),
100 "one_to_many" | "onetomany" | "1:n" => Some(RelationType::OneToMany),
101 "many_to_one" | "manytoone" | "n:1" => Some(RelationType::ManyToOne),
102 "many_to_many" | "manytomany" | "n:n" => Some(RelationType::ManyToMany),
103 _ => None,
104 }
105 }
106
107 pub fn is_list(&self) -> bool {
109 matches!(self, RelationType::OneToMany | RelationType::ManyToMany)
110 }
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Hash)]
115pub enum GraphQLScalar {
116 ID,
118 String,
120 Int,
122 Float,
124 Boolean,
126 DateTime,
128 Date,
130 Time,
132 JSON,
134 Decimal,
136 BigInt,
138 Custom(String),
140}
141
142impl GraphQLScalar {
143 pub fn from_sql_type(sql_type: &str) -> Self {
145 let lower = sql_type.to_lowercase();
146
147 if lower.contains("serial") || lower == "uuid" {
148 GraphQLScalar::ID
149 } else if lower.contains("int") || lower == "smallint" {
150 if lower.contains("big") {
151 GraphQLScalar::BigInt
152 } else {
153 GraphQLScalar::Int
154 }
155 } else if lower.contains("float") || lower.contains("double") || lower == "real" {
156 GraphQLScalar::Float
157 } else if lower.contains("numeric") || lower.contains("decimal") {
158 GraphQLScalar::Decimal
159 } else if lower == "boolean" || lower == "bool" {
160 GraphQLScalar::Boolean
161 } else if lower.contains("timestamp") || lower == "datetime" {
162 GraphQLScalar::DateTime
163 } else if lower == "date" {
164 GraphQLScalar::Date
165 } else if lower == "time" {
166 GraphQLScalar::Time
167 } else if lower == "json" || lower == "jsonb" {
168 GraphQLScalar::JSON
169 } else {
170 GraphQLScalar::String
171 }
172 }
173
174 pub fn to_sdl(&self) -> &str {
176 match self {
177 GraphQLScalar::ID => "ID",
178 GraphQLScalar::String => "String",
179 GraphQLScalar::Int => "Int",
180 GraphQLScalar::Float => "Float",
181 GraphQLScalar::Boolean => "Boolean",
182 GraphQLScalar::DateTime => "DateTime",
183 GraphQLScalar::Date => "Date",
184 GraphQLScalar::Time => "Time",
185 GraphQLScalar::JSON => "JSON",
186 GraphQLScalar::Decimal => "Decimal",
187 GraphQLScalar::BigInt => "BigInt",
188 GraphQLScalar::Custom(name) => name,
189 }
190 }
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
195pub enum ConsistencyLevel {
196 Strong,
198 #[default]
200 Eventual,
201 Bounded,
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
207pub enum DistanceMetric {
208 #[default]
210 Cosine,
211 Euclidean,
213 DotProduct,
215}
216
217#[derive(Debug, Clone)]
219pub struct BranchContext {
220 pub name: String,
222 pub as_of: Option<std::time::SystemTime>,
224}
225
226impl Default for BranchContext {
227 fn default() -> Self {
228 Self {
229 name: "main".to_string(),
230 as_of: None,
231 }
232 }
233}
234
235#[derive(Debug, Clone, Default)]
237pub struct ExecutionContext {
238 pub user_id: Option<String>,
240 pub roles: Vec<String>,
242 pub branch: BranchContext,
244 pub consistency: ConsistencyLevel,
246 pub headers: HashMap<String, String>,
248 pub metadata: HashMap<String, String>,
250}
251
252impl ExecutionContext {
253 pub fn new() -> Self {
255 Self::default()
256 }
257
258 pub fn with_user(mut self, user_id: impl Into<String>) -> Self {
260 self.user_id = Some(user_id.into());
261 self
262 }
263
264 pub fn with_role(mut self, role: impl Into<String>) -> Self {
266 self.roles.push(role.into());
267 self
268 }
269
270 pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
272 self.branch.name = branch.into();
273 self
274 }
275
276 pub fn with_as_of(mut self, timestamp: std::time::SystemTime) -> Self {
278 self.branch.as_of = Some(timestamp);
279 self
280 }
281
282 pub fn with_consistency(mut self, level: ConsistencyLevel) -> Self {
284 self.consistency = level;
285 self
286 }
287
288 pub fn has_role(&self, role: &str) -> bool {
290 self.roles.iter().any(|r| r == role)
291 }
292
293 pub fn is_authenticated(&self) -> bool {
295 self.user_id.is_some()
296 }
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
301pub enum ErrorCode {
302 ParseError,
304 ValidationError,
306 Unauthorized,
308 Forbidden,
310 NotFound,
312 InternalError,
314 QueryTooComplex,
316 RateLimited,
318 Timeout,
320}
321
322impl ErrorCode {
323 pub fn http_status(&self) -> u16 {
325 match self {
326 ErrorCode::ParseError | ErrorCode::ValidationError => 400,
327 ErrorCode::Unauthorized => 401,
328 ErrorCode::Forbidden => 403,
329 ErrorCode::NotFound => 404,
330 ErrorCode::QueryTooComplex | ErrorCode::RateLimited => 429,
331 ErrorCode::Timeout => 408,
332 ErrorCode::InternalError => 500,
333 }
334 }
335}
336
337pub fn to_pascal_case(s: &str) -> String {
339 s.split('_')
340 .filter(|part| !part.is_empty())
341 .map(|part| {
342 let mut chars = part.chars();
343 match chars.next() {
344 None => String::new(),
345 Some(first) => first.to_uppercase().chain(chars).collect(),
346 }
347 })
348 .collect()
349}
350
351pub fn to_camel_case(s: &str) -> String {
353 let pascal = to_pascal_case(s);
354 let mut chars = pascal.chars();
355 match chars.next() {
356 None => String::new(),
357 Some(first) => first.to_lowercase().chain(chars).collect(),
358 }
359}
360
361pub fn to_snake_case(s: &str) -> String {
363 let mut result = String::with_capacity(s.len() + 4);
364 let mut prev_was_upper = false;
365
366 for (i, c) in s.chars().enumerate() {
367 if c.is_uppercase() {
368 if i > 0 && !prev_was_upper {
369 result.push('_');
370 }
371 result.push(c.to_lowercase().next().unwrap());
372 prev_was_upper = true;
373 } else {
374 result.push(c);
375 prev_was_upper = false;
376 }
377 }
378
379 result
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385
386 #[test]
387 fn test_relation_type_from_str() {
388 assert_eq!(
389 RelationType::from_str("one_to_one"),
390 Some(RelationType::OneToOne)
391 );
392 assert_eq!(RelationType::from_str("1:n"), Some(RelationType::OneToMany));
393 assert_eq!(RelationType::from_str("n:1"), Some(RelationType::ManyToOne));
394 assert_eq!(
395 RelationType::from_str("many_to_many"),
396 Some(RelationType::ManyToMany)
397 );
398 assert_eq!(RelationType::from_str("invalid"), None);
399 }
400
401 #[test]
402 fn test_relation_type_is_list() {
403 assert!(!RelationType::OneToOne.is_list());
404 assert!(!RelationType::ManyToOne.is_list());
405 assert!(RelationType::OneToMany.is_list());
406 assert!(RelationType::ManyToMany.is_list());
407 }
408
409 #[test]
410 fn test_graphql_scalar_from_sql_type() {
411 assert_eq!(GraphQLScalar::from_sql_type("serial"), GraphQLScalar::ID);
412 assert_eq!(GraphQLScalar::from_sql_type("UUID"), GraphQLScalar::ID);
413 assert_eq!(GraphQLScalar::from_sql_type("INTEGER"), GraphQLScalar::Int);
414 assert_eq!(
415 GraphQLScalar::from_sql_type("BIGINT"),
416 GraphQLScalar::BigInt
417 );
418 assert_eq!(GraphQLScalar::from_sql_type("FLOAT"), GraphQLScalar::Float);
419 assert_eq!(
420 GraphQLScalar::from_sql_type("BOOLEAN"),
421 GraphQLScalar::Boolean
422 );
423 assert_eq!(
424 GraphQLScalar::from_sql_type("TIMESTAMP"),
425 GraphQLScalar::DateTime
426 );
427 assert_eq!(GraphQLScalar::from_sql_type("JSONB"), GraphQLScalar::JSON);
428 assert_eq!(
429 GraphQLScalar::from_sql_type("VARCHAR"),
430 GraphQLScalar::String
431 );
432 }
433
434 #[test]
435 fn test_to_pascal_case() {
436 assert_eq!(to_pascal_case("user_name"), "UserName");
437 assert_eq!(to_pascal_case("users"), "Users");
438 assert_eq!(to_pascal_case("post_comments"), "PostComments");
439 }
440
441 #[test]
442 fn test_to_camel_case() {
443 assert_eq!(to_camel_case("user_name"), "userName");
444 assert_eq!(to_camel_case("Users"), "users");
445 assert_eq!(to_camel_case("post_comments"), "postComments");
446 }
447
448 #[test]
449 fn test_to_snake_case() {
450 assert_eq!(to_snake_case("UserName"), "user_name");
451 assert_eq!(to_snake_case("postComments"), "post_comments");
452 assert_eq!(to_snake_case("ID"), "id");
453 }
454
455 #[test]
456 fn test_execution_context() {
457 let ctx = ExecutionContext::new()
458 .with_user("user123")
459 .with_role("admin")
460 .with_role("reader")
461 .with_branch("development")
462 .with_consistency(ConsistencyLevel::Strong);
463
464 assert_eq!(ctx.user_id, Some("user123".to_string()));
465 assert!(ctx.is_authenticated());
466 assert!(ctx.has_role("admin"));
467 assert!(ctx.has_role("reader"));
468 assert!(!ctx.has_role("writer"));
469 assert_eq!(ctx.branch.name, "development");
470 assert_eq!(ctx.consistency, ConsistencyLevel::Strong);
471 }
472
473 #[test]
474 fn test_error_code_http_status() {
475 assert_eq!(ErrorCode::ParseError.http_status(), 400);
476 assert_eq!(ErrorCode::Unauthorized.http_status(), 401);
477 assert_eq!(ErrorCode::Forbidden.http_status(), 403);
478 assert_eq!(ErrorCode::NotFound.http_status(), 404);
479 assert_eq!(ErrorCode::InternalError.http_status(), 500);
480 }
481}