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 /// Whether this mutation exposes the typed graphql-cascade `cascade` field
198 /// on its success payload.
199 ///
200 /// Set `true` via the authoring SDK's `@fraiseql.type(crud=True,
201 /// cascade=True)` / `@fraiseql.mutation(cascade=True)`, or TOML
202 /// `cascade = true`. Defaults to `false`, byte-identical to a schema
203 /// authored before this field existed. Before this field, the compiler
204 /// silently dropped the SDK flag.
205 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
206 pub cascade: bool,
207}
208
209impl Default for IntermediateMutation {
210 /// Hand-written so `changelog` defaults to `true` (matching the serde default),
211 /// not `bool`'s `false`. A `#[derive(Default)]` would silently opt
212 /// `..Default::default()`-built mutations out of the change log.
213 fn default() -> Self {
214 Self {
215 name: String::new(),
216 return_type: String::new(),
217 returns_list: false,
218 nullable: false,
219 arguments: Vec::new(),
220 description: None,
221 sql_source: None,
222 operation: None,
223 deprecated: None,
224 inject: IndexMap::new(),
225 invalidates_fact_tables: Vec::new(),
226 invalidates_views: Vec::new(),
227 changelog: default_changelog(),
228 input_style: InputStyle::Flatten,
229 changelog_pre_image: false,
230 cascade: false,
231 }
232 }
233}
234
235/// Serde default for [`IntermediateMutation::changelog`]: log by default (opt-out).
236const fn default_changelog() -> bool {
237 true
238}
239
240/// Auto-params configuration in intermediate format.
241///
242/// Each field is `Option<bool>`: `None` means "not specified — inherit from
243/// `[query_defaults]`"; `Some(v)` means explicitly set by the authoring-language decorator.
244#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
245pub struct IntermediateAutoParams {
246 /// Enable automatic limit parameter (None = inherit from query_defaults)
247 #[serde(default, skip_serializing_if = "Option::is_none")]
248 pub limit: Option<bool>,
249 /// Enable automatic offset parameter (None = inherit from query_defaults)
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub offset: Option<bool>,
252 /// Enable automatic where clause parameter (None = inherit from query_defaults)
253 #[serde(rename = "where", default, skip_serializing_if = "Option::is_none")]
254 pub where_clause: Option<bool>,
255 /// Enable automatic order_by parameter (None = inherit from query_defaults)
256 #[serde(default, skip_serializing_if = "Option::is_none")]
257 pub order_by: Option<bool>,
258}
259
260/// Global auto-param defaults for list queries (injected from TOML by the merger).
261///
262/// Never present in `schema.json` — set only at compile time via `[query_defaults]`
263/// in `fraiseql.toml`.
264///
265/// The `Default` implementation returns all-`true`, matching the historical behaviour
266/// when no `[query_defaults]` section is present in TOML.
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
268pub struct IntermediateQueryDefaults {
269 /// Default for `where` parameter
270 pub where_clause: bool,
271 /// Default for `order_by` parameter
272 pub order_by: bool,
273 /// Default for `limit` parameter
274 pub limit: bool,
275 /// Default for `offset` parameter
276 pub offset: bool,
277}
278
279impl Default for IntermediateQueryDefaults {
280 fn default() -> Self {
281 Self {
282 where_clause: true,
283 order_by: true,
284 limit: true,
285 offset: true,
286 }
287 }
288}