Skip to main content

sphereql_graphql/
lib.rs

1//! GraphQL integration for sphereQL spatial queries.
2//!
3//! Provides an `async-graphql` schema with queries for cone, shell, band,
4//! wedge, and region lookups, k-nearest-neighbor search, real-time
5//! subscriptions via a broadcast event bus, and the full category
6//! enrichment surface (concept paths, drill-down, domain groups, stats).
7
8pub mod category;
9pub mod category_types;
10pub mod context;
11pub mod query;
12pub mod subscription;
13pub mod types;
14
15pub use category::*;
16pub use category_types::*;
17pub use context::*;
18pub use query::*;
19pub use subscription::*;
20pub use types::*;
21
22/// Default maximum query depth applied to every schema this crate builds.
23///
24/// Without a depth bound, a client can submit a deeply-nested query that
25/// exhausts the resolver stack and the pipeline read lock. 10 is enough
26/// for legitimate category drill-down + spatial sub-selection.
27pub const DEFAULT_MAX_DEPTH: usize = 10;
28
29/// Default maximum query complexity applied to every schema this crate builds.
30///
31/// Each scalar field counts as 1 complexity unit by default; list-typed
32/// fields multiply by their list-size argument when that argument is
33/// resolvable at validation time. A 1,000-unit ceiling rejects fan-out
34/// queries that would touch more than a few thousand index entries per
35/// request.
36pub const DEFAULT_MAX_COMPLEXITY: usize = 1000;
37
38/// Merged GraphQL query root combining the spatial-only resolvers and
39/// the category-enrichment resolvers.
40#[derive(async_graphql::MergedObject, Default)]
41pub struct MergedQueryRoot(SphericalQueryRoot, CategoryQueryRoot);
42
43impl MergedQueryRoot {
44    pub fn new() -> Self {
45        Self::default()
46    }
47}
48
49/// Schema flavor that exposes both spatial and category queries.
50pub type UnifiedSchema =
51    async_graphql::Schema<MergedQueryRoot, async_graphql::EmptyMutation, SphericalSubscriptionRoot>;
52
53/// Build a [`UnifiedSchema`] from all four context resources: the
54/// spatial point index, the spatial event bus, the category-enrichment
55/// pipeline, and a [`TextEmbedder`](sphereql_embed::text_embedder::TextEmbedder)
56/// for resolvers that take text queries.
57///
58/// To run a spatial-only deployment, keep using [`build_schema`]; the
59/// pipeline + embedder context entries are unused there.
60///
61/// To run a category-only deployment, pass a no-op
62/// [`PointIndex`] created via
63/// [`create_default_index`] alongside the real pipeline; spatial
64/// resolvers will return empty results but won't error.
65pub fn build_unified_schema(
66    index: PointIndex,
67    event_bus: SpatialEventBus,
68    pipeline: CategoryPipelineHandle,
69    embedder: EmbedderHandle,
70) -> UnifiedSchema {
71    async_graphql::Schema::build(
72        MergedQueryRoot::new(),
73        async_graphql::EmptyMutation,
74        SphericalSubscriptionRoot,
75    )
76    .limit_depth(DEFAULT_MAX_DEPTH)
77    .limit_complexity(DEFAULT_MAX_COMPLEXITY)
78    .data(index)
79    .data(event_bus)
80    .data(pipeline)
81    .data(embedder)
82    .finish()
83}
84
85/// Convenience wrapper: build a unified schema from an in-memory list of
86/// [`CategorizedItemInput`]s and the default no-op embedder.
87///
88/// Intended for tests, examples, and quickstarts. Production callers
89/// should construct the pipeline themselves (so they control projection
90/// kind / config) and supply a real
91/// [`TextEmbedder`](sphereql_embed::text_embedder::TextEmbedder) before
92/// calling [`build_unified_schema`].
93pub fn build_unified_schema_from_items(
94    items: &[CategorizedItemInput],
95) -> Result<UnifiedSchema, sphereql_embed::pipeline::PipelineError> {
96    let pipeline = build_pipeline_handle_from_items(items)?;
97    Ok(build_unified_schema(
98        create_default_index(),
99        SpatialEventBus::new(16),
100        pipeline,
101        default_no_embedder_handle(),
102    ))
103}