fraiseql_core/schema/compiled/
federation_keys.rs1use std::{collections::HashSet, fmt};
38
39use super::CompiledSchema;
40use crate::{
41 runtime::suggest_similar,
42 schema::{FieldDefinition, FieldType},
43 utils::casing::to_camel_case,
44};
45
46#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum FederationKeyProblem {
51 UnknownType {
53 entity: String,
55 suggestions: Vec<String>,
57 },
58 DuplicateEntity {
60 entity: String,
62 },
63 EmbeddedType {
65 entity: String,
67 },
68 EmptyKey {
70 entity: String,
72 },
73 MalformedFieldSet {
76 entity: String,
78 field_set: String,
80 reason: String,
82 },
83 MissingField {
85 entity: String,
87 path: String,
89 owner: String,
91 hint: Option<String>,
93 },
94 SelectionOnLeaf {
96 entity: String,
98 path: String,
100 },
101 ObjectWithoutSelection {
103 entity: String,
105 path: String,
107 },
108 InvalidKeyFieldType {
111 entity: String,
113 path: String,
115 kind: &'static str,
117 },
118 UnknownNestedType {
120 entity: String,
122 path: String,
124 type_name: String,
126 },
127}
128
129impl fmt::Display for FederationKeyProblem {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 match self {
132 Self::UnknownType {
133 entity,
134 suggestions,
135 } => {
136 write!(
137 f,
138 "federation entity '{entity}' names no object type or interface this \
139 schema declares"
140 )?;
141 if !suggestions.is_empty() {
142 write!(f, " (did you mean: {}?)", suggestions.join(", "))?;
143 }
144 Ok(())
145 },
146 Self::DuplicateEntity { entity } => write!(
147 f,
148 "federation entity '{entity}' is declared more than once; one entity carries \
149 one `@key`, so every declaration after the first would be dropped from the SDL"
150 ),
151 Self::EmbeddedType { entity } => write!(
152 f,
153 "federation entity '{entity}' is an embedded value type: it has no identity of \
154 its own and no `sql_source`, so `_entities` could never resolve it. Drop it \
155 from `federation.entities` (it is served as part of its parent)"
156 ),
157 Self::EmptyKey { entity } => {
158 write!(f, "federation entity '{entity}' has an `@key` that selects no field")
159 },
160 Self::MalformedFieldSet {
161 entity,
162 field_set,
163 reason,
164 } => write!(
165 f,
166 "federation entity '{entity}' has `@key(fields: \"{field_set}\")`, which is not \
167 a field set: {reason}"
168 ),
169 Self::MissingField {
170 entity,
171 path,
172 owner,
173 hint,
174 } => {
175 write!(
176 f,
177 "federation entity '{entity}' is keyed on '{path}', but type '{owner}' has \
178 no field '{}'; the router would refuse the supergraph with \
179 KEY_INVALID_FIELDS",
180 path.rsplit('.').next().unwrap_or(path)
181 )?;
182 if let Some(hint) = hint {
183 write!(f, " (did you mean '{hint}'? keys name the published field)")?;
184 }
185 Ok(())
186 },
187 Self::SelectionOnLeaf { entity, path } => write!(
188 f,
189 "federation entity '{entity}' selects sub-fields of '{path}', which has none"
190 ),
191 Self::ObjectWithoutSelection { entity, path } => write!(
192 f,
193 "federation entity '{entity}' is keyed on '{path}', an object; a key must \
194 select the object's fields, e.g. '{path} {{ id }}'"
195 ),
196 Self::InvalidKeyFieldType { entity, path, kind } => write!(
197 f,
198 "federation entity '{entity}' is keyed on '{path}', which is {kind}; a key \
199 cannot contain a list, interface, union or input-object field \
200 (KEY_FIELDS_SELECT_INVALID_TYPE)"
201 ),
202 Self::UnknownNestedType {
203 entity,
204 path,
205 type_name,
206 } => write!(
207 f,
208 "federation entity '{entity}' selects into '{path}', whose type '{type_name}' \
209 this schema does not declare"
210 ),
211 }
212 }
213}
214
215#[derive(Debug)]
217struct Selection {
218 name: String,
219 children: Vec<Selection>,
220}
221
222fn parse_field_set(input: &str) -> Result<Vec<Selection>, String> {
224 let mut stack: Vec<Vec<Selection>> = vec![Vec::new()];
225 let mut chars = input.char_indices().peekable();
226
227 while let Some(&(start, c)) = chars.peek() {
228 if c.is_whitespace() || c == ',' {
229 chars.next();
230 } else if c == '_' || c.is_ascii_alphabetic() {
231 let mut end = start;
232 while let Some(&(i, c)) = chars.peek() {
233 if c == '_' || c.is_ascii_alphanumeric() {
234 end = i + c.len_utf8();
235 chars.next();
236 } else {
237 break;
238 }
239 }
240 let level = stack.last_mut().ok_or("unbalanced braces")?;
241 level.push(Selection {
242 name: input[start..end].to_string(),
243 children: Vec::new(),
244 });
245 } else if c == '{' {
246 chars.next();
247 let has_parent = stack
248 .last()
249 .is_some_and(|level| level.last().is_some_and(|s| s.children.is_empty()));
250 if !has_parent {
251 return Err("'{' must follow the field it selects into".to_string());
252 }
253 stack.push(Vec::new());
254 } else if c == '}' {
255 chars.next();
256 if stack.len() < 2 {
257 return Err("unbalanced braces".to_string());
258 }
259 let children = stack.pop().unwrap_or_default();
260 if children.is_empty() {
261 return Err("an empty selection '{}'".to_string());
262 }
263 let parent =
264 stack.last_mut().and_then(|level| level.last_mut()).ok_or("unbalanced braces")?;
265 parent.children = children;
266 } else {
267 return Err(format!(
268 "unexpected '{c}' (aliases, arguments, directives and fragments have no \
269 meaning in a key)"
270 ));
271 }
272 }
273
274 if stack.len() != 1 {
275 return Err("unbalanced braces".to_string());
276 }
277 Ok(stack.pop().unwrap_or_default())
278}
279
280fn lookup<'a>(
282 fields: &'a [FieldDefinition],
283 name: &str,
284) -> Result<&'a FieldDefinition, Option<String>> {
285 if let Some(field) = fields.iter().find(|f| f.name == name) {
286 return Ok(field);
287 }
288 let wanted = to_camel_case(name);
289 Err(fields
290 .iter()
291 .find(|f| to_camel_case(f.name.as_str()) == wanted)
292 .map(|f| f.name.to_string()))
293}
294
295impl CompiledSchema {
296 #[must_use]
303 pub fn federation_key_problems(&self) -> Vec<FederationKeyProblem> {
304 let Some(federation) = &self.federation else {
305 return Vec::new();
306 };
307 let mut problems = Vec::new();
308 let mut seen: HashSet<&str> = HashSet::new();
309
310 for entity in &federation.entities {
311 let name = entity.name.as_str();
312 if !seen.insert(name) {
313 problems.push(FederationKeyProblem::DuplicateEntity {
314 entity: name.to_string(),
315 });
316 continue;
317 }
318
319 let fields = if let Some(type_def) = self.find_type(name) {
320 if type_def.embedded {
321 problems.push(FederationKeyProblem::EmbeddedType {
322 entity: name.to_string(),
323 });
324 continue;
325 }
326 &type_def.fields
327 } else if let Some(interface) = self.find_interface(name) {
328 &interface.fields
329 } else {
330 let known: Vec<&str> = self
331 .types
332 .iter()
333 .map(|t| t.name.as_str())
334 .chain(self.interfaces.iter().map(|i| i.name.as_str()))
335 .collect();
336 problems.push(FederationKeyProblem::UnknownType {
337 entity: name.to_string(),
338 suggestions: suggest_similar(name, &known)
339 .into_iter()
340 .map(ToString::to_string)
341 .collect(),
342 });
343 continue;
344 };
345
346 let field_set = entity.key_fields.join(" ");
347 match parse_field_set(&field_set) {
348 Err(reason) => problems.push(FederationKeyProblem::MalformedFieldSet {
349 entity: name.to_string(),
350 field_set,
351 reason,
352 }),
353 Ok(selections) if selections.is_empty() => {
354 problems.push(FederationKeyProblem::EmptyKey {
355 entity: name.to_string(),
356 });
357 },
358 Ok(selections) => {
359 self.check_selections(name, name, "", fields, &selections, &mut problems);
360 },
361 }
362 }
363 problems
364 }
365
366 #[must_use]
368 pub fn federation_key_violations(&self) -> Vec<String> {
369 self.federation_key_problems().iter().map(ToString::to_string).collect()
370 }
371
372 fn check_selections(
373 &self,
374 entity: &str,
375 owner: &str,
376 prefix: &str,
377 fields: &[FieldDefinition],
378 selections: &[Selection],
379 problems: &mut Vec<FederationKeyProblem>,
380 ) {
381 for selection in selections {
382 let path = if prefix.is_empty() {
383 selection.name.clone()
384 } else {
385 format!("{prefix}.{}", selection.name)
386 };
387 let field = match lookup(fields, &selection.name) {
388 Ok(field) => field,
389 Err(hint) => {
390 problems.push(FederationKeyProblem::MissingField {
391 entity: entity.to_string(),
392 path,
393 owner: owner.to_string(),
394 hint,
395 });
396 continue;
397 },
398 };
399
400 let invalid = |kind| FederationKeyProblem::InvalidKeyFieldType {
401 entity: entity.to_string(),
402 path: path.clone(),
403 kind,
404 };
405 match &field.field_type {
406 FieldType::List(_) => problems.push(invalid("a list")),
407 FieldType::Interface(_) => problems.push(invalid("an interface")),
408 FieldType::Union(_) => problems.push(invalid("a union")),
409 FieldType::Input(_) => problems.push(invalid("an input object")),
410 FieldType::Object(type_name) => {
411 if selection.children.is_empty() {
412 problems.push(FederationKeyProblem::ObjectWithoutSelection {
413 entity: entity.to_string(),
414 path,
415 });
416 } else if let Some(nested) = self.find_type(type_name) {
417 self.check_selections(
418 entity,
419 type_name,
420 &path,
421 &nested.fields,
422 &selection.children,
423 problems,
424 );
425 } else {
426 problems.push(FederationKeyProblem::UnknownNestedType {
427 entity: entity.to_string(),
428 path,
429 type_name: type_name.clone(),
430 });
431 }
432 },
433 FieldType::String
434 | FieldType::Int
435 | FieldType::Float
436 | FieldType::Boolean
437 | FieldType::Id
438 | FieldType::DateTime
439 | FieldType::Date
440 | FieldType::Time
441 | FieldType::Json
442 | FieldType::Uuid
443 | FieldType::Decimal
444 | FieldType::Vector
445 | FieldType::BitVector
446 | FieldType::HalfVector
447 | FieldType::SparseVector
448 | FieldType::Scalar(_)
449 | FieldType::Enum(_) => {
450 if !selection.children.is_empty() {
451 problems.push(FederationKeyProblem::SelectionOnLeaf {
452 entity: entity.to_string(),
453 path,
454 });
455 }
456 },
457 }
458 }
459 }
460}