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}