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