Skip to main content

surrealdb_sql/statements/define/config/
graphql.rs

1use common::fmt::EscapeKwFreeIdent;
2use surrealdb_strand::Strand;
3use surrealdb_types::{SqlFormat, ToSql};
4
5use crate::TableName;
6
7#[derive(Clone, Debug, Default, PartialEq, Eq)]
8#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
9pub struct GraphQLConfig {
10	pub tables: TablesConfig,
11	pub functions: FunctionsConfig,
12	pub depth_limit: Option<u32>,
13	pub complexity_limit: Option<u32>,
14	pub introspection: IntrospectionConfig,
15}
16
17#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Hash)]
18#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
19pub enum TablesConfig {
20	#[default]
21	None,
22	Auto,
23	Include(
24		#[cfg_attr(feature = "arbitrary", arbitrary(with = crate::arbitrary::atleast_one))]
25		Vec<TableConfig>,
26	),
27	Exclude(
28		#[cfg_attr(feature = "arbitrary", arbitrary(with = crate::arbitrary::atleast_one))]
29		Vec<TableConfig>,
30	),
31}
32
33#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Hash)]
34#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
35pub struct TableConfig {
36	pub name: TableName,
37}
38
39#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Hash)]
40#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
41pub enum FunctionsConfig {
42	#[default]
43	None,
44	Auto,
45	// Arbitrary generation is skipped: function names stored here are bare
46	// `name(::name)*` identifiers (no `fn::` prefix), but `Strand`'s `Arbitrary`
47	// impl yields any UTF-8 string, which the `INCLUDE`/`EXCLUDE` syntax
48	// can't round-trip.
49	#[cfg_attr(feature = "arbitrary", arbitrary(skip))]
50	Include(Vec<Strand>),
51	#[cfg_attr(feature = "arbitrary", arbitrary(skip))]
52	Exclude(Vec<Strand>),
53}
54
55/// Controls whether GraphQL schema introspection is enabled.
56///
57/// When set to `None`, introspection queries (`__schema`, `__type`, etc.) are disabled,
58/// preventing clients from discovering the schema structure. This is useful in production
59/// to avoid leaking table/field names to unauthorized users.
60///
61/// Defaults to `Auto` (introspection enabled).
62#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Hash)]
63#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
64pub enum IntrospectionConfig {
65	#[default]
66	Auto,
67	None,
68}
69
70impl ToSql for IntrospectionConfig {
71	fn fmt_sql(&self, f: &mut String, _fmt: SqlFormat) {
72		match self {
73			IntrospectionConfig::Auto => f.push_str("AUTO"),
74			IntrospectionConfig::None => f.push_str("NONE"),
75		}
76	}
77}
78
79impl ToSql for GraphQLConfig {
80	fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
81		f.push_str("GRAPHQL");
82		f.push_str(" TABLES ");
83		self.tables.fmt_sql(f, fmt);
84		f.push_str(" FUNCTIONS ");
85		self.functions.fmt_sql(f, fmt);
86		if let Some(depth) = self.depth_limit {
87			f.push_str(&format!(" DEPTH {depth}"));
88		}
89		if let Some(complexity) = self.complexity_limit {
90			f.push_str(&format!(" COMPLEXITY {complexity}"));
91		}
92		// Only emit INTROSPECTION clause when it differs from the default (AUTO)
93		if matches!(self.introspection, IntrospectionConfig::None) {
94			f.push_str(" INTROSPECTION ");
95			self.introspection.fmt_sql(f, fmt);
96		}
97	}
98}
99
100impl ToSql for TablesConfig {
101	fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
102		match self {
103			TablesConfig::Auto => f.push_str("AUTO"),
104			TablesConfig::None => f.push_str("NONE"),
105			TablesConfig::Include(cs) => {
106				f.push_str("INCLUDE ");
107				for (i, table) in cs.iter().enumerate() {
108					if i > 0 {
109						f.push_str(", ");
110					}
111					table.fmt_sql(f, fmt);
112				}
113			}
114			TablesConfig::Exclude(cs) => {
115				f.push_str("EXCLUDE ");
116				for (i, table) in cs.iter().enumerate() {
117					if i > 0 {
118						f.push_str(", ");
119					}
120					table.fmt_sql(f, fmt);
121				}
122			}
123		}
124	}
125}
126
127impl ToSql for TableConfig {
128	fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
129		EscapeKwFreeIdent(self.name.as_str()).fmt_sql(f, fmt);
130	}
131}
132
133impl ToSql for FunctionsConfig {
134	fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
135		match self {
136			FunctionsConfig::Auto => f.push_str("AUTO"),
137			FunctionsConfig::None => f.push_str("NONE"),
138			FunctionsConfig::Include(cs) => {
139				f.push_str("INCLUDE ");
140				fmt_function_name_list(f, fmt, cs);
141			}
142			FunctionsConfig::Exclude(cs) => {
143				f.push_str("EXCLUDE ");
144				fmt_function_name_list(f, fmt, cs);
145			}
146		}
147	}
148}
149
150/// Render a list of custom function names as a comma-separated sequence of
151/// `fn::<name>` references — the same form the parser accepts, so the output
152/// round-trips through `DEFINE CONFIG GRAPHQL FUNCTIONS INCLUDE/EXCLUDE`. Each
153/// `::`-separated segment is escaped independently to keep the syntax legal
154/// even when a segment is a SurrealQL keyword.
155fn fmt_function_name_list(f: &mut String, fmt: SqlFormat, names: &[Strand]) {
156	for (i, name) in names.iter().enumerate() {
157		if i > 0 {
158			f.push_str(", ");
159		}
160		f.push_str("fn::");
161		let mut first = true;
162		for segment in name.as_str().split("::") {
163			if !first {
164				f.push_str("::");
165			}
166			first = false;
167			EscapeKwFreeIdent(segment).fmt_sql(f, fmt);
168		}
169	}
170}