Skip to content

Commit 4dbc1ce

Browse files
committed
fix(federation): keep renamed interfaces in their directives' referencers
Renaming an interface type left the old interface name in `Referencers.directives[d].interface_types` for every directive applied to it. The schema stayed valid, but directive lookups through the index (as used for `@key`, `@tag`, `@inaccessible` and other directive processing) named a type that no longer existed. `Referencers::rename_interface_type` now remaps directive target positions for the interface itself, as it already did for its fields and arguments.
1 parent aff43a1 commit 4dbc1ce

2 files changed

Lines changed: 202 additions & 0 deletions

File tree

‎apollo-federation/src/schema/position.rs‎

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8383,4 +8383,184 @@ mod tests {
83838383
}
83848384
"#);
83858385
}
8386+
8387+
/// Flattens a referencer index into an order-independent set of facts, so an incrementally
8388+
/// maintained index can be compared with one rebuilt from scratch.
8389+
fn referencer_facts(referencers: &Referencers) -> std::collections::BTreeSet<String> {
8390+
fn add<T: Debug>(
8391+
facts: &mut std::collections::BTreeSet<String>,
8392+
kind: &str,
8393+
key: &Name,
8394+
slot: &str,
8395+
positions: impl IntoIterator<Item = T>,
8396+
) {
8397+
facts.insert(format!("{kind} {key}"));
8398+
for position in positions {
8399+
facts.insert(format!("{kind} {key} {slot} {position:?}"));
8400+
}
8401+
}
8402+
let mut facts = std::collections::BTreeSet::new();
8403+
let f = &mut facts;
8404+
for (key, r) in &referencers.scalar_types {
8405+
add(f, "scalar", key, "object_fields", &r.object_fields);
8406+
add(
8407+
f,
8408+
"scalar",
8409+
key,
8410+
"object_field_arguments",
8411+
&r.object_field_arguments,
8412+
);
8413+
add(f, "scalar", key, "interface_fields", &r.interface_fields);
8414+
add(
8415+
f,
8416+
"scalar",
8417+
key,
8418+
"interface_field_arguments",
8419+
&r.interface_field_arguments,
8420+
);
8421+
add(f, "scalar", key, "union_fields", &r.union_fields);
8422+
add(
8423+
f,
8424+
"scalar",
8425+
key,
8426+
"input_object_fields",
8427+
&r.input_object_fields,
8428+
);
8429+
add(
8430+
f,
8431+
"scalar",
8432+
key,
8433+
"directive_arguments",
8434+
&r.directive_arguments,
8435+
);
8436+
}
8437+
for (key, r) in &referencers.object_types {
8438+
add(f, "object", key, "schema_roots", &r.schema_roots);
8439+
add(f, "object", key, "object_fields", &r.object_fields);
8440+
add(f, "object", key, "interface_fields", &r.interface_fields);
8441+
add(f, "object", key, "union_types", &r.union_types);
8442+
}
8443+
for (key, r) in &referencers.interface_types {
8444+
add(f, "interface", key, "object_types", &r.object_types);
8445+
add(f, "interface", key, "object_fields", &r.object_fields);
8446+
add(f, "interface", key, "interface_types", &r.interface_types);
8447+
add(f, "interface", key, "interface_fields", &r.interface_fields);
8448+
}
8449+
for (key, r) in &referencers.union_types {
8450+
add(f, "union", key, "object_fields", &r.object_fields);
8451+
add(f, "union", key, "interface_fields", &r.interface_fields);
8452+
}
8453+
for (key, r) in &referencers.enum_types {
8454+
add(f, "enum", key, "object_fields", &r.object_fields);
8455+
add(
8456+
f,
8457+
"enum",
8458+
key,
8459+
"object_field_arguments",
8460+
&r.object_field_arguments,
8461+
);
8462+
add(f, "enum", key, "interface_fields", &r.interface_fields);
8463+
add(
8464+
f,
8465+
"enum",
8466+
key,
8467+
"interface_field_arguments",
8468+
&r.interface_field_arguments,
8469+
);
8470+
add(
8471+
f,
8472+
"enum",
8473+
key,
8474+
"input_object_fields",
8475+
&r.input_object_fields,
8476+
);
8477+
add(
8478+
f,
8479+
"enum",
8480+
key,
8481+
"directive_arguments",
8482+
&r.directive_arguments,
8483+
);
8484+
}
8485+
for (key, r) in &referencers.input_object_types {
8486+
add(
8487+
f,
8488+
"input",
8489+
key,
8490+
"object_field_arguments",
8491+
&r.object_field_arguments,
8492+
);
8493+
add(
8494+
f,
8495+
"input",
8496+
key,
8497+
"interface_field_arguments",
8498+
&r.interface_field_arguments,
8499+
);
8500+
add(
8501+
f,
8502+
"input",
8503+
key,
8504+
"input_object_fields",
8505+
&r.input_object_fields,
8506+
);
8507+
add(
8508+
f,
8509+
"input",
8510+
key,
8511+
"directive_arguments",
8512+
&r.directive_arguments,
8513+
);
8514+
}
8515+
for (key, r) in &referencers.directives {
8516+
add(f, "directive", key, "targets", r.iter());
8517+
}
8518+
facts
8519+
}
8520+
8521+
/// Renames a type through the public entry point, then checks the edit contract: the compiler
8522+
/// schema is still valid, and the incrementally maintained referencers equal referencers
8523+
/// rebuilt from scratch from the edited schema.
8524+
fn rename_and_check(sdl: &str, old_name: Name, new_name: Name) -> FederationSchema {
8525+
let mut schema = FederationSchema::new(
8526+
Schema::parse_and_validate(sdl, "rename.graphql")
8527+
.unwrap()
8528+
.into_inner(),
8529+
)
8530+
.unwrap();
8531+
schema
8532+
.get_type(&old_name)
8533+
.unwrap()
8534+
.rename(&mut schema, new_name)
8535+
.unwrap();
8536+
let valid = schema.schema().clone().validate();
8537+
assert!(valid.is_ok(), "renamed schema is invalid: {valid:?}");
8538+
let rebuilt = FederationSchema::new(schema.schema().clone()).unwrap();
8539+
let actual = referencer_facts(schema.referencers());
8540+
let expected = referencer_facts(rebuilt.referencers());
8541+
let stale: Vec<_> = actual.difference(&expected).collect();
8542+
let missing: Vec<_> = expected.difference(&actual).collect();
8543+
assert!(
8544+
stale.is_empty() && missing.is_empty(),
8545+
"referencers diverge from a rebuild after renaming {old_name}\n\
8546+
stale: {stale:#?}\nmissing: {missing:#?}"
8547+
);
8548+
schema
8549+
}
8550+
8551+
#[test]
8552+
fn renamed_interface_keeps_its_directive_referencer_position() {
8553+
let schema = rename_and_check(
8554+
"directive @mark on INTERFACE interface I @mark { x: String } type Query { i: I }",
8555+
name!("I"),
8556+
name!("Renamed"),
8557+
);
8558+
let targets = &schema.referencers().directives["mark"].interface_types;
8559+
assert!(targets.contains(&InterfaceTypeDefinitionPosition {
8560+
type_name: name!("Renamed")
8561+
}));
8562+
assert!(!targets.contains(&InterfaceTypeDefinitionPosition {
8563+
type_name: name!("I")
8564+
}));
8565+
}
83868566
}

