Skip to main content

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}