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