‎apollo-federation/src/schema/referencer.rs‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,11 @@ impl Referencers {
519519
}
520520

521521
for (_directive_name, directive_refs) in self.directives.iter_mut() {
522+
Self::update_interface_type_positions(
523+
&mut directive_refs.interface_types,
524+
old_name,
525+
new_name,
526+
);
522527
Self::update_interface_field_positions(
523528
&mut directive_refs.interface_fields,
524529
old_name,
@@ -532,6 +537,23 @@ impl Referencers {
532537
}
533538
}
534539

540+
fn update_interface_type_positions(
541+
types: &mut IndexSet<InterfaceTypeDefinitionPosition>,
542+
old_type_name: &Name,
543+
new_type_name: &Name,
544+
) {
545+
let updated_types: Vec<_> = types
546+
.iter()
547+
.filter(|t| &t.type_name == old_type_name)
548+
.map(|_| InterfaceTypeDefinitionPosition {
549+
type_name: new_type_name.clone(),
550+
})
551+
.collect();
552+
553+
types.retain(|t| &t.type_name != old_type_name);
554+
types.extend(updated_types);
555+
}
556+
535557
fn update_interface_field_positions(
536558
fields: &mut IndexSet<InterfaceFieldDefinitionPosition>,
537559
old_type_name: &Name,

0 commit comments

Comments
 (0)