Skip to main content

fraiseql_cli/schema/intermediate/
types.rs

1//! Core type structs: `IntermediateType`, `IntermediateField`, `IntermediateEnum`,
2//! `IntermediateEnumValue`, `IntermediateScalar`, `IntermediateDeprecation`.
3
4use fraiseql_core::validation::ValidationRule;
5use serde::{Deserialize, Serialize};
6
7use super::fragments::IntermediateAppliedDirective;
8
9/// Type definition in intermediate format
10#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
11pub struct IntermediateType {
12    /// Type name (e.g., "User")
13    pub name: String,
14
15    /// Type fields
16    pub fields: Vec<IntermediateField>,
17
18    /// Type description (from docstring)
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub description: Option<String>,
21
22    /// Interfaces this type implements (GraphQL spec §3.6)
23    #[serde(default, skip_serializing_if = "Vec::is_empty")]
24    pub implements: Vec<String>,
25
26    /// Role required to see this type in introspection and access queries returning it.
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub requires_role: Option<String>,
29
30    /// Whether this type is a mutation error type (tagged with `@fraiseql.error`).
31    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
32    pub is_error: bool,
33
34    /// Whether this type implements the Relay Node interface.
35    /// When true, the compiler generates global node IDs (`base64("TypeName:uuid")`)
36    /// and validates that `pk_{entity}` (BIGINT) is present in the view's data JSONB.
37    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
38    pub relay: bool,
39
40    /// `@subscribable(tables=[...])` — the underlying base table(s) whose external
41    /// writes should be captured onto the Change Spine (#366).
42    ///
43    /// The compiler aggregates the set of `(name, tables)` for every type carrying
44    /// this into `CompiledSchema.subscribable`, which the
45    /// `generate_capture_trigger_ddl` generator turns into per-table capture
46    /// triggers. `None`/absent (the default) when the type is not subscribable.
47    ///
48    /// # Example
49    ///
50    /// ```json
51    /// {
52    ///   "name": "Post",
53    ///   "fields": [],
54    ///   "subscribable_tables": ["tb_post"]
55    /// }
56    /// ```
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub subscribable_tables: Option<Vec<String>>,
59
60    /// `@subscribable(..., pre_image=True)` — whether the capture triggers on this
61    /// type's tables also record the changed entity's pre-image (OLD) into
62    /// `object_data_before`, the out-of-band parity for a mutation's
63    /// `changelog_pre_image`. Only meaningful alongside `subscribable_tables`;
64    /// `false`/absent (the default) captures the after-image only.
65    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
66    pub subscribable_pre_image: bool,
67}
68
69/// Field definition in intermediate format
70///
71/// **NOTE**: Uses `type` field (not `field_type`)
72/// This is the language-agnostic format. Rust conversion happens in converter.
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub struct IntermediateField {
75    /// Field name (e.g., "id")
76    pub name: String,
77
78    /// Field type name (e.g., "Int", "String", "User")
79    ///
80    /// **Language-agnostic**: All languages use "type", not "`field_type`"
81    #[serde(rename = "type")]
82    pub field_type: String,
83
84    /// Is field nullable?
85    pub nullable: bool,
86
87    /// Field description (from docstring)
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub description: Option<String>,
90
91    /// Applied directives (e.g., @deprecated)
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub directives: Option<Vec<IntermediateAppliedDirective>>,
94
95    /// Scope required to access this field (field-level access control)
96    ///
97    /// When set, users must have this scope in their JWT to query this field.
98    /// Supports patterns like "read:Type.field" or custom scopes like "hr:view_pii".
99    ///
100    /// # Example
101    ///
102    /// ```json
103    /// {
104    ///   "name": "salary",
105    ///   "type": "Int",
106    ///   "nullable": false,
107    ///   "requires_scope": "read:Employee.salary"
108    /// }
109    /// ```
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub requires_scope: Option<String>,
112
113    /// Policy when the user lacks `requires_scope`: `"reject"` (default) or `"mask"`.
114    #[serde(default, skip_serializing_if = "Option::is_none")]
115    pub on_deny: Option<String>,
116
117    /// Whether this field is gated by the dynamic field authorizer at runtime.
118    ///
119    /// When `true`, the compiled field is marked policy-gated
120    /// (`FieldDefinition.authorize`) and a configured `FieldAuthorizer` is consulted
121    /// per row. Defaults to `false` when absent.
122    ///
123    /// # Example
124    ///
125    /// ```json
126    /// {
127    ///   "name": "email",
128    ///   "type": "String",
129    ///   "nullable": true,
130    ///   "authorize": true
131    /// }
132    /// ```
133    #[serde(default, skip_serializing_if = "Option::is_none")]
134    pub authorize: Option<bool>,
135
136    /// Named hierarchy reference for ID-based ltree operators.
137    /// References a key in the `hierarchies` config map.
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub hierarchy: Option<String>,
140}
141
142// =============================================================================
143// Enum Definitions
144// =============================================================================
145
146/// GraphQL enum type definition in intermediate format.
147///
148/// Enums represent a finite set of possible values.
149///
150/// # Example JSON
151///
152/// ```json
153/// {
154///   "name": "OrderStatus",
155///   "values": [
156///     {"name": "PENDING"},
157///     {"name": "PROCESSING"},
158///     {"name": "SHIPPED", "description": "Package has been shipped"},
159///     {"name": "DELIVERED"}
160///   ],
161///   "description": "Possible states of an order"
162/// }
163/// ```
164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165pub struct IntermediateEnum {
166    /// Enum type name (e.g., "OrderStatus")
167    pub name: String,
168
169    /// Possible values for this enum
170    pub values: Vec<IntermediateEnumValue>,
171
172    /// Enum description (from docstring)
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub description: Option<String>,
175}
176
177/// A single value within an enum type.
178///
179/// # Example JSON
180///
181/// ```json
182/// {
183///   "name": "ACTIVE",
184///   "description": "The item is currently active",
185///   "deprecated": {"reason": "Use ENABLED instead"}
186/// }
187/// ```
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
189pub struct IntermediateEnumValue {
190    /// Value name (e.g., "PENDING")
191    pub name: String,
192
193    /// Value description (from docstring)
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub description: Option<String>,
196
197    /// Deprecation info (if value is deprecated)
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub deprecated: Option<IntermediateDeprecation>,
200}
201
202/// Deprecation information for enum values or input fields.
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204pub struct IntermediateDeprecation {
205    /// Deprecation reason (what to use instead)
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub reason: Option<String>,
208}
209
210// =============================================================================
211// Custom Scalar Definitions
212// =============================================================================
213
214/// Custom scalar type definition in intermediate format.
215///
216/// Custom scalars allow applications to define domain-specific types with validation.
217/// Scalars are defined in language SDKs (Python, TypeScript, Java, Go, Rust)
218/// and compiled into the schema.
219///
220/// # Example JSON
221///
222/// ```json
223/// {
224///   "name": "Email",
225///   "description": "Valid email address",
226///   "specified_by_url": "https://tools.ietf.org/html/rfc5322",
227///   "base_type": "String",
228///   "validation_rules": [
229///     {
230///       "type": "pattern",
231///       "value": {
232///         "pattern": "^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$"
233///       }
234///     }
235///   ]
236/// }
237/// ```
238#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
239pub struct IntermediateScalar {
240    /// Scalar name (e.g., "Email", "Phone", "ISBN")
241    pub name: String,
242
243    /// Scalar description
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub description: Option<String>,
246
247    /// URL to specification/RFC (GraphQL spec §3.5.1)
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub specified_by_url: Option<String>,
250
251    /// Built-in validation rules
252    #[serde(default)]
253    pub validation_rules: Vec<ValidationRule>,
254
255    /// Base type for type aliases (e.g., "String" for Email scalar)
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub base_type: Option<String>,
258}