Skip to main content

fraiseql_cli/config/toml_schema/
server_settings.rs

1//! Server settings configuration for TOML schema (validation, debug, MCP).
2
3/// The `[mcp]` table: the server's own type, so a key the compiler reads is one the
4/// server reads (core owns section types).
5pub use fraiseql_core::schema::McpConfig;
6use serde::{Deserialize, Serialize};
7
8/// Query validation limits (depth and complexity).
9///
10/// ```toml
11/// [validation]
12/// max_query_depth = 10
13/// max_query_complexity = 100
14/// ```
15#[derive(Debug, Clone, Default, Deserialize, Serialize)]
16#[serde(default, deny_unknown_fields)]
17pub struct ValidationConfig {
18    /// Maximum allowed query nesting depth. `None` uses the server default (10).
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub max_query_depth: Option<u32>,
21
22    /// Maximum allowed query complexity score. `None` uses the server default (100).
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub max_query_complexity: Option<u32>,
25
26    /// Maximum bytes a single read may deliver, measured on the rows returned.
27    ///
28    /// Depth and complexity score the request; this bounds the answer, which is
29    /// the only cost a read from a materialised view actually has. `None` leaves
30    /// reads unbounded by size.
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub max_response_bytes: Option<u64>,
33
34    /// Maximum rows a top-level `first`/`last`/`limit` argument may request,
35    /// guarding against unbounded-pagination denial of service (#421). `None`
36    /// uses the server default (1000); the server also honours the
37    /// `FRAISEQL_MAX_PAGE_SIZE` environment variable as an override.
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub max_page_size: Option<u32>,
40
41    /// The deepest offset a client may page to (#1306): a GraphQL `offset:`, REST
42    /// `?offset=` or `?rel.offset=` beyond it is refused before any statement, naming the
43    /// relay connection as the path that pages at any depth. `None` (the default) refuses
44    /// none; the server also honours `FRAISEQL_MAX_OFFSET` as an override.
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub max_offset: Option<u32>,
47
48    /// The most (bucket, entity) cells an aggregate carrying a semi-additive measure forward
49    /// may read (#1459): each is one index seek, and a larger request is refused before the
50    /// measure is read. `None` uses the runtime default (250,000).
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub max_semi_additive_cells: Option<u64>,
53}
54
55impl ValidationConfig {
56    /// Refuse a `max_offset` of 0: it would refuse every offset page, which is not a
57    /// ceiling. Leave the key unset for none. Likewise `max_semi_additive_cells` (#1459).
58    ///
59    /// # Errors
60    ///
61    /// When `max_offset` or `max_semi_additive_cells` is 0.
62    pub fn validate(&self) -> anyhow::Result<()> {
63        if self.max_offset == Some(0) {
64            anyhow::bail!(
65                "[validation] max_offset = 0 would refuse every offset page. Set the deepest \
66                 offset clients may page to, or leave the key unset for no ceiling."
67            );
68        }
69        if self.max_semi_additive_cells == Some(0) {
70            anyhow::bail!(
71                "[validation] max_semi_additive_cells = 0 would refuse every aggregate over a \
72                 semi-additive measure. Set the most cells such an aggregate may read, or leave \
73                 the key unset for the default."
74            );
75        }
76        Ok(())
77    }
78}
79
80/// Debug/development configuration.
81///
82/// Controls features that should only be enabled during development or
83/// in trusted environments. All flags default to off.
84///
85/// ```toml
86/// [debug]
87/// enabled = true
88/// database_explain = true
89/// expose_sql = true
90/// ```
91#[derive(Debug, Clone, Deserialize, Serialize)]
92#[serde(default, deny_unknown_fields)]
93pub struct DebugConfig {
94    /// Master switch — all debug features require this to be `true`.
95    pub enabled: bool,
96
97    /// When `true`, the explain endpoint will also run `EXPLAIN` against the
98    /// database and include the query plan in the response.
99    pub database_explain: bool,
100
101    /// When `true`, the explain endpoint includes the generated SQL in the
102    /// response. Defaults to `true` (SQL is shown even without
103    /// `database_explain`).
104    pub expose_sql: bool,
105}
106
107impl Default for DebugConfig {
108    fn default() -> Self {
109        Self {
110            enabled:          false,
111            database_explain: false,
112            expose_sql:       true,
113        }
114    }
115}