fraiseql_cli/schema/intermediate/operations.rs
1//! Query/mutation structs: `IntermediateQuery`, `IntermediateMutation`,
2//! `IntermediateArgument`, `IntermediateAutoParams`, `IntermediateQueryDefaults`.
3
4use fraiseql_core::schema::InputStyle;
5use indexmap::IndexMap;
6use serde::{Deserialize, Serialize};
7
8use super::types::IntermediateDeprecation;
9
10/// Argument definition in intermediate format
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct IntermediateArgument {
13 /// Argument name
14 pub name: String,
15
16 /// Argument type name
17 ///
18 /// **Language-agnostic**: Uses "type", not "`arg_type`"
19 #[serde(rename = "type")]
20 pub arg_type: String,
21
22 /// Is argument optional?
23 pub nullable: bool,
24
25 /// Default value (JSON)
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub default: Option<serde_json::Value>,
28
29 /// Deprecation info (from @deprecated directive)
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub deprecated: Option<IntermediateDeprecation>,
32}
33
34/// Query definition in intermediate format
35#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
36pub struct IntermediateQuery {
37 /// Query name (e.g., "users")
38 pub name: String,
39
40 /// Return type name (e.g., "User")
41 pub return_type: String,
42
43 /// Returns a list?
44 #[serde(default)]
45 pub returns_list: bool,
46
47 /// Result is nullable?
48 #[serde(default)]
49 pub nullable: bool,
50
51 /// Query arguments
52 #[serde(default)]
53 pub arguments: Vec<IntermediateArgument>,
54
55 /// Query description (from docstring)
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub description: Option<String>,
58
59 /// SQL source (table/view name)
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub sql_source: Option<String>,
62
63 /// Auto-generated parameters config
64 #[serde(skip_serializing_if = "Option::is_none")]
65 pub auto_params: Option<IntermediateAutoParams>,
66
67 /// Deprecation info (from @deprecated directive)
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub deprecated: Option<IntermediateDeprecation>,
70
71 /// JSONB column name for extracting data (e.g., "data")
72 /// Used for tv_* (denormalized JSONB tables) pattern
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub jsonb_column: Option<String>,
75
76 /// Whether this is a Relay connection query.
77 /// When true, the compiler wraps results in `{ edges: [{ node, cursor }], pageInfo }`
78 /// and generates `first`/`after`/`last`/`before` arguments instead of `limit`/`offset`.
79 /// Requires `returns_list = true` and `sql_source` to be set.
80 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
81 pub relay: bool,
82
83 /// Server-injected parameters: SQL column name → source expression (e.g. `"jwt:org_id"`).
84 /// Not exposed as GraphQL arguments.
85 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
86 pub inject: IndexMap<String, String>,
87
88 /// Per-query result cache TTL in seconds. Overrides the global cache TTL for this query.
89 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub cache_ttl_seconds: Option<u64>,
91
92 /// Additional database views this query reads beyond the primary `sql_source`.
93 ///
94 /// Used for correct cache invalidation when a query JOINs or reads multiple views.
95 /// Each entry is validated as a safe SQL identifier at schema compile time.
96 #[serde(default, skip_serializing_if = "Vec::is_empty")]
97 pub additional_views: Vec<String>,
98
99 /// Role required to execute this query and see it in introspection.
100 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub requires_role: Option<String>,
102
103 /// Relay cursor column type: `"uuid"` for UUID PKs, `"int64"` (or absent) for bigint PKs.
104 /// Only meaningful when `relay = true`.
105 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub relay_cursor_type: Option<String>,
107}
108
109/// Mutation definition in intermediate format
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111pub struct IntermediateMutation {
112 /// Mutation name (e.g., "createUser")
113 pub name: String,
114
115 /// Return type name (e.g., "User")
116 pub return_type: String,
117
118 /// Returns a list?
119 #[serde(default)]
120 pub returns_list: bool,
121
122 /// Result is nullable?
123 #[serde(default)]
124 pub nullable: bool,
125
126 /// Mutation arguments
127 #[serde(default)]
128 pub arguments: Vec<IntermediateArgument>,
129
130 /// Mutation description (from docstring)
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub description: Option<String>,
133
134 /// SQL source (function name)
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub sql_source: Option<String>,
137
138 /// Operation type (CREATE, UPDATE, DELETE, CUSTOM)
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub operation: Option<String>,
141
142 /// Deprecation info (from @deprecated directive)
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub deprecated: Option<IntermediateDeprecation>,
145
146 /// Server-injected parameters: SQL parameter name → source expression (e.g. `"jwt:org_id"`).
147 /// Not exposed as GraphQL arguments.
148 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
149 pub inject: IndexMap<String, String>,
150
151 /// Fact tables whose version counter should be bumped after this mutation succeeds.
152 ///
153 /// Used for correct invalidation of analytic/aggregate cache entries.
154 #[serde(default, skip_serializing_if = "Vec::is_empty")]
155 pub invalidates_fact_tables: Vec<String>,
156
157 /// View names whose cached query results should be invalidated after this
158 /// mutation succeeds.
159 #[serde(default, skip_serializing_if = "Vec::is_empty")]
160 pub invalidates_views: Vec<String>,
161
162 /// Whether this mutation writes a Change-Spine change-log row (default `true`).
163 ///
164 /// Set `false` (via the authoring SDK's `@fraiseql.mutation(changelog=False)`)
165 /// to opt a single mutation out of the in-transaction change-log outbox while
166 /// the rest of the schema keeps logging. Defaults to `true` so a schema
167 /// authored before this field existed keeps logging.
168 #[serde(default = "default_changelog")]
169 pub changelog: bool,
170
171 /// How the GraphQL `input` argument is passed to the SQL function:
172 /// `"flatten"` (positional columns, the default) or `"jsonb"` (the whole
173 /// input as one `jsonb` arg). See [`InputStyle`].
174 ///
175 /// Orthogonal to [`operation`](IntermediateMutation::operation): set
176 /// `"jsonb"` (via the authoring SDK's
177 /// `@fraiseql.mutation(input_style="jsonb")`) so a backend using the
178 /// single-`jsonb`-wrapper convention can register the real DML verb and
179 /// still receive the whole input as one argument. Defaults to `"flatten"`,
180 /// byte-identical to a schema authored before this field existed.
181 #[serde(default, skip_serializing_if = "InputStyle::is_flatten")]
182 pub input_style: InputStyle,
183
184 /// Whether a successful, state-changing run of this mutation also records the
185 /// changed entity's pre-image (before-state) into the Change-Spine
186 /// `object_data_before` column, sourced from an optional `entity_before` on the
187 /// mutation's `app.mutation_response`.
188 ///
189 /// Set `true` (via the authoring SDK's
190 /// `@fraiseql.mutation(changelog_pre_image=True)` or TOML
191 /// `changelog_pre_image = true`) to opt an audit-sensitive mutation into an
192 /// inline Debezium-style `{before, after}`. Defaults to `false`,
193 /// byte-identical to a schema authored before this field existed.
194 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
195 pub changelog_pre_image: bool,
196}
197
198impl Default for IntermediateMutation {
199 /// Hand-written so `changelog` defaults to `true` (matching the serde default),
200 /// not `bool`'s `false`. A `#[derive(Default)]` would silently opt
201 /// `..Default::default()`-built mutations out of the change log.
202 fn default() -> Self {
203 Self {
204 name: String::new(),
205 return_type: String::new(),
206 returns_list: false,
207 nullable: false,
208 arguments: Vec::new(),
209 description: None,
210 sql_source: None,
211 operation: None,
212 deprecated: None,
213 inject: IndexMap::new(),
214 invalidates_fact_tables: Vec::new(),
215 invalidates_views: Vec::new(),
216 changelog: default_changelog(),
217 input_style: InputStyle::Flatten,
218 changelog_pre_image: false,
219 }
220 }
221}
222
223/// Serde default for [`IntermediateMutation::changelog`]: log by default (opt-out).
224const fn default_changelog() -> bool {
225 true
226}
227
228/// Auto-params configuration in intermediate format.
229///
230/// Each field is `Option<bool>`: `None` means "not specified — inherit from
231/// `[query_defaults]`"; `Some(v)` means explicitly set by the authoring-language decorator.
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
233pub struct IntermediateAutoParams {
234 /// Enable automatic limit parameter (None = inherit from query_defaults)
235 #[serde(default, skip_serializing_if = "Option::is_none")]
236 pub limit: Option<bool>,
237 /// Enable automatic offset parameter (None = inherit from query_defaults)
238 #[serde(default, skip_serializing_if = "Option::is_none")]
239 pub offset: Option<bool>,
240 /// Enable automatic where clause parameter (None = inherit from query_defaults)
241 #[serde(rename = "where", default, skip_serializing_if = "Option::is_none")]
242 pub where_clause: Option<bool>,
243 /// Enable automatic order_by parameter (None = inherit from query_defaults)
244 #[serde(default, skip_serializing_if = "Option::is_none")]
245 pub order_by: Option<bool>,
246}
247
248/// Global auto-param defaults for list queries (injected from TOML by the merger).
249///
250/// Never present in `schema.json` — set only at compile time via `[query_defaults]`
251/// in `fraiseql.toml`.
252///
253/// The `Default` implementation returns all-`true`, matching the historical behaviour
254/// when no `[query_defaults]` section is present in TOML.
255#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
256pub struct IntermediateQueryDefaults {
257 /// Default for `where` parameter
258 pub where_clause: bool,
259 /// Default for `order_by` parameter
260 pub order_by: bool,
261 /// Default for `limit` parameter
262 pub limit: bool,
263 /// Default for `offset` parameter
264 pub offset: bool,
265}
266
267impl Default for IntermediateQueryDefaults {
268 fn default() -> Self {
269 Self {
270 where_clause: true,
271 order_by: true,
272 limit: true,
273 offset: true,
274 }
275 }
276}