Skip to content

Commit

Permalink
graphql,store: child filters on interface-type fields
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilkisiela committed Jul 15, 2022
1 parent cb0a898 commit 15ffb98
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 34 deletions.
30 changes: 9 additions & 21 deletions graphql/src/schema/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ fn field_filter_input_values(
.get_named_type(name)
.ok_or_else(|| APISchemaError::TypeNotFound(name.clone()))?;
Ok(match named_type {
TypeDefinition::Object(_) => {
TypeDefinition::Object(_) | TypeDefinition::Interface(_) => {
let mut input_values = match ast::get_derived_from_directive(field) {
// Only add `where` filter fields for object and interface fields
// if they are not @derivedFrom
Expand All @@ -246,23 +246,6 @@ fn field_filter_input_values(
extend_with_child_filter_input_value(field, name, &mut input_values);
input_values
}
TypeDefinition::Interface(_) => {
// Only add `where` filter fields for object and interface fields
// if they are not @derivedFrom
if ast::get_derived_from_directive(field).is_some() {
vec![]
} else {
// We allow filtering with `where: { other: "some-id" }` and
// `where: { others: ["some-id", "other-id"] }`. In both cases,
// we allow ID strings as the values to be passed to these
// filters.
field_scalar_filter_input_values(
schema,
field,
&ScalarType::new(String::from("String")),
)
}
}
TypeDefinition::Scalar(ref t) => field_scalar_filter_input_values(schema, field, t),
TypeDefinition::Enum(ref t) => field_enum_filter_input_values(schema, field, t),
_ => vec![],
Expand Down Expand Up @@ -381,11 +364,14 @@ fn field_list_filter_input_values(
)
}
}
TypeDefinition::Interface(_) => {
TypeDefinition::Interface(parent) => {
if ast::get_derived_from_directive(field).is_some() {
(None, None)
(None, Some(parent.name.clone()))
} else {
(Some(Type::NamedType("String".into())), None)
(
Some(Type::NamedType("String".into())),
Some(parent.name.clone()),
)
}
}
TypeDefinition::Scalar(ref t) => (Some(Type::NamedType(t.name.to_owned())), None),
Expand Down Expand Up @@ -1174,6 +1160,7 @@ mod tests {
"name_ends_with_nocase",
"name_not_ends_with",
"name_not_ends_with_nocase",
"pets_",
"favoritePet",
"favoritePet_not",
"favoritePet_gt",
Expand All @@ -1194,6 +1181,7 @@ mod tests {
"favoritePet_ends_with_nocase",
"favoritePet_not_ends_with",
"favoritePet_not_ends_with_nocase",
"favoritePet_",
"_change_block"
]
.iter()
Expand Down
45 changes: 32 additions & 13 deletions graphql/src/store/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,21 +261,40 @@ fn build_child_filter_from_object(
let child_entity = schema
.object_or_interface(type_name)
.ok_or(QueryExecutionError::InvalidFilterError)?;
let filter = build_filter_from_object(child_entity, object, schema)?;
let filter = Box::new(build_filter_from_object(child_entity, object, schema)?);
let derived = field.is_derived();
let attr = match derived {
true => sast::get_derived_from_field(child_entity, field)
.ok_or(QueryExecutionError::InvalidFilterError)?
.name
.to_string(),
false => field_name,
};

Ok(EntityFilter::Child(Child {
attr: match derived {
true => sast::get_derived_from_field(child_entity, field)
.ok_or(QueryExecutionError::InvalidFilterError)?
.name
.to_string(),
false => field_name,
},
entity_type: EntityType::new(type_name.to_string()),
filter: Box::new(filter),
derived,
}))
if child_entity.is_interface() {
Ok(EntityFilter::Or(
child_entity
.object_types(schema.schema())
.expect("Interface is not implemented by any types")
.iter()
.map(|object_type| {
EntityFilter::Child(Child {
attr: attr.clone(),
entity_type: EntityType::new(object_type.name.to_string()),
filter: filter.clone(),
derived,
})
})
.collect(),
))
} else {
Ok(EntityFilter::Child(Child {
attr,
entity_type: EntityType::new(type_name.to_string()),
filter,
derived,
}))
}
}

/// Parses a list of GraphQL values into a vector of entity field values.
Expand Down
95 changes: 95 additions & 0 deletions graphql/tests/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ use test_store::{
const NETWORK_NAME: &str = "fake_network";
const SONGS_STRING: [&str; 5] = ["s0", "s1", "s2", "s3", "s4"];
const SONGS_BYTES: [&str; 5] = ["0xf0", "0xf1", "0xf2", "0xf3", "0xf4"];
const REVIEWS_STRING: [&str; 5] = ["r0", "r1", "r2", "r3", "r4"];
const REVIEWS_BYTES: [&str; 5] = ["0xf0", "0xf1", "0xf2", "0xf3", "0xf4"];

#[derive(Clone, Copy, Debug)]
enum IdType {
Expand All @@ -57,6 +59,13 @@ impl IdType {
}
}

fn reviews(&self) -> &[&str] {
match self {
IdType::String => REVIEWS_STRING.as_slice(),
IdType::Bytes => REVIEWS_BYTES.as_slice(),
}
}

fn as_str(&self) -> &str {
match self {
IdType::String => "String",
Expand Down Expand Up @@ -154,6 +163,7 @@ fn test_schema(id: DeploymentHash, id_type: IdType) -> Schema {
id: ID!
name: String!
members: [Musician!]! @derivedFrom(field: \"bands\")
reviews: [BandReview] @derivedFrom(field: \"band\")
originalSongs: [Song!]!
}
Expand All @@ -163,6 +173,7 @@ fn test_schema(id: DeploymentHash, id_type: IdType) -> Schema {
writtenBy: Musician!
publisher: Publisher!
band: Band @derivedFrom(field: \"originalSongs\")
reviews: [SongReview] @derivedFrom(field: \"song\")
}
type SongStat @entity {
Expand All @@ -174,6 +185,34 @@ fn test_schema(id: DeploymentHash, id_type: IdType) -> Schema {
type Publisher {
id: Bytes!
}
interface Review {
id: ID!
body: String!
author: User!
}
type SongReview implements Review @entity {
id: ID!
body: String!
song: Song
author: User!
}
type BandReview implements Review @entity {
id: ID!
body: String!
band: Band
author: User!
}
type User @entity {
id: ID!
name: String!
bandReviews: [BandReview] @derivedFrom(field: \"author\")
songReviews: [SongReview] @derivedFrom(field: \"author\")
reviews: [Review] @derivedFrom(field: \"author\")
}
";

Schema::parse(&SCHEMA.replace("@ID@", id_type.as_str()), id).expect("Test schema invalid")
Expand All @@ -199,6 +238,7 @@ async fn insert_test_entities(
.unwrap();

let s = id_type.songs();
let r = id_type.reviews();
let entities0 = vec![
entity! { __typename: "Musician", id: "m1", name: "John", mainBand: "b1", bands: vec!["b1", "b2"] },
entity! { __typename: "Musician", id: "m2", name: "Lisa", mainBand: "b1", bands: vec!["b1"] },
Expand All @@ -211,6 +251,12 @@ async fn insert_test_entities(
entity! { __typename: "Song", id: s[4], title: "Folk Tune", publisher: "0xb1", writtenBy: "m3" },
entity! { __typename: "SongStat", id: s[1], played: 10 },
entity! { __typename: "SongStat", id: s[2], played: 15 },
entity! { __typename: "BandReview", id: r[1], body: "Bad musicians", band: "b1", author: "u1" },
entity! { __typename: "BandReview", id: r[2], body: "Good amateurs", band: "b2", author: "u2" },
entity! { __typename: "SongReview", id: r[3], body: "Bad", song: s[2], author: "u1" },
entity! { __typename: "SongReview", id: r[4], body: "Good", song: s[3], author: "u2" },
entity! { __typename: "User", id: "u1", name: "Baden" },
entity! { __typename: "User", id: "u2", name: "Goodwill"},
];

let entities1 = vec![
Expand Down Expand Up @@ -653,6 +699,55 @@ fn can_query_with_child_filter_on_derived_named_type_field() {
})
}

#[test]
fn can_query_an_interface_with_child_filter_on_named_type_field() {
const QUERY: &str = "
query {
reviews(first: 100, orderBy: id, where: { author_: { name_starts_with: \"Good\" } }) {
body
author {
name
}
}
}";

run_query(QUERY, |result, _| {
let exp = object! {
reviews: vec![
object! { body: "Good amateurs", author: object! { name: "Goodwill" } },
object! { body: "Good", author: object! { name: "Goodwill" } },
]
};

let data = extract_data!(result).unwrap();
assert_eq!(data, exp);
})
}

#[test]
fn can_query_with_child_filter_on_interface_field() {
const QUERY: &str = "
query {
users(first: 100, orderBy: id, where: { reviews_: { body_starts_with: \"Good\" } }) {
name
reviews {
body
}
}
}";

run_query(QUERY, |result, _| {
let exp = object! {
users: vec![
object! { name: "Goodwill", reviews: vec![ object! { body: "Good amateurs" }, object! { body: "Good" } ] },
]
};

let data = extract_data!(result).unwrap();
assert_eq!(data, exp);
})
}

#[test]
fn root_fragments_are_expanded() {
const QUERY: &str = r#"
Expand Down

0 comments on commit 15ffb98

Please sign in to comment.