use ahash::HashMap;
use crate::query_planner::{
ast::selection_item::SelectionItem, federation_spec::FederationRules,
state::supergraph_state::SupergraphState,
};
pub type KeyFields = Vec<String>;
pub type TypeKeys = Vec<KeyFields>;
pub type KeysByType = HashMap<String, TypeKeys>;
#[derive(Debug, Default)]
pub struct EntityKeys(HashMap<String, KeysByType>);
impl EntityKeys {
pub fn new(supergraph: &SupergraphState) -> Self {
let mut subgraphs: HashMap<String, KeysByType> = HashMap::default();
for (type_name, definition) in &supergraph.definitions {
for join_type in definition.join_types() {
let (true, Some(key)) = (join_type.resolvable, &join_type.key) else {
continue;
};
let Some(subgraph) = supergraph.known_subgraphs.get(&join_type.graph_id) else {
continue;
};
let selection =
FederationRules::parse_key(supergraph, &join_type.graph_id, type_name, key);
let fields = selection
.selection_set
.items
.iter()
.filter_map(|item| match item {
SelectionItem::Field(field) if field.name != "__typename" => {
Some(field.name.clone())
}
_ => None,
})
.collect();
subgraphs
.entry(subgraph.clone())
.or_default()
.entry(type_name.clone())
.or_default()
.push(fields);
}
}
Self(subgraphs)
}
pub fn of_subgraph(&self, subgraph: &str) -> Option<&KeysByType> {
self.0.get(subgraph)
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::{EntityKeys, TypeKeys};
use crate::query_planner::{
state::supergraph_state::SupergraphState, utils::parsing::parse_schema,
};
fn keys_of<'a>(keys: &'a EntityKeys, subgraph: &str, type_name: &str) -> Vec<Vec<&'a str>> {
let type_keys: &TypeKeys = keys
.of_subgraph(subgraph)
.and_then(|types| types.get(type_name))
.unwrap_or_else(|| panic!("no keys of {type_name} in {subgraph}"));
let mut keys: Vec<Vec<&str>> = type_keys
.iter()
.map(|fields| {
let mut fields: Vec<&str> = fields.iter().map(String::as_str).collect();
fields.sort_unstable();
fields
})
.collect();
keys.sort();
keys
}
#[test]
fn resolvable_keys_of_each_subgraph() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("fixture/tests/keys-mashup.supergraph.graphql");
let sdl = std::fs::read_to_string(path).expect("fixture should be readable");
let supergraph = SupergraphState::new(&parse_schema(&sdl));
let keys = &supergraph.entity_keys;
assert_eq!(keys_of(keys, "a", "A"), [["id"]]);
assert_eq!(keys_of(keys, "b", "A"), [["compositeId", "id"]]);
assert!(keys
.of_subgraph("a")
.is_some_and(|types| !types.contains_key("B")));
assert_eq!(keys_of(keys, "b", "B"), [["id"]]);
}
#[test]
fn typename_key_has_no_fields() {
let schema = parse_schema(
r#"
enum join__Graph {
CONTENT @join__graph(name: "content", url: "")
PETSEARCH @join__graph(name: "petsearch", url: "")
}
type Query @join__type(graph: CONTENT) {
favoritesPage: FavoritesPage @join__field(graph: CONTENT)
}
type FavoritesPage
@join__type(graph: CONTENT, key: "__typename")
@join__type(graph: PETSEARCH, key: "__typename") {
title: String @join__field(graph: CONTENT)
petCount: Int @join__field(graph: PETSEARCH)
}
"#,
);
let supergraph = SupergraphState::new(&schema);
let keys = &supergraph.entity_keys;
let no_fields: [[&str; 0]; 1] = [[]];
assert_eq!(keys_of(keys, "petsearch", "FavoritesPage"), no_fields);
assert_eq!(keys_of(keys, "content", "FavoritesPage"), no_fields);
}
}