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