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}