Resolve nested object boolean expressions (#680)

<!-- Thank you for submitting this PR! :) -->

## Description

This adds the ability to describe nested object boolean expressions,
which become `fieldPath` items in the generated
`ndc_models::ComparisonTarget::Column` items. This allows us to describe
filtering a `User` based on some element in their nested `address` field
(like `postcode`, for example).

Like the other `BooleanExpressionType` work, this remains behind a
feature flag so should make no user-facing changes.

It is also missing a whole heap of metadata resolve checks, going to
follow with these after doing the happy path to unblock other work.

V3_GIT_ORIGIN_REV_ID: c89e2942a651d349fca97706affcf40d91afeefb
This commit is contained in:
Daniel Harvey 2024-06-11 13:56:33 +01:00 committed by hasura-bot
parent 1c6b1dffc8
commit 75ced29d11
23 changed files with 2188 additions and 161 deletions

View File

@ -103,8 +103,9 @@ ALTER SEQUENCE public.movie_analytics_id_seq OWNED BY public.movie_analytics.id;
ALTER TABLE ONLY public.movie_analytics ALTER COLUMN id SET DEFAULT nextval('public.movie_analytics_id_seq'::regclass);
-- `institution` table for testing queries into JSON objects
CREATE TYPE public.country AS (name text, continent text);
CREATE TYPE public.location AS (city text, country text, campuses text []);
CREATE TYPE public.location AS (city text, country country, campuses text []);
CREATE TYPE public.staff AS (
first_name text,
@ -128,7 +129,7 @@ VALUES (
'Queen Mary University of London',
ROW(
'London',
'UK',
ROW('UK','Europe') :: country,
ARRAY ['Mile End','Whitechapel','Charterhouse Square','West Smithfield']
)::location,
ARRAY [ROW('Peter','Landin',ARRAY['Computer Science','Education'],
@ -139,9 +140,9 @@ VALUES (
(
2,
'Chalmers University of Technology',
Row(
ROW(
'Gothenburg',
'Sweden',
ROW('Sweden','Europe') :: country,
ARRAY ['Johanneberg','Lindholmen']
)::location,
ARRAY [ROW('John','Hughes',ARRAY['Computer Science','Functional Programming','Software Testing'],

View File

@ -379,6 +379,27 @@
}
}
},
"country": {
"description": "A country",
"fields": {
"name": {
"description": "The country's name",
"arguments": {},
"type": {
"type": "named",
"name": "String"
}
},
"continent": {
"description": "The country's continent",
"arguments": {},
"type": {
"type": "named",
"name": "String"
}
}
}
},
"location": {
"description": "A location",
"fields": {
@ -395,7 +416,7 @@
"arguments": {},
"type": {
"type": "named",
"name": "String"
"name": "country"
}
},
"campuses": {

View File

@ -13,7 +13,9 @@
]
},
"location_country": {
"country": "UK"
"country": {
"name": "UK"
}
},
"staff": [
{
@ -39,7 +41,9 @@
"campuses": ["Johanneberg", "Lindholmen"]
},
"location_country": {
"country": "Sweden"
"country": {
"name": "Sweden"
}
},
"staff": [
{
@ -82,7 +86,9 @@
"campuses": null
},
"location_country": {
"country": null
"country": {
"name": null
}
},
"staff": null,
"staff_first_name": null,

View File

@ -111,6 +111,49 @@
]
}
},
{
"kind": "ObjectType",
"version": "v1",
"definition": {
"name": "country",
"fields": [
{
"name": "name",
"type": "String"
},
{
"name": "continent",
"type": "String"
}
],
"graphql": {
"typeName": "Country"
},
"dataConnectorTypeMapping": [
{
"dataConnectorName": "db",
"dataConnectorObjectType": "country"
}
]
}
},
{
"kind": "TypePermissions",
"version": "v1",
"definition": {
"typeName": "country",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": ["name", "continent"]
}
}
]
}
},
{
"kind": "ObjectType",
"version": "v1",
@ -123,7 +166,7 @@
},
{
"name": "country",
"type": "String"
"type": "country"
},
{
"name": "campuses",

View File

@ -6,7 +6,9 @@ query MyQuery {
campuses
}
location_country: location {
country
country {
name
}
}
staff {
last_name

View File

@ -0,0 +1,84 @@
[
{
"data": {
"match_london": [
{
"id": 1,
"location": {
"city": "London",
"campuses": [
"Mile End",
"Whitechapel",
"Charterhouse Square",
"West Smithfield"
]
},
"location_country": {
"country": {
"name": "UK"
}
},
"staff": [
{
"last_name": "Landin",
"specialities": ["Computer Science", "Education"]
}
],
"staff_first_name": [
{
"first_name": "Peter"
}
],
"departments": [
"Humanities and Social Sciences",
"Science and Engineering",
"Medicine and Dentistry"
]
}
],
"match_uk": [
{
"id": 1,
"location": {
"city": "London",
"campuses": [
"Mile End",
"Whitechapel",
"Charterhouse Square",
"West Smithfield"
]
},
"location_country": {
"country": {
"name": "UK"
}
},
"staff": [
{
"last_name": "Landin",
"specialities": ["Computer Science", "Education"]
}
],
"staff_first_name": [
{
"first_name": "Peter"
}
],
"departments": [
"Humanities and Social Sciences",
"Science and Engineering",
"Medicine and Dentistry"
]
}
]
}
},
{
"data": null,
"errors": [
{
"message": "validation failed: no such field on type Query: InstitutionMany"
}
]
}
]

View File

@ -0,0 +1,481 @@
{
"version": "v2",
"subgraphs": [
{
"name": "default",
"objects": [
{
"kind": "BooleanExpressionType",
"version": "v1",
"definition": {
"name": "string_bool_exp",
"operand": {
"scalar": {
"type": "String",
"comparisonOperators": [
{ "name": "_eq", "argumentType": "String!" }
],
"dataConnectorOperatorMapping": [
{
"dataConnectorName": "db",
"dataConnectorScalarType": "String",
"operatorMapping": {}
}
]
}
},
"logicalOperators": { "enable": true },
"isNull": { "enable": true },
"graphql": {
"typeName": "String_Comparison_Exp"
}
}
},
{
"kind": "BooleanExpressionType",
"version": "v1",
"definition": {
"name": "int_bool_exp",
"operand": {
"scalar": {
"type": "Int",
"comparisonOperators": [
{ "name": "_eq", "argumentType": "Int!" }
],
"dataConnectorOperatorMapping": [
{
"dataConnectorName": "db",
"dataConnectorScalarType": "Int",
"operatorMapping": {}
}
]
}
},
"logicalOperators": { "enable": true },
"isNull": { "enable": true },
"graphql": {
"typeName": "Int_Comparison_Exp"
}
}
},
{
"kind": "BooleanExpressionType",
"version": "v1",
"definition": {
"name": "institution_bool_exp",
"operand": {
"object": {
"type": "institution",
"comparableFields": [
{
"fieldName": "id",
"booleanExpressionType": "int_bool_exp"
},
{
"fieldName": "name",
"booleanExpressionType": "string_bool_exp"
},
{
"fieldName": "location",
"booleanExpressionType": "location_bool_exp"
}
],
"comparableRelationships": []
}
},
"logicalOperators": { "enable": true },
"isNull": { "enable": true },
"graphql": { "typeName": "InstitutionBoolExp" }
}
},
{
"kind": "ObjectType",
"version": "v1",
"definition": {
"name": "institution",
"fields": [
{
"name": "id",
"type": "Int!"
},
{
"name": "name",
"type": "String!"
},
{
"name": "location",
"type": "location"
},
{
"name": "staff",
"type": "[staff_member]"
},
{
"name": "departments",
"type": "[String]"
}
],
"graphql": {
"typeName": "Institution"
},
"dataConnectorTypeMapping": [
{
"dataConnectorName": "db",
"dataConnectorObjectType": "institution",
"fieldMapping": {
"id": {
"column": {
"name": "id"
}
},
"name": {
"column": {
"name": "name"
}
},
"location": {
"column": {
"name": "location"
}
},
"staff": {
"column": {
"name": "staff"
}
},
"departments": {
"column": {
"name": "departments"
}
}
}
}
]
}
},
{
"kind": "TypePermissions",
"version": "v1",
"definition": {
"typeName": "institution",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": [
"id",
"name",
"location",
"staff",
"departments"
]
}
}
]
}
},
{
"kind": "BooleanExpressionType",
"version": "v1",
"definition": {
"name": "country_bool_exp",
"operand": {
"object": {
"type": "country",
"comparableFields": [
{
"fieldName": "name",
"booleanExpressionType": "string_bool_exp"
},
{
"fieldName": "continent",
"booleanExpressionType": "string_bool_exp"
}
],
"comparableRelationships": []
}
},
"logicalOperators": { "enable": true },
"isNull": { "enable": true },
"graphql": { "typeName": "CountryBoolExp" }
}
},
{
"kind": "BooleanExpressionType",
"version": "v1",
"definition": {
"name": "location_bool_exp",
"operand": {
"object": {
"type": "location",
"comparableFields": [
{
"fieldName": "city",
"booleanExpressionType": "string_bool_exp"
},
{
"fieldName": "country",
"booleanExpressionType": "country_bool_exp"
}
],
"comparableRelationships": []
}
},
"logicalOperators": { "enable": true },
"isNull": { "enable": true },
"graphql": { "typeName": "LocationBoolExp" }
}
},
{
"kind": "ObjectType",
"version": "v1",
"definition": {
"name": "country",
"fields": [
{
"name": "name",
"type": "String"
},
{
"name": "continent",
"type": "String"
}
],
"graphql": {
"typeName": "Country"
},
"dataConnectorTypeMapping": [
{
"dataConnectorName": "db",
"dataConnectorObjectType": "country",
"fieldMapping": {
"name": {
"column": {
"name": "name"
}
},
"continent": {
"column": {
"name": "continent"
}
}
}
}
]
}
},
{
"kind": "ObjectType",
"version": "v1",
"definition": {
"name": "location",
"fields": [
{
"name": "city",
"type": "String"
},
{
"name": "country",
"type": "country"
},
{
"name": "campuses",
"type": "[String]"
}
],
"graphql": {
"typeName": "Location"
},
"dataConnectorTypeMapping": [
{
"dataConnectorName": "db",
"dataConnectorObjectType": "location",
"fieldMapping": {
"city": {
"column": {
"name": "city"
}
},
"country": {
"column": {
"name": "country"
}
},
"campuses": {
"column": {
"name": "campuses"
}
}
}
}
]
}
},
{
"kind": "TypePermissions",
"version": "v1",
"definition": {
"typeName": "country",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": ["name", "continent"]
}
}
]
}
},
{
"kind": "TypePermissions",
"version": "v1",
"definition": {
"typeName": "location",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": ["city", "country", "campuses"]
}
}
]
}
},
{
"kind": "ObjectType",
"version": "v1",
"definition": {
"name": "staff_member",
"fields": [
{
"name": "first_name",
"type": "String"
},
{
"name": "last_name",
"type": "String"
},
{
"name": "specialities",
"type": "[String]"
}
],
"graphql": {
"typeName": "StaffMember"
},
"dataConnectorTypeMapping": [
{
"dataConnectorName": "db",
"dataConnectorObjectType": "staff",
"fieldMapping": {
"first_name": {
"column": {
"name": "first_name"
}
},
"last_name": {
"column": {
"name": "last_name"
}
},
"specialities": {
"column": {
"name": "specialities"
}
}
}
}
]
}
},
{
"kind": "TypePermissions",
"version": "v1",
"definition": {
"typeName": "staff_member",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": ["first_name", "last_name", "specialities"]
}
}
]
}
},
{
"kind": "Model",
"version": "v1",
"definition": {
"name": "institutions",
"arguments": [],
"objectType": "institution",
"source": {
"dataConnectorName": "db",
"collection": "institution",
"argumentMapping": {}
},
"filterExpressionType": "institution_bool_exp",
"graphql": {
"selectUniques": [],
"selectMany": {
"queryRootField": "InstitutionMany"
}
},
"orderableFields": [
{
"fieldName": "id",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "name",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "location",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "staff",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "departments",
"orderByDirections": {
"enableAll": true
}
}
]
}
},
{
"kind": "ModelPermissions",
"version": "v1",
"definition": {
"modelName": "institutions",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
}
]
}
}
]
}
]
}

