Skip to main content

oxirs_gql/
lib.rs

1//! # OxiRS GraphQL - GraphQL Interface for RDF
2//!
3//! [![Version](https://img.shields.io/badge/version-0.3.3-blue)](https://github.com/cool-japan/oxirs/releases)
4//! [![docs.rs](https://docs.rs/oxirs-gql/badge.svg)](https://docs.rs/oxirs-gql)
5//!
6//! **Status**: Production Release (v0.3.3)
7//! **Stability**: Public APIs are stable. Production-ready with comprehensive testing.
8//!
9//! GraphQL interface for RDF data with automatic schema generation from ontologies.
10//! Enables modern GraphQL clients to query knowledge graphs with intuitive GraphQL syntax.
11//!
12//! ## Features
13//!
14//! - **Automatic Schema Generation** - Generate GraphQL schemas from RDF vocabularies
15//! - **GraphQL to SPARQL** - Transparent translation of GraphQL queries to SPARQL
16//! - **Type Mapping** - Map RDF classes to GraphQL types
17//! - **Query Support** - Full GraphQL query capabilities
18//! - **Subscriptions** - WebSocket-based subscriptions (experimental)
19//!
20//! ## See Also
21//!
22//! - [`oxirs-core`](https://docs.rs/oxirs-core) - RDF data model
23//! - [`oxirs-arq`](https://docs.rs/oxirs-arq) - SPARQL query engine
24
25use anyhow::Result;
26use oxirs_core::model::{
27    BlankNode, GraphName, Literal as OxiLiteral, NamedNode, Quad, Subject, Term, Variable,
28};
29use oxirs_core::ConcreteStore;
30use std::sync::{Arc, Mutex};
31use std::time::Duration;
32
33// Re-export QueryResults for other modules
34pub use oxirs_core::query::QueryResults;
35
36// Module declarations are below after the main code
37
38/// Render a bound SPARQL [`Term`] as its plain (bracket-free) lexical
39/// value: the bare IRI for a named node, the bare label for a blank node,
40/// and the lexical form for a literal.
41///
42/// `Term`'s `Display`/`to_string()` instead produces N-Triples-style
43/// serialization (`<http://example.org/x>` for a named node), which is the
44/// right form to re-embed in another SPARQL query but the *wrong* form for
45/// GraphQL scalar output or for round-tripping back through this crate's
46/// own `escape_iri`-style helpers (which expect a bare IRI to wrap in
47/// `<...>` themselves).
48fn term_to_plain_string(term: &Term) -> String {
49    match term {
50        Term::NamedNode(node) => node.as_str().to_string(),
51        Term::BlankNode(node) => format!("_:{}", node.as_str()),
52        Term::Literal(literal) => literal.value().to_string(),
53        Term::Variable(var) => var.as_str().to_string(),
54        Term::QuotedTriple(_) => term.to_string(),
55    }
56}
57
58/// RDF store wrapper for GraphQL integration
59pub struct RdfStore {
60    store: Arc<Mutex<ConcreteStore>>,
61}
62
63impl std::fmt::Debug for RdfStore {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        f.debug_struct("RdfStore")
66            .field("store", &"Store { ... }")
67            .finish()
68    }
69}
70
71impl RdfStore {
72    pub fn new() -> Result<Self> {
73        Ok(Self {
74            store: Arc::new(Mutex::new(ConcreteStore::new()?)),
75        })
76    }
77
78    pub fn open<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
79        Ok(Self {
80            store: Arc::new(Mutex::new(ConcreteStore::open(path)?)),
81        })
82    }
83
84    /// Execute a SPARQL query and return results
85    pub fn query(&self, query: &str) -> Result<QueryResults> {
86        use oxirs_core::query::{QueryEngine, QueryResult};
87
88        let store = self
89            .store
90            .lock()
91            .map_err(|e| anyhow::anyhow!("Mutex lock error: {}", e))?;
92        let engine = QueryEngine::new();
93        let result = engine
94            .query(query, &*store)
95            .map_err(|e| anyhow::anyhow!("SPARQL query error: {}", e))?;
96
97        match result {
98            QueryResult::Select {
99                variables: _,
100                bindings,
101            } => {
102                let mut solutions = Vec::new();
103                for binding in bindings {
104                    let mut solution = oxirs_core::query::Solution::new();
105                    for (var_name, term) in binding {
106                        if let Ok(var) = oxirs_core::model::Variable::new(&var_name) {
107                            solution.bind(var, term);
108                        }
109                    }
110                    solutions.push(solution);
111                }
112                Ok(QueryResults::Solutions(solutions))
113            }
114            QueryResult::Ask(result) => Ok(QueryResults::Boolean(result)),
115            QueryResult::Construct(triples) => {
116                // Return triples directly (not quads)
117                Ok(QueryResults::Graph(triples))
118            }
119        }
120    }
121
122    /// Get count of triples in the store
123    pub fn triple_count(&self) -> Result<usize> {
124        let query = "SELECT (COUNT(*) as ?count) WHERE { ?s ?p ?o }";
125        if let QueryResults::Solutions(solutions) = self.query(query)? {
126            if let Some(solution) = solutions.first() {
127                let count_var = Variable::new("count")
128                    .map_err(|e| anyhow::anyhow!("Failed to create count variable: {}", e))?;
129                if let Some(Term::Literal(lit)) = solution.get(&count_var) {
130                    let count = lit.value().parse::<usize>().map_err(|e| {
131                        anyhow::anyhow!("Failed to parse count value '{}': {}", lit.value(), e)
132                    })?;
133                    return Ok(count);
134                }
135            }
136        }
137        Ok(0)
138    }
139
140    /// Get subjects with optional limit
141    ///
142    /// Note: the `LIMIT`/pagination is applied in Rust, *not* appended to
143    /// the SPARQL text -- the query engine's parser only accepts a bare
144    /// `WHERE { ... }` graph pattern with nothing trailing after the
145    /// closing brace, so `... WHERE { ... } LIMIT n` fails to parse.
146    /// Deduplication (`DISTINCT`) is likewise applied in Rust: the query
147    /// engine parses the `SELECT`/`WHERE` boundary only and does not act on
148    /// `DISTINCT` itself.
149    pub fn get_subjects(&self, limit: Option<usize>) -> Result<Vec<String>> {
150        let query = "SELECT DISTINCT ?s WHERE { ?s ?p ?o }";
151        let mut subjects = Vec::new();
152        let mut seen = std::collections::HashSet::new();
153
154        let subject_var = Variable::new("s")
155            .map_err(|e| anyhow::anyhow!("Failed to create subject variable: {}", e))?;
156
157        if let QueryResults::Solutions(solutions) = self.query(query)? {
158            for solution in &solutions {
159                if let Some(subject) = solution.get(&subject_var) {
160                    let value = term_to_plain_string(subject);
161                    if seen.insert(value.clone()) {
162                        subjects.push(value);
163                    }
164                }
165            }
166        }
167
168        if let Some(limit) = limit {
169            subjects.truncate(limit);
170        }
171
172        Ok(subjects)
173    }
174
175    /// Get predicates with optional limit (see the notes on
176    /// [`Self::get_subjects`] about why `LIMIT`/deduplication are applied
177    /// in Rust).
178    pub fn get_predicates(&self, limit: Option<usize>) -> Result<Vec<String>> {
179        let query = "SELECT DISTINCT ?p WHERE { ?s ?p ?o }";
180        let mut predicates = Vec::new();
181        let mut seen = std::collections::HashSet::new();
182
183        let predicate_var = Variable::new("p")
184            .map_err(|e| anyhow::anyhow!("Failed to create predicate variable: {}", e))?;
185
186        if let QueryResults::Solutions(solutions) = self.query(query)? {
187            for solution in &solutions {
188                if let Some(predicate) = solution.get(&predicate_var) {
189                    let value = term_to_plain_string(predicate);
190                    if seen.insert(value.clone()) {
191                        predicates.push(value);
192                    }
193                }
194            }
195        }
196
197        if let Some(limit) = limit {
198            predicates.truncate(limit);
199        }
200
201        Ok(predicates)
202    }
203
204    /// Get objects with optional limit (see the notes on
205    /// [`Self::get_subjects`] about why `LIMIT`/deduplication are applied
206    /// in Rust).
207    pub fn get_objects(&self, limit: Option<usize>) -> Result<Vec<(String, String)>> {
208        let query = "SELECT DISTINCT ?o WHERE { ?s ?p ?o }";
209        let mut objects = Vec::new();
210        let mut seen = std::collections::HashSet::new();
211
212        let object_var = Variable::new("o")
213            .map_err(|e| anyhow::anyhow!("Failed to create object variable: {}", e))?;
214
215        if let QueryResults::Solutions(solutions) = self.query(query)? {
216            for solution in &solutions {
217                if let Some(object) = solution.get(&object_var) {
218                    let object_type = match object {
219                        Term::NamedNode(_) => "IRI".to_string(),
220                        Term::BlankNode(_) => "BlankNode".to_string(),
221                        Term::Literal(_) => "Literal".to_string(),
222                        Term::Variable(_) => "Variable".to_string(),
223                        Term::QuotedTriple(_) => "QuotedTriple".to_string(),
224                    };
225                    let entry = (term_to_plain_string(object), object_type);
226                    if seen.insert(entry.clone()) {
227                        objects.push(entry);
228                    }
229                }
230            }
231        }
232
233        if let Some(limit) = limit {
234            objects.truncate(limit);
235        }
236
237        Ok(objects)
238    }
239
240    /// Insert a triple into the store
241    pub fn insert_triple(&mut self, subject: &str, predicate: &str, object: &str) -> Result<()> {
242        // Parse terms
243        let subject = if let Some(stripped) = subject.strip_prefix("_:") {
244            Subject::BlankNode(BlankNode::new(stripped)?)
245        } else {
246            Subject::NamedNode(NamedNode::new(subject)?)
247        };
248
249        let predicate = NamedNode::new(predicate)?;
250
251        let object = if object.starts_with("\"") && object.ends_with("\"") {
252            // It's a literal
253            let literal_value = &object[1..object.len() - 1];
254            Term::Literal(OxiLiteral::new_simple_literal(literal_value))
255        } else if let Some(stripped) = object.strip_prefix("_:") {
256            // It's a blank node
257            Term::BlankNode(BlankNode::new(stripped)?)
258        } else {
259            // It's a named node
260            Term::NamedNode(NamedNode::new(object)?)
261        };
262
263        let quad = Quad::new(subject, predicate, object, GraphName::DefaultGraph);
264        let store = self
265            .store
266            .lock()
267            .map_err(|e| anyhow::anyhow!("Mutex lock error: {}", e))?;
268        store.insert_quad(quad)?;
269        Ok(())
270    }
271
272    /// Insert a quad into the store
273    pub fn insert(&self, quad: &oxirs_core::model::Quad) -> Result<()> {
274        let store = self
275            .store
276            .lock()
277            .map_err(|e| anyhow::anyhow!("Mutex lock error: {}", e))?;
278        store.insert_quad(quad.clone())?;
279        Ok(())
280    }
281
282    /// Load data from a file
283    pub fn load_file<P: AsRef<std::path::Path>>(&mut self, path: P, format: &str) -> Result<()> {
284        use oxirs_core::parser::{Parser, RdfFormat};
285        use std::fs;
286
287        let format = match format.to_lowercase().as_str() {
288            "turtle" | "ttl" => RdfFormat::Turtle,
289            "ntriples" | "nt" => RdfFormat::NTriples,
290            "rdfxml" | "rdf" => RdfFormat::RdfXml,
291            "jsonld" | "json" => RdfFormat::JsonLd,
292            _ => return Err(anyhow::anyhow!("Unsupported format: {}", format)),
293        };
294
295        // Read file content
296        let content = fs::read_to_string(path)?;
297
298        // Parse content to quads
299        let parser = Parser::new(format);
300        let quads = parser.parse_str_to_quads(&content)?;
301
302        // Insert quads into store
303        let store = self
304            .store
305            .lock()
306            .map_err(|e| anyhow::anyhow!("Mutex lock error: {}", e))?;
307        for quad in quads {
308            store.insert_quad(quad)?;
309        }
310
311        Ok(())
312    }
313}
314
315#[cfg(test)]
316mod rdf_store_query_tests {
317    use super::*;
318
319    fn build_store() -> RdfStore {
320        let mut store = RdfStore::new().expect("failed to create test store");
321        store
322            .insert_triple(
323                "http://example.org/alice",
324                "http://xmlns.com/foaf/0.1/name",
325                "\"Alice\"",
326            )
327            .expect("insert triple should succeed");
328        store
329            .insert_triple(
330                "http://example.org/bob",
331                "http://xmlns.com/foaf/0.1/name",
332                "\"Bob\"",
333            )
334            .expect("insert triple should succeed");
335        store
336    }
337
338    /// Regression test: `get_subjects`/`get_predicates`/`get_objects` used
339    /// to return `Term::to_string()` (N-Triples-style, e.g.
340    /// `"<http://example.org/alice>"`) instead of the bare IRI/literal
341    /// value, so every caller comparing against a plain IRI string (as the
342    /// `subjects`/`predicates`/`objects` GraphQL fields' consumers do)
343    /// would never match.
344    #[test]
345    fn test_get_subjects_returns_bare_iris_not_bracketed() {
346        let store = build_store();
347        let subjects = store
348            .get_subjects(None)
349            .expect("get_subjects should succeed");
350        let subjects: std::collections::HashSet<String> = subjects.into_iter().collect();
351
352        assert!(subjects.contains("http://example.org/alice"));
353        assert!(subjects.contains("http://example.org/bob"));
354        assert!(
355            !subjects.iter().any(|s| s.starts_with('<')),
356            "subjects must not be bracket-wrapped: {subjects:?}"
357        );
358    }
359
360    #[test]
361    fn test_get_predicates_returns_bare_iris_not_bracketed() {
362        let store = build_store();
363        let predicates = store
364            .get_predicates(None)
365            .expect("get_predicates should succeed");
366
367        assert_eq!(
368            predicates,
369            vec!["http://xmlns.com/foaf/0.1/name".to_string()]
370        );
371    }
372
373    #[test]
374    fn test_get_objects_returns_plain_literal_values() {
375        let store = build_store();
376        let objects = store.get_objects(None).expect("get_objects should succeed");
377        let values: std::collections::HashSet<String> =
378            objects.into_iter().map(|(value, _)| value).collect();
379
380        assert!(values.contains("Alice"));
381        assert!(values.contains("Bob"));
382        assert!(
383            !values.iter().any(|v| v.starts_with('"')),
384            "literal values must not retain quote delimiters: {values:?}"
385        );
386    }
387
388    /// Regression test: the query engine's SPARQL parser rejects
389    /// `WHERE { ... } LIMIT n` (it requires the graph pattern's closing
390    /// brace to be the last thing in the query), so `limit` must be
391    /// enforced in Rust rather than appended to the SPARQL text.
392    #[test]
393    fn test_get_subjects_respects_limit_without_breaking_the_query() {
394        let store = build_store();
395        let subjects = store
396            .get_subjects(Some(1))
397            .expect("get_subjects with a limit should still succeed");
398        assert_eq!(subjects.len(), 1);
399    }
400}
401
402/// Mock store for testing GraphQL functionality
403#[derive(Debug)]
404pub struct MockStore;
405
406impl MockStore {
407    pub fn new() -> Result<Self> {
408        Ok(Self)
409    }
410
411    pub fn open(_path: String) -> Result<Self> {
412        Ok(Self)
413    }
414}
415
416// Individual modules
417pub mod aggregation;
418pub mod ai;
419pub mod api_explorer;
420pub mod ast;
421pub mod auto_caching_strategies;
422pub mod auto_schema_resolver;
423pub mod custom_directives;
424pub mod custom_type_mappings;
425pub mod directive_registry;
426pub mod enhanced_errors;
427pub mod execution;
428pub mod federation;
429pub mod file_upload;
430pub mod graphiql_integration;
431pub mod historical_cost_estimator;
432pub mod horizontal_scaling;
433pub mod hybrid_optimizer;
434pub mod intelligent_federation_gateway;
435pub mod intelligent_query_cache;
436pub mod introspection;
437pub mod live_queries;
438pub mod mapping;
439pub mod ml_optimizer;
440pub mod optimizer;
441pub mod owl_enhanced_schema;
442pub mod pagination_filtering;
443pub mod parallel_field_resolver;
444pub mod parser;
445pub mod persisted_queries;
446pub mod playground_integration;
447pub mod quantum_optimizer;
448pub mod query_builder;
449pub mod query_debugger;
450pub mod query_prefetcher;
451pub mod rate_limiting;
452pub mod rdf_scalars;
453pub mod request_deduplication;
454pub mod resolvers;
455pub mod schema;
456pub mod schema_cache;
457pub mod schema_docs_generator;
458pub mod schema_generator;
459pub mod schema_loader;
460pub mod schema_sdl;
461pub mod schema_types;
462pub mod server;
463pub mod sse_subscriptions;
464pub mod subscriptions;
465pub mod types;
466pub mod validation;
467pub mod validation_spec;
468pub mod zero_trust_security;
469
470// v0.2.0 Operational Enhancements
471pub mod api_versioning;
472pub mod blue_green_deployment;
473pub mod canary_release;
474pub mod circuit_breaker;
475pub mod graphql_mesh;
476pub mod multi_region;
477pub mod performance_insights;
478pub mod schema_changelog;
479pub mod visual_schema_designer;
480
481// v0.3.0 Security & Integration Features
482pub mod content_security_policy;
483pub mod edge_caching;
484pub mod query_sanitization;
485pub mod response_streaming;
486pub mod webhook_support;
487
488// v0.4.0 Advanced Observability & Protocol Features
489pub mod cost_based_optimizer;
490pub mod incremental_execution;
491pub mod query_batching;
492pub mod query_plan_visualizer;
493pub mod query_result_streaming;
494
495// v0.5.0 Advanced Observability & Monitoring
496pub mod graphql_span_attributes;
497pub mod performance_anomaly_detector;
498pub mod performance_heatmap;
499pub mod query_pattern_analyzer;
500pub mod trace_correlation;
501pub mod trace_sampling;
502pub mod trace_visualization;
503pub mod tracing_exporters;
504
505// Advanced performance modules
506pub mod advanced_cache;
507pub mod advanced_security_system;
508pub mod ai_query_predictor;
509pub mod async_streaming;
510pub mod benchmarking;
511pub mod dataloader;
512pub mod neuromorphic_query_processor;
513pub mod performance;
514pub mod predictive_analytics;
515pub mod quantum_real_time_analytics;
516pub mod system_monitor;
517
518// Ultra-modern enterprise modules (July 5, 2025 enhancements)
519pub mod advanced_query_planner;
520pub mod advanced_subscriptions;
521pub mod ai_orchestration_engine;
522pub mod observability;
523
524// Production-ready features (November 2025)
525pub mod production;
526
527// v0.6.0 Enhanced Subscriptions, Multi-tenancy, and Query Caching
528pub mod cache;
529pub mod multitenancy;
530pub mod subscription;
531// v1.1.0 Enhanced tenant module with schema registry, query filter, rate limiter
532pub mod tenant;
533// v1.1.0 Apollo Federation v2 Subgraph Support
534pub mod federation_v2;
535
536// v1.2.0 GraphQL Schema Version Registry
537pub mod schema_registry;
538
539// v1.1.0 Relay Cursor-based Pagination
540pub mod cursor_pagination;
541
542// v1.2.0 GraphQL subscription lifecycle management
543pub mod subscription_manager;
544
545// v1.2.0 DataLoader / batch resolver for GraphQL N+1 resolution
546pub mod batch_resolver;
547
548// v1.5.0 GraphQL __schema / __type introspection engine
549pub mod type_introspection;
550
551// Organized module groups
552pub mod core;
553pub mod distributed_cache;
554pub mod docs;
555pub mod dynamic_query_planner;
556pub mod features;
557pub mod networking;
558pub mod rdf;
559
560// v1.6.0 Field-level resolver pipeline with middleware
561pub mod field_resolver;
562
563// v1.7.0 GraphQL query complexity analysis and limiting
564pub mod query_complexity;
565
566// v1.8.0 GraphQL error formatting, classification, and aggregation
567pub mod error_formatter;
568
569// v1.9.0 Custom GraphQL directive processing engine
570pub mod directive_processor;
571
572// v1.10.0 GraphQL pagination handler (Relay cursor spec)
573pub mod pagination_handler;
574
575// v1.11.0 GraphQL field-level validation rules engine
576pub mod field_validator;
577
578// v1.1.0 round 16 GraphQL enum type resolution and coercion
579pub mod enum_resolver;
580
581// v1.1.0 round 15 GraphQL argument type coercion and validation
582pub mod argument_coercer;
583
584// Juniper-based implementation with proper RDF integration (enabled)
585pub mod juniper_schema;
586pub mod juniper_server; // Complex Hyper v1 version - API issues fixed
587pub mod simple_juniper_server; // Simplified version
588
589// Juniper integration - comprehensive RDF GraphQL support
590pub use juniper_schema::{create_schema, GraphQLContext, Schema as JuniperSchema};
591pub use simple_juniper_server::{
592    start_graphql_server, start_graphql_server_with_config, GraphQLServerBuilder,
593    GraphQLServerConfig, JuniperGraphQLServer,
594};
595
596// Intelligent query caching
597pub use intelligent_query_cache::{
598    IntelligentCacheConfig, IntelligentQueryCache, QueryPattern, QueryUsageStats,
599};
600
601// Advanced Juniper server with full Hyper v1 support
602pub use juniper_server::{
603    start_graphql_server as start_advanced_graphql_server,
604    start_graphql_server_with_config as start_advanced_graphql_server_with_config,
605    GraphQLServerBuilder as AdvancedGraphQLServerBuilder,
606    GraphQLServerConfig as AdvancedGraphQLServerConfig,
607    JuniperGraphQLServer as AdvancedJuniperGraphQLServer,
608};
609
610#[cfg(test)]
611mod tests;
612
613/// GraphQL server configuration
614#[derive(Debug, Clone)]
615pub struct GraphQLConfig {
616    pub enable_introspection: bool,
617    pub enable_playground: bool,
618    pub max_query_depth: Option<usize>,
619    pub max_query_complexity: Option<usize>,
620    pub validation_config: validation::ValidationConfig,
621    pub enable_query_validation: bool,
622    pub distributed_cache_config: Option<distributed_cache::CacheConfig>,
623    /// Whether the raw, unauthenticated SPARQL passthrough field
624    /// (`sparql(query: String!): String`) is exposed in the schema at all.
625    /// **Disabled by default**: it bypasses every GraphQL-level
626    /// depth/complexity limit and lets any client run arbitrary SPARQL
627    /// against the store, with no authentication or authorization of its
628    /// own. Only enable this on a deployment where the GraphQL endpoint
629    /// itself is already access-controlled.
630    pub enable_sparql_field: bool,
631    /// Wall-clock timeout applied to each raw SPARQL passthrough query,
632    /// only relevant when `enable_sparql_field` is `true`.
633    pub sparql_query_timeout: Duration,
634    /// Whether to attempt automatic GraphQL schema generation from the
635    /// RDF store's vocabulary (`rdfs:Class`/`owl:Class` definitions) in
636    /// addition to the built-in `hello`/`version`/`triples`/`subjects`/
637    /// `predicates`/`objects` fields. Falls back to the built-in fields
638    /// alone when the store has no discoverable vocabulary, or when
639    /// generation fails.
640    pub auto_generate_schema: bool,
641    /// Maximum accepted HTTP request body size, in bytes.
642    pub max_request_body_size: usize,
643    /// Idle/read timeout applied to each socket read while parsing an
644    /// HTTP request (Slowloris protection).
645    pub request_read_timeout: Duration,
646    /// Maximum number of concurrent HTTP connections served.
647    pub max_connections: usize,
648    /// Per-client-IP rate limiting configuration for incoming HTTP
649    /// requests. **Disabled by default** (`None`): set this to enforce a
650    /// request budget (token bucket / sliding window / fixed window /
651    /// adaptive-under-load) before any GraphQL parsing/execution happens.
652    /// See [`rate_limiting::RateLimiter`].
653    pub rate_limit_config: Option<rate_limiting::RateLimitConfig>,
654}
655
656impl Default for GraphQLConfig {
657    fn default() -> Self {
658        Self {
659            enable_introspection: true,
660            enable_playground: true,
661            max_query_depth: Some(10),
662            max_query_complexity: Some(1000),
663            validation_config: validation::ValidationConfig::default(),
664            enable_query_validation: true,
665            distributed_cache_config: None, // Disabled by default
666            enable_sparql_field: false,
667            sparql_query_timeout: Duration::from_secs(30),
668            auto_generate_schema: true,
669            max_request_body_size: 10 * 1024 * 1024,
670            request_read_timeout: Duration::from_secs(30),
671            max_connections: 1024,
672            rate_limit_config: None,
673        }
674    }
675}
676
677impl GraphQLConfig {
678    /// Merge the ergonomic top-level `max_query_depth`/`max_query_complexity`
679    /// shorthands into a full [`validation::ValidationConfig`], overriding
680    /// whatever `validation_config.max_depth`/`max_complexity` already
681    /// contain. This is the single place those two fields take effect --
682    /// previously they were accepted by `with_config` but never read
683    /// anywhere, so setting them had no effect on query validation.
684    fn effective_validation_config(&self) -> validation::ValidationConfig {
685        let mut validation_config = self.validation_config.clone();
686        if let Some(max_depth) = self.max_query_depth {
687            validation_config.max_depth = max_depth;
688        }
689        if let Some(max_complexity) = self.max_query_complexity {
690            validation_config.max_complexity = max_complexity;
691        }
692        validation_config
693    }
694}
695
696#[cfg(test)]
697mod graphql_config_tests {
698    use super::*;
699
700    /// Regression test: `max_query_depth`/`max_query_complexity` must
701    /// actually override the validation config used to build the
702    /// `QueryValidator`, not just sit on `GraphQLConfig` unread.
703    #[test]
704    fn test_max_query_depth_and_complexity_override_validation_config() {
705        let config = GraphQLConfig {
706            max_query_depth: Some(3),
707            max_query_complexity: Some(42),
708            ..Default::default()
709        };
710
711        let effective = config.effective_validation_config();
712        assert_eq!(effective.max_depth, 3);
713        assert_eq!(effective.max_complexity, 42);
714    }
715
716    /// When left `None`, the base `validation_config`'s own limits must be
717    /// preserved rather than being reset to some other default.
718    #[test]
719    fn test_none_depth_and_complexity_preserve_validation_config_defaults() {
720        let base_validation_config = validation::ValidationConfig {
721            max_depth: 7,
722            max_complexity: 77,
723            ..Default::default()
724        };
725
726        let config = GraphQLConfig {
727            max_query_depth: None,
728            max_query_complexity: None,
729            validation_config: base_validation_config,
730            ..Default::default()
731        };
732
733        let effective = config.effective_validation_config();
734        assert_eq!(effective.max_depth, 7);
735        assert_eq!(effective.max_complexity, 77);
736    }
737
738    /// Regression test: the fully-implemented `rate_limiting` module used
739    /// to be unreachable from `GraphQLServer` entirely; `rate_limit_config`
740    /// must default to disabled (no surprise behavior change for existing
741    /// deployments) while remaining settable.
742    #[test]
743    fn test_rate_limit_config_defaults_to_disabled_but_is_settable() {
744        assert!(GraphQLConfig::default().rate_limit_config.is_none());
745
746        let config = GraphQLConfig {
747            rate_limit_config: Some(rate_limiting::RateLimitConfig {
748                max_requests: 5,
749                ..Default::default()
750            }),
751            ..Default::default()
752        };
753        assert_eq!(
754            config
755                .rate_limit_config
756                .expect("should be set")
757                .max_requests,
758            5
759        );
760    }
761}
762
763/// Main GraphQL server
764pub struct GraphQLServer {
765    config: GraphQLConfig,
766    store: Arc<RdfStore>,
767    cache: Option<Arc<distributed_cache::GraphQLQueryCache>>,
768}
769
770impl GraphQLServer {
771    pub fn new(store: Arc<RdfStore>) -> Self {
772        Self {
773            config: GraphQLConfig::default(),
774            store,
775            cache: None,
776        }
777    }
778
779    pub fn new_with_mock(_store: Arc<MockStore>) -> Result<Self> {
780        // For backward compatibility during transition
781        let rdf_store = Arc::new(
782            RdfStore::new()
783                .map_err(|e| anyhow::anyhow!("Failed to create RDF store for mock: {}", e))?,
784        );
785        Ok(Self {
786            config: GraphQLConfig::default(),
787            store: rdf_store,
788            cache: None,
789        })
790    }
791
792    pub fn with_config(mut self, config: GraphQLConfig) -> Self {
793        self.config = config;
794        self
795    }
796
797    /// Enable distributed caching
798    pub async fn with_distributed_cache(
799        mut self,
800        cache_config: distributed_cache::CacheConfig,
801    ) -> Result<Self> {
802        let cache = Arc::new(distributed_cache::GraphQLQueryCache::new(cache_config).await?);
803        self.cache = Some(cache);
804        Ok(self)
805    }
806
807    /// Get cache statistics if caching is enabled
808    pub async fn get_cache_stats(&self) -> Option<distributed_cache::CacheStats> {
809        if let Some(cache) = &self.cache {
810            cache.get_stats().await.ok()
811        } else {
812            None
813        }
814    }
815
816    pub async fn start(&self, addr: &str) -> Result<()> {
817        tracing::info!("Starting GraphQL server on {}", addr);
818
819        // `GraphQLConfig::max_query_depth`/`max_query_complexity` are the
820        // ergonomic top-level knobs; thread them into the full
821        // `ValidationConfig` before constructing the validator (previously
822        // these fields were parsed by `with_config` but never read).
823        let validation_config = self.config.effective_validation_config();
824
825        // Create a basic schema with Query type
826        let mut schema = types::Schema::new();
827
828        // Add a Query type with more fields
829        let mut query_type = types::ObjectType::new("Query".to_string())
830            .with_description("The root query type for RDF data access".to_string())
831            .with_field(
832                "hello".to_string(),
833                types::FieldType::new(
834                    "hello".to_string(),
835                    types::GraphQLType::Scalar(types::BuiltinScalars::string()),
836                )
837                .with_description("A simple greeting message".to_string()),
838            )
839            .with_field(
840                "version".to_string(),
841                types::FieldType::new(
842                    "version".to_string(),
843                    types::GraphQLType::Scalar(types::BuiltinScalars::string()),
844                )
845                .with_description("OxiRS GraphQL version".to_string()),
846            )
847            .with_field(
848                "triples".to_string(),
849                types::FieldType::new(
850                    "triples".to_string(),
851                    types::GraphQLType::Scalar(types::BuiltinScalars::int()),
852                )
853                .with_description("Count of triples in the store".to_string()),
854            )
855            .with_field(
856                "subjects".to_string(),
857                types::FieldType::new(
858                    "subjects".to_string(),
859                    types::GraphQLType::List(Box::new(types::GraphQLType::Scalar(
860                        types::BuiltinScalars::string(),
861                    ))),
862                )
863                .with_description("List of subject IRIs".to_string())
864                .with_argument(
865                    "limit".to_string(),
866                    types::ArgumentType::new(
867                        "limit".to_string(),
868                        types::GraphQLType::Scalar(types::BuiltinScalars::int()),
869                    )
870                    .with_default_value(ast::Value::IntValue(10))
871                    .with_description("Maximum number of subjects to return".to_string()),
872                ),
873            )
874            .with_field(
875                "predicates".to_string(),
876                types::FieldType::new(
877                    "predicates".to_string(),
878                    types::GraphQLType::List(Box::new(types::GraphQLType::Scalar(
879                        types::BuiltinScalars::string(),
880                    ))),
881                )
882                .with_description("List of predicate IRIs".to_string())
883                .with_argument(
884                    "limit".to_string(),
885                    types::ArgumentType::new(
886                        "limit".to_string(),
887                        types::GraphQLType::Scalar(types::BuiltinScalars::int()),
888                    )
889                    .with_default_value(ast::Value::IntValue(10))
890                    .with_description("Maximum number of predicates to return".to_string()),
891                ),
892            )
893            .with_field(
894                "objects".to_string(),
895                types::FieldType::new(
896                    "objects".to_string(),
897                    types::GraphQLType::List(Box::new(types::GraphQLType::Scalar(
898                        types::BuiltinScalars::string(),
899                    ))),
900                )
901                .with_description("List of objects".to_string())
902                .with_argument(
903                    "limit".to_string(),
904                    types::ArgumentType::new(
905                        "limit".to_string(),
906                        types::GraphQLType::Scalar(types::BuiltinScalars::int()),
907                    )
908                    .with_default_value(ast::Value::IntValue(10))
909                    .with_description("Maximum number of objects to return".to_string()),
910                ),
911            );
912
913        // The raw SPARQL passthrough field bypasses all GraphQL-level
914        // depth/complexity limits and lets any client run arbitrary SPARQL
915        // against the store, with no authentication or authorization of
916        // its own, so it is opt-in only (see
917        // `GraphQLConfig::enable_sparql_field`, default `false`).
918        if self.config.enable_sparql_field {
919            query_type = query_type.with_field(
920                "sparql".to_string(),
921                types::FieldType::new(
922                    "sparql".to_string(),
923                    types::GraphQLType::Scalar(types::BuiltinScalars::string()),
924                )
925                .with_description("Execute a raw SPARQL query".to_string())
926                .with_argument(
927                    "query".to_string(),
928                    types::ArgumentType::new(
929                        "query".to_string(),
930                        types::GraphQLType::NonNull(Box::new(types::GraphQLType::Scalar(
931                            types::BuiltinScalars::string(),
932                        ))),
933                    )
934                    .with_description("The SPARQL query to execute".to_string()),
935                ),
936            );
937        }
938
939        // Add introspection fields if enabled. `__schema`/`__type` are
940        // modeled as real `Object` types (registered via
941        // `introspection::introspection_meta_types`) rather than opaque
942        // `Scalar`s, so that `QueryExecutor::complete_value` honors client
943        // selection sets on them (see `QueryExecutor::project_introspection_value`).
944        if self.config.enable_introspection {
945            for meta_type in introspection::introspection_meta_types() {
946                schema.add_type(meta_type);
947            }
948
949            query_type = query_type
950                .with_field(
951                    "__schema".to_string(),
952                    types::FieldType::new(
953                        "__schema".to_string(),
954                        types::GraphQLType::NonNull(Box::new(types::GraphQLType::Object(
955                            types::ObjectType::new("__Schema".to_string()),
956                        ))),
957                    )
958                    .with_description("Access the current type schema of this server".to_string()),
959                )
960                .with_field(
961                    "__type".to_string(),
962                    types::FieldType::new(
963                        "__type".to_string(),
964                        types::GraphQLType::Object(types::ObjectType::new("__Type".to_string())),
965                    )
966                    .with_description("Request the type information of a single type".to_string())
967                    .with_argument(
968                        "name".to_string(),
969                        types::ArgumentType::new(
970                            "name".to_string(),
971                            types::GraphQLType::NonNull(Box::new(types::GraphQLType::Scalar(
972                                types::BuiltinScalars::string(),
973                            ))),
974                        )
975                        .with_description("The name of the type to introspect".to_string()),
976                    ),
977                );
978        }
979
980        // Attempt to auto-generate additional object types and root query
981        // fields from the store's RDF vocabulary (`rdfs:Class`/`owl:Class`
982        // declarations), per `GraphQLConfig::auto_generate_schema` (default
983        // `true`). This is a genuine best-effort enhancement: any failure
984        // to extract a vocabulary or generate a schema from it is logged
985        // and simply falls back to the hardcoded fields added above, which
986        // remain present unconditionally.
987        let mut auto_schema_resolver: Option<Arc<auto_schema_resolver::AutoSchemaResolver>> = None;
988        let mut generated_type_names: Vec<String> = Vec::new();
989
990        if self.config.auto_generate_schema {
991            match schema_generator::SchemaGenerator::new()
992                .extract_vocabulary_from_store(&self.store)
993            {
994                Ok(vocabulary) if !vocabulary.classes.is_empty() => {
995                    let gen_config = schema_types::SchemaGenerationConfig {
996                        // The top-level `sparql` field above already covers
997                        // raw SPARQL passthrough when opted into; don't add
998                        // a second copy from the generator.
999                        enable_sparql_field: false,
1000                        ..Default::default()
1001                    };
1002                    let generator = schema_generator::SchemaGenerator::new()
1003                        .with_config(gen_config)
1004                        .with_vocabulary(vocabulary.clone());
1005
1006                    match generator.generate_schema() {
1007                        Ok(generated) => {
1008                            let generated_query_fields = match generated.get_type("Query") {
1009                                Some(types::GraphQLType::Object(obj)) => obj.fields.clone(),
1010                                _ => std::collections::HashMap::new(),
1011                            };
1012
1013                            for (type_name, generated_type) in generated.types {
1014                                if matches!(
1015                                    type_name.as_str(),
1016                                    "Query" | "Mutation" | "Subscription"
1017                                ) {
1018                                    continue;
1019                                }
1020                                schema.add_type(generated_type);
1021                            }
1022
1023                            for (field_name, field_def) in generated_query_fields {
1024                                query_type.fields.entry(field_name).or_insert(field_def);
1025                            }
1026
1027                            let resolver = auto_schema_resolver::AutoSchemaResolver::new(
1028                                Arc::clone(&self.store),
1029                                vocabulary,
1030                            );
1031                            generated_type_names = resolver.generated_type_names();
1032                            auto_schema_resolver = Some(Arc::new(resolver));
1033                        }
1034                        Err(e) => {
1035                            tracing::warn!(
1036                                "Auto schema generation failed, falling back to built-in fields only: {}",
1037                                e
1038                            );
1039                        }
1040                    }
1041                }
1042                Ok(_) => {
1043                    tracing::debug!(
1044                        "Store has no discoverable rdfs:Class/owl:Class vocabulary; using built-in GraphQL fields only"
1045                    );
1046                }
1047                Err(e) => {
1048                    tracing::warn!(
1049                        "Failed to extract RDF vocabulary from store, falling back to built-in fields only: {}",
1050                        e
1051                    );
1052                }
1053            }
1054        }
1055
1056        schema.add_type(types::GraphQLType::Object(query_type));
1057        schema.set_query_type("Query".to_string());
1058
1059        // Create the server with resolvers
1060        let schema_clone = schema.clone();
1061        let mut server = server::Server::new(schema.clone())
1062            .with_playground(self.config.enable_playground)
1063            .with_introspection(self.config.enable_introspection)
1064            .with_max_body_size(self.config.max_request_body_size)
1065            .with_read_timeout(self.config.request_read_timeout)
1066            .with_max_connections(self.config.max_connections);
1067
1068        if let Some(ref rate_limit_config) = self.config.rate_limit_config {
1069            server = server.with_rate_limiting(rate_limit_config.clone());
1070        }
1071
1072        // Configure validation if enabled
1073        if self.config.enable_query_validation {
1074            server = server.with_validation(validation_config, schema_clone.clone());
1075        }
1076
1077        // Any RDF-class object type produced by auto schema generation must
1078        // be resolved "eagerly" (see `execution::QueryExecutor::add_eager_type`)
1079        // since `AutoSchemaResolver` returns one fully materialized value
1080        // tree per field rather than participating in per-type resolver
1081        // dispatch, which has no way to distinguish between items of a list.
1082        for type_name in &generated_type_names {
1083            server.add_eager_object_type(type_name.clone());
1084        }
1085
1086        // Set up the root Query resolver: hardcoded RDF fields (and the
1087        // opt-in raw SPARQL passthrough), GraphQL introspection, and -- when
1088        // present -- fields generated from the store's RDF vocabulary.
1089        let query_resolvers = resolvers::QueryResolvers::new_with_sparql_config(
1090            Arc::clone(&self.store),
1091            self.config.enable_sparql_field,
1092            self.config.sparql_query_timeout,
1093        );
1094        let introspection_resolver = Arc::new(introspection::IntrospectionResolver::new(Arc::new(
1095            schema_clone,
1096        )));
1097        let root_resolver = QueryRootResolver {
1098            rdf_resolver: query_resolvers.rdf_resolver(),
1099            introspection_resolver,
1100            auto_schema_resolver,
1101        };
1102        server.add_resolver("Query".to_string(), Arc::new(root_resolver));
1103
1104        // Parse the address
1105        let socket_addr: std::net::SocketAddr = addr
1106            .parse()
1107            .map_err(|e| anyhow::anyhow!("Invalid address '{}': {}", addr, e))?;
1108
1109        server.start(socket_addr).await
1110    }
1111}
1112
1113/// Root `Query`-type field resolver that dispatches each requested field to
1114/// the appropriate underlying resolver: the hardcoded RDF/SPARQL fields
1115/// (see [`resolvers::RdfResolver`]), GraphQL introspection (`__schema`/
1116/// `__type`, see [`introspection::IntrospectionResolver`]), and -- when the
1117/// store's vocabulary yielded at least one RDF class -- the fields
1118/// generated by [`schema_generator::SchemaGenerator`] (see
1119/// [`auto_schema_resolver::AutoSchemaResolver`]).
1120struct QueryRootResolver {
1121    rdf_resolver: Arc<resolvers::RdfResolver>,
1122    introspection_resolver: Arc<introspection::IntrospectionResolver>,
1123    auto_schema_resolver: Option<Arc<auto_schema_resolver::AutoSchemaResolver>>,
1124}
1125
1126#[async_trait::async_trait]
1127impl execution::FieldResolver for QueryRootResolver {
1128    async fn resolve_field(
1129        &self,
1130        field_name: &str,
1131        args: &std::collections::HashMap<String, ast::Value>,
1132        context: &execution::ExecutionContext,
1133    ) -> Result<ast::Value> {
1134        match field_name {
1135            "__schema" | "__type" => {
1136                self.introspection_resolver
1137                    .resolve_field(field_name, args, context)
1138                    .await
1139            }
1140            _ => {
1141                if let Some(ref auto_resolver) = self.auto_schema_resolver {
1142                    if auto_resolver.handles(field_name) {
1143                        return auto_resolver.resolve_field(field_name, args, context).await;
1144                    }
1145                }
1146                self.rdf_resolver
1147                    .resolve_field(field_name, args, context)
1148                    .await
1149            }
1150        }
1151    }
1152}
1153
1154// Comprehensive module declarations moved to top of file to avoid duplicates