Skip to main content

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, Default, 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
162/// Auto-params configuration in intermediate format.
163///
164/// Each field is `Option<bool>`: `None` means "not specified — inherit from
165/// `[query_defaults]`"; `Some(v)` means explicitly set by the authoring-language decorator.
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167pub struct IntermediateAutoParams {
168    /// Enable automatic limit parameter (None = inherit from query_defaults)
169    #[serde(default, skip_serializing_if = "Option::is_none")]
170    pub limit:        Option<bool>,
171    /// Enable automatic offset parameter (None = inherit from query_defaults)
172    #[serde(default, skip_serializing_if = "Option::is_none")]
173    pub offset:       Option<bool>,
174    /// Enable automatic where clause parameter (None = inherit from query_defaults)
175    #[serde(rename = "where", default, skip_serializing_if = "Option::is_none")]
176    pub where_clause: Option<bool>,
177    /// Enable automatic order_by parameter (None = inherit from query_defaults)
178    #[serde(default, skip_serializing_if = "Option::is_none")]
179    pub order_by:     Option<bool>,
180}
181
182/// Global auto-param defaults for list queries (injected from TOML by the merger).
183///
184/// Never present in `schema.json` — set only at compile time via `[query_defaults]`
185/// in `fraiseql.toml`.
186///
187/// The `Default` implementation returns all-`true`, matching the historical behaviour
188/// when no `[query_defaults]` section is present in TOML.
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
190pub struct IntermediateQueryDefaults {
191    /// Default for `where` parameter
192    pub where_clause: bool,
193    /// Default for `order_by` parameter
194    pub order_by:     bool,
195    /// Default for `limit` parameter
196    pub limit:        bool,
197    /// Default for `offset` parameter
198    pub offset:       bool,
199}
200
201impl Default for IntermediateQueryDefaults {
202    fn default() -> Self {
203        Self {
204            where_clause: true,
205            order_by:     true,
206            limit:        true,
207            offset:       true,
208        }
209    }
210}