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}