View File

@ -0,0 +1,46 @@
query MyQuery {
match_london: InstitutionMany(
where: { location: { city: { _eq: "London" } } }
) {
id
location {
city
campuses
}
location_country: location {
country {
name
}
}
staff {
last_name
specialities
}
staff_first_name: staff {
first_name
}
departments
}
match_uk: InstitutionMany(
where: { location: { country: { name: { _eq: "UK" } } } }
) {
id
location {
city
campuses
}
location_country: location {
country {
name
}
}
staff {
last_name
specialities
}
staff_first_name: staff {
first_name
}
departments
}
}

View File

@ -0,0 +1,9 @@
[
{
"x-hasura-role": "admin"
},
{
"x-hasura-role": "user_1",
"x-hasura-user-id": "1"
}
]

View File

@ -230,6 +230,13 @@ fn test_model_select_many_boolean_expression_type() -> anyhow::Result<()> {
common::test_execution_expectation(test_path_string, &[common_metadata_path_string])
}
#[test]
fn test_model_select_many_where_nested_select() -> anyhow::Result<()> {
let test_path_string = "execute/models/select_many/where/nested_select";
let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json";
common::test_execution_expectation(test_path_string, &[common_metadata_path_string])
}
#[test]
fn test_model_select_many_where_is_null() -> anyhow::Result<()> {
let test_path_string = "execute/models/select_many/where/is_null";

View File

@ -1138,6 +1138,26 @@
}
},
"compositeTypes": {
"country": {
"name": "country",
"fields": {
"continent": {
"name": "continent",
"type": {
"scalarType": "text"
},
"description": null
},
"name": {
"name": "name",
"type": {
"scalarType": "text"
},
"description": null
}
},
"description": null
},
"location": {
"name": "location",
"fields": {
@ -1160,7 +1180,7 @@
"country": {
"name": "country",
"type": {
"scalarType": "text"
"compositeType": "country"
},
"description": null
}

View File

@ -95,13 +95,16 @@ impl FieldError {
pub fn to_graphql_error(&self, path: Option<Vec<gql::http::PathSegment>>) -> GraphQLError {
let details = self.get_details();
match self {
Self::InternalError(_internal) => GraphQLError {
message: "internal error".into(),
path,
// Internal errors showing up in the API response is not desirable.
// Hence, extensions are masked for internal errors.
extensions: None,
},
Self::InternalError(_internal) => {
println!("{_internal}");
GraphQLError {
message: "internal error".into(),
path,
// Internal errors showing up in the API response is not desirable.
// Hence, extensions are masked for internal errors.
extensions: None,
}
}
e => GraphQLError {
message: e.to_string(),
path,

View File

@ -56,6 +56,20 @@ pub(crate) fn resolve_filter_expression<'s>(
Ok(resolved_filter_expression)
}
fn get_boolean_expression_annotation(
annotation: &schema::Annotation,
) -> Result<&BooleanExpressionAnnotation, error::Error> {
match annotation {
schema::Annotation::Input(InputAnnotation::BooleanExpression(
boolean_expression_annotation,
)) => Ok(boolean_expression_annotation),
_ => Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: annotation.clone(),
}
.into()),
}
}
// Build the NDC filter expression by traversing the relationships when present
fn build_filter_expression<'s>(
field: &normalized_ast::InputField<'s, GDS>,
@ -64,13 +78,33 @@ fn build_filter_expression<'s>(
type_mappings: &'s BTreeMap<Qualified<CustomTypeName>, metadata_resolve::TypeMapping>,
usage_counts: &mut UsagesCounts,
) -> Result<ndc_models::Expression, error::Error> {
match field.info.generic {
let boolean_expression_annotation = get_boolean_expression_annotation(field.info.generic)?;
build_filter_expression_from_boolean_expression(
boolean_expression_annotation,
field,
relationships,
data_connector_link,
type_mappings,
&mut vec![],
usage_counts,
)
}
// build filter expression, specifically matching on BooleanExpressionAnnotation
fn build_filter_expression_from_boolean_expression<'s>(
boolean_expression_annotation: &'s BooleanExpressionAnnotation,
field: &normalized_ast::InputField<'s, GDS>,
relationships: &mut BTreeMap<NDCRelationshipName, LocalModelRelationshipInfo<'s>>,
data_connector_link: &'s DataConnectorLink,
type_mappings: &'s BTreeMap<Qualified<CustomTypeName>, metadata_resolve::TypeMapping>,
field_path: &mut Vec<DataConnectorColumnName>,
usage_counts: &mut UsagesCounts,
) -> Result<ndc_models::Expression, error::Error> {
match boolean_expression_annotation {
// "_and"
schema::Annotation::Input(InputAnnotation::BooleanExpression(
BooleanExpressionAnnotation::BooleanExpressionArgument {
field: schema::ModelFilterArgument::AndOp,
},
)) => {
BooleanExpressionAnnotation::BooleanExpressionArgument {
field: schema::ModelFilterArgument::AndOp,
} => {
let mut and_expressions = Vec::new();
// The "_and" field value should be a list
let and_values = field.value.as_list()?;
@ -91,11 +125,9 @@ fn build_filter_expression<'s>(
})
}
// "_or"
schema::Annotation::Input(InputAnnotation::BooleanExpression(
BooleanExpressionAnnotation::BooleanExpressionArgument {
field: schema::ModelFilterArgument::OrOp,
},
)) => {
BooleanExpressionAnnotation::BooleanExpressionArgument {
field: schema::ModelFilterArgument::OrOp,
} => {
let mut or_expressions = Vec::new();
// The "_or" field value should be a list
let or_values = field.value.as_list()?;
@ -116,11 +148,9 @@ fn build_filter_expression<'s>(
})
}
// "_not"
schema::Annotation::Input(InputAnnotation::BooleanExpression(
BooleanExpressionAnnotation::BooleanExpressionArgument {
field: schema::ModelFilterArgument::NotOp,
},
)) => {
BooleanExpressionAnnotation::BooleanExpressionArgument {
field: schema::ModelFilterArgument::NotOp,
} => {
// The "_not" field value should be an object
let not_value = field.value.as_object()?;
@ -136,73 +166,38 @@ fn build_filter_expression<'s>(
})
}
// The column that we want to use for filtering.
schema::Annotation::Input(InputAnnotation::BooleanExpression(
BooleanExpressionAnnotation::BooleanExpressionArgument {
field:
schema::ModelFilterArgument::Field {
field_name,
object_type,
},
},
)) => {
BooleanExpressionAnnotation::BooleanExpressionArgument {
field:
schema::ModelFilterArgument::Field {
field_name,
object_type,
},
} => {
let FieldMapping { column, .. } =
get_field_mapping_of_field_name(type_mappings, object_type, field_name)?;
let mut expressions = Vec::new();
for (_op_name, op_value) in field.value.as_object()? {
match op_value.info.generic {
schema::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::IsNullOperation,
)) => {
let expression = build_is_null_expression(column.clone(), &op_value.value)?;
expressions.push(expression);
}
schema::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ComparisonOperation { operator_mapping },
)) => {
let operator =
operator_mapping
.get(&data_connector_link.name)
.ok_or_else(|| {
error::InternalEngineError::OperatorMappingError(
error::OperatorMappingError::MissingEntryForDataConnector {
column_name: column.clone(),
data_connector_name: data_connector_link.name.clone(),
},
)
})?;
let expression = build_binary_comparison_expression(
operator,
column.clone(),
&op_value.value,
);
expressions.push(expression)
}
annotation => Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: annotation.clone(),
})?,
}
}
Ok(ndc_models::Expression::And { expressions })
build_comparison_expression(
field,
field_path,
&column,
data_connector_link,
type_mappings,
)
}
// Relationship field used for filtering.
// This relationship can either point to another relationship or a column.
schema::Annotation::Input(InputAnnotation::BooleanExpression(
BooleanExpressionAnnotation::BooleanExpressionArgument {
field:
schema::ModelFilterArgument::RelationshipField(FilterRelationshipAnnotation {
relationship_name,
relationship_type,
source_type,
target_source,
target_type,
target_model_name,
mappings,
}),
},
)) => {
BooleanExpressionAnnotation::BooleanExpressionArgument {
field:
schema::ModelFilterArgument::RelationshipField(FilterRelationshipAnnotation {
relationship_name,
relationship_type,
source_type,
target_source,
target_type,
target_model_name,
mappings,
}),
} => {
// Add the target model being used in the usage counts
count_model(target_model_name, usage_counts);
@ -244,17 +239,104 @@ fn build_filter_expression<'s>(
relationship: ndc_relationship_name.0,
arguments: BTreeMap::new(),
};
Ok(ndc_models::Expression::Exists {
in_collection: exists_in_relationship,
predicate: Some(Box::new(exists_filter_clause)),
})
}
annotation => Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: annotation.clone(),
other_boolean_annotation => Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: schema::Annotation::Input(InputAnnotation::BooleanExpression(
other_boolean_annotation.clone(),
)),
})?,
}
}
fn build_comparison_expression<'s>(
field: &normalized_ast::InputField<'s, GDS>,
field_path: &mut Vec<DataConnectorColumnName>,
column: &DataConnectorColumnName,
data_connector_link: &'s DataConnectorLink,
type_mappings: &'s BTreeMap<Qualified<CustomTypeName>, metadata_resolve::TypeMapping>,
) -> Result<ndc_models::Expression, error::Error> {
let mut expressions = Vec::new();
println!("build_comparison_expression");
for (_op_name, op_value) in field.value.as_object()? {
println!("{op_value:?}");
match op_value.info.generic {
schema::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::IsNullOperation,
)) => {
let expression =
build_is_null_expression(column.clone(), &op_value.value, field_path.clone())?;
expressions.push(expression);
}
schema::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ComparisonOperation { operator_mapping },
)) => {
let operator =
operator_mapping
.get(&data_connector_link.name)
.ok_or_else(|| {
error::InternalEngineError::OperatorMappingError(
error::OperatorMappingError::MissingEntryForDataConnector {
column_name: column.clone(),
data_connector_name: data_connector_link.name.clone(),
},
)
})?;
let expression = build_binary_comparison_expression(
operator,
column.clone(),
&op_value.value,
field_path.clone(),
);
expressions.push(expression)
}
schema::Annotation::Input(InputAnnotation::BooleanExpression(
BooleanExpressionAnnotation::BooleanExpressionArgument {
field:
schema::ModelFilterArgument::Field {
field_name: inner_field_name,
object_type: inner_object_type,
},
},
)) => {
// get correct inner column name
let FieldMapping {
column: inner_column,
..
} = get_field_mapping_of_field_name(
type_mappings,
inner_object_type,
inner_field_name,
)?;
// add it to the path
field_path.push(inner_column);
let inner_expression = build_comparison_expression(
op_value,
field_path,
column,
data_connector_link,
type_mappings,
)?;
expressions.push(inner_expression);
}
annotation => Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: annotation.clone(),
})?,
}
}
Ok(ndc_models::Expression::And { expressions })
}
/// get column name for field name
fn get_field_mapping_of_field_name(
type_mappings: &BTreeMap<Qualified<CustomTypeName>, metadata_resolve::TypeMapping>,
@ -299,17 +381,27 @@ fn resolve_filter_object<'s>(
Ok(ndc_models::Expression::And { expressions })
}
/// Only pass a path if there are items in it
fn to_ndc_field_path(field_path: Vec<DataConnectorColumnName>) -> Option<Vec<String>> {
if field_path.is_empty() {
None
} else {
Some(field_path.into_iter().map(|s| s.0).collect())
}
}
/// Generate a binary comparison operator
fn build_binary_comparison_expression(
operator: &DataConnectorOperatorName,
column: DataConnectorColumnName,
value: &normalized_ast::Value<'_, GDS>,
field_path: Vec<DataConnectorColumnName>,
) -> ndc_models::Expression {
ndc_models::Expression::BinaryComparisonOperator {
column: ndc_models::ComparisonTarget::Column {
name: column.0,
path: Vec::new(),
field_path: None,
field_path: to_ndc_field_path(field_path),
},
operator: operator.0.clone(),
value: ndc_models::ComparisonValue::Scalar {
@ -322,13 +414,14 @@ fn build_binary_comparison_expression(
fn build_is_null_expression(
column: DataConnectorColumnName,
value: &normalized_ast::Value<'_, GDS>,
field_path: Vec<DataConnectorColumnName>,
) -> Result<ndc_models::Expression, error::Error> {
// Build an 'IsNull' unary comparison expression
let unary_comparison_expression = ndc_models::Expression::UnaryComparisonOperator {
column: ndc_models::ComparisonTarget::Column {
name: column.0,
path: Vec::new(),
field_path: None,
field_path: to_ndc_field_path(field_path),
},
operator: ndc_models::UnaryComparisonOperator::IsNull,
};

View File

@ -160,6 +160,7 @@ pub async fn query_post(
configuration: Configuration<'_>,
query_request: &ndc_models::QueryRequest,
) -> Result<ndc_models::QueryResponse, Error> {
println!("{query_request:?}");
let tracer = tracing_util::global_tracer();
tracer
.in_span_async(

View File

@ -9,8 +9,8 @@ mod scalar;
mod types;
pub use types::{
BooleanExpressionGraphqlConfig, BooleanExpressionGraphqlFieldConfig, BooleanExpressionTypes,
BooleanExpressionsOutput, ComparisonExpressionInfo, ResolvedObjectBooleanExpressionType,
ResolvedScalarBooleanExpressionType,
BooleanExpressionsOutput, ComparisonExpressionInfo, ObjectComparisonExpressionInfo,
ResolvedObjectBooleanExpressionType, ResolvedScalarBooleanExpressionType,
};
pub fn resolve(
@ -43,9 +43,6 @@ pub fn resolve(
);
}
let all_boolean_expression_names: BTreeSet<_> =
raw_boolean_expression_types.keys().cloned().collect();
// let's resolve scalar types first
let mut boolean_expression_scalar_types = BTreeMap::new();
@ -74,19 +71,19 @@ pub fn resolve(
let mut boolean_expression_object_types = BTreeMap::new();
for (boolean_expression_type_name, (subgraph, boolean_expression_type)) in
raw_boolean_expression_types
&raw_boolean_expression_types
{
if let BooleanExpressionOperand::Object(boolean_expression_object_operand) =
&boolean_expression_type.operand
{
let object_boolean_expression_type = object::resolve_object_boolean_expression_type(
&boolean_expression_type_name,
boolean_expression_type_name,
boolean_expression_object_operand,
subgraph,
&boolean_expression_type.graphql,
object_types,
&boolean_expression_scalar_types,
&all_boolean_expression_names,
&raw_boolean_expression_types,
graphql_config,
)?;

View File

@ -1,6 +1,7 @@
pub use super::{
BooleanExpressionGraphqlConfig, BooleanExpressionGraphqlFieldConfig, ComparisonExpressionInfo,
ResolvedObjectBooleanExpressionType, ResolvedScalarBooleanExpressionType,
ObjectComparisonExpressionInfo, ResolvedObjectBooleanExpressionType,
ResolvedScalarBooleanExpressionType,
};
use crate::helpers::types::mk_name;
use crate::stages::{graphql_config, object_types, type_permissions};
@ -10,7 +11,7 @@ use crate::Qualified;
use lang_graphql::ast::common::{self as ast};
use open_dds::{
boolean_expression::{
BooleanExpressionComparableField, BooleanExpressionObjectOperand,
BooleanExpressionComparableField, BooleanExpressionObjectOperand, BooleanExpressionOperand,
BooleanExpressionTypeGraphQlConfiguration, DataConnectorOperatorMapping,
},
data_connector::{DataConnectorName, DataConnectorOperatorName},
@ -29,7 +30,13 @@ pub(crate) fn resolve_object_boolean_expression_type(
Qualified<CustomTypeName>,
ResolvedScalarBooleanExpressionType,
>,
all_boolean_expression_names: &BTreeSet<Qualified<CustomTypeName>>,
raw_boolean_expression_types: &BTreeMap<
Qualified<CustomTypeName>,
(
&String,
&open_dds::boolean_expression::BooleanExpressionTypeV1,
),
>,
graphql_config: &graphql_config::GraphqlConfig,
) -> Result<ResolvedObjectBooleanExpressionType, Error> {
let qualified_object_type_name = Qualified::new(
@ -53,7 +60,7 @@ pub(crate) fn resolve_object_boolean_expression_type(
&object_type_representation.object_type,
boolean_expression_type_name,
subgraph,
all_boolean_expression_names,
raw_boolean_expression_types,
)?;
let _allowed_data_connectors = resolve_data_connector_types(
@ -67,9 +74,11 @@ pub(crate) fn resolve_object_boolean_expression_type(
.as_ref()
.map(|object_boolean_graphql_config| {
resolve_object_boolean_graphql(
boolean_expression_type_name,
object_boolean_graphql_config,
&comparable_fields,
scalar_boolean_expression_types,
raw_boolean_expression_types,
subgraph,
graphql_config,
)
@ -90,7 +99,6 @@ pub(crate) fn resolve_object_boolean_expression_type(
// has information for this data connector
fn resolve_data_connector_types(
boolean_expression_type_name: &Qualified<CustomTypeName>,
object_type_representation: &type_permissions::ObjectTypeWithPermissions,
scalar_boolean_expression_types: &BTreeMap<
Qualified<CustomTypeName>,
@ -108,34 +116,30 @@ fn resolve_data_connector_types(
.data_connector_names()
{
for (comparable_field_name, comparable_field_type) in comparable_fields {
let scalar_boolean_expression = scalar_boolean_expression_types
.get(comparable_field_type)
.ok_or_else(|| Error::BooleanExpressionError {
boolean_expression_error:
BooleanExpressionError::ScalarBooleanExpressionCouldNotBeFound {
boolean_expression: boolean_expression_type_name.clone(),
scalar_boolean_expression: comparable_field_type.clone(),
},
})?;
// currently this throws an error if any data connector specified in the ObjectType
// does not have matching data connector entries for each scalar boolean expression
// type
// Not sure if this is correct behaviour, or if we only want to make this a problem
// when making these available in the GraphQL schema
if !scalar_boolean_expression
.data_connector_operator_mappings
.contains_key(data_connector_name)
// we don't mind if we can't find this, we've already checked validity so anything
// else will be an object-flavoured boolean expression
if let Some(scalar_boolean_expression) =
scalar_boolean_expression_types.get(comparable_field_type)
{
return Err(Error::BooleanExpressionError {
boolean_expression_error:
BooleanExpressionError::DataConnectorMappingMissingForField {
field: comparable_field_name.clone(),
boolean_expression_name: boolean_expression_type_name.clone(),
data_connector_name: data_connector_name.clone(),
},
});
};
// currently this throws an error if any data connector specified in the ObjectType
// does not have matching data connector entries for each scalar boolean expression
// type
// Not sure if this is correct behaviour, or if we only want to make this a problem
// when making these available in the GraphQL schema
if !scalar_boolean_expression
.data_connector_operator_mappings
.contains_key(data_connector_name)
{
return Err(Error::BooleanExpressionError {
boolean_expression_error:
BooleanExpressionError::DataConnectorMappingMissingForField {
field: comparable_field_name.clone(),
boolean_expression_name: boolean_expression_type_name.clone(),
data_connector_name: data_connector_name.clone(),
},
});
};
}
}
working_data_connectors.insert(data_connector_name.clone());
@ -145,13 +149,22 @@ fn resolve_data_connector_types(
}
// validate graphql config
// we use the raw boolean expression types for lookup
fn resolve_object_boolean_graphql(
boolean_expression_type_name: &Qualified<CustomTypeName>,
boolean_expression_graphql_config: &BooleanExpressionTypeGraphQlConfiguration,
comparable_fields: &BTreeMap<FieldName, Qualified<CustomTypeName>>,
scalar_boolean_expression_types: &BTreeMap<
Qualified<CustomTypeName>,
ResolvedScalarBooleanExpressionType,
>,
raw_boolean_expression_types: &BTreeMap<
Qualified<CustomTypeName>,
(
&String,
&open_dds::boolean_expression::BooleanExpressionTypeV1,
),
>,
subgraph: &str,
graphql_config: &graphql_config::GraphqlConfig,
) -> Result<BooleanExpressionGraphqlConfig, Error> {
@ -160,6 +173,8 @@ fn resolve_object_boolean_graphql(
let mut scalar_fields = BTreeMap::new();
let mut object_fields = BTreeMap::new();
let filter_graphql_config = graphql_config
.query
.filter_input_config
@ -169,10 +184,10 @@ fn resolve_object_boolean_graphql(
})?;
for (comparable_field_name, comparable_field_type_name) in comparable_fields {
// Generate comparison expression for fields mapped to simple scalar type
if let Some(scalar_boolean_expression_type) =
scalar_boolean_expression_types.get(comparable_field_type_name)
{
// Generate comparison expression for fields mapped to simple scalar type
if let Some(graphql_name) = &scalar_boolean_expression_type.graphql_name {
let mut operators = BTreeMap::new();
for (op_name, op_definition) in &scalar_boolean_expression_type.comparison_operators
@ -204,12 +219,42 @@ fn resolve_object_boolean_graphql(
);
};
}
} else {
// if this field isn't a scalar, let's see if it's an object instead
let (field_subgraph, raw_boolean_expression_type) = lookup_raw_boolean_expression(
boolean_expression_type_name,
comparable_field_type_name,
raw_boolean_expression_types,
)?;
if let (Some(graphql_name), BooleanExpressionOperand::Object(object_operand)) = (
raw_boolean_expression_type
.graphql
.as_ref()
.map(|gql| gql.type_name.clone()),
&raw_boolean_expression_type.operand,
) {
let graphql_type_name = mk_name(&graphql_name.0).map(ast::TypeName)?;
object_fields.insert(
comparable_field_name.clone(),
ObjectComparisonExpressionInfo {
object_type_name: comparable_field_type_name.clone(),
underlying_object_type_name: Qualified::new(
(*field_subgraph).to_string(),
object_operand.r#type.clone(),
),
graphql_type_name: graphql_type_name.clone(),
},
);
}
}
}
Ok(BooleanExpressionGraphqlConfig {
type_name: boolean_expression_graphql_name,
scalar_fields,
object_fields,
graphql_config: (BooleanExpressionGraphqlFieldConfig {
where_field_name: filter_graphql_config.where_field_name.clone(),
and_operator_name: filter_graphql_config.operator_names.and.clone(),
@ -244,7 +289,13 @@ fn resolve_comparable_fields(
object_type_representation: &object_types::ObjectTypeRepresentation,
boolean_expression_type_name: &Qualified<CustomTypeName>,
subgraph: &str,
all_boolean_expression_names: &BTreeSet<Qualified<CustomTypeName>>,
raw_boolean_expression_types: &BTreeMap<
Qualified<CustomTypeName>,
(
&String,
&open_dds::boolean_expression::BooleanExpressionTypeV1,
),
>,
) -> Result<BTreeMap<FieldName, Qualified<CustomTypeName>>, Error> {
let mut resolved_comparable_fields = BTreeMap::new();
@ -269,21 +320,45 @@ fn resolve_comparable_fields(
);
// lookup the boolean expression type to check it exists
if all_boolean_expression_names.contains(&field_boolean_expression_type) {
resolved_comparable_fields.insert(
comparable_field.field_name.clone(),
field_boolean_expression_type,
);
} else {
return Err(
BooleanExpressionError::ScalarBooleanExpressionCouldNotBeFound {
boolean_expression: boolean_expression_type_name.clone(),
scalar_boolean_expression: field_boolean_expression_type.clone(),
}
.into(),
);
}
let _raw_boolean_expression_type = lookup_raw_boolean_expression(
boolean_expression_type_name,
&field_boolean_expression_type,
raw_boolean_expression_types,
)?;
resolved_comparable_fields.insert(
comparable_field.field_name.clone(),
field_boolean_expression_type,
);
}
Ok(resolved_comparable_fields)
}
fn lookup_raw_boolean_expression<'a>(
parent_boolean_expression_name: &Qualified<CustomTypeName>,
boolean_expression_name: &Qualified<CustomTypeName>,
raw_boolean_expression_types: &'a BTreeMap<
Qualified<CustomTypeName>,
(
&String,
&open_dds::boolean_expression::BooleanExpressionTypeV1,
),
>,
) -> Result<
&'a (
&'a String,
&'a open_dds::boolean_expression::BooleanExpressionTypeV1,
),
Error,
> {
raw_boolean_expression_types
.get(boolean_expression_name)
.ok_or_else(|| {
BooleanExpressionError::BooleanExpressionCouldNotBeFound {
parent_boolean_expression: parent_boolean_expression_name.clone(),
child_boolean_expression: boolean_expression_name.clone(),
}
.into()
})
}

View File

@ -55,6 +55,13 @@ pub struct ComparisonExpressionInfo {
pub is_null_operator_name: ast::Name,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct ObjectComparisonExpressionInfo {
pub graphql_type_name: ast::TypeName,
pub object_type_name: Qualified<CustomTypeName>,
pub underlying_object_type_name: Qualified<CustomTypeName>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct BooleanExpressionGraphqlFieldConfig {
pub where_field_name: ast::Name,
@ -66,6 +73,7 @@ pub struct BooleanExpressionGraphqlFieldConfig {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct BooleanExpressionGraphqlConfig {
pub type_name: ast::TypeName,
pub object_fields: BTreeMap<FieldName, ObjectComparisonExpressionInfo>,
pub scalar_fields: BTreeMap<FieldName, ComparisonExpressionInfo>,
pub graphql_config: BooleanExpressionGraphqlFieldConfig,
}

View File

@ -318,6 +318,7 @@ pub fn resolve_boolean_expression_graphql_config(
Ok(boolean_expressions::BooleanExpressionGraphqlConfig {
type_name: where_type_name,
scalar_fields,
object_fields: BTreeMap::new(),
graphql_config: (boolean_expressions::BooleanExpressionGraphqlFieldConfig {
where_field_name: filter_graphql_config.where_field_name.clone(),
and_operator_name: filter_graphql_config.operator_names.and.clone(),

View File

@ -642,10 +642,10 @@ pub enum BooleanExpressionError {
name: Qualified<CustomTypeName>,
model: Qualified<ModelName>,
},
#[error("unknown scalar boolean expression type {scalar_boolean_expression:} is used in boolean expression {boolean_expression:}")]
ScalarBooleanExpressionCouldNotBeFound {
boolean_expression: Qualified<CustomTypeName>,
scalar_boolean_expression: Qualified<CustomTypeName>,
#[error("could not find boolean expression type {child_boolean_expression:} referenced within boolean expression {parent_boolean_expression:}")]
BooleanExpressionCouldNotBeFound {
parent_boolean_expression: Qualified<CustomTypeName>,
child_boolean_expression: Qualified<CustomTypeName>,
},
#[error("the boolean expression type {name:} used in model {model:} corresponds to object type {boolean_expression_object_type:} whereas the model's object type is {model_object_type:}")]
BooleanExpressionTypeForInvalidObjectTypeInModel {

View File

@ -1 +1 @@
unknown scalar boolean expression type postgres_int_comparison_bool_exp (in subgraph __unknown_namespace) is used in boolean expression author_bool_exp (in subgraph __unknown_namespace)
could not find boolean expression type postgres_int_comparison_bool_exp (in subgraph __unknown_namespace) referenced within boolean expression author_bool_exp (in subgraph __unknown_namespace)

View File

@ -101,6 +101,7 @@ fn build_builtin_operator_schema(
// Build input fields for fields that we are allowed to compare
fn build_comparable_fields_schema(
gds: &GDS,
object_type_name: &Qualified<CustomTypeName>,
object_type_representation: &ObjectTypeWithRelationships,
boolean_expression_info: &BooleanExpressionGraphqlConfig,
@ -108,7 +109,7 @@ fn build_comparable_fields_schema(
) -> Result<BTreeMap<ast::Name, gql_schema::Namespaced<GDS, gql_schema::InputField<GDS>>>, Error> {
let mut input_fields = BTreeMap::new();
// column fields
// scalar column fields
for (field_name, comparison_expression) in &boolean_expression_info.scalar_fields {
let field_graphql_name = mk_name(field_name.clone().0.as_str())?;
let registered_type_name =
@ -140,6 +141,53 @@ fn build_comparable_fields_schema(
);
input_fields.insert(field_graphql_name, input_field);
}
// object column fields
for (field_name, object_comparison_expression) in &boolean_expression_info.object_fields {
let field_graphql_name = mk_name(field_name.clone().0.as_str())?;
let registered_type_name =
builder.register_type(TypeId::InputObjectBooleanExpressionType {
graphql_type_name: object_comparison_expression.graphql_type_name.clone(),
gds_type_name: object_comparison_expression.object_type_name.clone(),
});
let field_object_type_representation = gds
.metadata
.object_types
.get(&object_comparison_expression.underlying_object_type_name)
.unwrap();
let field_type = ast::TypeContainer::named_null(registered_type_name);
let annotation = types::Annotation::Input(InputAnnotation::BooleanExpression(
BooleanExpressionAnnotation::BooleanExpressionArgument {
field: types::ModelFilterArgument::Field {
field_name: field_name.clone(),
object_type: object_type_name.clone(),
},
},
));
let field_permissions: HashMap<Role, Option<types::NamespaceAnnotation>> =
permissions::get_allowed_roles_for_type(field_object_type_representation)
.map(|role| (role.clone(), None))
.collect();
let input_field = builder.conditional_namespaced(
gql_schema::InputField::<GDS>::new(
field_graphql_name.clone(),
None,
annotation,
field_type,
None,
gql_schema::DeprecationStatus::NotDeprecated,
),
field_permissions,
);
input_fields.insert(field_graphql_name, input_field);
}
Ok(input_fields)
}
@ -328,6 +376,7 @@ fn build_schema_with_object_boolean_expression_type(
// add in all fields that are directly comparable
input_fields.extend(build_comparable_fields_schema(
gds,
&object_boolean_expression_type.object_type,
object_type_representation,
boolean_expression_info,
@ -375,6 +424,7 @@ fn build_schema_with_boolean_expression_type(
// add in all fields that are directly comparable
input_fields.extend(build_comparable_fields_schema(
gds,
&boolean_expression_object_type.object_type,
object_type_representation,
boolean_expression_info,

View File

@ -455,6 +455,18 @@ pub(crate) fn get_entity_union_permissions(
permissions
}
/// Are we allowed to access a given type at all?
/// If we are allowed to access at least one field, yes
pub(crate) fn get_allowed_roles_for_type(
object_type_representation: &metadata_resolve::ObjectTypeWithRelationships,
) -> impl Iterator<Item = &'_ Role> {
object_type_representation
.object_type
.fields
.keys()
.flat_map(|field_name| get_allowed_roles_for_field(object_type_representation, field_name))
}
/// Build namespace annotations for each field based on the type permissions
pub(crate) fn get_allowed_roles_for_field<'a>(
object_type_representation: &'a metadata_resolve::ObjectTypeWithRelationships,