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