From d177c6ffdb519de1d707370ca47c2eb3486abe9b Mon Sep 17 00:00:00 2001 From: Naveen Naidu Date: Tue, 23 Jan 2024 16:50:37 +0530 Subject: [PATCH] Implement relationships in boolean exp, order_by and predicates (#254) V3_GIT_ORIGIN_REV_ID: bc0fb85552f141f7e887d61c15c5e455f87ac02a --- v3/engine/src/execute/error.rs | 27 + v3/engine/src/execute/ir/filter.rs | 309 +++++-- v3/engine/src/execute/ir/model_selection.rs | 25 +- v3/engine/src/execute/ir/order_by.rs | 242 +++++- v3/engine/src/execute/ir/permissions.rs | 94 +- .../src/execute/ir/query_root/node_field.rs | 8 +- .../src/execute/ir/query_root/select_many.rs | 21 +- .../src/execute/ir/query_root/select_one.rs | 12 +- v3/engine/src/execute/ir/relationship.rs | 112 ++- v3/engine/src/execute/ir/selection_set.rs | 46 +- .../src/execute/query_plan/model_selection.rs | 4 +- v3/engine/src/metadata/resolved/error.rs | 34 +- v3/engine/src/metadata/resolved/metadata.rs | 25 +- v3/engine/src/metadata/resolved/model.rs | 193 ++++- .../src/metadata/resolved/relationship.rs | 31 + v3/engine/src/metadata/resolved/subgraph.rs | 15 + v3/engine/src/schema/model_filter.rs | 96 +++ v3/engine/src/schema/model_order_by.rs | 93 ++ v3/engine/src/schema/permissions.rs | 5 +- v3/engine/src/schema/types.rs | 7 + .../schema/types/output_type/relationship.rs | 77 +- .../postgres_connector_schema.json | 44 +- .../relationships/common_metadata.json | 619 ++++++++++++++ .../relationships/object/nested/expected.json | 306 +++++++ .../relationships/object/nested/metadata.json | 1 + .../relationships/object/nested/request.gql | 24 + .../object/nested/session_variables.json | 9 + .../relationships/object/simple/expected.json | 126 +++ .../relationships/object/simple/metadata.json | 1 + .../relationships/object/simple/request.gql | 20 + .../object/simple/session_variables.json | 9 + .../array/nested/expected.json | 157 ++++ .../array/nested/metadata.json | 91 ++ .../array/nested/request.gql | 20 + .../array/nested/session_variables.json | 9 + .../array/simple/expected.json | 195 +++++ .../array/simple/metadata.json | 86 ++ .../array/simple/request.gql | 32 + .../array/simple/session_variables.json | 9 + .../common_metadata.json | 723 ++++++++++++++++ .../object/nested/expected.json | 62 ++ .../object/nested/metadata.json | 91 ++ .../object/nested/request.gql | 22 + .../object/nested/session_variables.json | 9 + .../object/simple/expected.json | 108 +++ .../object/simple/metadata.json | 86 ++ .../object/simple/request.gql | 18 + .../object/simple/session_variables.json | 9 + .../two_relationship_fields/expected.json | 58 ++ .../two_relationship_fields/metadata.json | 130 +++ .../two_relationship_fields/request.gql | 13 + .../session_variables.json | 9 + .../on_two_fields/expected.json | 124 +++ .../on_two_fields/metadata.json | 99 +++ .../on_two_fields/request.gql | 11 + .../on_two_fields/session_variables.json | 10 + .../relationships/array/nested/expected.json | 608 +++++++++++++ .../relationships/array/nested/metadata.json | 1 + .../relationships/array/nested/request.gql | 31 + .../array/nested/session_variables.json | 9 + .../relationships/array/simple/expected.json | 314 +++++++ .../relationships/array/simple/metadata.json | 1 + .../relationships/array/simple/request.gql | 50 ++ .../array/simple/session_variables.json | 9 + .../where/relationships/common_metadata.json | 807 ++++++++++++++++++ .../relationships/object/nested/expected.json | 184 ++++ .../relationships/object/nested/metadata.json | 1 + .../relationships/object/nested/request.gql | 29 + .../object/nested/session_variables.json | 9 + .../relationships/object/simple/expected.json | 162 ++++ .../relationships/object/simple/metadata.json | 1 + .../relationships/object/simple/request.gql | 23 + .../object/simple/session_variables.json | 9 + v3/engine/tests/execution.rs | 235 +++++ .../tests/generate_ir/get_by_id/expected.json | 35 +- .../tests/generate_ir/get_many/expected.json | 5 +- .../get_many_model_count/expected.json | 306 ++----- .../generate_ir/get_many_user_2/expected.json | 63 +- .../generate_ir/get_many_where/expected.json | 66 +- v3/engine/tests/schema.json | 309 ++++++- 80 files changed, 7473 insertions(+), 610 deletions(-) create mode 100644 v3/engine/tests/execute/models/select_many/order_by/relationships/common_metadata.json create mode 100644 v3/engine/tests/execute/models/select_many/order_by/relationships/object/nested/expected.json create mode 100644 v3/engine/tests/execute/models/select_many/order_by/relationships/object/nested/metadata.json create mode 100644 v3/engine/tests/execute/models/select_many/order_by/relationships/object/nested/request.gql create mode 100644 v3/engine/tests/execute/models/select_many/order_by/relationships/object/nested/session_variables.json create mode 100644 v3/engine/tests/execute/models/select_many/order_by/relationships/object/simple/expected.json create mode 100644 v3/engine/tests/execute/models/select_many/order_by/relationships/object/simple/metadata.json create mode 100644 v3/engine/tests/execute/models/select_many/order_by/relationships/object/simple/request.gql create mode 100644 v3/engine/tests/execute/models/select_many/order_by/relationships/object/simple/session_variables.json create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/array/nested/expected.json create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/array/nested/metadata.json create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/array/nested/request.gql create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/array/nested/session_variables.json create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/array/simple/expected.json create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/array/simple/metadata.json create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/array/simple/request.gql create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/array/simple/session_variables.json create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/common_metadata.json create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/object/nested/expected.json create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/object/nested/metadata.json create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/object/nested/request.gql create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/object/nested/session_variables.json create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/object/simple/expected.json create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/object/simple/metadata.json create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/object/simple/request.gql create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/object/simple/session_variables.json create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/object/two_relationship_fields/expected.json create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/object/two_relationship_fields/metadata.json create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/object/two_relationship_fields/request.gql create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/object/two_relationship_fields/session_variables.json create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/on_two_fields/expected.json create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/on_two_fields/metadata.json create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/on_two_fields/request.gql create mode 100644 v3/engine/tests/execute/models/select_many/relationship_predicates/on_two_fields/session_variables.json create mode 100644 v3/engine/tests/execute/models/select_many/where/relationships/array/nested/expected.json create mode 100644 v3/engine/tests/execute/models/select_many/where/relationships/array/nested/metadata.json create mode 100644 v3/engine/tests/execute/models/select_many/where/relationships/array/nested/request.gql create mode 100644 v3/engine/tests/execute/models/select_many/where/relationships/array/nested/session_variables.json create mode 100644 v3/engine/tests/execute/models/select_many/where/relationships/array/simple/expected.json create mode 100644 v3/engine/tests/execute/models/select_many/where/relationships/array/simple/metadata.json create mode 100644 v3/engine/tests/execute/models/select_many/where/relationships/array/simple/request.gql create mode 100644 v3/engine/tests/execute/models/select_many/where/relationships/array/simple/session_variables.json create mode 100644 v3/engine/tests/execute/models/select_many/where/relationships/common_metadata.json create mode 100644 v3/engine/tests/execute/models/select_many/where/relationships/object/nested/expected.json create mode 100644 v3/engine/tests/execute/models/select_many/where/relationships/object/nested/metadata.json create mode 100644 v3/engine/tests/execute/models/select_many/where/relationships/object/nested/request.gql create mode 100644 v3/engine/tests/execute/models/select_many/where/relationships/object/nested/session_variables.json create mode 100644 v3/engine/tests/execute/models/select_many/where/relationships/object/simple/expected.json create mode 100644 v3/engine/tests/execute/models/select_many/where/relationships/object/simple/metadata.json create mode 100644 v3/engine/tests/execute/models/select_many/where/relationships/object/simple/request.gql create mode 100644 v3/engine/tests/execute/models/select_many/where/relationships/object/simple/session_variables.json diff --git a/v3/engine/src/execute/error.rs b/v3/engine/src/execute/error.rs index 58d10bd3ac5..97919ec7984 100644 --- a/v3/engine/src/execute/error.rs +++ b/v3/engine/src/execute/error.rs @@ -1,6 +1,7 @@ use gql::{ast::common as ast, http::GraphQLError}; use lang_graphql as gql; use open_dds::{ + models::ModelName, relationships::RelationshipName, session_variables::SessionVariable, types::{CustomTypeName, FieldName}, @@ -23,6 +24,32 @@ pub enum InternalDeveloperError { field_name: ast::Name, }, + #[error("No source data connector specified for relationship argument field {argument_name} of type {type_name}")] + NoTargetSourceDataConnectorForRelationshipArgument { + argument_name: ast::Name, + type_name: Qualified, + }, + + #[error("No source model specified for relationship argument field {argument_name} of type {type_name}")] + NoSourceModelForRelationshipArgument { + argument_name: ast::Name, + type_name: Qualified, + }, + + #[error("No data connector specified for source model {source_model_name} which has a relationship argument field {argument_name} of type {type_name}")] + NoModelSourceDataConnectorForRelationshipArgument { + argument_name: ast::Name, + type_name: Qualified, + source_model_name: Qualified, + }, + + #[error("No type mappings specified for source model {source_model_name} which has a relationship argument field {argument_name} of type {type_name}")] + NoModelSourceTypeMappingsForRelationshipArgument { + argument_name: ast::Name, + type_name: Qualified, + source_model_name: Qualified, + }, + #[error("No function/procedure specified for command field {field_name} of type {type_name}")] NoFunctionOrProcedure { type_name: ast::TypeName, diff --git a/v3/engine/src/execute/ir/filter.rs b/v3/engine/src/execute/ir/filter.rs index 3459e4c6d47..81dccdf4ff9 100644 --- a/v3/engine/src/execute/ir/filter.rs +++ b/v3/engine/src/execute/ir/filter.rs @@ -1,104 +1,219 @@ +use std::collections::BTreeMap; + use indexmap::IndexMap; use lang_graphql::ast::common as ast; use lang_graphql::normalized_ast; use ndc_client as gdc; +use serde::Serialize; use crate::execute::error; -use crate::schema::types; +use crate::execute::model_tracking::{count_model, UsagesCounts}; +use crate::schema::types::output_type::relationship::FilterRelationshipAnnotation; +use crate::schema::types::{self}; use crate::schema::types::{InputAnnotation, ModelInputAnnotation}; use crate::schema::GDS; +use super::relationship::LocalModelRelationshipInfo; +use crate::execute::ir::selection_set::NDCRelationshipName; + +#[derive(Debug, Serialize)] +pub(crate) struct ResolvedFilterExpression<'s> { + pub expressions: Vec, + // relationships that were used in the filter expression. This is helpful + // for collecting relatinships and sending collection_relationships + pub relationships: BTreeMap>, +} + /// Generates the IR for GraphQL 'where' boolean expression -pub(crate) fn resolve_filter_expression( - fields: &IndexMap>, -) -> Result, error::Error> { +pub(crate) fn resolve_filter_expression<'s>( + fields: &IndexMap>, + usage_counts: &mut UsagesCounts, +) -> Result, error::Error> { let mut expressions = Vec::new(); - for (_field_name, field) in fields { - match field.info.generic { - // "_and" - types::Annotation::Input(InputAnnotation::Model( - ModelInputAnnotation::ModelFilterArgument { - field: types::ModelFilterArgument::AndOp, - }, - )) => { - let values = field.value.as_list()?; - let expression = gdc::models::Expression::And { - expressions: values - .iter() - .map(|value| { - Ok(gdc::models::Expression::And { - expressions: resolve_filter_expression(value.as_object()?)?, - }) - }) - .collect::, error::Error>>()?, - }; - expressions.push(expression); + let mut relationships = BTreeMap::new(); + for field in fields.values() { + let relationship_paths = Vec::new(); + let expression = + build_filter_expression(field, relationship_paths, &mut relationships, usage_counts)?; + expressions.extend(expression); + } + let resolved_filter_expression = ResolvedFilterExpression { + expressions, + relationships, + }; + Ok(resolved_filter_expression) +} + +// Build the NDC filter expression by traversing the relationships when present +pub(crate) fn build_filter_expression<'s>( + field: &normalized_ast::InputField<'s, GDS>, + // The path to access the relationship column. If the column is a + // non-relationship column, this will be empty. The paths contains the names + // of relationships (in order) that needs to be traversed to access the + // column. + mut relationship_paths: Vec, + relationships: &mut BTreeMap>, + usage_counts: &mut UsagesCounts, +) -> Result, error::Error> { + match field.info.generic { + // "_and" + types::Annotation::Input(InputAnnotation::Model( + ModelInputAnnotation::ModelFilterArgument { + field: types::ModelFilterArgument::AndOp, + }, + )) => { + let mut expressions = Vec::new(); + let values = field.value.as_list()?; + + for value in values.iter() { + let resolved_filter_expression = + resolve_filter_expression(value.as_object()?, usage_counts)?; + expressions.extend(resolved_filter_expression.expressions); + relationships.extend(resolved_filter_expression.relationships); } - // "_or" - types::Annotation::Input(InputAnnotation::Model( - ModelInputAnnotation::ModelFilterArgument { - field: types::ModelFilterArgument::OrOp, - }, - )) => { - let values = field.value.as_list()?; - let expression = gdc::models::Expression::Or { - expressions: values - .iter() - .map(|value| { - Ok(gdc::models::Expression::And { - expressions: resolve_filter_expression(value.as_object()?)?, - }) - }) - .collect::, error::Error>>()?, - }; - expressions.push(expression); + + let expression = gdc::models::Expression::And { expressions }; + + Ok(vec![expression]) + } + // "_or" + types::Annotation::Input(InputAnnotation::Model( + ModelInputAnnotation::ModelFilterArgument { + field: types::ModelFilterArgument::OrOp, + }, + )) => { + let mut expressions = Vec::new(); + let values = field.value.as_list()?; + + for value in values.iter() { + let resolved_filter_expression = + resolve_filter_expression(value.as_object()?, usage_counts)?; + expressions.extend(resolved_filter_expression.expressions); + relationships.extend(resolved_filter_expression.relationships); } - // "_not" - types::Annotation::Input(InputAnnotation::Model( - ModelInputAnnotation::ModelFilterArgument { - field: types::ModelFilterArgument::NotOp, - }, - )) => { - let value = field.value.as_object()?; - expressions.push(gdc::models::Expression::Not { - expression: Box::new(gdc::models::Expression::And { - expressions: resolve_filter_expression(value)?, - }), - }) - } - types::Annotation::Input(InputAnnotation::Model( - ModelInputAnnotation::ModelFilterArgument { - field: types::ModelFilterArgument::Field { ndc_column: column }, - }, - )) => { - for (op_name, op_value) in field.value.as_object()? { - let expression = match op_name.as_str() { - "_eq" => build_binary_comparison_expression( - gdc::models::BinaryComparisonOperator::Equal, + + let expression = gdc::models::Expression::Or { expressions }; + + Ok(vec![expression]) + } + // "_not" + types::Annotation::Input(InputAnnotation::Model( + ModelInputAnnotation::ModelFilterArgument { + field: types::ModelFilterArgument::NotOp, + }, + )) => { + let mut expressions = Vec::new(); + let value = field.value.as_object()?; + + let resolved_filter_expression = resolve_filter_expression(value, usage_counts)?; + relationships.extend(resolved_filter_expression.relationships); + + expressions.push(gdc::models::Expression::Not { + expression: Box::new(gdc::models::Expression::And { + expressions: resolved_filter_expression.expressions, + }), + }); + Ok(expressions) + } + // The column that we want to use for filtering. If the column happens + // to be a relationship column, we'll have to join all the paths to + // specify NDC, what relationships needs to be traversed to access this + // column. The order decides how to access the column. + types::Annotation::Input(InputAnnotation::Model( + ModelInputAnnotation::ModelFilterArgument { + field: types::ModelFilterArgument::Field { ndc_column: column }, + }, + )) => { + let mut expressions = Vec::new(); + for (op_name, op_value) in field.value.as_object()? { + let expression = match op_name.as_str() { + "_eq" => build_binary_comparison_expression( + gdc::models::BinaryComparisonOperator::Equal, + column.clone(), + &op_value.value, + &relationship_paths, + ), + "_is_null" => build_is_null_expression( + column.clone(), + &op_value.value, + &relationship_paths, + )?, + other => { + let operator = gdc::models::BinaryComparisonOperator::Other { + name: other.to_string(), + }; + build_binary_comparison_expression( + operator, column.clone(), &op_value.value, - ), - "_is_null" => build_is_null_expression(column.clone(), &op_value.value)?, - other => { - let operator = gdc::models::BinaryComparisonOperator::Other { - name: other.to_string(), - }; - build_binary_comparison_expression( - operator, - column.clone(), - &op_value.value, - ) - } - }; - expressions.push(expression) - } + &relationship_paths, + ) + } + }; + expressions.push(expression) } - annotation => Err(error::InternalEngineError::UnexpectedAnnotation { - annotation: annotation.clone(), - })?, + Ok(expressions) } + // Relationship field used for filtering. + // This relationship can either point to another relationship or a column. + types::Annotation::Input(InputAnnotation::Model( + ModelInputAnnotation::ModelFilterArgument { + field: + types::ModelFilterArgument::RelationshipField(FilterRelationshipAnnotation { + relationship_name, + relationship_type, + source_type, + source_data_connector, + source_type_mappings, + target_source, + target_type, + target_model_name, + mappings, + }), + }, + )) => { + // Add the target model being used in the usage counts + count_model(target_model_name.clone(), usage_counts); + + let ndc_relationship_name = NDCRelationshipName::new(source_type, relationship_name)?; + relationships.insert( + ndc_relationship_name.clone(), + LocalModelRelationshipInfo { + relationship_name, + relationship_type, + source_type, + source_data_connector, + source_type_mappings, + target_source, + target_type, + mappings, + }, + ); + + let mut expressions = Vec::new(); + + // This map contains the relationships or the columns of the + // relationship that needs to be used for ordering. + let argument_value_map = field.value.as_object()?; + // Add the current relationship to the relationship paths. + relationship_paths.push(ndc_relationship_name); + // Keep track of relationship paths as we keep traversing down the + // relationships. + for argument in argument_value_map.values() { + let expression = build_filter_expression( + argument, + relationship_paths.clone(), + relationships, + usage_counts, + )?; + expressions.extend(expression); + } + Ok(expressions) + } + annotation => Err(error::InternalEngineError::UnexpectedAnnotation { + annotation: annotation.clone(), + })?, } - Ok(expressions) } /// Generate a binary comparison operator @@ -106,11 +221,14 @@ fn build_binary_comparison_expression( operator: gdc::models::BinaryComparisonOperator, column: String, value: &normalized_ast::Value<'_, GDS>, + relationship_paths: &Vec, ) -> gdc::models::Expression { + let path_elements = build_path_elements(relationship_paths); + gdc::models::Expression::BinaryComparisonOperator { column: gdc::models::ComparisonTarget::Column { name: column, - path: vec![], + path: path_elements, }, operator, value: gdc::models::ComparisonValue::Scalar { @@ -123,12 +241,15 @@ fn build_binary_comparison_expression( fn build_is_null_expression( column: String, value: &normalized_ast::Value<'_, GDS>, + relationship_paths: &Vec, ) -> Result { + let path_elements = build_path_elements(relationship_paths); + // Build an 'IsNull' unary comparison expression let unary_comparison_expression = gdc::models::Expression::UnaryComparisonOperator { column: gdc::models::ComparisonTarget::Column { name: column, - path: vec![], + path: path_elements, }, operator: gdc::models::UnaryComparisonOperator::IsNull, }; @@ -144,3 +265,21 @@ fn build_is_null_expression( }) } } + +pub fn build_path_elements( + relationship_paths: &Vec, +) -> Vec { + let mut path_elements = Vec::new(); + for path in relationship_paths { + path_elements.push(gdc::models::PathElement { + relationship: path.0.clone(), + arguments: BTreeMap::new(), + // 'AND' predicate indicates that the column can be accessed + // by joining all the relationships paths provided + predicate: Box::new(gdc::models::Expression::And { + expressions: Vec::new(), + }), + }) + } + path_elements +} diff --git a/v3/engine/src/execute/ir/model_selection.rs b/v3/engine/src/execute/ir/model_selection.rs index 33ecf0a636d..8f1b3156d66 100644 --- a/v3/engine/src/execute/ir/model_selection.rs +++ b/v3/engine/src/execute/ir/model_selection.rs @@ -7,6 +7,8 @@ use open_dds::types::CustomTypeName; use serde::Serialize; use std::collections::BTreeMap; +use super::filter::ResolvedFilterExpression; +use super::order_by::ResolvedOrderBy; use super::permissions; use super::selection_set; use crate::execute::error; @@ -28,7 +30,7 @@ pub struct ModelSelection<'s> { pub(crate) arguments: BTreeMap, // The boolean expression that would fetch a single row from this model - pub(crate) filter_clause: Vec, + pub(crate) filter_clause: ResolvedFilterExpression<'s>, // Limit pub(crate) limit: Option, @@ -37,7 +39,7 @@ pub struct ModelSelection<'s> { pub(crate) offset: Option, // Order by - pub(crate) order_by: Option, + pub(crate) order_by: Option>, // Fields requested from the model pub(crate) selection: selection_set::ResultSelectionSet<'s>, @@ -50,21 +52,30 @@ pub(crate) fn model_selection_ir<'s>( data_type: &Qualified, model_source: &'s resolved::model::ModelSource, arguments: BTreeMap, - mut filter_clauses: Vec, - permissions_predicate: &resolved::model::FilterPermission, + mut filter_clauses: ResolvedFilterExpression<'s>, + permissions_predicate: &'s resolved::model::FilterPermission, limit: Option, offset: Option, - order_by: Option, + order_by: Option>, session_variables: &SessionVariables, usage_counts: &mut UsagesCounts, ) -> Result, error::Error> { match permissions_predicate { resolved::model::FilterPermission::AllowAll => {} resolved::model::FilterPermission::Filter(predicate) => { - filter_clauses.push(permissions::process_model_predicate( + let permissions_predicate_relationship_paths = Vec::new(); + let mut permissions_predicate_relationships = BTreeMap::new(); + let processed_model_perdicate = permissions::process_model_predicate( predicate, session_variables, - )?); + permissions_predicate_relationship_paths, + &mut permissions_predicate_relationships, + usage_counts, + )?; + filter_clauses.expressions.push(processed_model_perdicate); + for (rel_name, rel_info) in permissions_predicate_relationships { + filter_clauses.relationships.insert(rel_name, rel_info); + } } }; let field_mappings = model_source diff --git a/v3/engine/src/execute/ir/order_by.rs b/v3/engine/src/execute/ir/order_by.rs index f9f2fa92394..0028a54681e 100644 --- a/v3/engine/src/execute/ir/order_by.rs +++ b/v3/engine/src/execute/ir/order_by.rs @@ -1,61 +1,53 @@ +use std::collections::BTreeMap; + +use gdc::models::PathElement; use lang_graphql::normalized_ast::{self as normalized_ast, InputField}; use ndc_client as gdc; +use serde::Serialize; +use crate::execute::model_tracking::{count_model, UsagesCounts}; +use crate::schema::types::output_type::relationship::OrderByRelationshipAnnotation; use crate::schema::types::{Annotation, InputAnnotation, ModelInputAnnotation}; +use super::relationship::LocalModelRelationshipInfo; +use super::selection_set::NDCRelationshipName; + use crate::execute::error; use crate::schema::types; use crate::schema::GDS; -pub fn build_ndc_order_by( - args_field: &InputField, -) -> Result { +#[derive(Debug, Serialize)] +pub(crate) struct ResolvedOrderBy<'s> { + pub(crate) order_by: gdc::models::OrderBy, + // relationships that were used in the order_by expression. This is helpful + // for collecting relatinships and sending collection_relationships + pub(crate) relationships: BTreeMap>, +} + +pub(crate) fn build_ndc_order_by<'s>( + args_field: &InputField<'s, GDS>, + usage_counts: &mut UsagesCounts, +) -> Result, error::Error> { match &args_field.value { normalized_ast::Value::Object(arguments) => { let mut ndc_order_elements = Vec::new(); - for (_name, argument) in arguments { - match argument.info.generic { - Annotation::Input(InputAnnotation::Model( - types::ModelInputAnnotation::ModelOrderByArgument { ndc_column }, - )) => { - let order_by_value = argument.value.as_enum()?; - let order_direction = match &order_by_value.info.generic { - Annotation::Input(InputAnnotation::Model( - ModelInputAnnotation::ModelOrderByDirection { direction }, - )) => match &direction { - types::ModelOrderByDirection::Asc => { - gdc::models::OrderDirection::Asc - } - types::ModelOrderByDirection::Desc => { - gdc::models::OrderDirection::Desc - } - }, - &annotation => { - return Err(error::InternalEngineError::UnexpectedAnnotation { - annotation: annotation.clone(), - })? - } - }; - - let order_element = gdc::models::OrderByElement { - order_direction, - target: gdc::models::OrderByTarget::Column { - name: ndc_column.clone(), - path: Vec::new(), - }, - }; - - ndc_order_elements.push(order_element); - } - annotation => { - return Err(error::InternalEngineError::UnexpectedAnnotation { - annotation: annotation.clone(), - })?; - } - } + let mut relationships = BTreeMap::new(); + // TODO: use argument.values? + for argument in arguments.values() { + let relationship_paths = Vec::new(); + let order_by_element = build_ndc_order_by_element( + argument, + relationship_paths, + &mut relationships, + usage_counts, + )?; + ndc_order_elements.extend(order_by_element); } - Ok(gdc::models::OrderBy { - elements: ndc_order_elements, + Ok(ResolvedOrderBy { + order_by: gdc::models::OrderBy { + elements: ndc_order_elements, + }, + relationships, }) } _ => Err(error::InternalEngineError::InternalGeneric { @@ -63,3 +55,163 @@ pub fn build_ndc_order_by( })?, } } + +// Build the NDC OrderByElement by traversing the relationships when present +// For eg: If we have the following order_by query: +// Track(order_by: {Album: {Artist: {ArtistId: Asc}, AlbumId: Asc}}, limit: 15) +// where, '{Album: {Artist: {ArtistId: Asc}, AlbumId: Asc}}' will be annotated as 'ModelOrderByRelationshipArgument' +// the `OrderByElement` will be: +// [ +// gdc::models::OrderByElement { +// order_direction: Asc, +// target: gdc::models::OrderByTarget::Column { +// name: "ArtistId", +// path: ["TrackAlbum", "AlbumArtist"] +// } +// }, +// gdc::models::OrderByElement { +// order_direction: Asc, +// target: gdc::models::OrderByTarget::Column { +// name: "AlbumId", +// path: ["TrackAlbum"] +// } +// } +// ] + +pub(crate) fn build_ndc_order_by_element<'s>( + argument: &InputField<'s, GDS>, + // The path to access the relationship column. If the column is a + // non-relationship column, this will be empty. The paths contains + // the names of relationships (in order) that needs to be traversed + // to access the column. + mut relationship_paths: Vec, + relationships: &mut BTreeMap>, + usage_counts: &mut UsagesCounts, +) -> Result, error::Error> { + match argument.info.generic { + // The column that we want to use for ordering. If the column happens to be + // a relationship column, we'll have to join all the paths to specify NDC, + // what relationships needs to be traversed to access this column + Annotation::Input(InputAnnotation::Model( + types::ModelInputAnnotation::ModelOrderByArgument { ndc_column }, + )) => { + let order_by_value = argument.value.as_enum()?; + let order_direction = match &order_by_value.info.generic { + Annotation::Input(InputAnnotation::Model( + ModelInputAnnotation::ModelOrderByDirection { direction }, + )) => match &direction { + types::ModelOrderByDirection::Asc => gdc::models::OrderDirection::Asc, + types::ModelOrderByDirection::Desc => gdc::models::OrderDirection::Desc, + }, + &annotation => { + return Err(error::InternalEngineError::UnexpectedAnnotation { + annotation: annotation.clone(), + })? + } + }; + + let mut order_by_element_path = Vec::new(); + // When using a nested relationship column, you'll have to provide all the relationships(paths) + // NDC has to traverse to access the column. The ordering of that paths is important. + // The order decides how to access the column. + // + // For example, if you have a model called `User` with a relationship column called `Posts` + // which has a relationship column called `Comments` which has a non-relationship column + // called `text`, you'll have to provide the following paths to access the `text` column: + // ["UserPosts", "PostsComments"] + for path in relationship_paths.iter() { + order_by_element_path.push(PathElement { + relationship: path.0.clone(), + arguments: BTreeMap::new(), + // 'AND' predicate indicates that the column can be accessed + // by joining all the relationships paths provided + predicate: Box::new(gdc::models::Expression::And { + // TODO(naveen): Add expressions here, when we support sorting with predicates. + // + // There are two types of sorting: + // 1. Sorting without predicates + // 2. Sorting with predicates + // + // In the 1st sort, we sort all the elements of the results either in ascending + // or descing order based on the order_by argument. + // + // In the 2nd sort, we want fetch the entire result but only sort a subset + // of result and put those sorted set either at the beginning or at the end of the + // result. + // + // Currently we only support the 1st type of sort. Hence we don't have any expressions/predicate. + expressions: Vec::new(), + }), + }) + } + + let order_element = gdc::models::OrderByElement { + order_direction, + // TODO(naveen): When aggregates are supported, extend this to support other gdc::models::OrderByTarget + target: gdc::models::OrderByTarget::Column { + name: ndc_column.clone(), + path: order_by_element_path, + }, + }; + + Ok(vec![order_element]) + } + // A relationship is being used to order the results. This relationship can + // either point to another relationship or a column. + Annotation::Input(InputAnnotation::Model( + types::ModelInputAnnotation::ModelOrderByRelationshipArgument( + OrderByRelationshipAnnotation { + relationship_name, + relationship_type, + source_type, + source_data_connector, + source_type_mappings, + target_source, + target_type, + target_model_name, + mappings, + }, + ), + )) => { + let ndc_relationship_name = NDCRelationshipName::new(source_type, relationship_name)?; + + relationships.insert( + ndc_relationship_name.clone(), + LocalModelRelationshipInfo { + relationship_name, + relationship_type, + source_type, + source_data_connector, + source_type_mappings, + target_source, + target_type, + mappings, + }, + ); + + // Add the target model being used in the usage counts + count_model(target_model_name.clone(), usage_counts); + + // This map contains the relationships or the columns of the relationship that needs to be used for ordering. + let argument_value_map = argument.value.as_object()?; + let mut order_by_elements = Vec::new(); + + // Add the current relationship to the relationship paths. + relationship_paths.push(ndc_relationship_name); + + for argument in argument_value_map.values() { + let order_by_element = build_ndc_order_by_element( + argument, + relationship_paths.clone(), + relationships, + usage_counts, + )?; + order_by_elements.extend(order_by_element); + } + Ok(order_by_elements) + } + annotation => Err(error::InternalEngineError::UnexpectedAnnotation { + annotation: annotation.clone(), + })?, + } +} diff --git a/v3/engine/src/execute/ir/permissions.rs b/v3/engine/src/execute/ir/permissions.rs index 9a1cccb442c..8a49e6d71c0 100644 --- a/v3/engine/src/execute/ir/permissions.rs +++ b/v3/engine/src/execute/ir/permissions.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use hasura_authn_core::{SessionVariableValue, SessionVariables}; use lang_graphql::normalized_ast; use ndc_client as gdc; @@ -5,6 +7,7 @@ use ndc_client as gdc; use open_dds::{permissions::ValueExpression, types::InbuiltType}; use crate::execute::error::{Error, InternalDeveloperError, InternalEngineError, InternalError}; +use crate::execute::model_tracking::{count_model, UsagesCounts}; use crate::metadata::resolved; use crate::metadata::resolved::subgraph::{ @@ -13,6 +16,9 @@ use crate::metadata::resolved::subgraph::{ use crate::schema::types; use crate::schema::GDS; +use super::relationship::LocalModelRelationshipInfo; +use super::selection_set::NDCRelationshipName; + /// Fetch filter expression from the namespace annotation /// of the field call. If the filter predicate namespace annotation /// is not found, then an error will be thrown. @@ -37,9 +43,12 @@ pub(crate) fn get_select_filter_predicate<'s>( ))) } -pub(crate) fn process_model_predicate( - model_predicate: &resolved::model::ModelPredicate, +pub(crate) fn process_model_predicate<'s>( + model_predicate: &'s resolved::model::ModelPredicate, session_variables: &SessionVariables, + mut relationship_paths: Vec, + relationships: &mut BTreeMap>, + usage_counts: &mut UsagesCounts, ) -> Result { match model_predicate { resolved::model::ModelPredicate::UnaryFieldComparison { @@ -49,6 +58,7 @@ pub(crate) fn process_model_predicate( } => Ok(make_permission_unary_boolean_expression( ndc_column.clone(), operator, + &relationship_paths, )?), resolved::model::ModelPredicate::BinaryFieldComparison { field: _, @@ -62,9 +72,16 @@ pub(crate) fn process_model_predicate( operator, value, session_variables, + &relationship_paths, )?), resolved::model::ModelPredicate::Not(predicate) => { - let expr = process_model_predicate(predicate, session_variables)?; + let expr = process_model_predicate( + predicate, + session_variables, + relationship_paths, + relationships, + usage_counts, + )?; Ok(gdc::models::Expression::Not { expression: Box::new(expr), }) @@ -72,27 +89,68 @@ pub(crate) fn process_model_predicate( resolved::model::ModelPredicate::And(predicates) => { let exprs = predicates .iter() - .map(|p| process_model_predicate(p, session_variables)) + .map(|p| { + process_model_predicate( + p, + session_variables, + relationship_paths.clone(), + relationships, + usage_counts, + ) + }) .collect::, Error>>()?; Ok(gdc::models::Expression::And { expressions: exprs }) } resolved::model::ModelPredicate::Or(predicates) => { let exprs = predicates .iter() - .map(|p| process_model_predicate(p, session_variables)) + .map(|p| { + process_model_predicate( + p, + session_variables, + relationship_paths.clone(), + relationships, + usage_counts, + ) + }) .collect::, Error>>()?; Ok(gdc::models::Expression::Or { expressions: exprs }) } - // TODO: implement this - // TODO: naveen: When we can use models in predicates, make sure to - // include those models in the 'models_used' field of the IR's. This is - // for tracking the models used in query. resolved::model::ModelPredicate::Relationship { - name: _, - predicate: _, - } => Err(InternalEngineError::InternalGeneric { - description: "'relationship' model predicate is not supported yet.".to_string(), - })?, + relationship_info, + predicate, + } => { + let relationship_name = (NDCRelationshipName::new( + &relationship_info.source_type, + &relationship_info.relationship_name, + ))?; + + relationship_paths.push(relationship_name.clone()); + relationships.insert( + relationship_name.clone(), + LocalModelRelationshipInfo { + relationship_name: &relationship_info.relationship_name, + relationship_type: &relationship_info.relationship_type, + source_type: &relationship_info.source_type, + source_data_connector: &relationship_info.source_data_connector, + source_type_mappings: &relationship_info.source_type_mappings, + target_source: &relationship_info.target_source, + target_type: &relationship_info.target_type, + mappings: &relationship_info.mappings, + }, + ); + + // Add the target model being used in the usage counts + count_model(relationship_info.target_model_name.clone(), usage_counts); + + process_model_predicate( + predicate, + session_variables, + relationship_paths, + relationships, + usage_counts, + ) + } } } @@ -102,13 +160,15 @@ fn make_permission_binary_boolean_expression( operator: &ndc_client::models::BinaryComparisonOperator, value_expression: &ValueExpression, session_variables: &SessionVariables, + relationship_paths: &Vec, ) -> Result { + let path_elements = super::filter::build_path_elements(relationship_paths); let ndc_expression_value = make_value_from_value_expression(value_expression, argument_type, session_variables)?; Ok(gdc::models::Expression::BinaryComparisonOperator { column: gdc::models::ComparisonTarget::Column { name: ndc_column, - path: vec![], + path: path_elements, }, operator: operator.clone(), value: ndc_expression_value, @@ -118,11 +178,13 @@ fn make_permission_binary_boolean_expression( fn make_permission_unary_boolean_expression( ndc_column: String, operator: &ndc_client::models::UnaryComparisonOperator, + relationship_paths: &Vec, ) -> Result { + let path_elements = super::filter::build_path_elements(relationship_paths); Ok(gdc::models::Expression::UnaryComparisonOperator { column: gdc::models::ComparisonTarget::Column { name: ndc_column, - path: vec![], + path: path_elements, }, operator: *operator, }) diff --git a/v3/engine/src/execute/ir/query_root/node_field.rs b/v3/engine/src/execute/ir/query_root/node_field.rs index 9e06afa91df..6c26d02fcf3 100644 --- a/v3/engine/src/execute/ir/query_root/node_field.rs +++ b/v3/engine/src/execute/ir/query_root/node_field.rs @@ -10,6 +10,7 @@ use serde::Serialize; use std::collections::{BTreeMap, HashMap}; use crate::execute::error; +use crate::execute::ir::filter::ResolvedFilterExpression; use crate::execute::ir::model_selection; use crate::execute::model_tracking::UsagesCounts; use crate::metadata::resolved; @@ -122,7 +123,7 @@ pub(crate) fn relay_node_ir<'n, 's>( typename_mapping.type_name ), })?; - let filter_clauses = global_id + let filter_clause_expressions = global_id .id .iter() .map(|(field_name, val)| { @@ -148,6 +149,11 @@ pub(crate) fn relay_node_ir<'n, 's>( let mut usage_counts = UsagesCounts::new(); + let filter_clauses = ResolvedFilterExpression { + expressions: filter_clause_expressions, + relationships: BTreeMap::new(), + }; + let model_selection = model_selection::model_selection_ir( &new_selection_set, &typename_mapping.type_name, diff --git a/v3/engine/src/execute/ir/query_root/select_many.rs b/v3/engine/src/execute/ir/query_root/select_many.rs index 8ae103bc6a2..83420555866 100644 --- a/v3/engine/src/execute/ir/query_root/select_many.rs +++ b/v3/engine/src/execute/ir/query_root/select_many.rs @@ -13,6 +13,7 @@ use std::collections::BTreeMap; use super::error; use crate::execute::ir::arguments; use crate::execute::ir::filter; +use crate::execute::ir::filter::ResolvedFilterExpression; use crate::execute::ir::model_selection; use crate::execute::ir::order_by::build_ndc_order_by; use crate::execute::ir::permissions; @@ -49,10 +50,17 @@ pub(crate) fn select_many_generate_ir<'n, 's>( ) -> Result, error::Error> { let mut limit = None; let mut offset = None; - let mut filter_clause = Vec::new(); + let mut filter_clause = ResolvedFilterExpression { + expressions: Vec::new(), + relationships: BTreeMap::new(), + }; let mut order_by = None; let mut model_arguments = BTreeMap::new(); + // Add the name of the root model + let mut usage_counts = UsagesCounts::new(); + count_model(model_name.clone(), &mut usage_counts); + for argument in field_call.arguments.values() { match argument.info.generic { annotation @ Annotation::Input(types::InputAnnotation::Model( @@ -65,7 +73,10 @@ pub(crate) fn select_many_generate_ir<'n, 's>( offset = Some(argument.value.as_int_u32()?) } ModelInputAnnotation::ModelFilterExpression => { - filter_clause = filter::resolve_filter_expression(argument.value.as_object()?)? + filter_clause = filter::resolve_filter_expression( + argument.value.as_object()?, + &mut usage_counts, + )?; } ModelInputAnnotation::ModelArgumentsExpression => match &argument.value { normalized_ast::Value::Object(arguments) => { @@ -83,7 +94,7 @@ pub(crate) fn select_many_generate_ir<'n, 's>( })?, }, ModelInputAnnotation::ModelOrderByExpression => { - order_by = Some(build_ndc_order_by(argument)?) + order_by = Some(build_ndc_order_by(argument, &mut usage_counts)?) } _ => { return Err(error::InternalEngineError::UnexpectedAnnotation { @@ -100,10 +111,6 @@ pub(crate) fn select_many_generate_ir<'n, 's>( } } - // Add the name of the root model - let mut usage_counts = UsagesCounts::new(); - count_model(model_name.clone(), &mut usage_counts); - let model_selection = model_selection::model_selection_ir( &field.selection_set, data_type, diff --git a/v3/engine/src/execute/ir/query_root/select_one.rs b/v3/engine/src/execute/ir/query_root/select_one.rs index e93b6601606..df413212881 100644 --- a/v3/engine/src/execute/ir/query_root/select_one.rs +++ b/v3/engine/src/execute/ir/query_root/select_one.rs @@ -2,6 +2,8 @@ //! //! A 'select_one' operation fetches zero or one row from a model +use std::collections::BTreeMap; + /// Generates the IR for a 'select_one' operation // TODO: Remove once TypeMapping has more than one variant use hasura_authn_core::SessionVariables; @@ -12,6 +14,7 @@ use serde::Serialize; use super::error; use crate::execute::ir::arguments; +use crate::execute::ir::filter::ResolvedFilterExpression; use crate::execute::ir::model_selection; use crate::execute::ir::permissions; use crate::execute::model_tracking::{count_model, UsagesCounts}; @@ -59,7 +62,7 @@ pub(crate) fn select_one_generate_ir<'n, 's>( description: format!("type '{:}' not found in source type_mappings", data_type), })?; - let mut filter_clause = vec![]; + let mut filter_clause_expressions = vec![]; let mut model_argument_fields = Vec::new(); for argument in field_call.arguments.values() { match argument.info.generic { @@ -87,7 +90,7 @@ pub(crate) fn select_one_generate_ir<'n, 's>( value: argument.value.as_json(), }, }; - filter_clause.push(ndc_expression); + filter_clause_expressions.push(ndc_expression); } _ => Err(error::InternalEngineError::UnexpectedAnnotation { annotation: annotation.clone(), @@ -108,6 +111,11 @@ pub(crate) fn select_one_generate_ir<'n, 's>( let mut usage_counts = UsagesCounts::new(); count_model(model_name.clone(), &mut usage_counts); + let filter_clause = ResolvedFilterExpression { + expressions: filter_clause_expressions, + relationships: BTreeMap::new(), + }; + let model_selection = model_selection::model_selection_ir( &field.selection_set, data_type, diff --git a/v3/engine/src/execute/ir/relationship.rs b/v3/engine/src/execute/ir/relationship.rs index a505e8b74f4..b42bd775f4f 100644 --- a/v3/engine/src/execute/ir/relationship.rs +++ b/v3/engine/src/execute/ir/relationship.rs @@ -11,16 +11,22 @@ use open_dds::{ use ndc_client as ndc; use serde::Serialize; -use super::order_by::build_ndc_order_by; use super::permissions; use super::selection_set::FieldSelection; use super::{ commands::generate_function_based_command, filter::resolve_filter_expression, - model_selection::model_selection_ir, + filter::ResolvedFilterExpression, model_selection::model_selection_ir, +}; +use super::{ + order_by::{build_ndc_order_by, ResolvedOrderBy}, + selection_set::NDCRelationshipName, }; use crate::execute::model_tracking::{count_model, UsagesCounts}; -use crate::metadata::resolved::subgraph::serialize_qualified_btreemap; +use crate::metadata::resolved::{ + relationship::{relationship_execution_category, RelationshipExecutionCategory}, + subgraph::serialize_qualified_btreemap, +}; use crate::schema::types::output_type::relationship::{ ModelRelationshipAnnotation, ModelTargetSource, }; @@ -40,11 +46,15 @@ use crate::{ #[derive(Debug, Serialize)] pub(crate) struct LocalModelRelationshipInfo<'s> { - pub annotation: &'s ModelRelationshipAnnotation, + pub relationship_name: &'s RelationshipName, + pub relationship_type: &'s RelationshipType, + pub source_type: &'s Qualified, pub source_data_connector: &'s resolved::data_connector::DataConnector, #[serde(serialize_with = "serialize_qualified_btreemap")] pub source_type_mappings: &'s BTreeMap, resolved::types::TypeMapping>, pub target_source: &'s ModelTargetSource, + pub target_type: &'s Qualified, + pub mappings: &'s Vec, } #[derive(Debug, Serialize)] @@ -79,17 +89,22 @@ pub(crate) fn process_model_relationship_definition( relationship_info: &LocalModelRelationshipInfo, ) -> Result { let &LocalModelRelationshipInfo { - annotation, + relationship_name, + relationship_type, + source_type, source_data_connector, source_type_mappings, target_source, + + target_type, + mappings, } = relationship_info; let mut column_mapping = BTreeMap::new(); for resolved::relationship::RelationshipModelMapping { source_field: source_field_path, target_field: target_field_path, - } in annotation.mappings.iter() + } in mappings.iter() { if !matches!( relationship_execution_category( @@ -103,14 +118,14 @@ pub(crate) fn process_model_relationship_definition( } else { let source_column = get_field_mapping_of_field_name( source_type_mappings, - &annotation.source_type, - &annotation.relationship_name, + source_type, + relationship_name, &source_field_path.field_name, )?; let target_column = get_field_mapping_of_field_name( &target_source.model.type_mappings, - &annotation.target_type, - &annotation.relationship_name, + target_type, + relationship_name, &target_field_path.field_name, )?; @@ -120,7 +135,7 @@ pub(crate) fn process_model_relationship_definition( { Err(error::InternalEngineError::MappingExistsInRelationship { source_column: source_field_path.field_name.clone(), - relationship_name: annotation.relationship_name.clone(), + relationship_name: relationship_name.clone(), })? } } @@ -128,7 +143,7 @@ pub(crate) fn process_model_relationship_definition( let ndc_relationship = ndc_client::models::Relationship { column_mapping, relationship_type: { - match annotation.relationship_type { + match relationship_type { RelationshipType::Object => ndc_client::models::RelationshipType::Object, RelationshipType::Array => ndc_client::models::RelationshipType::Array, } @@ -197,32 +212,6 @@ pub(crate) fn process_command_relationship_definition( Ok(ndc_relationship) } -enum RelationshipExecutionCategory { - // Push down relationship definition to the data connector - Local, - // Use foreach in the data connector to fetch related rows for multiple objects in a single request - RemoteForEach, -} - -#[allow(clippy::match_single_binding)] -fn relationship_execution_category( - source_connector: &resolved::data_connector::DataConnector, - target_connector: &resolved::data_connector::DataConnector, - relationship_capabilities: &resolved::relationship::RelationshipCapabilities, -) -> RelationshipExecutionCategory { - // It's a local relationship if the source and target connectors are the same and - // the connector supports relationships. - if target_connector.name == source_connector.name && relationship_capabilities.relationships { - RelationshipExecutionCategory::Local - } else { - match relationship_capabilities.foreach { - // TODO: When we support naive relationships for connectors not implementing foreach, - // add another match arm / return enum variant - () => RelationshipExecutionCategory::RemoteForEach, - } - } -} - pub(crate) fn generate_model_relationship_ir<'s>( field: &Field<'s, GDS>, annotation: &'s ModelRelationshipAnnotation, @@ -237,7 +226,10 @@ pub(crate) fn generate_model_relationship_ir<'s>( let mut limit = None; let mut offset = None; - let mut filter_clause = Vec::new(); + let mut filter_clause = ResolvedFilterExpression { + expressions: Vec::new(), + relationships: BTreeMap::new(), + }; let mut order_by = None; for argument in field_call.arguments.values() { @@ -252,10 +244,13 @@ pub(crate) fn generate_model_relationship_ir<'s>( offset = Some(argument.value.as_int_u32()?) } ModelInputAnnotation::ModelFilterExpression => { - filter_clause = resolve_filter_expression(argument.value.as_object()?)? + filter_clause = resolve_filter_expression( + argument.value.as_object()?, + usage_counts, + )? } ModelInputAnnotation::ModelOrderByExpression => { - order_by = Some(build_ndc_order_by(argument)?) + order_by = Some(build_ndc_order_by(argument, usage_counts)?) } _ => { return Err(error::InternalEngineError::UnexpectedAnnotation { @@ -278,7 +273,6 @@ pub(crate) fn generate_model_relationship_ir<'s>( } } } - let target_source = annotation .target_source @@ -385,10 +379,10 @@ pub(crate) fn build_local_model_relationship<'s>( data_connector: &'s resolved::data_connector::DataConnector, type_mappings: &'s BTreeMap, resolved::types::TypeMapping>, target_source: &'s ModelTargetSource, - filter_clause: Vec, + filter_clause: ResolvedFilterExpression<'s>, limit: Option, offset: Option, - order_by: Option, + order_by: Option>, session_variables: &SessionVariables, usage_counts: &mut UsagesCounts, ) -> Result, error::Error> { @@ -406,24 +400,19 @@ pub(crate) fn build_local_model_relationship<'s>( usage_counts, )?; let rel_info = LocalModelRelationshipInfo { - annotation, + relationship_name: &annotation.relationship_name, + relationship_type: &annotation.relationship_type, + source_type: &annotation.source_type, source_data_connector: data_connector, source_type_mappings: type_mappings, target_source, + target_type: &annotation.target_type, + mappings: &annotation.mappings, }; - // Relationship names needs to be unique across the IR. This is so that, the - // NDC can use these names to figure out what joins to use. - // A single "source type" can have only one relationship with a given name, - // hence the relationship name in the IR is a tuple between the source type - // and the relationship name. - // Relationship name = (source_type, relationship_name) - let relationship_name = - serde_json::to_string(&(&annotation.source_type, &annotation.relationship_name))?; - Ok(FieldSelection::ModelRelationshipLocal { query: relationships_ir, - name: relationship_name, + name: NDCRelationshipName::new(&annotation.source_type, &annotation.relationship_name)?, relationship_info: rel_info, }) } @@ -461,12 +450,10 @@ pub(crate) fn build_local_command_relationship<'s>( // hence the relationship name in the IR is a tuple between the source type // and the relationship name. // Relationship name = (source_type, relationship_name) - let relationship_name = - serde_json::to_string(&(&annotation.source_type, &annotation.relationship_name))?; Ok(FieldSelection::CommandRelationshipLocal { ir: relationships_ir, - name: relationship_name, + name: NDCRelationshipName::new(&annotation.source_type, &annotation.relationship_name)?, relationship_info: rel_info, }) } @@ -478,10 +465,10 @@ pub(crate) fn build_remote_relationship<'n, 's>( annotation: &'s ModelRelationshipAnnotation, type_mappings: &'s BTreeMap, resolved::types::TypeMapping>, target_source: &'s ModelTargetSource, - filter_clause: Vec, + filter_clause: ResolvedFilterExpression<'s>, limit: Option, offset: Option, - order_by: Option, + order_by: Option>, session_variables: &SessionVariables, usage_counts: &mut UsagesCounts, ) -> Result, error::Error> { @@ -535,7 +522,10 @@ pub(crate) fn build_remote_relationship<'n, 's>( name: target_value_variable, }, }; - remote_relationships_ir.filter_clause.push(comparison_exp); + remote_relationships_ir + .filter_clause + .expressions + .push(comparison_exp); } let rel_info = RemoteModelRelationshipInfo { annotation, diff --git a/v3/engine/src/execute/ir/selection_set.rs b/v3/engine/src/execute/ir/selection_set.rs index c243284bd85..55c4867dcae 100644 --- a/v3/engine/src/execute/ir/selection_set.rs +++ b/v3/engine/src/execute/ir/selection_set.rs @@ -2,8 +2,10 @@ use hasura_authn_core::SessionVariables; use indexmap::IndexMap; use lang_graphql::ast::common::Alias; use lang_graphql::normalized_ast; +use open_dds::relationships::RelationshipName; use open_dds::types::{CustomTypeName, FieldName}; -use serde::Serialize; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use super::commands::FunctionBasedCommand; @@ -29,15 +31,15 @@ pub(crate) enum FieldSelection<'s> { }, ModelRelationshipLocal { query: ModelSelection<'s>, - /// Relationship names needs to be unique across the IR. This field contains - /// the uniquely generated relationship name. `ModelRelationshipAnnotation` - /// contains a relationship name but that is the name from the metadata. - name: String, + // Relationship names needs to be unique across the IR. This field contains + // the uniquely generated relationship name. `ModelRelationshipAnnotation` + // contains a relationship name but that is the name from the metadata. + name: NDCRelationshipName, relationship_info: LocalModelRelationshipInfo<'s>, }, CommandRelationshipLocal { ir: FunctionBasedCommand<'s>, - name: String, + name: NDCRelationshipName, relationship_info: LocalCommandRelationshipInfo<'s>, }, ModelRelationshipRemote { @@ -50,6 +52,38 @@ pub(crate) enum FieldSelection<'s> { }, } +/// The unique relationship name that is passed to NDC +// Relationship names needs to be unique across the IR. This is so that, the +// NDC can use these names to figure out what joins to use. +// A single "source type" can have only one relationship with a given name, +// hence the relationship name in the IR is a tuple between the source type +// and the relationship name. +// Relationship name = (source_type, relationship_name) +#[derive( + Serialize, + Deserialize, + Clone, + Debug, + PartialEq, + Eq, + Hash, + derive_more::Display, + JsonSchema, + PartialOrd, + Ord, +)] +pub struct NDCRelationshipName(pub(crate) String); + +impl NDCRelationshipName { + pub fn new( + source_type: &Qualified, + relationship_name: &RelationshipName, + ) -> Result { + let name = serde_json::to_string(&(source_type, relationship_name))?; + Ok(NDCRelationshipName(name)) + } +} + /// IR that represents the selected fields of an output type. #[derive(Debug, Serialize)] pub(crate) struct ResultSelectionSet<'s> { diff --git a/v3/engine/src/execute/query_plan/model_selection.rs b/v3/engine/src/execute/query_plan/model_selection.rs index e8f03b3b110..0e6491f5118 100644 --- a/v3/engine/src/execute/query_plan/model_selection.rs +++ b/v3/engine/src/execute/query_plan/model_selection.rs @@ -19,8 +19,8 @@ pub(crate) fn ndc_query<'s, 'ir>( fields: Some(ndc_fields), limit: ir.limit, offset: ir.offset, - order_by: ir.order_by.clone(), - predicate: match ir.filter_clause.as_slice() { + order_by: ir.order_by.as_ref().map(|x| x.order_by.clone()), + predicate: match ir.filter_clause.expressions.as_slice() { [] => None, [expression] => Some(expression.clone()), expressions => Some(ndc::models::Expression::And { diff --git a/v3/engine/src/metadata/resolved/error.rs b/v3/engine/src/metadata/resolved/error.rs index 9949ca7735c..9b556bcd6fe 100644 --- a/v3/engine/src/metadata/resolved/error.rs +++ b/v3/engine/src/metadata/resolved/error.rs @@ -34,7 +34,7 @@ pub enum Error { #[error("the data type {data_type:} for model {model_name:} has not been defined")] UnknownModelDataType { model_name: Qualified, - data_type: CustomTypeName, + data_type: Qualified, }, #[error( "the following argument in model {model_name:} is defined more than once: {argument_name:}" @@ -198,6 +198,20 @@ pub enum Error { "model source is required for model '{model_name:}' to resolve select permission predicate" )] ModelSourceRequiredForPredicate { model_name: Qualified }, + #[error( + "both model source for model '{source_model_name:}' and target source for model '{target_model_name}' are required to resolve select permission predicate with relationships" + )] + ModelAndTargetSourceRequiredForRelationshipPredicate { + source_model_name: Qualified, + target_model_name: Qualified, + }, + #[error( + "no relationship predicate is defined for relationship '{relationship_name:}' in model '{model_name:}'" + )] + NoPredicateDefinedForRelationshipPredicate { + model_name: Qualified, + relationship_name: RelationshipName, + }, #[error("unknown field '{field_name:}' used in select permissions of model '{model_name:}'")] UnknownFieldInSelectPermissionsDefinition { field_name: FieldName, @@ -208,6 +222,18 @@ pub enum Error { field_name: FieldName, model_name: Qualified, }, + #[error("relationship '{relationship_name:}' used in select permissions of model '{model_name:}' does not exist on type {type_name:}")] + UnknownRelationshipInSelectPermissionsPredicate { + relationship_name: RelationshipName, + model_name: Qualified, + type_name: Qualified, + }, + #[error("The model '{target_model_name:}' corresponding to the relationship '{relationship_name:}' used in select permissions of model '{model_name:}' is not defined")] + UnknownModelUsedInRelationshipSelectPermissionsPredicate { + model_name: Qualified, + target_model_name: Qualified, + relationship_name: RelationshipName, + }, #[error( "Invalid operator used in model '{model_name:}' select permission: '{operator_name:}'" )] @@ -385,6 +411,12 @@ pub enum Error { relationship_name: RelationshipName, data_connector_name: Qualified, }, + #[error("The target data connector {data_connector_name} for relationship {relationship_name} on type {type_name} has not defined any capabilities")] + NoRelationshipCapabilitiesDefined { + type_name: Qualified, + relationship_name: RelationshipName, + data_connector_name: Qualified, + }, #[error("model {model_name:} with arguments is unsupported as a global ID source")] ModelWithArgumentsAsGlobalIdSource { model_name: Qualified }, #[error("capabilities for the data connector {data_connector:} have not been defined")] diff --git a/v3/engine/src/metadata/resolved/metadata.rs b/v3/engine/src/metadata/resolved/metadata.rs index dab57ad0ab8..a91e3b46662 100644 --- a/v3/engine/src/metadata/resolved/metadata.rs +++ b/v3/engine/src/metadata/resolved/metadata.rs @@ -376,24 +376,37 @@ pub fn resolve_metadata(metadata: open_dds::Metadata) -> Result } } + // Note: Model permissions's predicate can include the relationship field, + // hence Model permissions should be resolved after the relationships of a + // model is resolved. for open_dds::accessor::QualifiedObject { subgraph, object: permissions, } in &metadata_accessor.model_permissions { let model_name = Qualified::new(subgraph.to_string(), permissions.model_name.clone()); - let model = models.get_mut(&model_name).ok_or_else(|| { - Error::UnknownModelInModelSelectPermissions { - model_name: model_name.clone(), - } - })?; + let model = + models + .get(&model_name) + .ok_or_else(|| Error::UnknownModelInModelSelectPermissions { + model_name: model_name.clone(), + })?; if model.select_permissions.is_none() { - model.select_permissions = Some(resolve_model_select_permissions( + let select_permissions = Some(resolve_model_select_permissions( model, subgraph, permissions, &data_connectors, + &types, + &models, // This is required to get the model for the relationship target )?); + + let model = models.get_mut(&model_name).ok_or_else(|| { + Error::UnknownModelInModelSelectPermissions { + model_name: model_name.clone(), + } + })?; + model.select_permissions = select_permissions; } else { return Err(Error::DuplicateModelSelectPermission { model_name: model_name.clone(), diff --git a/v3/engine/src/metadata/resolved/model.rs b/v3/engine/src/metadata/resolved/model.rs index 9613bf518c5..1ed1acb8f43 100644 --- a/v3/engine/src/metadata/resolved/model.rs +++ b/v3/engine/src/metadata/resolved/model.rs @@ -13,10 +13,13 @@ use crate::metadata::resolved::types::{mk_name, FieldDefinition, TypeMapping}; use crate::metadata::resolved::types::{ resolve_type_mappings, ScalarTypeInfo, TypeMappingToResolve, TypeRepresentation, }; +use crate::schema::types::output_type::relationship::{ + ModelTargetSource, PredicateRelationshipAnnotation, +}; use indexmap::IndexMap; -use lang_graphql::ast::common as ast; +use lang_graphql::ast::common::{self as ast}; use ndc_client as ndc; -use open_dds::permissions::NullableModelPredicate; +use open_dds::permissions::{NullableModelPredicate, RelationshipPredicate}; use open_dds::{ arguments::ArgumentName, data_connector::DataConnectorName, @@ -25,13 +28,15 @@ use open_dds::{ OperatorName, OrderableField, }, permissions::{self, ModelPermissionsV1, Role, ValueExpression}, - relationships::RelationshipName, types::{CustomTypeName, FieldName, InbuiltType}, }; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap, HashSet}; use std::iter; +use super::relationship::RelationshipTarget; +use super::types::ObjectTypeRepresentation; + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct SelectUniqueGraphQlDefinition { pub query_root_field: ast::Name, @@ -118,10 +123,9 @@ pub enum ModelPredicate { argument_type: QualifiedTypeReference, value: ValueExpression, }, - // TODO: Remote relationships are disallowed for now Relationship { - name: RelationshipName, - predicate: Option>, + relationship_info: PredicateRelationshipAnnotation, + predicate: Box, }, And(Vec), @@ -215,19 +219,11 @@ pub fn resolve_model( let qualified_object_type_name = Qualified::new(subgraph.to_string(), model.object_type.to_owned()); let qualified_model_name = Qualified::new(subgraph.to_string(), model.name.clone()); - let object_type_representation = match types.get(&qualified_object_type_name) { - Some(TypeRepresentation::Object(object_type_representation)) => { - Ok(object_type_representation) - } - Some(type_rep) => Err(Error::InvalidTypeRepresentation { - model_name: qualified_model_name.clone(), - type_representation: type_rep.clone(), - }), - None => Err(Error::UnknownModelDataType { - model_name: qualified_model_name.clone(), - data_type: model.object_type.clone(), - }), - }?; + let object_type_representation = get_model_object_type_representation( + types, + &qualified_object_type_name, + &qualified_model_name, + )?; if model.global_id_source { // Check if there are any global fields present in the related // object type, if the model is marked as a global source. @@ -406,6 +402,8 @@ fn resolve_model_predicate( subgraph: &str, data_connectors: &HashMap, DataConnectorContext>, fields: &IndexMap, + types: &HashMap, TypeRepresentation>, + models: &IndexMap, Model>, // type_representation: &TypeRepresentation, ) -> Result { match model_predicate { @@ -491,29 +489,120 @@ fn resolve_model_predicate( }) } } - permissions::ModelPredicate::Relationship(_) => { - Err(Error::UnsupportedFeature { - message: "'relationship' model predicate is not supported yet.".into(), - }) - /* TODO: uncomment when we support this + permissions::ModelPredicate::Relationship(RelationshipPredicate { name, predicate }) => { if let Some(nested_predicate) = predicate { - let resolved_predicate = - resolve_model_predicate(nested_predicate, model_name, type_representation)?; - Ok(ModelPredicate::Relationship { - name: name.clone(), - predicate: Some(Box::new(resolved_predicate)), - }) + let object_type_representation = + get_model_object_type_representation(types, &model.data_type, &model.name)?; + let relationship_field_name = mk_name(&name.0)?; + let relationship = &object_type_representation + .relationships + .get(&relationship_field_name) + .ok_or_else(|| Error::UnknownRelationshipInSelectPermissionsPredicate { + relationship_name: name.clone(), + model_name: model.name.clone(), + type_name: model.data_type.clone(), + })?; + + match &relationship.target { + RelationshipTarget::Command { .. } => Err(Error::UnsupportedFeature { + message: "Predicate cannot be built using command relationships" + .to_string(), + }), + RelationshipTarget::Model { + model_name, + relationship_type, + target_typename, + mappings, + } => { + let target_model = models.get(model_name).ok_or_else(|| { + Error::UnknownModelUsedInRelationshipSelectPermissionsPredicate { + model_name: model.name.clone(), + target_model_name: model_name.clone(), + relationship_name: name.clone(), + } + })?; + + // predicates with relationships is currently only supported for local relationships + if let (Some(target_model_source), Some(model_source)) = + (&target_model.source, &model.source) + { + if target_model_source.data_connector.name + == model_source.data_connector.name + { + let target_source = ModelTargetSource::from_model_source( + target_model_source, + relationship, + ) + .map_err(|_| Error::NoRelationshipCapabilitiesDefined { + relationship_name: relationship.name.clone(), + type_name: model.data_type.clone(), + data_connector_name: target_model_source + .data_connector + .name + .clone(), + })?; + + let annotation = PredicateRelationshipAnnotation { + source_type: relationship.source.clone(), + relationship_name: relationship.name.clone(), + target_model_name: model_name.clone(), + target_source: target_source.clone(), + target_type: target_typename.clone(), + relationship_type: relationship_type.clone(), + mappings: mappings.clone(), + source_data_connector: model_source.data_connector.clone(), + + source_type_mappings: model_source.type_mappings.clone(), + }; + + let target_model_predicate = resolve_model_predicate( + nested_predicate, + target_model, + // local relationships exists in the same subgraph as the source model + subgraph, + data_connectors, + &target_model.type_fields, + types, + models, + )?; + + Ok(ModelPredicate::Relationship { + relationship_info: annotation, + predicate: Box::new(target_model_predicate), + }) + } else { + Err(Error::UnsupportedFeature { + message: "Predicate cannot be built using remote relationships" + .to_string(), + }) + } + } else { + Err( + Error::ModelAndTargetSourceRequiredForRelationshipPredicate { + source_model_name: model.name.clone(), + target_model_name: target_model.name.clone(), + }, + ) + } + } + } } else { - Ok(ModelPredicate::Relationship { - name: name.clone(), - predicate: None, + Err(Error::NoPredicateDefinedForRelationshipPredicate { + model_name: model.name.clone(), + relationship_name: name.clone(), }) } - */ } permissions::ModelPredicate::Not(predicate) => { - let resolved_predicate = - resolve_model_predicate(predicate, model, subgraph, data_connectors, fields)?; + let resolved_predicate = resolve_model_predicate( + predicate, + model, + subgraph, + data_connectors, + fields, + types, + models, + )?; Ok(ModelPredicate::Not(Box::new(resolved_predicate))) } permissions::ModelPredicate::And(predicates) => { @@ -525,6 +614,8 @@ fn resolve_model_predicate( subgraph, data_connectors, fields, + types, + models, )?); } Ok(ModelPredicate::And(resolved_predicates)) @@ -538,6 +629,8 @@ fn resolve_model_predicate( subgraph, data_connectors, fields, + types, + models, )?); } Ok(ModelPredicate::Or(resolved_predicates)) @@ -550,6 +643,8 @@ pub fn resolve_model_select_permissions( subgraph: &str, model_permissions: &ModelPermissionsV1, data_connectors: &HashMap, DataConnectorContext>, + types: &HashMap, TypeRepresentation>, + models: &IndexMap, Model>, ) -> Result, Error> { let mut validated_permissions = HashMap::new(); for model_permission in &model_permissions.permissions { @@ -561,6 +656,8 @@ pub fn resolve_model_select_permissions( subgraph, data_connectors, &model.type_fields, + types, + models, ) .map(FilterPermission::Filter)?, NullableModelPredicate::Null(()) => FilterPermission::AllowAll, @@ -929,3 +1026,27 @@ pub fn resolve_model_source( ndc_validation::validate_ndc(&model.name, model, data_connector_context.schema)?; Ok(()) } + +/// Gets the `ObjectTypeRepresentation` of the type identified with the +/// `data_type`, it will throw an error if the type is not found to be an object +/// or if the model has an unknown data type. +pub(crate) fn get_model_object_type_representation<'s>( + types: &'s HashMap, TypeRepresentation>, + data_type: &Qualified, + model_name: &Qualified, +) -> Result<&'s ObjectTypeRepresentation, crate::metadata::resolved::error::Error> { + let object_type_representation = match types.get(data_type) { + Some(TypeRepresentation::Object(object_type_representation)) => { + Ok(object_type_representation) + } + Some(type_rep) => Err(Error::InvalidTypeRepresentation { + model_name: model_name.clone(), + type_representation: type_rep.clone(), + }), + None => Err(Error::UnknownModelDataType { + model_name: model_name.clone(), + data_type: data_type.clone(), + }), + }?; + Ok(object_type_representation) +} diff --git a/v3/engine/src/metadata/resolved/relationship.rs b/v3/engine/src/metadata/resolved/relationship.rs index 27545365a08..52875c8e25c 100644 --- a/v3/engine/src/metadata/resolved/relationship.rs +++ b/v3/engine/src/metadata/resolved/relationship.rs @@ -58,6 +58,9 @@ pub struct RelationshipCommandMapping { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct Relationship { pub name: RelationshipName, + // `ast::Name` representation of `RelationshipName`. This is used to avoid + // the recurring conversion between `RelationshipName` to `ast::Name` during + // relationship IR generation pub field_name: ast::Name, pub source: Qualified, pub target: RelationshipTarget, @@ -72,6 +75,34 @@ pub struct RelationshipCapabilities { pub relationships: bool, } +pub enum RelationshipExecutionCategory { + // Push down relationship definition to the data connector + Local, + // Use foreach in the data connector to fetch related rows for multiple objects in a single request + RemoteForEach, +} + +#[allow(clippy::match_single_binding)] +pub fn relationship_execution_category( + source_connector: &DataConnector, + target_connector: &DataConnector, + target_source_relationship_capabilities: &RelationshipCapabilities, +) -> RelationshipExecutionCategory { + // It's a local relationship if the source and target connectors are the same and + // the connector supports relationships. + if target_connector.name == source_connector.name + && target_source_relationship_capabilities.relationships + { + RelationshipExecutionCategory::Local + } else { + match target_source_relationship_capabilities.foreach { + // TODO: When we support naive relationships for connectors not implementing foreach, + // add another match arm / return enum variant + () => RelationshipExecutionCategory::RemoteForEach, + } + } +} + fn resolve_relationship_source_mapping<'a>( relationship_name: &'a RelationshipName, source_type_name: &'a Qualified, diff --git a/v3/engine/src/metadata/resolved/subgraph.rs b/v3/engine/src/metadata/resolved/subgraph.rs index 9dea46d0a78..bc269016918 100644 --- a/v3/engine/src/metadata/resolved/subgraph.rs +++ b/v3/engine/src/metadata/resolved/subgraph.rs @@ -37,6 +37,21 @@ pub enum QualifiedTypeName { Custom(Qualified), } +pub fn serialize_optional_qualified_btreemap( + optional_map: &Option, V>>, + s: S, +) -> Result +where + S: serde::Serializer, + V: Serialize, + T: Display + Serialize, +{ + match optional_map { + Some(map) => serialize_qualified_btreemap(map, s), + None => s.serialize_none(), + } +} + pub fn serialize_qualified_btreemap( map: &BTreeMap, V>, s: S, diff --git a/v3/engine/src/schema/model_filter.rs b/v3/engine/src/schema/model_filter.rs index 22c23b7507b..5c4c1b63bdd 100644 --- a/v3/engine/src/schema/model_filter.rs +++ b/v3/engine/src/schema/model_filter.rs @@ -4,9 +4,14 @@ use lang_graphql::schema as gql_schema; use open_dds::models::ModelName; use std::collections::HashMap; +use super::types::output_type::get_object_type_representation; +use super::types::output_type::relationship::{FilterRelationshipAnnotation, ModelTargetSource}; use super::types::{input_type, output_type, InputAnnotation, ModelInputAnnotation, TypeId}; use crate::metadata::resolved; use crate::metadata::resolved::model::ComparisonExpressionInfo; +use crate::metadata::resolved::relationship::{ + relationship_execution_category, RelationshipExecutionCategory, RelationshipTarget, +}; use crate::metadata::resolved::subgraph::{Qualified, QualifiedTypeReference}; use crate::metadata::resolved::types::mk_name; use crate::schema::permissions; @@ -149,6 +154,97 @@ pub fn build_model_filter_expression_input_schema( ); input_fields.insert(field_graphql_name, input_field); } + + // relationship fields + // TODO(naveen): Add support for command relationships + for (rel_name, relationship) in object_type_representation.relationships.iter() { + if let RelationshipTarget::Model { + model_name, + relationship_type, + target_typename, + mappings, + } = &relationship.target + { + let target_model = gds.metadata.models.get(model_name).ok_or_else(|| { + crate::schema::Error::InternalModelNotFound { + model_name: model_name.clone(), + } + })?; + + let target_object_type_representation = + get_object_type_representation(gds, &target_model.data_type)?; + + // Build relationship field in filter expression only when both + // the target_model and source model are backed by a source + if let (Some(target_source), Some(model_source)) = + (&target_model.source, &model.source) + { + let target_model_source = + ModelTargetSource::from_model_source(target_source, relationship)?; + + // filter expression with relationships is currently only supported for local relationships + if let RelationshipExecutionCategory::Local = relationship_execution_category( + &model_source.data_connector, + &target_source.data_connector, + &target_model_source.capabilities, + ) { + if target_source.data_connector.name == model_source.data_connector.name { + // If the relationship target model does not have filterExpressionType do not include + // it in the source model filter expression input type. + if let Some(target_model_filter_expression) = + target_model.graphql_api.filter_expression.as_ref() + { + let target_model_filter_expression_type_name = + &target_model_filter_expression.where_type_name; + + let annotation = FilterRelationshipAnnotation { + source_type: relationship.source.clone(), + relationship_name: relationship.name.clone(), + target_source: target_model_source.clone(), + target_type: target_typename.clone(), + target_model_name: target_model.name.clone(), + relationship_type: relationship_type.clone(), + mappings: mappings.clone(), + source_data_connector: model_source.data_connector.clone(), + source_type_mappings: model_source.type_mappings.clone(), + }; + + input_fields.insert( + rel_name.clone(), + builder.conditional_namespaced( + gql_schema::InputField::::new( + rel_name.clone(), + None, + types::Annotation::Input(InputAnnotation::Model( + ModelInputAnnotation::ModelFilterArgument { + field: + types::ModelFilterArgument::RelationshipField( + annotation, + ), + }, + )), + ast::TypeContainer::named_null( + gql_schema::RegisteredTypeName::new( + target_model_filter_expression_type_name.0.clone(), + ), + ), + None, + gql_schema::DeprecationStatus::NotDeprecated, + ), + permissions::get_model_relationship_namespace_annotations( + target_model, + object_type_representation, + target_object_type_representation, + mappings, + ), + ), + ); + } + } + } + } + } + } } Ok(gql_schema::TypeInfo::InputObject( diff --git a/v3/engine/src/schema/model_order_by.rs b/v3/engine/src/schema/model_order_by.rs index 72ff74244fd..f3792feb8d4 100644 --- a/v3/engine/src/schema/model_order_by.rs +++ b/v3/engine/src/schema/model_order_by.rs @@ -2,10 +2,15 @@ use hasura_authn_core::Role; use lang_graphql::ast::common as ast; use lang_graphql::schema as gql_schema; use open_dds::models::ModelName; +use open_dds::relationships::RelationshipType; use std::collections::HashMap; +use super::types::output_type::relationship::{ModelTargetSource, OrderByRelationshipAnnotation}; use super::types::{output_type::get_object_type_representation, Annotation, TypeId}; use crate::metadata::resolved; +use crate::metadata::resolved::relationship::{ + relationship_execution_category, RelationshipExecutionCategory, RelationshipTarget, +}; use crate::metadata::resolved::subgraph::Qualified; use crate::metadata::resolved::types::mk_name; use crate::schema::permissions; @@ -128,6 +133,94 @@ pub fn build_model_order_by_input_schema( ); fields.insert(graphql_field_name, input_field); } + + // relationship fields + // TODO(naveen): Add support for command relationships. + for (rel_name, relationship) in object_type_representation.relationships.iter() { + if let RelationshipTarget::Model { + model_name, + relationship_type, + target_typename, + mappings, + } = &relationship.target + { + let target_model = gds.metadata.models.get(model_name).ok_or_else(|| { + crate::schema::Error::InternalModelNotFound { + model_name: model_name.clone(), + } + })?; + + let target_object_type_representation = + get_object_type_representation(gds, &target_model.data_type)?; + + // Build relationship field in filter expression only when both + // the target_model and source model are backed by a source + if let (Some(target_source), Some(model_source)) = + (&target_model.source, &model.source) + { + let target_model_source = + ModelTargetSource::from_model_source(target_source, relationship)?; + // order_by expression with relationships is currently only supported for local relationships + if let RelationshipExecutionCategory::Local = relationship_execution_category( + &model_source.data_connector, + &target_source.data_connector, + &target_model_source.capabilities, + ) { + // TODO(naveen): Support Array relationships in order_by when the support for aggregates is implemented + if let RelationshipType::Object = relationship_type { + // If the relationship target model does not have orderByExpressionType do not include + // it in the source model order_by input type. + if let Some(target_model_order_by_expression) = + target_model.graphql_api.order_by_expression.as_ref() + { + let target_model_order_by_expression_type_name = + &target_model_order_by_expression.order_by_type_name; + + let annotation = OrderByRelationshipAnnotation { + source_type: relationship.source.clone(), + relationship_name: relationship.name.clone(), + target_model_name: model_name.clone(), + target_source: target_model_source.clone(), + target_type: target_typename.clone(), + relationship_type: relationship_type.clone(), + mappings: mappings.clone(), + source_data_connector: model_source.data_connector.clone(), + + source_type_mappings: model_source.type_mappings.clone(), + }; + + fields.insert( + rel_name.clone(), + builder.conditional_namespaced( + gql_schema::InputField::new( + rel_name.clone(), + None, + types::Annotation::Input(types::InputAnnotation::Model( + types::ModelInputAnnotation::ModelOrderByRelationshipArgument(annotation), + )), + ast::TypeContainer::named_null( + gql_schema::RegisteredTypeName::new( + target_model_order_by_expression_type_name.0.clone(), + ), + ), + None, + gql_schema::DeprecationStatus::NotDeprecated, + ), + permissions::get_model_relationship_namespace_annotations( + target_model, + object_type_representation, + target_object_type_representation, + mappings, + ), + ), + ); + } + } + } + } + } + } + Ok(gql_schema::TypeInfo::InputObject( gql_schema::InputObject::new(type_name.clone(), None, fields), )) diff --git a/v3/engine/src/schema/permissions.rs b/v3/engine/src/schema/permissions.rs index 017caa3a5c9..cfd25f3675d 100644 --- a/v3/engine/src/schema/permissions.rs +++ b/v3/engine/src/schema/permissions.rs @@ -54,13 +54,12 @@ pub(crate) fn get_select_one_namespace_annotations( /// We need to check the permissions of the source and target fields /// in the relationship mappings. pub(crate) fn get_model_relationship_namespace_annotations( - model: &resolved::model::Model, + target_model: &resolved::model::Model, source_object_type_representation: &ObjectTypeRepresentation, target_object_type_representation: &ObjectTypeRepresentation, mappings: &[resolved::relationship::RelationshipModelMapping], ) -> HashMap> { - let select_permissions = get_select_permissions_namespace_annotations(model); - + let select_permissions = get_select_permissions_namespace_annotations(target_model); select_permissions .into_iter() .filter(|(role, _)| { diff --git a/v3/engine/src/schema/types.rs b/v3/engine/src/schema/types.rs index bb2cd034d20..570c2dd8a08 100644 --- a/v3/engine/src/schema/types.rs +++ b/v3/engine/src/schema/types.rs @@ -25,6 +25,10 @@ use crate::{ }; use strum_macros::Display; +use self::output_type::relationship::{ + FilterRelationshipAnnotation, OrderByRelationshipAnnotation, +}; + pub mod inbuilt_type; pub mod input_type; pub mod output_type; @@ -63,6 +67,7 @@ pub enum ModelFilterArgument { OrOp, NotOp, Field { ndc_column: String }, + RelationshipField(FilterRelationshipAnnotation), } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] @@ -162,6 +167,8 @@ pub enum ModelInputAnnotation { ModelOrderByArgument { ndc_column: String, }, + ModelOrderByRelationshipArgument(OrderByRelationshipAnnotation), + ModelOrderByDirection { direction: ModelOrderByDirection, }, diff --git a/v3/engine/src/schema/types/output_type/relationship.rs b/v3/engine/src/schema/types/output_type/relationship.rs index c4d24cd19a8..1d134f56f2b 100644 --- a/v3/engine/src/schema/types/output_type/relationship.rs +++ b/v3/engine/src/schema/types/output_type/relationship.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use open_dds::{ commands::{CommandName, FunctionName}, models::ModelName, @@ -10,7 +12,7 @@ use serde::{Deserialize, Serialize}; use crate::{ metadata::resolved::{ self, - subgraph::{Qualified, QualifiedTypeReference}, + subgraph::{serialize_qualified_btreemap, Qualified, QualifiedTypeReference}, }, schema::{self, types::CommandSourceDetail}, }; @@ -26,6 +28,48 @@ pub struct ModelRelationshipAnnotation { pub mappings: Vec, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct FilterRelationshipAnnotation { + pub relationship_name: RelationshipName, + pub relationship_type: RelationshipType, + pub source_type: Qualified, + pub source_data_connector: resolved::data_connector::DataConnector, + #[serde(serialize_with = "serialize_qualified_btreemap")] + pub source_type_mappings: BTreeMap, resolved::types::TypeMapping>, + pub target_source: ModelTargetSource, + pub target_type: Qualified, + pub target_model_name: Qualified, + pub mappings: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct OrderByRelationshipAnnotation { + pub relationship_name: RelationshipName, + pub relationship_type: RelationshipType, + pub source_type: Qualified, + pub source_data_connector: resolved::data_connector::DataConnector, + #[serde(serialize_with = "serialize_qualified_btreemap")] + pub source_type_mappings: BTreeMap, resolved::types::TypeMapping>, + pub target_source: ModelTargetSource, + pub target_type: Qualified, + pub target_model_name: Qualified, + pub mappings: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct PredicateRelationshipAnnotation { + pub relationship_name: RelationshipName, + pub relationship_type: RelationshipType, + pub source_type: Qualified, + pub source_data_connector: resolved::data_connector::DataConnector, + #[serde(serialize_with = "serialize_qualified_btreemap")] + pub source_type_mappings: BTreeMap, resolved::types::TypeMapping>, + pub target_source: ModelTargetSource, + pub target_type: Qualified, + pub target_model_name: Qualified, + pub mappings: Vec, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct ModelTargetSource { pub(crate) model: resolved::model::ModelSource, @@ -40,21 +84,26 @@ impl ModelTargetSource { model .source .as_ref() - .map(|model_source| { - Ok(Self { - model: model_source.clone(), - capabilities: relationship - .target_capabilities - .as_ref() - .ok_or_else(|| schema::Error::InternalMissingRelationshipCapabilities { - type_name: relationship.source.clone(), - relationship: relationship.name.clone(), - })? - .clone(), - }) - }) + .map(|model_source| Self::from_model_source(model_source, relationship)) .transpose() } + + pub fn from_model_source( + model_source: &resolved::model::ModelSource, + relationship: &resolved::relationship::Relationship, + ) -> Result { + Ok(Self { + model: model_source.clone(), + capabilities: relationship + .target_capabilities + .as_ref() + .ok_or_else(|| schema::Error::InternalMissingRelationshipCapabilities { + type_name: relationship.source.clone(), + relationship: relationship.name.clone(), + })? + .clone(), + }) + } } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/v3/engine/tests/execute/common_metadata/postgres_connector_schema.json b/v3/engine/tests/execute/common_metadata/postgres_connector_schema.json index 0bdc9516192..0a45cdc4a07 100644 --- a/v3/engine/tests/execute/common_metadata/postgres_connector_schema.json +++ b/v3/engine/tests/execute/common_metadata/postgres_connector_schema.json @@ -464,6 +464,14 @@ "type": "named", "name": "Int" } + }, + "GenreId": { + "description": "The track's genre ID", + "arguments": {}, + "type": { + "type": "named", + "name": "Int" + } } } }, @@ -495,6 +503,27 @@ } } } + }, + "Genre": { + "description": "A Genre", + "fields": { + "GenreId": { + "description": "The genre's primary key", + "arguments": {}, + "type": { + "type": "named", + "name": "Int" + } + }, + "Name": { + "description": "The genre's name", + "arguments": {}, + "type": { + "type": "named", + "name": "String" + } + } + } } }, "collections": [ @@ -651,6 +680,19 @@ "deletable": false, "uniqueness_constraints": {}, "foreign_keys": {} + }, + { + "name": "Genre", + "arguments": {}, + "type": "Genre", + "uniqueness_constraints": { + "PK_Genre": { + "unique_columns": [ + "GenreId" + ] + } + }, + "foreign_keys": {} } ], "functions": [ @@ -756,4 +798,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/order_by/relationships/common_metadata.json b/v3/engine/tests/execute/models/select_many/order_by/relationships/common_metadata.json new file mode 100644 index 00000000000..668eca3543d --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/order_by/relationships/common_metadata.json @@ -0,0 +1,619 @@ +{ + "version": "v2", + "subgraphs": [ + { + "name": "default", + "objects": [ + { + "definition": { + "name": "Album", + "fields": [ + { + "name": "AlbumId", + "type": "Int" + }, + { + "name": "Title", + "type": "String" + }, + { + "name": "ArtistId", + "type": "Int" + } + ], + "graphql": { + "typeName": "Album" + } + }, + "version": "v1", + "kind": "ObjectType" + }, + { + "definition": { + "name": "Track", + "fields": [ + { + "name": "TrackId", + "type": "Int" + }, + { + "name": "Name", + "type": "String" + }, + { + "name": "AlbumId", + "type": "Int" + } + ], + "graphql": { + "typeName": "Track" + } + }, + "version": "v1", + "kind": "ObjectType" + }, + { + "kind": "DataConnectorScalarRepresentation", + "version": "v1", + "definition": { + "dataConnectorName": "db", + "dataConnectorScalarType": "String", + "representation": "String", + "graphql": { + "comparisonExpressionTypeName": "String_Comparison_Exp" + } + } + }, + { + "kind": "ScalarType", + "version": "v1", + "definition": { + "name": "CustomString", + "graphql": { + "typeName": "CustomString" + } + } + }, + { + "definition": { + "name": "Albums", + "objectType": "Album", + "source": { + "dataConnectorName": "db", + "collection": "Album", + "typeMapping": { + "Album": { + "fieldMapping": { + "AlbumId": { + "column": "AlbumId" + }, + "Title": { + "column": "Title" + }, + "ArtistId": { + "column": "ArtistId" + } + } + } + } + }, + "graphql": { + "selectUniques": [ + { + "queryRootField": "AlbumByID", + "uniqueIdentifier": [ + "AlbumId" + ] + } + ], + "selectMany": { + "queryRootField": "Album" + }, + "filterExpressionType": "Album_Where_Exp", + "orderByExpressionType": "Album_Order_By" + }, + "filterableFields": [ + { + "fieldName": "AlbumId", + "operators": { + "enableAll": true + } + }, + { + "fieldName": "Title", + "operators": { + "enableAll": true + } + }, + { + "fieldName": "ArtistId", + "operators": { + "enableAll": true + } + } + ], + "orderableFields": [ + { + "fieldName": "AlbumId", + "orderByDirections": { + "enableAll": true + } + }, + { + "fieldName": "Title", + "orderByDirections": { + "enableAll": true + } + }, + { + "fieldName": "ArtistId", + "orderByDirections": { + "enableAll": true + } + } + ] + }, + "version": "v1", + "kind": "Model" + }, + { + "definition": { + "name": "Tracks", + "objectType": "Track", + "source": { + "dataConnectorName": "db", + "collection": "Track", + "typeMapping": { + "Track": { + "fieldMapping": { + "TrackId": { + "column": "TrackId" + }, + "Name": { + "column": "Name" + }, + "AlbumId": { + "column": "AlbumId" + } + } + } + } + }, + "graphql": { + "selectUniques": [ + { + "queryRootField": "TrackByID", + "uniqueIdentifier": [ + "TrackId" + ] + } + ], + "selectMany": { + "queryRootField": "Track" + }, + "filterExpressionType": "Track_Where_Exp", + "orderByExpressionType": "Track_Order_By" + }, + "filterableFields": [ + { + "fieldName": "TrackId", + "operators": { + "enableAll": true + } + }, + { + "fieldName": "Name", + "operators": { + "enableAll": true + } + }, + { + "fieldName": "AlbumId", + "operators": { + "enableAll": true + } + } + ], + "orderableFields": [ + { + "fieldName": "TrackId", + "orderByDirections": { + "enableAll": true + } + }, + { + "fieldName": "Name", + "orderByDirections": { + "enableAll": true + } + }, + { + "fieldName": "AlbumId", + "orderByDirections": { + "enableAll": true + } + } + ] + }, + "version": "v1", + "kind": "Model" + }, + { + "definition": { + "typeName": "Album", + "permissions": [ + { + "role": "admin", + "output": { + "allowedFields": [ + "AlbumId", + "Title", + "ArtistId" + ] + } + }, + { + "role": "user", + "output": { + "allowedFields": [ + "AlbumId", + "Title", + "ArtistId" + ] + } + } + ] + }, + "version": "v1", + "kind": "TypePermissions" + }, + { + "definition": { + "typeName": "Track", + "permissions": [ + { + "role": "admin", + "output": { + "allowedFields": [ + "TrackId", + "Name", + "AlbumId" + ] + } + }, + { + "role": "user", + "output": { + "allowedFields": [ + "TrackId", + "Name", + "AlbumId" + ] + } + } + ] + }, + "version": "v1", + "kind": "TypePermissions" + }, + { + "definition": { + "modelName": "Albums", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": null + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + }, + { + "definition": { + "modelName": "Tracks", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": null + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + }, + { + "definition": { + "source": "Album", + "name": "Tracks", + "target": { + "model": { + "name": "Tracks", + "relationshipType": "Array" + } + }, + "mapping": [ + { + "source": { + "fieldPath": [ + { + "fieldName": "AlbumId" + } + ] + }, + "target": { + "modelField": [ + { + "fieldName": "AlbumId" + } + ] + } + } + ] + }, + "version": "v1", + "kind": "Relationship" + }, + { + "definition": { + "source": "Track", + "name": "Album", + "target": { + "model": { + "name": "Albums", + "relationshipType": "Object" + } + }, + "mapping": [ + { + "source": { + "fieldPath": [ + { + "fieldName": "AlbumId" + } + ] + }, + "target": { + "modelField": [ + { + "fieldName": "AlbumId" + } + ] + } + } + ] + }, + "version": "v1", + "kind": "Relationship" + }, + { + "definition": { + "source": "Track", + "name": "TrackAlbums", + "target": { + "model": { + "name": "Albums", + "relationshipType": "Array" + } + }, + "mapping": [ + { + "source": { + "fieldPath": [ + { + "fieldName": "AlbumId" + } + ] + }, + "target": { + "modelField": [ + { + "fieldName": "AlbumId" + } + ] + } + } + ] + }, + "version": "v1", + "kind": "Relationship" + }, + { + "definition": { + "name": "Artist", + "fields": [ + { + "name": "ArtistId", + "type": "Int" + }, + { + "name": "Name", + "type": "String" + } + ], + "graphql": { + "typeName": "Artist" + } + }, + "version": "v1", + "kind": "ObjectType" + }, + { + "definition": { + "name": "Artists", + "objectType": "Artist", + "source": { + "dataConnectorName": "db", + "collection": "Artist", + "typeMapping": { + "Artist": { + "fieldMapping": { + "ArtistId": { + "column": "ArtistId" + }, + "Name": { + "column": "Name" + } + } + } + } + }, + "graphql": { + "selectUniques": [ + { + "queryRootField": "ArtistByID", + "uniqueIdentifier": [ + "ArtistId" + ] + } + ], + "selectMany": { + "queryRootField": "Artist" + }, + "filterExpressionType": "Artist_Where_Exp", + "orderByExpressionType": "Artist_Order_By" + }, + "filterableFields": [ + { + "fieldName": "ArtistId", + "operators": { + "enableAll": true + } + }, + { + "fieldName": "Name", + "operators": { + "enableAll": true + } + } + ], + "orderableFields": [ + { + "fieldName": "ArtistId", + "orderByDirections": { + "enableAll": true + } + }, + { + "fieldName": "Name", + "orderByDirections": { + "enableAll": true + } + } + ] + }, + "version": "v1", + "kind": "Model" + }, + { + "definition": { + "modelName": "Artists", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": null + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + }, + { + "definition": { + "typeName": "Artist", + "permissions": [ + { + "role": "admin", + "output": { + "allowedFields": [ + "ArtistId", + "Name" + ] + } + }, + { + "role": "user", + "output": { + "allowedFields": [ + "ArtistId", + "Name" + ] + } + } + ] + }, + "version": "v1", + "kind": "TypePermissions" + }, + { + "definition": { + "source": "Album", + "name": "Artist", + "target": { + "model": { + "name": "Artists", + "relationshipType": "Object" + } + }, + "mapping": [ + { + "source": { + "fieldPath": [ + { + "fieldName": "ArtistId" + } + ] + }, + "target": { + "modelField": [ + { + "fieldName": "ArtistId" + } + ] + } + } + ] + }, + "version": "v1", + "kind": "Relationship" + }, + { + "kind": "DataConnectorScalarRepresentation", + "version": "v1", + "definition": { + "dataConnectorName": "db", + "dataConnectorScalarType": "Int", + "representation": "Int", + "graphql": { + "comparisonExpressionTypeName": "Int_comparison" + } + } + } + ] + } + ] +} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/order_by/relationships/object/nested/expected.json b/v3/engine/tests/execute/models/select_many/order_by/relationships/object/nested/expected.json new file mode 100644 index 00000000000..5e64dab34e8 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/order_by/relationships/object/nested/expected.json @@ -0,0 +1,306 @@ +[ + { + "data": { + "Track": [ + { + "Album": { + "AlbumId": 1, + "Artist": { + "ArtistId": 1 + }, + "Title": "For Those About To Rock We Salute You" + } + }, + { + "Album": { + "AlbumId": 1, + "Artist": { + "ArtistId": 1 + }, + "Title": "For Those About To Rock We Salute You" + } + }, + { + "Album": { + "AlbumId": 1, + "Artist": { + "ArtistId": 1 + }, + "Title": "For Those About To Rock We Salute You" + } + }, + { + "Album": { + "AlbumId": 1, + "Artist": { + "ArtistId": 1 + }, + "Title": "For Those About To Rock We Salute You" + } + }, + { + "Album": { + "AlbumId": 1, + "Artist": { + "ArtistId": 1 + }, + "Title": "For Those About To Rock We Salute You" + } + }, + { + "Album": { + "AlbumId": 1, + "Artist": { + "ArtistId": 1 + }, + "Title": "For Those About To Rock We Salute You" + } + }, + { + "Album": { + "AlbumId": 1, + "Artist": { + "ArtistId": 1 + }, + "Title": "For Those About To Rock We Salute You" + } + }, + { + "Album": { + "AlbumId": 1, + "Artist": { + "ArtistId": 1 + }, + "Title": "For Those About To Rock We Salute You" + } + }, + { + "Album": { + "AlbumId": 1, + "Artist": { + "ArtistId": 1 + }, + "Title": "For Those About To Rock We Salute You" + } + }, + { + "Album": { + "AlbumId": 1, + "Artist": { + "ArtistId": 1 + }, + "Title": "For Those About To Rock We Salute You" + } + }, + { + "Album": { + "AlbumId": 4, + "Artist": { + "ArtistId": 1 + }, + "Title": "Let There Be Rock" + } + }, + { + "Album": { + "AlbumId": 4, + "Artist": { + "ArtistId": 1 + }, + "Title": "Let There Be Rock" + } + }, + { + "Album": { + "AlbumId": 4, + "Artist": { + "ArtistId": 1 + }, + "Title": "Let There Be Rock" + } + }, + { + "Album": { + "AlbumId": 4, + "Artist": { + "ArtistId": 1 + }, + "Title": "Let There Be Rock" + } + }, + { + "Album": { + "AlbumId": 4, + "Artist": { + "ArtistId": 1 + }, + "Title": "Let There Be Rock" + } + } + ], + "TrackOrderByWithFilter": [ + { + "Album": { + "AlbumId": 2, + "Artist": { + "ArtistId": 2 + }, + "Title": "Balls to the Wall" + } + } + ] + } + }, + { + "data": { + "Track": [ + { + "Album": { + "AlbumId": 1, + "Artist": { + "ArtistId": 1 + }, + "Title": "For Those About To Rock We Salute You" + } + }, + { + "Album": { + "AlbumId": 1, + "Artist": { + "ArtistId": 1 + }, + "Title": "For Those About To Rock We Salute You" + } + }, + { + "Album": { + "AlbumId": 1, + "Artist": { + "ArtistId": 1 + }, + "Title": "For Those About To Rock We Salute You" + } + }, + { + "Album": { + "AlbumId": 1, + "Artist": { + "ArtistId": 1 + }, + "Title": "For Those About To Rock We Salute You" + } + }, + { + "Album": { + "AlbumId": 1, + "Artist": { + "ArtistId": 1 + }, + "Title": "For Those About To Rock We Salute You" + } + }, + { + "Album": { + "AlbumId": 1, + "Artist": { + "ArtistId": 1 + }, + "Title": "For Those About To Rock We Salute You" + } + }, + { + "Album": { + "AlbumId": 1, + "Artist": { + "ArtistId": 1 + }, + "Title": "For Those About To Rock We Salute You" + } + }, + { + "Album": { + "AlbumId": 1, + "Artist": { + "ArtistId": 1 + }, + "Title": "For Those About To Rock We Salute You" + } + }, + { + "Album": { + "AlbumId": 1, + "Artist": { + "ArtistId": 1 + }, + "Title": "For Those About To Rock We Salute You" + } + }, + { + "Album": { + "AlbumId": 1, + "Artist": { + "ArtistId": 1 + }, + "Title": "For Those About To Rock We Salute You" + } + }, + { + "Album": { + "AlbumId": 4, + "Artist": { + "ArtistId": 1 + }, + "Title": "Let There Be Rock" + } + }, + { + "Album": { + "AlbumId": 4, + "Artist": { + "ArtistId": 1 + }, + "Title": "Let There Be Rock" + } + }, + { + "Album": { + "AlbumId": 4, + "Artist": { + "ArtistId": 1 + }, + "Title": "Let There Be Rock" + } + }, + { + "Album": { + "AlbumId": 4, + "Artist": { + "ArtistId": 1 + }, + "Title": "Let There Be Rock" + } + }, + { + "Album": { + "AlbumId": 4, + "Artist": { + "ArtistId": 1 + }, + "Title": "Let There Be Rock" + } + } + ], + "TrackOrderByWithFilter": [ + { + "Album": { + "AlbumId": 2, + "Artist": { + "ArtistId": 2 + }, + "Title": "Balls to the Wall" + } + } + ] + } + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/order_by/relationships/object/nested/metadata.json b/v3/engine/tests/execute/models/select_many/order_by/relationships/object/nested/metadata.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/order_by/relationships/object/nested/metadata.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/order_by/relationships/object/nested/request.gql b/v3/engine/tests/execute/models/select_many/order_by/relationships/object/nested/request.gql new file mode 100644 index 00000000000..f892b4eeaa6 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/order_by/relationships/object/nested/request.gql @@ -0,0 +1,24 @@ +query MyQuery { + Track(order_by: {Album: {Artist: {ArtistId: Asc}, AlbumId: Asc}}, limit: 15) { + Album { + AlbumId + Artist { + ArtistId + } + Title + } + } + TrackOrderByWithFilter: Track( + order_by: {Album: {Artist: {ArtistId: Desc}, AlbumId: Asc}} + where: {AlbumId: {_eq: 2}} + limit: 15 + ) { + Album { + AlbumId + Artist { + ArtistId + } + Title + } + } +} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/order_by/relationships/object/nested/session_variables.json b/v3/engine/tests/execute/models/select_many/order_by/relationships/object/nested/session_variables.json new file mode 100644 index 00000000000..655455cc70e --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/order_by/relationships/object/nested/session_variables.json @@ -0,0 +1,9 @@ +[ + { + "x-hasura-role": "admin" + }, + { + "x-hasura-role": "user", + "x-hasura-user-id": "2" + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/order_by/relationships/object/simple/expected.json b/v3/engine/tests/execute/models/select_many/order_by/relationships/object/simple/expected.json new file mode 100644 index 00000000000..2344503e6ac --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/order_by/relationships/object/simple/expected.json @@ -0,0 +1,126 @@ +[ + { + "data": { + "Track": [ + { + "Album": { + "ArtistId": 275, + "Title": "Koyaanisqatsi (Soundtrack from the Motion Picture)" + } + }, + { + "Album": { + "ArtistId": 274, + "Title": "Mozart: Chamber Music" + } + }, + { + "Album": { + "ArtistId": 273, + "Title": "Monteverdi: L'Orfeo" + } + } + ], + "TrackOrderByWithFilter": [ + { + "TrackId": 5, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + }, + { + "TrackId": 4, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + }, + { + "TrackId": 3, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + }, + { + "TrackId": 2, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + } + ] + } + }, + { + "data": { + "Track": [ + { + "Album": { + "ArtistId": 275, + "Title": "Koyaanisqatsi (Soundtrack from the Motion Picture)" + } + }, + { + "Album": { + "ArtistId": 274, + "Title": "Mozart: Chamber Music" + } + }, + { + "Album": { + "ArtistId": 273, + "Title": "Monteverdi: L'Orfeo" + } + } + ], + "TrackOrderByWithFilter": [ + { + "TrackId": 5, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + }, + { + "TrackId": 4, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + }, + { + "TrackId": 3, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + }, + { + "TrackId": 2, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + } + ] + } + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/order_by/relationships/object/simple/metadata.json b/v3/engine/tests/execute/models/select_many/order_by/relationships/object/simple/metadata.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/order_by/relationships/object/simple/metadata.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/order_by/relationships/object/simple/request.gql b/v3/engine/tests/execute/models/select_many/order_by/relationships/object/simple/request.gql new file mode 100644 index 00000000000..6b52897dddc --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/order_by/relationships/object/simple/request.gql @@ -0,0 +1,20 @@ +query MyQuery { + Track(order_by: {Album: {ArtistId: Desc}}, limit: 3) { + Album { + ArtistId + Title + } + } + TrackOrderByWithFilter: Track( + order_by: {Album: {ArtistId: Asc}, TrackId: Desc} + where: {Album: {Artist: {ArtistId: {_eq: 2}}}} + ) { + TrackId + Album { + Artist { + ArtistId + Name + } + } + } +} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/order_by/relationships/object/simple/session_variables.json b/v3/engine/tests/execute/models/select_many/order_by/relationships/object/simple/session_variables.json new file mode 100644 index 00000000000..655455cc70e --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/order_by/relationships/object/simple/session_variables.json @@ -0,0 +1,9 @@ +[ + { + "x-hasura-role": "admin" + }, + { + "x-hasura-role": "user", + "x-hasura-user-id": "2" + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/array/nested/expected.json b/v3/engine/tests/execute/models/select_many/relationship_predicates/array/nested/expected.json new file mode 100644 index 00000000000..0b3fb5944be --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/array/nested/expected.json @@ -0,0 +1,157 @@ +[ + { + "data": { + "Album": [ + { + "AlbumId": 1, + "Tracks": [ + { + "TrackAlbums": [ + { + "AlbumId": 1, + "Title": "For Those About To Rock We Salute You" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 1, + "Title": "For Those About To Rock We Salute You" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 1, + "Title": "For Those About To Rock We Salute You" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 1, + "Title": "For Those About To Rock We Salute You" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 1, + "Title": "For Those About To Rock We Salute You" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 1, + "Title": "For Those About To Rock We Salute You" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 1, + "Title": "For Those About To Rock We Salute You" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 1, + "Title": "For Those About To Rock We Salute You" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 1, + "Title": "For Those About To Rock We Salute You" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 1, + "Title": "For Those About To Rock We Salute You" + } + ] + } + ] + }, + { + "AlbumId": 2, + "Tracks": [ + { + "TrackAlbums": [ + { + "AlbumId": 2, + "Title": "Balls to the Wall" + } + ] + } + ] + } + ], + "AlbumWithFilterAndPredicate": [ + { + "AlbumId": 3, + "Tracks": [ + { + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + } + ] + } + ] + } + }, + { + "data": { + "Album": [ + { + "AlbumId": 2, + "Tracks": [ + { + "TrackAlbums": [ + { + "AlbumId": 2, + "Title": "Balls to the Wall" + } + ] + } + ] + } + ], + "AlbumWithFilterAndPredicate": [] + } + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/array/nested/metadata.json b/v3/engine/tests/execute/models/select_many/relationship_predicates/array/nested/metadata.json new file mode 100644 index 00000000000..fc3f11d5522 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/array/nested/metadata.json @@ -0,0 +1,91 @@ +{ + "version": "v2", + "subgraphs": [ + { + "name": "default", + "objects": [ + { + "definition": { + "modelName": "Albums", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": { + "relationship": { + "name": "Tracks", + "predicate": { + "relationship": { + "name": "TrackAlbums", + "predicate": { + "fieldComparison": { + "field": "AlbumId", + "operator": "_eq", + "value": { + "sessionVariable": "x-hasura-user-id" + } + } + } + } + } + } + } + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + }, + { + "definition": { + "modelName": "Tracks", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": null + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + }, + { + "definition": { + "modelName": "Artists", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": null + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + } + ] + } + ] +} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/array/nested/request.gql b/v3/engine/tests/execute/models/select_many/relationship_predicates/array/nested/request.gql new file mode 100644 index 00000000000..3e766276289 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/array/nested/request.gql @@ -0,0 +1,20 @@ +query MyQuery { + Album(limit: 2) { + AlbumId + Tracks { + TrackAlbums { + AlbumId + Title + } + } + } + AlbumWithFilterAndPredicate: Album(limit: 2, where: {Tracks: {TrackId: {_eq: 3}}}) { + AlbumId + Tracks { + TrackAlbums { + AlbumId + Title + } + } + } +} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/array/nested/session_variables.json b/v3/engine/tests/execute/models/select_many/relationship_predicates/array/nested/session_variables.json new file mode 100644 index 00000000000..655455cc70e --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/array/nested/session_variables.json @@ -0,0 +1,9 @@ +[ + { + "x-hasura-role": "admin" + }, + { + "x-hasura-role": "user", + "x-hasura-user-id": "2" + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/array/simple/expected.json b/v3/engine/tests/execute/models/select_many/relationship_predicates/array/simple/expected.json new file mode 100644 index 00000000000..0c5320a5791 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/array/simple/expected.json @@ -0,0 +1,195 @@ +[ + { + "data": { + "Album": [ + { + "AlbumId": 1, + "Title": "For Those About To Rock We Salute You", + "ArtistId": 1, + "Tracks": [ + { + "AlbumId": 1, + "Name": "For Those About To Rock (We Salute You)", + "TrackId": 1 + }, + { + "AlbumId": 1, + "Name": "Put The Finger On You", + "TrackId": 6 + }, + { + "AlbumId": 1, + "Name": "Let's Get It Up", + "TrackId": 7 + }, + { + "AlbumId": 1, + "Name": "Inject The Venom", + "TrackId": 8 + }, + { + "AlbumId": 1, + "Name": "Snowballed", + "TrackId": 9 + }, + { + "AlbumId": 1, + "Name": "Evil Walks", + "TrackId": 10 + }, + { + "AlbumId": 1, + "Name": "C.O.D.", + "TrackId": 11 + }, + { + "AlbumId": 1, + "Name": "Breaking The Rules", + "TrackId": 12 + }, + { + "AlbumId": 1, + "Name": "Night Of The Long Knives", + "TrackId": 13 + }, + { + "AlbumId": 1, + "Name": "Spellbound", + "TrackId": 14 + } + ] + }, + { + "AlbumId": 2, + "Title": "Balls to the Wall", + "ArtistId": 2, + "Tracks": [ + { + "AlbumId": 2, + "Name": "Balls to the Wall", + "TrackId": 2 + } + ] + } + ], + "AlbumPredicateOrderBy": [ + { + "AlbumId": 347, + "Title": "Koyaanisqatsi (Soundtrack from the Motion Picture)", + "ArtistId": 275, + "Tracks": [ + { + "AlbumId": 347, + "Name": "Koyaanisqatsi", + "TrackId": 3503 + } + ] + }, + { + "AlbumId": 346, + "Title": "Mozart: Chamber Music", + "ArtistId": 274, + "Tracks": [ + { + "AlbumId": 346, + "Name": "Quintet for Horn, Violin, 2 Violas, and Cello in E Flat Major, K. 407/386c: III. Allegro", + "TrackId": 3502 + } + ] + } + ], + "AlbumWithFilterAndPredicate": [ + { + "AlbumId": 1, + "Title": "For Those About To Rock We Salute You", + "ArtistId": 1, + "Tracks": [ + { + "AlbumId": 1, + "Name": "For Those About To Rock (We Salute You)", + "TrackId": 1 + }, + { + "AlbumId": 1, + "Name": "Put The Finger On You", + "TrackId": 6 + }, + { + "AlbumId": 1, + "Name": "Let's Get It Up", + "TrackId": 7 + }, + { + "AlbumId": 1, + "Name": "Inject The Venom", + "TrackId": 8 + }, + { + "AlbumId": 1, + "Name": "Snowballed", + "TrackId": 9 + }, + { + "AlbumId": 1, + "Name": "Evil Walks", + "TrackId": 10 + }, + { + "AlbumId": 1, + "Name": "C.O.D.", + "TrackId": 11 + }, + { + "AlbumId": 1, + "Name": "Breaking The Rules", + "TrackId": 12 + }, + { + "AlbumId": 1, + "Name": "Night Of The Long Knives", + "TrackId": 13 + }, + { + "AlbumId": 1, + "Name": "Spellbound", + "TrackId": 14 + } + ] + } + ] + } + }, + { + "data": { + "Album": [ + { + "AlbumId": 2, + "Title": "Balls to the Wall", + "ArtistId": 2, + "Tracks": [ + { + "AlbumId": 2, + "Name": "Balls to the Wall", + "TrackId": 2 + } + ] + } + ], + "AlbumPredicateOrderBy": [ + { + "AlbumId": 2, + "Title": "Balls to the Wall", + "ArtistId": 2, + "Tracks": [ + { + "AlbumId": 2, + "Name": "Balls to the Wall", + "TrackId": 2 + } + ] + } + ], + "AlbumWithFilterAndPredicate": [] + } + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/array/simple/metadata.json b/v3/engine/tests/execute/models/select_many/relationship_predicates/array/simple/metadata.json new file mode 100644 index 00000000000..b02ce9d3974 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/array/simple/metadata.json @@ -0,0 +1,86 @@ +{ + "version": "v2", + "subgraphs": [ + { + "name": "default", + "objects": [ + { + "definition": { + "modelName": "Albums", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": { + "relationship": { + "name": "Tracks", + "predicate": { + "fieldComparison": { + "field": "TrackId", + "operator": "_eq", + "value": { + "sessionVariable": "x-hasura-user-id" + } + } + } + } + } + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + }, + { + "definition": { + "modelName": "Tracks", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": null + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + }, + { + "definition": { + "modelName": "Artists", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": null + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + } + ] + } + ] +} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/array/simple/request.gql b/v3/engine/tests/execute/models/select_many/relationship_predicates/array/simple/request.gql new file mode 100644 index 00000000000..33cfb6674c1 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/array/simple/request.gql @@ -0,0 +1,32 @@ +query MyQuery { + Album(limit: 2) { + AlbumId + Title + ArtistId + Tracks { + AlbumId + Name + TrackId + } + } + AlbumPredicateOrderBy: Album(limit: 2, order_by: {AlbumId: Desc}) { + AlbumId + Title + ArtistId + Tracks { + AlbumId + Name + TrackId + } + } + AlbumWithFilterAndPredicate: Album(limit: 2, where: {Tracks: {TrackId: {_eq: 1}}}) { + AlbumId + Title + ArtistId + Tracks { + AlbumId + Name + TrackId + } + } +} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/array/simple/session_variables.json b/v3/engine/tests/execute/models/select_many/relationship_predicates/array/simple/session_variables.json new file mode 100644 index 00000000000..655455cc70e --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/array/simple/session_variables.json @@ -0,0 +1,9 @@ +[ + { + "x-hasura-role": "admin" + }, + { + "x-hasura-role": "user", + "x-hasura-user-id": "2" + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/common_metadata.json b/v3/engine/tests/execute/models/select_many/relationship_predicates/common_metadata.json new file mode 100644 index 00000000000..5df8349b8a7 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/common_metadata.json @@ -0,0 +1,723 @@ +{ + "version": "v2", + "subgraphs": [ + { + "name": "default", + "objects": [ + { + "definition": { + "name": "Album", + "fields": [ + { + "name": "AlbumId", + "type": "Int" + }, + { + "name": "Title", + "type": "String" + }, + { + "name": "ArtistId", + "type": "Int" + } + ], + "graphql": { + "typeName": "Album" + } + }, + "version": "v1", + "kind": "ObjectType" + }, + { + "definition": { + "name": "Track", + "fields": [ + { + "name": "TrackId", + "type": "Int" + }, + { + "name": "Name", + "type": "String" + }, + { + "name": "AlbumId", + "type": "Int" + }, + { + "name": "GenreId", + "type": "Int" + } + ], + "graphql": { + "typeName": "Track" + } + }, + "version": "v1", + "kind": "ObjectType" + }, + { + "definition": { + "name": "Genre", + "fields": [ + { + "name": "GenreId", + "type": "Int" + }, + { + "name": "Name", + "type": "String" + } + ], + "graphql": { + "typeName": "Genre" + } + }, + "version": "v1", + "kind": "ObjectType" + }, + { + "kind": "DataConnectorScalarRepresentation", + "version": "v1", + "definition": { + "dataConnectorName": "db", + "dataConnectorScalarType": "String", + "representation": "String", + "graphql": { + "comparisonExpressionTypeName": "String_Comparison_Exp" + } + } + }, + { + "kind": "ScalarType", + "version": "v1", + "definition": { + "name": "CustomString", + "graphql": { + "typeName": "CustomString" + } + } + }, + { + "definition": { + "name": "Albums", + "objectType": "Album", + "source": { + "dataConnectorName": "db", + "collection": "Album", + "typeMapping": { + "Album": { + "fieldMapping": { + "AlbumId": { + "column": "AlbumId" + }, + "Title": { + "column": "Title" + }, + "ArtistId": { + "column": "ArtistId" + } + } + } + } + }, + "graphql": { + "selectUniques": [ + { + "queryRootField": "AlbumByID", + "uniqueIdentifier": [ + "AlbumId" + ] + } + ], + "selectMany": { + "queryRootField": "Album" + }, + "filterExpressionType": "Album_Where_Exp", + "orderByExpressionType": "Album_Order_By" + }, + "filterableFields": [ + { + "fieldName": "AlbumId", + "operators": { + "enableAll": true + } + }, + { + "fieldName": "Title", + "operators": { + "enableAll": true + } + }, + { + "fieldName": "ArtistId", + "operators": { + "enableAll": true + } + } + ], + "orderableFields": [ + { + "fieldName": "AlbumId", + "orderByDirections": { + "enableAll": true + } + }, + { + "fieldName": "Title", + "orderByDirections": { + "enableAll": true + } + }, + { + "fieldName": "ArtistId", + "orderByDirections": { + "enableAll": true + } + } + ] + }, + "version": "v1", + "kind": "Model" + }, + { + "definition": { + "name": "Tracks", + "objectType": "Track", + "source": { + "dataConnectorName": "db", + "collection": "Track", + "typeMapping": { + "Track": { + "fieldMapping": { + "TrackId": { + "column": "TrackId" + }, + "Name": { + "column": "Name" + }, + "AlbumId": { + "column": "AlbumId" + }, + "GenreId": { + "column": "GenreId" + } + } + } + } + }, + "graphql": { + "selectUniques": [ + { + "queryRootField": "TrackByID", + "uniqueIdentifier": [ + "TrackId" + ] + } + ], + "selectMany": { + "queryRootField": "Track" + }, + "filterExpressionType": "Track_Where_Exp", + "orderByExpressionType": "Track_Order_By" + }, + "filterableFields": [ + { + "fieldName": "TrackId", + "operators": { + "enableAll": true + } + }, + { + "fieldName": "Name", + "operators": { + "enableAll": true + } + }, + { + "fieldName": "AlbumId", + "operators": { + "enableAll": true + } + }, + { + "fieldName": "GenreId", + "operators": { + "enableAll": true + } + } + ], + "orderableFields": [ + { + "fieldName": "TrackId", + "orderByDirections": { + "enableAll": true + } + }, + { + "fieldName": "Name", + "orderByDirections": { + "enableAll": true + } + }, + { + "fieldName": "AlbumId", + "orderByDirections": { + "enableAll": true + } + }, + { + "fieldName": "GenreId", + "orderByDirections": { + "enableAll": true + } + } + ] + }, + "version": "v1", + "kind": "Model" + }, + { + "definition": { + "name": "Genres", + "objectType": "Genre", + "source": { + "dataConnectorName": "db", + "collection": "Genre", + "typeMapping": { + "Genre": { + "fieldMapping": { + "GenreId": { + "column": "GenreId" + }, + "Name": { + "column": "Name" + } + } + } + } + }, + "graphql": { + "selectUniques": [ + { + "queryRootField": "GenreByID", + "uniqueIdentifier": [ + "GenreId" + ] + } + ], + "selectMany": { + "queryRootField": "Genre" + }, + "filterExpressionType": "Genre_Where_Exp", + "orderByExpressionType": "Genre_Order_By" + }, + "filterableFields": [ + { + "fieldName": "GenreId", + "operators": { + "enableAll": true + } + }, + { + "fieldName": "Name", + "operators": { + "enableAll": true + } + } + ], + "orderableFields": [ + { + "fieldName": "GenreId", + "orderByDirections": { + "enableAll": true + } + }, + { + "fieldName": "Name", + "orderByDirections": { + "enableAll": true + } + } + ] + }, + "version": "v1", + "kind": "Model" + }, + { + "definition": { + "typeName": "Album", + "permissions": [ + { + "role": "admin", + "output": { + "allowedFields": [ + "AlbumId", + "Title", + "ArtistId" + ] + } + }, + { + "role": "user", + "output": { + "allowedFields": [ + "AlbumId", + "Title", + "ArtistId" + ] + } + } + ] + }, + "version": "v1", + "kind": "TypePermissions" + }, + { + "definition": { + "typeName": "Track", + "permissions": [ + { + "role": "admin", + "output": { + "allowedFields": [ + "TrackId", + "Name", + "AlbumId", + "GenreId" + ] + } + }, + { + "role": "user", + "output": { + "allowedFields": [ + "TrackId", + "Name", + "AlbumId", + "GenreId" + ] + } + } + ] + }, + "version": "v1", + "kind": "TypePermissions" + }, + { + "definition": { + "typeName": "Genre", + "permissions": [ + { + "role": "admin", + "output": { + "allowedFields": [ + "GenreId", + "Name" + ] + } + }, + { + "role": "user", + "output": { + "allowedFields": [ + "GenreId", + "Name" + ] + } + } + ] + }, + "version": "v1", + "kind": "TypePermissions" + }, + { + "definition": { + "source": "Album", + "name": "Tracks", + "target": { + "model": { + "name": "Tracks", + "relationshipType": "Array" + } + }, + "mapping": [ + { + "source": { + "fieldPath": [ + { + "fieldName": "AlbumId" + } + ] + }, + "target": { + "modelField": [ + { + "fieldName": "AlbumId" + } + ] + } + } + ] + }, + "version": "v1", + "kind": "Relationship" + }, + { + "definition": { + "source": "Track", + "name": "Album", + "target": { + "model": { + "name": "Albums", + "relationshipType": "Object" + } + }, + "mapping": [ + { + "source": { + "fieldPath": [ + { + "fieldName": "AlbumId" + } + ] + }, + "target": { + "modelField": [ + { + "fieldName": "AlbumId" + } + ] + } + } + ] + }, + "version": "v1", + "kind": "Relationship" + }, + { + "definition": { + "source": "Track", + "name": "Genre", + "target": { + "model": { + "name": "Genres", + "relationshipType": "Object" + } + }, + "mapping": [ + { + "source": { + "fieldPath": [ + { + "fieldName": "GenreId" + } + ] + }, + "target": { + "modelField": [ + { + "fieldName": "GenreId" + } + ] + } + } + ] + }, + "version": "v1", + "kind": "Relationship" + }, + { + "definition": { + "source": "Track", + "name": "TrackAlbums", + "target": { + "model": { + "name": "Albums", + "relationshipType": "Array" + } + }, + "mapping": [ + { + "source": { + "fieldPath": [ + { + "fieldName": "AlbumId" + } + ] + }, + "target": { + "modelField": [ + { + "fieldName": "AlbumId" + } + ] + } + } + ] + }, + "version": "v1", + "kind": "Relationship" + }, + { + "definition": { + "name": "Artist", + "fields": [ + { + "name": "ArtistId", + "type": "Int" + }, + { + "name": "Name", + "type": "String" + } + ], + "graphql": { + "typeName": "Artist" + } + }, + "version": "v1", + "kind": "ObjectType" + }, + { + "definition": { + "name": "Artists", + "objectType": "Artist", + "source": { + "dataConnectorName": "db", + "collection": "Artist", + "typeMapping": { + "Artist": { + "fieldMapping": { + "ArtistId": { + "column": "ArtistId" + }, + "Name": { + "column": "Name" + } + } + } + } + }, + "graphql": { + "selectUniques": [ + { + "queryRootField": "ArtistByID", + "uniqueIdentifier": [ + "ArtistId" + ] + } + ], + "selectMany": { + "queryRootField": "Artist" + }, + "filterExpressionType": "Artist_Where_Exp", + "orderByExpressionType": "Artist_Order_By" + }, + "filterableFields": [ + { + "fieldName": "ArtistId", + "operators": { + "enableAll": true + } + }, + { + "fieldName": "Name", + "operators": { + "enableAll": true + } + } + ], + "orderableFields": [ + { + "fieldName": "ArtistId", + "orderByDirections": { + "enableAll": true + } + }, + { + "fieldName": "Name", + "orderByDirections": { + "enableAll": true + } + } + ] + }, + "version": "v1", + "kind": "Model" + }, + { + "definition": { + "typeName": "Artist", + "permissions": [ + { + "role": "admin", + "output": { + "allowedFields": [ + "ArtistId", + "Name" + ] + } + }, + { + "role": "user", + "output": { + "allowedFields": [ + "ArtistId", + "Name" + ] + } + } + ] + }, + "version": "v1", + "kind": "TypePermissions" + }, + { + "definition": { + "source": "Album", + "name": "Artist", + "target": { + "model": { + "name": "Artists", + "relationshipType": "Object" + } + }, + "mapping": [ + { + "source": { + "fieldPath": [ + { + "fieldName": "ArtistId" + } + ] + }, + "target": { + "modelField": [ + { + "fieldName": "ArtistId" + } + ] + } + } + ] + }, + "version": "v1", + "kind": "Relationship" + }, + { + "kind": "DataConnectorScalarRepresentation", + "version": "v1", + "definition": { + "dataConnectorName": "db", + "dataConnectorScalarType": "Int", + "representation": "Int", + "graphql": { + "comparisonExpressionTypeName": "Int_comparison" + } + } + } + ] + } + ] +} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/object/nested/expected.json b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/nested/expected.json new file mode 100644 index 00000000000..dc88ddd9fe7 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/nested/expected.json @@ -0,0 +1,62 @@ +[ + { + "data": { + "Track": [ + { + "TrackId": 1, + "Album": { + "Artist": { + "ArtistId": 1, + "Name": "AC/DC" + } + } + }, + { + "TrackId": 2, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + } + ], + "TrackWithFilterAndPredicate": [ + { + "TrackId": 3, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + } + ] + } + }, + { + "data": { + "Track": [ + { + "TrackId": 1, + "Album": { + "Artist": { + "ArtistId": 1, + "Name": "AC/DC" + } + } + }, + { + "TrackId": 6, + "Album": { + "Artist": { + "ArtistId": 1, + "Name": "AC/DC" + } + } + } + ], + "TrackWithFilterAndPredicate": [] + } + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/object/nested/metadata.json b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/nested/metadata.json new file mode 100644 index 00000000000..43a78b7d85b --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/nested/metadata.json @@ -0,0 +1,91 @@ +{ + "version": "v2", + "subgraphs": [ + { + "name": "default", + "objects": [ + { + "definition": { + "modelName": "Albums", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": null + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + }, + { + "definition": { + "modelName": "Tracks", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": { + "relationship": { + "name": "Album", + "predicate": { + "relationship": { + "name": "Artist", + "predicate": { + "fieldComparison": { + "field": "ArtistId", + "operator": "_eq", + "value": { + "sessionVariable": "x-hasura-user-id" + } + } + } + } + } + } + } + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + }, + { + "definition": { + "modelName": "Artists", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": null + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + } + ] + } + ] +} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/object/nested/request.gql b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/nested/request.gql new file mode 100644 index 00000000000..5e43141184c --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/nested/request.gql @@ -0,0 +1,22 @@ +query MyQuery { + Track(limit: 2) { + TrackId + Album { + Artist { + ArtistId + Name + } + } + } + TrackWithFilterAndPredicate: Track( + where: {_and: [{Album: {Artist: {ArtistId: {_eq: 2}}}}, {TrackId: {_eq: 3}}]} + ) { + TrackId + Album { + Artist { + ArtistId + Name + } + } + } +} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/object/nested/session_variables.json b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/nested/session_variables.json new file mode 100644 index 00000000000..1516bed8a37 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/nested/session_variables.json @@ -0,0 +1,9 @@ +[ + { + "x-hasura-role": "admin" + }, + { + "x-hasura-role": "user", + "x-hasura-user-id": "1" + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/object/simple/expected.json b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/simple/expected.json new file mode 100644 index 00000000000..0567d917c2f --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/simple/expected.json @@ -0,0 +1,108 @@ +[ + { + "data": { + "Track": [ + { + "AlbumId": 1, + "Name": "For Those About To Rock (We Salute You)", + "Album": { + "Title": "For Those About To Rock We Salute You" + } + }, + { + "AlbumId": 2, + "Name": "Balls to the Wall", + "Album": { + "Title": "Balls to the Wall" + } + } + ], + "TrackWithFilterAndPredicate": [ + { + "AlbumId": 1, + "Name": "For Those About To Rock (We Salute You)", + "Album": { + "Title": "For Those About To Rock We Salute You" + } + }, + { + "AlbumId": 1, + "Name": "Put The Finger On You", + "Album": { + "Title": "For Those About To Rock We Salute You" + } + }, + { + "AlbumId": 1, + "Name": "Let's Get It Up", + "Album": { + "Title": "For Those About To Rock We Salute You" + } + }, + { + "AlbumId": 1, + "Name": "Inject The Venom", + "Album": { + "Title": "For Those About To Rock We Salute You" + } + }, + { + "AlbumId": 1, + "Name": "Snowballed", + "Album": { + "Title": "For Those About To Rock We Salute You" + } + }, + { + "AlbumId": 1, + "Name": "Evil Walks", + "Album": { + "Title": "For Those About To Rock We Salute You" + } + }, + { + "AlbumId": 1, + "Name": "C.O.D.", + "Album": { + "Title": "For Those About To Rock We Salute You" + } + }, + { + "AlbumId": 1, + "Name": "Breaking The Rules", + "Album": { + "Title": "For Those About To Rock We Salute You" + } + }, + { + "AlbumId": 1, + "Name": "Night Of The Long Knives", + "Album": { + "Title": "For Those About To Rock We Salute You" + } + }, + { + "AlbumId": 1, + "Name": "Spellbound", + "Album": { + "Title": "For Those About To Rock We Salute You" + } + } + ] + } + }, + { + "data": { + "Track": [ + { + "AlbumId": 2, + "Name": "Balls to the Wall", + "Album": { + "Title": "Balls to the Wall" + } + } + ], + "TrackWithFilterAndPredicate": [] + } + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/object/simple/metadata.json b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/simple/metadata.json new file mode 100644 index 00000000000..116b8fd0b0a --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/simple/metadata.json @@ -0,0 +1,86 @@ +{ + "version": "v2", + "subgraphs": [ + { + "name": "default", + "objects": [ + { + "definition": { + "modelName": "Albums", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": null + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + }, + { + "definition": { + "modelName": "Tracks", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": { + "relationship": { + "name": "Album", + "predicate": { + "fieldComparison": { + "field": "Title", + "operator": "_eq", + "value": { + "sessionVariable": "x-hasura-album-title" + } + } + } + } + } + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + }, + { + "definition": { + "modelName": "Artists", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": null + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + } + ] + } + ] +} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/object/simple/request.gql b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/simple/request.gql new file mode 100644 index 00000000000..92e0b01fde4 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/simple/request.gql @@ -0,0 +1,18 @@ +query MyQuery { + Track(limit: 2) { + AlbumId + Name + Album { + Title + } + } + TrackWithFilterAndPredicate: Track( + where: {_and: [{Album: {Title: {_eq: "For Those About To Rock We Salute You"}}}, {AlbumId: {_eq: 1}}]} + ) { + AlbumId + Name + Album { + Title + } + } +} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/object/simple/session_variables.json b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/simple/session_variables.json new file mode 100644 index 00000000000..1086253ee43 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/simple/session_variables.json @@ -0,0 +1,9 @@ +[ + { + "x-hasura-role": "admin" + }, + { + "x-hasura-role": "user", + "x-hasura-album-title": "Balls to the Wall" + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/object/two_relationship_fields/expected.json b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/two_relationship_fields/expected.json new file mode 100644 index 00000000000..164b1770a28 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/two_relationship_fields/expected.json @@ -0,0 +1,58 @@ +[ + { + "data": { + "Album": [ + { + "Tracks": [ + { + "Album": { + "AlbumId": 1 + }, + "Genre": { + "GenreId": 1 + }, + "Name": "For Those About To Rock (We Salute You)" + }, + { + "Album": { + "AlbumId": 1 + }, + "Genre": { + "GenreId": 1 + }, + "Name": "Put The Finger On You" + } + ] + } + ] + } + }, + { + "data": { + "Album": [ + { + "Tracks": [ + { + "Album": { + "AlbumId": 1 + }, + "Genre": { + "GenreId": 1 + }, + "Name": "For Those About To Rock (We Salute You)" + }, + { + "Album": { + "AlbumId": 1 + }, + "Genre": { + "GenreId": 1 + }, + "Name": "Put The Finger On You" + } + ] + } + ] + } + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/object/two_relationship_fields/metadata.json b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/two_relationship_fields/metadata.json new file mode 100644 index 00000000000..6d7d9f1cea7 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/two_relationship_fields/metadata.json @@ -0,0 +1,130 @@ +{ + "version": "v2", + "subgraphs": [ + { + "name": "default", + "objects": [ + { + "definition": { + "modelName": "Albums", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": { + "relationship": { + "name": "Tracks", + "predicate": { + "and": [ + { + "relationship": { + "name": "Album", + "predicate": { + "fieldComparison": { + "field": "AlbumId", + "operator": "_eq", + "value": { + "sessionVariable": "x-hasura-user-id" + } + } + } + } + }, + { + "relationship": { + "name": "Genre", + "predicate": { + "fieldComparison": { + "field": "GenreId", + "operator": "_eq", + "value": { + "sessionVariable": "x-hasura-user-id" + } + } + } + } + } + ] + } + } + } + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + }, + { + "definition": { + "modelName": "Tracks", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": null + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + }, + { + "definition": { + "modelName": "Artists", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": null + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + }, + { + "definition": { + "modelName": "Genres", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": null + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + } + ] + } + ] +} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/object/two_relationship_fields/request.gql b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/two_relationship_fields/request.gql new file mode 100644 index 00000000000..1b1da823739 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/two_relationship_fields/request.gql @@ -0,0 +1,13 @@ +query MyQuery { + Album(limit: 1) { + Tracks(limit: 2) { + Album { + AlbumId + } + Genre { + GenreId + } + Name + } + } +} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/object/two_relationship_fields/session_variables.json b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/two_relationship_fields/session_variables.json new file mode 100644 index 00000000000..1516bed8a37 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/object/two_relationship_fields/session_variables.json @@ -0,0 +1,9 @@ +[ + { + "x-hasura-role": "admin" + }, + { + "x-hasura-role": "user", + "x-hasura-user-id": "1" + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/on_two_fields/expected.json b/v3/engine/tests/execute/models/select_many/relationship_predicates/on_two_fields/expected.json new file mode 100644 index 00000000000..46f19dd71c9 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/on_two_fields/expected.json @@ -0,0 +1,124 @@ +[ + { + "data": { + "Album": [ + { + "Tracks": [ + { + "TrackId": 1, + "Name": "For Those About To Rock (We Salute You)", + "Album": { + "Title": "For Those About To Rock We Salute You" + } + }, + { + "TrackId": 6, + "Name": "Put The Finger On You", + "Album": { + "Title": "For Those About To Rock We Salute You" + } + }, + { + "TrackId": 7, + "Name": "Let's Get It Up", + "Album": { + "Title": "For Those About To Rock We Salute You" + } + }, + { + "TrackId": 8, + "Name": "Inject The Venom", + "Album": { + "Title": "For Those About To Rock We Salute You" + } + }, + { + "TrackId": 9, + "Name": "Snowballed", + "Album": { + "Title": "For Those About To Rock We Salute You" + } + }, + { + "TrackId": 10, + "Name": "Evil Walks", + "Album": { + "Title": "For Those About To Rock We Salute You" + } + }, + { + "TrackId": 11, + "Name": "C.O.D.", + "Album": { + "Title": "For Those About To Rock We Salute You" + } + }, + { + "TrackId": 12, + "Name": "Breaking The Rules", + "Album": { + "Title": "For Those About To Rock We Salute You" + } + }, + { + "TrackId": 13, + "Name": "Night Of The Long Knives", + "Album": { + "Title": "For Those About To Rock We Salute You" + } + }, + { + "TrackId": 14, + "Name": "Spellbound", + "Album": { + "Title": "For Those About To Rock We Salute You" + } + } + ] + }, + { + "Tracks": [ + { + "TrackId": 2, + "Name": "Balls to the Wall", + "Album": { + "Title": "Balls to the Wall" + } + } + ] + } + ] + } + }, + { + "data": { + "Album": [ + { + "Tracks": [ + { + "TrackId": 3, + "Name": "Fast As a Shark", + "Album": { + "Title": "Restless and Wild" + } + }, + { + "TrackId": 4, + "Name": "Restless and Wild", + "Album": { + "Title": "Restless and Wild" + } + }, + { + "TrackId": 5, + "Name": "Princess of the Dawn", + "Album": { + "Title": "Restless and Wild" + } + } + ] + } + ] + } + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/on_two_fields/metadata.json b/v3/engine/tests/execute/models/select_many/relationship_predicates/on_two_fields/metadata.json new file mode 100644 index 00000000000..f641034bac1 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/on_two_fields/metadata.json @@ -0,0 +1,99 @@ +{ + "version": "v2", + "subgraphs": [ + { + "name": "default", + "objects": [ + { + "definition": { + "modelName": "Albums", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": { + "relationship": { + "name": "Tracks", + "predicate": { + "fieldComparison": { + "field": "TrackId", + "operator": "_eq", + "value": { + "sessionVariable": "x-hasura-user-id" + } + } + } + } + } + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + }, + { + "definition": { + "modelName": "Tracks", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": { + "relationship": { + "name": "Album", + "predicate": { + "fieldComparison": { + "field": "Title", + "operator": "_eq", + "value": { + "sessionVariable": "x-hasura-album-title" + } + } + } + } + } + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + }, + { + "definition": { + "modelName": "Artists", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": null + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + } + ] + } + ] +} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/on_two_fields/request.gql b/v3/engine/tests/execute/models/select_many/relationship_predicates/on_two_fields/request.gql new file mode 100644 index 00000000000..547b854bb90 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/on_two_fields/request.gql @@ -0,0 +1,11 @@ +query MyQuery { + Album(limit: 2) { + Tracks { + TrackId + Name + Album { + Title + } + } + } +} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/relationship_predicates/on_two_fields/session_variables.json b/v3/engine/tests/execute/models/select_many/relationship_predicates/on_two_fields/session_variables.json new file mode 100644 index 00000000000..83c82dca93b --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/relationship_predicates/on_two_fields/session_variables.json @@ -0,0 +1,10 @@ +[ + { + "x-hasura-role": "admin" + }, + { + "x-hasura-role": "user", + "x-hasura-user-id": "3", + "x-hasura-album-title": "Restless and Wild" + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/where/relationships/array/nested/expected.json b/v3/engine/tests/execute/models/select_many/where/relationships/array/nested/expected.json new file mode 100644 index 00000000000..837b5ec1831 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/where/relationships/array/nested/expected.json @@ -0,0 +1,608 @@ +[ + { + "data": { + "Album": [ + { + "AlbumId": 3, + "Tracks": [ + { + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + } + ] + }, + { + "AlbumId": 3, + "Tracks": [ + { + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + } + ] + }, + { + "AlbumId": 3, + "Tracks": [ + { + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + } + ] + } + ], + "AlbumAnd": [ + { + "AlbumId": 3, + "Tracks": [ + { + "TrackId": 3, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 4, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 5, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + } + ] + }, + { + "AlbumId": 3, + "Tracks": [ + { + "TrackId": 3, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 4, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 5, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + } + ] + }, + { + "AlbumId": 3, + "Tracks": [ + { + "TrackId": 3, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 4, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 5, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + } + ] + } + ], + "AlbumOr": [ + { + "AlbumId": 2, + "Tracks": [ + { + "TrackId": 2, + "TrackAlbums": [ + { + "AlbumId": 2, + "Title": "Balls to the Wall" + } + ] + } + ] + }, + { + "AlbumId": 3, + "Tracks": [ + { + "TrackId": 3, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 4, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 5, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + } + ] + }, + { + "AlbumId": 3, + "Tracks": [ + { + "TrackId": 3, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 4, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 5, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + } + ] + }, + { + "AlbumId": 3, + "Tracks": [ + { + "TrackId": 3, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 4, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 5, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + } + ] + } + ] + } + }, + { + "data": { + "Album": [ + { + "AlbumId": 3, + "Tracks": [ + { + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + } + ] + }, + { + "AlbumId": 3, + "Tracks": [ + { + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + } + ] + }, + { + "AlbumId": 3, + "Tracks": [ + { + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + } + ] + } + ], + "AlbumAnd": [ + { + "AlbumId": 3, + "Tracks": [ + { + "TrackId": 3, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 4, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 5, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + } + ] + }, + { + "AlbumId": 3, + "Tracks": [ + { + "TrackId": 3, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 4, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 5, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + } + ] + }, + { + "AlbumId": 3, + "Tracks": [ + { + "TrackId": 3, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 4, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 5, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + } + ] + } + ], + "AlbumOr": [ + { + "AlbumId": 2, + "Tracks": [ + { + "TrackId": 2, + "TrackAlbums": [ + { + "AlbumId": 2, + "Title": "Balls to the Wall" + } + ] + } + ] + }, + { + "AlbumId": 3, + "Tracks": [ + { + "TrackId": 3, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 4, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 5, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + } + ] + }, + { + "AlbumId": 3, + "Tracks": [ + { + "TrackId": 3, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 4, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 5, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + } + ] + }, + { + "AlbumId": 3, + "Tracks": [ + { + "TrackId": 3, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 4, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + }, + { + "TrackId": 5, + "TrackAlbums": [ + { + "AlbumId": 3, + "Title": "Restless and Wild" + } + ] + } + ] + } + ] + } + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/where/relationships/array/nested/metadata.json b/v3/engine/tests/execute/models/select_many/where/relationships/array/nested/metadata.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/where/relationships/array/nested/metadata.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/where/relationships/array/nested/request.gql b/v3/engine/tests/execute/models/select_many/where/relationships/array/nested/request.gql new file mode 100644 index 00000000000..499c169c9e8 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/where/relationships/array/nested/request.gql @@ -0,0 +1,31 @@ +query MyQuery { + Album(where: {Tracks: {TrackAlbums: {AlbumId: {_eq: 3}}}}) { + AlbumId + Tracks { + TrackAlbums { + AlbumId + Title + } + } + } + AlbumAnd: Album(where: {_and: [{Tracks: {TrackAlbums: {AlbumId: {_eq: 3}}}}, {AlbumId: {_eq: 3}}]}) { + AlbumId + Tracks { + TrackId + TrackAlbums { + AlbumId + Title + } + } + } + AlbumOr: Album(where: {_or: [{Tracks: {TrackAlbums: {AlbumId: {_eq: 3}}}}, {AlbumId: {_eq: 2}}]}) { + AlbumId + Tracks { + TrackId + TrackAlbums { + AlbumId + Title + } + } + } +} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/where/relationships/array/nested/session_variables.json b/v3/engine/tests/execute/models/select_many/where/relationships/array/nested/session_variables.json new file mode 100644 index 00000000000..655455cc70e --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/where/relationships/array/nested/session_variables.json @@ -0,0 +1,9 @@ +[ + { + "x-hasura-role": "admin" + }, + { + "x-hasura-role": "user", + "x-hasura-user-id": "2" + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/where/relationships/array/simple/expected.json b/v3/engine/tests/execute/models/select_many/where/relationships/array/simple/expected.json new file mode 100644 index 00000000000..caf9e179ed0 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/where/relationships/array/simple/expected.json @@ -0,0 +1,314 @@ +[ + { + "data": { + "Album": [ + { + "AlbumId": 3, + "Title": "Restless and Wild", + "ArtistId": 2, + "Tracks": [ + { + "AlbumId": 3, + "Name": "Fast As a Shark", + "TrackId": 3 + }, + { + "AlbumId": 3, + "Name": "Restless and Wild", + "TrackId": 4 + }, + { + "AlbumId": 3, + "Name": "Princess of the Dawn", + "TrackId": 5 + } + ] + } + ], + "AlbumAnd": [ + { + "AlbumId": 3, + "Title": "Restless and Wild", + "ArtistId": 2, + "Tracks": [ + { + "AlbumId": 3, + "Name": "Fast As a Shark", + "TrackId": 3 + }, + { + "AlbumId": 3, + "Name": "Restless and Wild", + "TrackId": 4 + }, + { + "AlbumId": 3, + "Name": "Princess of the Dawn", + "TrackId": 5 + } + ] + } + ], + "AlbumOr": [ + { + "AlbumId": 2, + "Title": "Balls to the Wall", + "ArtistId": 2, + "Tracks": [ + { + "AlbumId": 2, + "Name": "Balls to the Wall", + "TrackId": 2 + } + ] + }, + { + "AlbumId": 3, + "Title": "Restless and Wild", + "ArtistId": 2, + "Tracks": [ + { + "AlbumId": 3, + "Name": "Fast As a Shark", + "TrackId": 3 + }, + { + "AlbumId": 3, + "Name": "Restless and Wild", + "TrackId": 4 + }, + { + "AlbumId": 3, + "Name": "Princess of the Dawn", + "TrackId": 5 + } + ] + }, + { + "AlbumId": 3, + "Title": "Restless and Wild", + "ArtistId": 2, + "Tracks": [ + { + "AlbumId": 3, + "Name": "Fast As a Shark", + "TrackId": 3 + }, + { + "AlbumId": 3, + "Name": "Restless and Wild", + "TrackId": 4 + }, + { + "AlbumId": 3, + "Name": "Princess of the Dawn", + "TrackId": 5 + } + ] + }, + { + "AlbumId": 3, + "Title": "Restless and Wild", + "ArtistId": 2, + "Tracks": [ + { + "AlbumId": 3, + "Name": "Fast As a Shark", + "TrackId": 3 + }, + { + "AlbumId": 3, + "Name": "Restless and Wild", + "TrackId": 4 + }, + { + "AlbumId": 3, + "Name": "Princess of the Dawn", + "TrackId": 5 + } + ] + } + ], + "AlbumTracksTwoRelationships": [ + { + "Tracks": [ + { + "Genre": { + "GenreId": 1 + }, + "Album": { + "AlbumId": 1 + }, + "Name": "For Those About To Rock (We Salute You)" + }, + { + "Genre": { + "GenreId": 1 + }, + "Album": { + "AlbumId": 1 + }, + "Name": "Put The Finger On You" + } + ] + } + ] + } + }, + { + "data": { + "Album": [ + { + "AlbumId": 3, + "Title": "Restless and Wild", + "ArtistId": 2, + "Tracks": [ + { + "AlbumId": 3, + "Name": "Fast As a Shark", + "TrackId": 3 + }, + { + "AlbumId": 3, + "Name": "Restless and Wild", + "TrackId": 4 + }, + { + "AlbumId": 3, + "Name": "Princess of the Dawn", + "TrackId": 5 + } + ] + } + ], + "AlbumAnd": [ + { + "AlbumId": 3, + "Title": "Restless and Wild", + "ArtistId": 2, + "Tracks": [ + { + "AlbumId": 3, + "Name": "Fast As a Shark", + "TrackId": 3 + }, + { + "AlbumId": 3, + "Name": "Restless and Wild", + "TrackId": 4 + }, + { + "AlbumId": 3, + "Name": "Princess of the Dawn", + "TrackId": 5 + } + ] + } + ], + "AlbumOr": [ + { + "AlbumId": 2, + "Title": "Balls to the Wall", + "ArtistId": 2, + "Tracks": [ + { + "AlbumId": 2, + "Name": "Balls to the Wall", + "TrackId": 2 + } + ] + }, + { + "AlbumId": 3, + "Title": "Restless and Wild", + "ArtistId": 2, + "Tracks": [ + { + "AlbumId": 3, + "Name": "Fast As a Shark", + "TrackId": 3 + }, + { + "AlbumId": 3, + "Name": "Restless and Wild", + "TrackId": 4 + }, + { + "AlbumId": 3, + "Name": "Princess of the Dawn", + "TrackId": 5 + } + ] + }, + { + "AlbumId": 3, + "Title": "Restless and Wild", + "ArtistId": 2, + "Tracks": [ + { + "AlbumId": 3, + "Name": "Fast As a Shark", + "TrackId": 3 + }, + { + "AlbumId": 3, + "Name": "Restless and Wild", + "TrackId": 4 + }, + { + "AlbumId": 3, + "Name": "Princess of the Dawn", + "TrackId": 5 + } + ] + }, + { + "AlbumId": 3, + "Title": "Restless and Wild", + "ArtistId": 2, + "Tracks": [ + { + "AlbumId": 3, + "Name": "Fast As a Shark", + "TrackId": 3 + }, + { + "AlbumId": 3, + "Name": "Restless and Wild", + "TrackId": 4 + }, + { + "AlbumId": 3, + "Name": "Princess of the Dawn", + "TrackId": 5 + } + ] + } + ], + "AlbumTracksTwoRelationships": [ + { + "Tracks": [ + { + "Genre": { + "GenreId": 1 + }, + "Album": { + "AlbumId": 1 + }, + "Name": "For Those About To Rock (We Salute You)" + }, + { + "Genre": { + "GenreId": 1 + }, + "Album": { + "AlbumId": 1 + }, + "Name": "Put The Finger On You" + } + ] + } + ] + } + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/where/relationships/array/simple/metadata.json b/v3/engine/tests/execute/models/select_many/where/relationships/array/simple/metadata.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/where/relationships/array/simple/metadata.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/where/relationships/array/simple/request.gql b/v3/engine/tests/execute/models/select_many/where/relationships/array/simple/request.gql new file mode 100644 index 00000000000..20d18116c8b --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/where/relationships/array/simple/request.gql @@ -0,0 +1,50 @@ +query MyQuery { + Album(where: {Tracks: {TrackId: {_eq: 3}}}) { + AlbumId + Title + ArtistId + Tracks { + AlbumId + Name + TrackId + } + } + AlbumAnd: Album( + where: {_and: [{Tracks: {TrackId: {_eq: 3}}}, {ArtistId: {_eq: 2}}]} + ) { + AlbumId + Title + ArtistId + Tracks { + AlbumId + Name + TrackId + } + } + AlbumOr: Album( + where: {_or: [{Tracks: {TrackId: {_eq: 3}}}, {ArtistId: {_eq: 2}}]} + ) { + AlbumId + Title + ArtistId + Tracks { + AlbumId + Name + TrackId + } + } + AlbumTracksTwoRelationships: Album( + where: {Tracks: {Album: {AlbumId: {_eq: 1}}, Genre: {GenreId: {_eq: 1}}}} + limit: 1 + ) { + Tracks(limit: 2) { + Genre { + GenreId + } + Album { + AlbumId + } + Name + } + } +} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/where/relationships/array/simple/session_variables.json b/v3/engine/tests/execute/models/select_many/where/relationships/array/simple/session_variables.json new file mode 100644 index 00000000000..655455cc70e --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/where/relationships/array/simple/session_variables.json @@ -0,0 +1,9 @@ +[ + { + "x-hasura-role": "admin" + }, + { + "x-hasura-role": "user", + "x-hasura-user-id": "2" + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/where/relationships/common_metadata.json b/v3/engine/tests/execute/models/select_many/where/relationships/common_metadata.json new file mode 100644 index 00000000000..2509387c073 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/where/relationships/common_metadata.json @@ -0,0 +1,807 @@ +{ + "version": "v2", + "subgraphs": [ + { + "name": "default", + "objects": [ + { + "definition": { + "name": "Album", + "fields": [ + { + "name": "AlbumId", + "type": "Int" + }, + { + "name": "Title", + "type": "String" + }, + { + "name": "ArtistId", + "type": "Int" + } + ], + "graphql": { + "typeName": "Album" + } + }, + "version": "v1", + "kind": "ObjectType" + }, + { + "definition": { + "name": "Track", + "fields": [ + { + "name": "TrackId", + "type": "Int" + }, + { + "name": "Name", + "type": "String" + }, + { + "name": "AlbumId", + "type": "Int" + }, + { + "name": "GenreId", + "type": "Int" + } + ], + "graphql": { + "typeName": "Track" + } + }, + "version": "v1", + "kind": "ObjectType" + }, + { + "definition": { + "name": "Genre", + "fields": [ + { + "name": "GenreId", + "type": "Int" + }, + { + "name": "Name", + "type": "String" + } + ], + "graphql": { + "typeName": "Genre" + } + }, + "version": "v1", + "kind": "ObjectType" + }, + { + "kind": "DataConnectorScalarRepresentation", + "version": "v1", + "definition": { + "dataConnectorName": "db", + "dataConnectorScalarType": "String", + "representation": "String", + "graphql": { + "comparisonExpressionTypeName": "String_Comparison_Exp" + } + } + }, + { + "kind": "ScalarType", + "version": "v1", + "definition": { + "name": "CustomString", + "graphql": { + "typeName": "CustomString" + } + } + }, + { + "definition": { + "name": "Albums", + "objectType": "Album", + "source": { + "dataConnectorName": "db", + "collection": "Album", + "typeMapping": { + "Album": { + "fieldMapping": { + "AlbumId": { + "column": "AlbumId" + }, + "Title": { + "column": "Title" + }, + "ArtistId": { + "column": "ArtistId" + } + } + } + } + }, + "graphql": { + "selectUniques": [ + { + "queryRootField": "AlbumByID", + "uniqueIdentifier": [ + "AlbumId" + ] + } + ], + "selectMany": { + "queryRootField": "Album" + }, + "filterExpressionType": "Album_Where_Exp", + "orderByExpressionType": "Album_Order_By" + }, + "filterableFields": [ + { + "fieldName": "AlbumId", + "operators": { + "enableAll": true + } + }, + { + "fieldName": "Title", + "operators": { + "enableAll": true + } + }, + { + "fieldName": "ArtistId", + "operators": { + "enableAll": true + } + } + ], + "orderableFields": [ + { + "fieldName": "AlbumId", + "orderByDirections": { + "enableAll": true + } + }, + { + "fieldName": "Title", + "orderByDirections": { + "enableAll": true + } + }, + { + "fieldName": "ArtistId", + "orderByDirections": { + "enableAll": true + } + } + ] + }, + "version": "v1", + "kind": "Model" + }, + { + "definition": { + "name": "Tracks", + "objectType": "Track", + "source": { + "dataConnectorName": "db", + "collection": "Track", + "typeMapping": { + "Track": { + "fieldMapping": { + "TrackId": { + "column": "TrackId" + }, + "Name": { + "column": "Name" + }, + "AlbumId": { + "column": "AlbumId" + }, + "GenreId": { + "column": "GenreId" + } + } + } + } + }, + "graphql": { + "selectUniques": [ + { + "queryRootField": "TrackByID", + "uniqueIdentifier": [ + "TrackId" + ] + } + ], + "selectMany": { + "queryRootField": "Track" + }, + "filterExpressionType": "Track_Where_Exp", + "orderByExpressionType": "Track_Order_By" + }, + "filterableFields": [ + { + "fieldName": "TrackId", + "operators": { + "enableAll": true + } + }, + { + "fieldName": "Name", + "operators": { + "enableAll": true + } + }, + { + "fieldName": "AlbumId", + "operators": { + "enableAll": true + } + }, + { + "fieldName": "GenreId", + "operators": { + "enableAll": true + } + } + ], + "orderableFields": [ + { + "fieldName": "TrackId", + "orderByDirections": { + "enableAll": true + } + }, + { + "fieldName": "Name", + "orderByDirections": { + "enableAll": true + } + }, + { + "fieldName": "AlbumId", + "orderByDirections": { + "enableAll": true + } + }, + { + "fieldName": "GenreId", + "orderByDirections": { + "enableAll": true + } + } + ] + }, + "version": "v1", + "kind": "Model" + }, + { + "definition": { + "name": "Genres", + "objectType": "Genre", + "source": { + "dataConnectorName": "db", + "collection": "Genre", + "typeMapping": { + "Genre": { + "fieldMapping": { + "GenreId": { + "column": "GenreId" + }, + "Name": { + "column": "Name" + } + } + } + } + }, + "graphql": { + "selectUniques": [ + { + "queryRootField": "GenreByID", + "uniqueIdentifier": [ + "GenreId" + ] + } + ], + "selectMany": { + "queryRootField": "Genre" + }, + "filterExpressionType": "Genre_Where_Exp", + "orderByExpressionType": "Genre_Order_By" + }, + "filterableFields": [ + { + "fieldName": "GenreId", + "operators": { + "enableAll": true + } + }, + { + "fieldName": "Name", + "operators": { + "enableAll": true + } + } + ], + "orderableFields": [ + { + "fieldName": "GenreId", + "orderByDirections": { + "enableAll": true + } + }, + { + "fieldName": "Name", + "orderByDirections": { + "enableAll": true + } + } + ] + }, + "version": "v1", + "kind": "Model" + }, + { + "definition": { + "typeName": "Album", + "permissions": [ + { + "role": "admin", + "output": { + "allowedFields": [ + "AlbumId", + "Title", + "ArtistId" + ] + } + }, + { + "role": "user", + "output": { + "allowedFields": [ + "AlbumId", + "Title", + "ArtistId" + ] + } + } + ] + }, + "version": "v1", + "kind": "TypePermissions" + }, + { + "definition": { + "typeName": "Track", + "permissions": [ + { + "role": "admin", + "output": { + "allowedFields": [ + "TrackId", + "Name", + "AlbumId", + "GenreId" + ] + } + }, + { + "role": "user", + "output": { + "allowedFields": [ + "TrackId", + "Name", + "AlbumId", + "GenreId" + ] + } + } + ] + }, + "version": "v1", + "kind": "TypePermissions" + }, + { + "definition": { + "typeName": "Genre", + "permissions": [ + { + "role": "admin", + "output": { + "allowedFields": [ + "GenreId", + "Name" + ] + } + }, + { + "role": "user", + "output": { + "allowedFields": [ + "GenreId", + "Name" + ] + } + } + ] + }, + "version": "v1", + "kind": "TypePermissions" + }, + { + "definition": { + "modelName": "Albums", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": null + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + }, + { + "definition": { + "modelName": "Tracks", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": null + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + }, + { + "definition": { + "modelName": "Genres", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": null + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + }, + { + "definition": { + "source": "Album", + "name": "Tracks", + "target": { + "model": { + "name": "Tracks", + "relationshipType": "Array" + } + }, + "mapping": [ + { + "source": { + "fieldPath": [ + { + "fieldName": "AlbumId" + } + ] + }, + "target": { + "modelField": [ + { + "fieldName": "AlbumId" + } + ] + } + } + ] + }, + "version": "v1", + "kind": "Relationship" + }, + { + "definition": { + "source": "Track", + "name": "Album", + "target": { + "model": { + "name": "Albums", + "relationshipType": "Object" + } + }, + "mapping": [ + { + "source": { + "fieldPath": [ + { + "fieldName": "AlbumId" + } + ] + }, + "target": { + "modelField": [ + { + "fieldName": "AlbumId" + } + ] + } + } + ] + }, + "version": "v1", + "kind": "Relationship" + }, + { + "definition": { + "source": "Track", + "name": "Genre", + "target": { + "model": { + "name": "Genres", + "relationshipType": "Object" + } + }, + "mapping": [ + { + "source": { + "fieldPath": [ + { + "fieldName": "GenreId" + } + ] + }, + "target": { + "modelField": [ + { + "fieldName": "GenreId" + } + ] + } + } + ] + }, + "version": "v1", + "kind": "Relationship" + }, + { + "definition": { + "source": "Track", + "name": "TrackAlbums", + "target": { + "model": { + "name": "Albums", + "relationshipType": "Array" + } + }, + "mapping": [ + { + "source": { + "fieldPath": [ + { + "fieldName": "AlbumId" + } + ] + }, + "target": { + "modelField": [ + { + "fieldName": "AlbumId" + } + ] + } + } + ] + }, + "version": "v1", + "kind": "Relationship" + }, + { + "definition": { + "name": "Artist", + "fields": [ + { + "name": "ArtistId", + "type": "Int" + }, + { + "name": "Name", + "type": "String" + } + ], + "graphql": { + "typeName": "Artist" + } + }, + "version": "v1", + "kind": "ObjectType" + }, + { + "definition": { + "name": "Artists", + "objectType": "Artist", + "source": { + "dataConnectorName": "db", + "collection": "Artist", + "typeMapping": { + "Artist": { + "fieldMapping": { + "ArtistId": { + "column": "ArtistId" + }, + "Name": { + "column": "Name" + } + } + } + } + }, + "graphql": { + "selectUniques": [ + { + "queryRootField": "ArtistByID", + "uniqueIdentifier": [ + "ArtistId" + ] + } + ], + "selectMany": { + "queryRootField": "Artist" + }, + "filterExpressionType": "Artist_Where_Exp", + "orderByExpressionType": "Artist_Order_By" + }, + "filterableFields": [ + { + "fieldName": "ArtistId", + "operators": { + "enableAll": true + } + }, + { + "fieldName": "Name", + "operators": { + "enableAll": true + } + } + ], + "orderableFields": [ + { + "fieldName": "ArtistId", + "orderByDirections": { + "enableAll": true + } + }, + { + "fieldName": "Name", + "orderByDirections": { + "enableAll": true + } + } + ] + }, + "version": "v1", + "kind": "Model" + }, + { + "definition": { + "modelName": "Artists", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user", + "select": { + "filter": null + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + }, + { + "definition": { + "typeName": "Artist", + "permissions": [ + { + "role": "admin", + "output": { + "allowedFields": [ + "ArtistId", + "Name" + ] + } + }, + { + "role": "user", + "output": { + "allowedFields": [ + "ArtistId", + "Name" + ] + } + } + ] + }, + "version": "v1", + "kind": "TypePermissions" + }, + { + "definition": { + "source": "Album", + "name": "Artist", + "target": { + "model": { + "name": "Artists", + "relationshipType": "Object" + } + }, + "mapping": [ + { + "source": { + "fieldPath": [ + { + "fieldName": "ArtistId" + } + ] + }, + "target": { + "modelField": [ + { + "fieldName": "ArtistId" + } + ] + } + } + ] + }, + "version": "v1", + "kind": "Relationship" + }, + { + "kind": "DataConnectorScalarRepresentation", + "version": "v1", + "definition": { + "dataConnectorName": "db", + "dataConnectorScalarType": "Int", + "representation": "Int", + "graphql": { + "comparisonExpressionTypeName": "Int_comparison" + } + } + } + ] + } + ] +} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/where/relationships/object/nested/expected.json b/v3/engine/tests/execute/models/select_many/where/relationships/object/nested/expected.json new file mode 100644 index 00000000000..121fb8f3d34 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/where/relationships/object/nested/expected.json @@ -0,0 +1,184 @@ +[ + { + "data": { + "Track": [ + { + "TrackId": 2, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + }, + { + "TrackId": 3, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + }, + { + "TrackId": 4, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + }, + { + "TrackId": 5, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + } + ], + "TrackAnd": [ + { + "TrackId": 3, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + } + ], + "TrackOr": [ + { + "TrackId": 2, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + }, + { + "TrackId": 3, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + }, + { + "TrackId": 4, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + }, + { + "TrackId": 5, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + } + ] + } + }, + { + "data": { + "Track": [ + { + "TrackId": 2, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + }, + { + "TrackId": 3, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + }, + { + "TrackId": 4, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + }, + { + "TrackId": 5, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + } + ], + "TrackAnd": [ + { + "TrackId": 3, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + } + ], + "TrackOr": [ + { + "TrackId": 2, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + }, + { + "TrackId": 3, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + }, + { + "TrackId": 4, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + }, + { + "TrackId": 5, + "Album": { + "Artist": { + "ArtistId": 2, + "Name": "Accept" + } + } + } + ] + } + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/where/relationships/object/nested/metadata.json b/v3/engine/tests/execute/models/select_many/where/relationships/object/nested/metadata.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/where/relationships/object/nested/metadata.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/where/relationships/object/nested/request.gql b/v3/engine/tests/execute/models/select_many/where/relationships/object/nested/request.gql new file mode 100644 index 00000000000..1248cfc2d82 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/where/relationships/object/nested/request.gql @@ -0,0 +1,29 @@ +query MyQuery { + Track(where: {Album: {Artist: {ArtistId: {_eq: 2}}}}) { + TrackId + Album { + Artist { + ArtistId + Name + } + } + } + TrackAnd:Track(where: {_and: [{Album: {Artist: {ArtistId: {_eq: 2}}}}, {TrackId: {_eq: 3}}]} ) { + TrackId + Album { + Artist { + ArtistId + Name + } + } + } + TrackOr:Track(where: {_or: [{Album: {Artist: {ArtistId: {_eq: 2}}}}, {TrackId: {_eq: 3}}]} ) { + TrackId + Album { + Artist { + ArtistId + Name + } + } + } +} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/where/relationships/object/nested/session_variables.json b/v3/engine/tests/execute/models/select_many/where/relationships/object/nested/session_variables.json new file mode 100644 index 00000000000..655455cc70e --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/where/relationships/object/nested/session_variables.json @@ -0,0 +1,9 @@ +[ + { + "x-hasura-role": "admin" + }, + { + "x-hasura-role": "user", + "x-hasura-user-id": "2" + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/where/relationships/object/simple/expected.json b/v3/engine/tests/execute/models/select_many/where/relationships/object/simple/expected.json new file mode 100644 index 00000000000..cb0d584f1e2 --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/where/relationships/object/simple/expected.json @@ -0,0 +1,162 @@ +[ + { + "data": { + "Track": [ + { + "AlbumId": 3, + "Name": "Fast As a Shark", + "Album": { + "Title": "Restless and Wild" + } + }, + { + "AlbumId": 3, + "Name": "Restless and Wild", + "Album": { + "Title": "Restless and Wild" + } + }, + { + "AlbumId": 3, + "Name": "Princess of the Dawn", + "Album": { + "Title": "Restless and Wild" + } + } + ], + "TrackAnd": [ + { + "AlbumId": 3, + "Name": "Fast As a Shark", + "Album": { + "Title": "Restless and Wild" + } + }, + { + "AlbumId": 3, + "Name": "Restless and Wild", + "Album": { + "Title": "Restless and Wild" + } + }, + { + "AlbumId": 3, + "Name": "Princess of the Dawn", + "Album": { + "Title": "Restless and Wild" + } + } + ], + "TrackOr": [ + { + "AlbumId": 2, + "Name": "Balls to the Wall", + "Album": { + "Title": "Balls to the Wall" + } + }, + { + "AlbumId": 3, + "Name": "Fast As a Shark", + "Album": { + "Title": "Restless and Wild" + } + }, + { + "AlbumId": 3, + "Name": "Restless and Wild", + "Album": { + "Title": "Restless and Wild" + } + }, + { + "AlbumId": 3, + "Name": "Princess of the Dawn", + "Album": { + "Title": "Restless and Wild" + } + } + ] + } + }, + { + "data": { + "Track": [ + { + "AlbumId": 3, + "Name": "Fast As a Shark", + "Album": { + "Title": "Restless and Wild" + } + }, + { + "AlbumId": 3, + "Name": "Restless and Wild", + "Album": { + "Title": "Restless and Wild" + } + }, + { + "AlbumId": 3, + "Name": "Princess of the Dawn", + "Album": { + "Title": "Restless and Wild" + } + } + ], + "TrackAnd": [ + { + "AlbumId": 3, + "Name": "Fast As a Shark", + "Album": { + "Title": "Restless and Wild" + } + }, + { + "AlbumId": 3, + "Name": "Restless and Wild", + "Album": { + "Title": "Restless and Wild" + } + }, + { + "AlbumId": 3, + "Name": "Princess of the Dawn", + "Album": { + "Title": "Restless and Wild" + } + } + ], + "TrackOr": [ + { + "AlbumId": 2, + "Name": "Balls to the Wall", + "Album": { + "Title": "Balls to the Wall" + } + }, + { + "AlbumId": 3, + "Name": "Fast As a Shark", + "Album": { + "Title": "Restless and Wild" + } + }, + { + "AlbumId": 3, + "Name": "Restless and Wild", + "Album": { + "Title": "Restless and Wild" + } + }, + { + "AlbumId": 3, + "Name": "Princess of the Dawn", + "Album": { + "Title": "Restless and Wild" + } + } + ] + } + } +] \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/where/relationships/object/simple/metadata.json b/v3/engine/tests/execute/models/select_many/where/relationships/object/simple/metadata.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/where/relationships/object/simple/metadata.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/where/relationships/object/simple/request.gql b/v3/engine/tests/execute/models/select_many/where/relationships/object/simple/request.gql new file mode 100644 index 00000000000..6f998b08c3b --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/where/relationships/object/simple/request.gql @@ -0,0 +1,23 @@ +query MyQuery { + Track(where: {Album: {Title: {_eq: "Restless and Wild"}}}) { + AlbumId + Name + Album { + Title + } + } + TrackAnd: Track(where: {_and: [{Album: {Title: {_eq: "Restless and Wild"}}}, {AlbumId: {_eq: 3}} ]} ) { + AlbumId + Name + Album { + Title + } + } + TrackOr: Track(where: {_or: [{Album: {Title: {_eq: "Restless and Wild"}}}, {AlbumId: {_eq: 2}} ]} ) { + AlbumId + Name + Album { + Title + } + } +} \ No newline at end of file diff --git a/v3/engine/tests/execute/models/select_many/where/relationships/object/simple/session_variables.json b/v3/engine/tests/execute/models/select_many/where/relationships/object/simple/session_variables.json new file mode 100644 index 00000000000..655455cc70e --- /dev/null +++ b/v3/engine/tests/execute/models/select_many/where/relationships/object/simple/session_variables.json @@ -0,0 +1,9 @@ +[ + { + "x-hasura-role": "admin" + }, + { + "x-hasura-role": "user", + "x-hasura-user-id": "2" + } +] \ No newline at end of file diff --git a/v3/engine/tests/execution.rs b/v3/engine/tests/execution.rs index 56edac234b1..923dd3d3744 100644 --- a/v3/engine/tests/execution.rs +++ b/v3/engine/tests/execution.rs @@ -102,6 +102,40 @@ fn test_model_select_many_type_permission_order_by() { common::test_execution_expectation(test_path_string, &[common_metadata_path_string]); } +// Relationships in order_by expressions +// What is being tested: +// 1. Object relationships in order_by expressions (Simple, Nested Object relationships). We also test multi column boolean expressions + +#[test] +fn test_model_select_many_order_by_object_relationship_simple() { + let test_path_string = "execute/models/select_many/order_by/relationships/object/simple"; + let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json"; + let boolean_exp_rel_metadata_path_string = + "execute/models/select_many/order_by/relationships/common_metadata.json"; + common::test_execution_expectation( + test_path_string, + &[ + common_metadata_path_string, + boolean_exp_rel_metadata_path_string, + ], + ); +} + +#[test] +fn test_model_select_many_order_by_object_relationship_nested() { + let test_path_string = "execute/models/select_many/order_by/relationships/object/nested"; + let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json"; + let boolean_exp_rel_metadata_path_string = + "execute/models/select_many/order_by/relationships/common_metadata.json"; + common::test_execution_expectation( + test_path_string, + &[ + common_metadata_path_string, + boolean_exp_rel_metadata_path_string, + ], + ); +} + #[test] fn test_model_select_many_type_permission_where() { let test_path_string = "execute/models/select_many/type_permission/where"; @@ -152,6 +186,71 @@ fn test_model_select_many_where_ndc_operators() { common::test_execution_expectation(test_path_string, &[common_metadata_path_string]); } +// Relationships in boolean expressions +// What is being tested: +// 1. Array relationships in boolean expressions (Simple, Nested array relationships). We also test multi column boolean expressions + +#[test] +fn test_model_select_many_where_array_relationship_simple() { + let test_path_string = "execute/models/select_many/where/relationships/array/simple"; + let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json"; + let boolean_exp_rel_metadata_path_string = + "execute/models/select_many/where/relationships/common_metadata.json"; + common::test_execution_expectation( + test_path_string, + &[ + common_metadata_path_string, + boolean_exp_rel_metadata_path_string, + ], + ); +} + +#[test] +fn test_model_select_many_where_array_relationship_nested() { + let test_path_string = "execute/models/select_many/where/relationships/array/nested"; + let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json"; + let boolean_exp_rel_metadata_path_string = + "execute/models/select_many/where/relationships/common_metadata.json"; + common::test_execution_expectation( + test_path_string, + &[ + common_metadata_path_string, + boolean_exp_rel_metadata_path_string, + ], + ); +} + +// Object relationships in boolean expressions (Simple, Nested object relationships). We also test multi column boolean expressions +#[test] +fn test_model_select_many_where_object_relationship_simple() { + let test_path_string = "execute/models/select_many/where/relationships/object/simple"; + let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json"; + let boolean_exp_rel_metadata_path_string = + "execute/models/select_many/where/relationships/common_metadata.json"; + common::test_execution_expectation( + test_path_string, + &[ + common_metadata_path_string, + boolean_exp_rel_metadata_path_string, + ], + ); +} + +#[test] +fn test_model_select_many_where_object_relationship_nested() { + let test_path_string = "execute/models/select_many/where/relationships/object/nested"; + let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json"; + let boolean_exp_rel_metadata_path_string = + "execute/models/select_many/where/relationships/common_metadata.json"; + common::test_execution_expectation( + test_path_string, + &[ + common_metadata_path_string, + boolean_exp_rel_metadata_path_string, + ], + ); +} + #[test] fn test_model_select_many_object_type_input_arguments() { let test_path_string = "execute/models/select_many/object_type_input_arguments"; @@ -556,3 +655,139 @@ fn test_command_procedures_multiple_arguments() { ], ); } + +// Tests using relationships in predicates +// Array relationship +#[test] +fn test_model_select_many_relationship_predicate_array_simple() { + let test_path_string = "execute/models/select_many/relationship_predicates/array/simple"; + let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json"; + let boolean_exp_rel_metadata_path_string = + "execute/models/select_many/relationship_predicates/common_metadata.json"; + common::test_execution_expectation( + test_path_string, + &[ + common_metadata_path_string, + boolean_exp_rel_metadata_path_string, + ], + ); +} + +// Tests using relationships in predicates + +// Nested Array relationship +#[test] +fn test_model_select_many_relationship_predicate_array_nested() { + let test_path_string = "execute/models/select_many/relationship_predicates/array/nested"; + let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json"; + let boolean_exp_rel_metadata_path_string = + "execute/models/select_many/relationship_predicates/common_metadata.json"; + common::test_execution_expectation( + test_path_string, + &[ + common_metadata_path_string, + boolean_exp_rel_metadata_path_string, + ], + ); +} + +// Tests using relationships in predicates +// Object relationship +#[test] +fn test_model_select_many_relationship_predicate_object_simple() { + let test_path_string = "execute/models/select_many/relationship_predicates/object/simple"; + let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json"; + let boolean_exp_rel_metadata_path_string = + "execute/models/select_many/relationship_predicates/common_metadata.json"; + common::test_execution_expectation( + test_path_string, + &[ + common_metadata_path_string, + boolean_exp_rel_metadata_path_string, + ], + ); +} + +// Tests using relationships in predicates +// Nested bject relationship +#[test] +fn test_model_select_many_relationship_predicate_object_nested() { + let test_path_string = "execute/models/select_many/relationship_predicates/object/nested"; + let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json"; + let boolean_exp_rel_metadata_path_string = + "execute/models/select_many/relationship_predicates/common_metadata.json"; + common::test_execution_expectation( + test_path_string, + &[ + common_metadata_path_string, + boolean_exp_rel_metadata_path_string, + ], + ); +} + +// Tests using relationships in predicates +// We have the following relationships: +// 1. 'Tracks' array relationship to 'Album' model +// 2. 'Album' object relationship to 'Track' model +// +// Predicates using the relationship are defined on both the models as follows: +// 1. The select permission for 'user' role on 'Album' model is defined as: +// Select only those Album whose `TrackId` from the relationship `Track` is equal to "x-hasura-user-id" +// 2. The select permission for 'user' role on 'Track' model is defined as: +// Select only those Track whose `Title` from the relationship `Album` is equal to "x-hasura-album-title" +// +// In this test, we test what happens when we query both the `Tracks` and `Album` relationship in the same query. +// The query we make is: +// query MyQuery { +// Album(limit: 1) { +// Tracks { +// TrackId +// Name +// Album { +// Title +// } +// } +// } +// } +// We expect the following results: +// Fetch all the tracks of the Albums whose `TrackId` is equal to "x-hasura-user-id" and then +// filter those tracks based on the "x-hasura-album-title" value. +#[test] +fn test_model_select_many_relationship_predicate_on_two_fields() { + let test_path_string = "execute/models/select_many/relationship_predicates/on_two_fields"; + let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json"; + let boolean_exp_rel_metadata_path_string = + "execute/models/select_many/relationship_predicates/common_metadata.json"; + common::test_execution_expectation( + test_path_string, + &[ + common_metadata_path_string, + boolean_exp_rel_metadata_path_string, + ], + ); +} + +// Tests using relationships in predicates +// We have the following relationships: +// 1. 'Tracks' object relationship to 'Album' model +// 2. 'Album' object relationship to 'Track' model +// 3. 'Genre' object relationship to 'Track' model +// +// We have the following select permission defined for "user" role +// It filters only those Albums whose Tracks's Album's AlbumnId is equal to "x-hasura-user-id" and +// whose Tracks's Genre's GenreId is equal to "x-hasura-genre-name" +#[test] +fn test_model_select_many_relationship_predicate_object_two_relationship_fields() { + let test_path_string = + "execute/models/select_many/relationship_predicates/object/two_relationship_fields"; + let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json"; + let boolean_exp_rel_metadata_path_string = + "execute/models/select_many/relationship_predicates/common_metadata.json"; + common::test_execution_expectation( + test_path_string, + &[ + common_metadata_path_string, + boolean_exp_rel_metadata_path_string, + ], + ); +} diff --git a/v3/engine/tests/generate_ir/get_by_id/expected.json b/v3/engine/tests/generate_ir/get_by_id/expected.json index 1c607b95d11..90a2ce5fddf 100644 --- a/v3/engine/tests/generate_ir/get_by_id/expected.json +++ b/v3/engine/tests/generate_ir/get_by_id/expected.json @@ -90,23 +90,26 @@ }, "collection": "articles", "arguments": {}, - "filter_clause": [ - { - "type": "binary_comparison_operator", - "column": { - "type": "column", - "name": "id", - "path": [] - }, - "operator": { - "type": "equal" - }, - "value": { - "type": "scalar", - "value": 1 + "filter_clause": { + "expressions": [ + { + "type": "binary_comparison_operator", + "column": { + "type": "column", + "name": "id", + "path": [] + }, + "operator": { + "type": "equal" + }, + "value": { + "type": "scalar", + "value": 1 + } } - } - ], + ], + "relationships": {} + }, "limit": null, "offset": null, "order_by": null, diff --git a/v3/engine/tests/generate_ir/get_many/expected.json b/v3/engine/tests/generate_ir/get_many/expected.json index 699c77665a2..c75b2a497d5 100644 --- a/v3/engine/tests/generate_ir/get_many/expected.json +++ b/v3/engine/tests/generate_ir/get_many/expected.json @@ -92,7 +92,10 @@ }, "collection": "articles", "arguments": {}, - "filter_clause": [], + "filter_clause": { + "expressions": [], + "relationships": {} + }, "limit": 1, "offset": 1, "order_by": null, diff --git a/v3/engine/tests/generate_ir/get_many_model_count/expected.json b/v3/engine/tests/generate_ir/get_many_model_count/expected.json index ddafde3413a..6034f1e5583 100644 --- a/v3/engine/tests/generate_ir/get_many_model_count/expected.json +++ b/v3/engine/tests/generate_ir/get_many_model_count/expected.json @@ -449,7 +449,10 @@ }, "collection": "articles", "arguments": {}, - "filter_clause": [], + "filter_clause": { + "expressions": [], + "relationships": {} + }, "limit": null, "offset": null, "order_by": null, @@ -472,7 +475,10 @@ }, "collection": "authors", "arguments": {}, - "filter_clause": [], + "filter_clause": { + "expressions": [], + "relationships": {} + }, "limit": null, "offset": null, "order_by": null, @@ -495,7 +501,10 @@ }, "collection": "articles", "arguments": {}, - "filter_clause": [], + "filter_clause": { + "expressions": [], + "relationships": {} + }, "limit": null, "offset": null, "order_by": null, @@ -516,82 +525,11 @@ }, "name": "[{\"subgraph\":\"default\",\"name\":\"author\"},\"Articles\"]", "relationship_info": { - "annotation": { - "source_type": { - "subgraph": "default", - "name": "author" - }, - "relationship_name": "Articles", - "model_name": { - "subgraph": "default", - "name": "Articles" - }, - "target_source": { - "model": { - "data_connector": { - "name": { - "subgraph": "default", - "name": "db" - }, - "url": { - "singleUrl": "http://postgres_connector:8100/" - }, - "headers": { - "hasura-m-auth-token": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!#$&'()*+,/:;=?@[]\"" - } - }, - "collection": "articles", - "type_mappings": { - "{\"subgraph\":\"default\",\"name\":\"article\"}": { - "Object": { - "field_mappings": { - "article_id": { - "column": "id", - "column_type": { - "type": "named", - "name": "int4" - } - }, - "author_id": { - "column": "author_id", - "column_type": { - "type": "named", - "name": "int4" - } - }, - "title": { - "column": "title", - "column_type": { - "type": "named", - "name": "varchar" - } - } - } - } - } - }, - "argument_mappings": {} - }, - "capabilities": { - "foreach": null, - "relationships": true - } - }, - "target_type": { - "subgraph": "default", - "name": "article" - }, - "relationship_type": "Array", - "mappings": [ - { - "source_field": { - "fieldName": "author_id" - }, - "target_field": { - "fieldName": "author_id" - } - } - ] + "relationship_name": "Articles", + "relationship_type": "Array", + "source_type": { + "subgraph": "default", + "name": "author" }, "source_data_connector": { "name": { @@ -684,7 +622,21 @@ "foreach": null, "relationships": true } - } + }, + "target_type": { + "subgraph": "default", + "name": "article" + }, + "mappings": [ + { + "source_field": { + "fieldName": "author_id" + }, + "target_field": { + "fieldName": "author_id" + } + } + ] } } } @@ -693,82 +645,11 @@ }, "name": "[{\"subgraph\":\"default\",\"name\":\"article\"},\"Author\"]", "relationship_info": { - "annotation": { - "source_type": { - "subgraph": "default", - "name": "article" - }, - "relationship_name": "Author", - "model_name": { - "subgraph": "default", - "name": "Authors" - }, - "target_source": { - "model": { - "data_connector": { - "name": { - "subgraph": "default", - "name": "db" - }, - "url": { - "singleUrl": "http://postgres_connector:8100/" - }, - "headers": { - "hasura-m-auth-token": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!#$&'()*+,/:;=?@[]\"" - } - }, - "collection": "authors", - "type_mappings": { - "{\"subgraph\":\"default\",\"name\":\"author\"}": { - "Object": { - "field_mappings": { - "author_id": { - "column": "id", - "column_type": { - "type": "named", - "name": "int8" - } - }, - "first_name": { - "column": "first_name", - "column_type": { - "type": "named", - "name": "varchar" - } - }, - "last_name": { - "column": "last_name", - "column_type": { - "type": "named", - "name": "varchar" - } - } - } - } - } - }, - "argument_mappings": {} - }, - "capabilities": { - "foreach": null, - "relationships": true - } - }, - "target_type": { - "subgraph": "default", - "name": "author" - }, - "relationship_type": "Object", - "mappings": [ - { - "source_field": { - "fieldName": "author_id" - }, - "target_field": { - "fieldName": "author_id" - } - } - ] + "relationship_name": "Author", + "relationship_type": "Object", + "source_type": { + "subgraph": "default", + "name": "article" }, "source_data_connector": { "name": { @@ -861,7 +742,21 @@ "foreach": null, "relationships": true } - } + }, + "target_type": { + "subgraph": "default", + "name": "author" + }, + "mappings": [ + { + "source_field": { + "fieldName": "author_id" + }, + "target_field": { + "fieldName": "author_id" + } + } + ] } } } @@ -870,82 +765,11 @@ }, "name": "[{\"subgraph\":\"default\",\"name\":\"commandArticle\"},\"article\"]", "relationship_info": { - "annotation": { - "source_type": { - "subgraph": "default", - "name": "commandArticle" - }, - "relationship_name": "article", - "model_name": { - "subgraph": "default", - "name": "Articles" - }, - "target_source": { - "model": { - "data_connector": { - "name": { - "subgraph": "default", - "name": "db" - }, - "url": { - "singleUrl": "http://postgres_connector:8100/" - }, - "headers": { - "hasura-m-auth-token": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!#$&'()*+,/:;=?@[]\"" - } - }, - "collection": "articles", - "type_mappings": { - "{\"subgraph\":\"default\",\"name\":\"article\"}": { - "Object": { - "field_mappings": { - "article_id": { - "column": "id", - "column_type": { - "type": "named", - "name": "int4" - } - }, - "author_id": { - "column": "author_id", - "column_type": { - "type": "named", - "name": "int4" - } - }, - "title": { - "column": "title", - "column_type": { - "type": "named", - "name": "varchar" - } - } - } - } - } - }, - "argument_mappings": {} - }, - "capabilities": { - "foreach": null, - "relationships": true - } - }, - "target_type": { - "subgraph": "default", - "name": "article" - }, - "relationship_type": "Object", - "mappings": [ - { - "source_field": { - "fieldName": "article_id" - }, - "target_field": { - "fieldName": "article_id" - } - } - ] + "relationship_name": "article", + "relationship_type": "Object", + "source_type": { + "subgraph": "default", + "name": "commandArticle" }, "source_data_connector": { "name": { @@ -1038,7 +862,21 @@ "foreach": null, "relationships": true } - } + }, + "target_type": { + "subgraph": "default", + "name": "article" + }, + "mappings": [ + { + "source_field": { + "fieldName": "article_id" + }, + "target_field": { + "fieldName": "article_id" + } + } + ] } } } diff --git a/v3/engine/tests/generate_ir/get_many_user_2/expected.json b/v3/engine/tests/generate_ir/get_many_user_2/expected.json index 7363fd96999..204d1b01e08 100644 --- a/v3/engine/tests/generate_ir/get_many_user_2/expected.json +++ b/v3/engine/tests/generate_ir/get_many_user_2/expected.json @@ -90,41 +90,44 @@ }, "collection": "articles", "arguments": {}, - "filter_clause": [ - { - "type": "and", - "expressions": [ - { - "type": "binary_comparison_operator", - "column": { - "type": "column", - "name": "title", - "path": [] - }, - "operator": { - "type": "other", - "name": "_like" - }, - "value": { - "type": "scalar", - "value": "%Functional%" - } - }, - { - "type": "not", - "expression": { - "type": "unary_comparison_operator", + "filter_clause": { + "expressions": [ + { + "type": "and", + "expressions": [ + { + "type": "binary_comparison_operator", "column": { "type": "column", - "name": "author_id", + "name": "title", "path": [] }, - "operator": "is_null" + "operator": { + "type": "other", + "name": "_like" + }, + "value": { + "type": "scalar", + "value": "%Functional%" + } + }, + { + "type": "not", + "expression": { + "type": "unary_comparison_operator", + "column": { + "type": "column", + "name": "author_id", + "path": [] + }, + "operator": "is_null" + } } - } - ] - } - ], + ] + } + ], + "relationships": {} + }, "limit": null, "offset": null, "order_by": null, diff --git a/v3/engine/tests/generate_ir/get_many_where/expected.json b/v3/engine/tests/generate_ir/get_many_where/expected.json index 574ddc56595..612ed2cfd52 100644 --- a/v3/engine/tests/generate_ir/get_many_where/expected.json +++ b/v3/engine/tests/generate_ir/get_many_where/expected.json @@ -92,24 +92,27 @@ }, "collection": "articles", "arguments": {}, - "filter_clause": [ - { - "type": "binary_comparison_operator", - "column": { - "type": "column", - "name": "title", - "path": [] - }, - "operator": { - "type": "other", - "name": "_like" - }, - "value": { - "type": "scalar", - "value": "random" + "filter_clause": { + "expressions": [ + { + "type": "binary_comparison_operator", + "column": { + "type": "column", + "name": "title", + "path": [] + }, + "operator": { + "type": "other", + "name": "_like" + }, + "value": { + "type": "scalar", + "value": "random" + } } - } - ], + ], + "relationships": {} + }, "limit": 1, "offset": null, "order_by": null, @@ -246,20 +249,23 @@ }, "collection": "authors", "arguments": {}, - "filter_clause": [ - { - "type": "not", - "expression": { - "type": "unary_comparison_operator", - "column": { - "type": "column", - "name": "first_name", - "path": [] - }, - "operator": "is_null" + "filter_clause": { + "expressions": [ + { + "type": "not", + "expression": { + "type": "unary_comparison_operator", + "column": { + "type": "column", + "name": "first_name", + "path": [] + }, + "operator": "is_null" + } } - } - ], + ], + "relationships": {} + }, "limit": null, "offset": null, "order_by": null, diff --git a/v3/engine/tests/schema.json b/v3/engine/tests/schema.json index 3ea9347c56d..e67f82936cc 100644 --- a/v3/engine/tests/schema.json +++ b/v3/engine/tests/schema.json @@ -13,9 +13,9 @@ } }, "headers": { - "hasura-m-auth-token": { - "value": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!#$&'()*+,/:;=?@[]\"" - } + "hasura-m-auth-token": { + "value": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!#$&'()*+,/:;=?@[]\"" + } }, "schema": { "scalar_types": { @@ -414,6 +414,14 @@ "type": "named", "name": "int4" } + }, + "GenreId": { + "description": "The track's genre ID", + "arguments": {}, + "type": { + "type": "named", + "name": "int4" + } } } }, @@ -445,6 +453,27 @@ } } } + }, + "Genre": { + "description": "A Genre", + "fields": { + "GenreId": { + "description": "The genre's primary key", + "arguments": {}, + "type": { + "type": "named", + "name": "int4" + } + }, + "Name": { + "description": "The genre's name", + "arguments": {}, + "type": { + "type": "named", + "name": "varchar" + } + } + } } }, "collections": [ @@ -601,6 +630,19 @@ "deletable": false, "uniqueness_constraints": {}, "foreign_keys": {} + }, + { + "name": "Genre", + "arguments": {}, + "type": "Genre", + "uniqueness_constraints": { + "PK_Genre": { + "unique_columns": [ + "GenreId" + ] + } + }, + "foreign_keys": {} } ], "functions": [ @@ -785,6 +827,10 @@ { "name": "AlbumId", "type": "Int" + }, + { + "name": "GenreId", + "type": "Int" } ], "graphql": { @@ -814,6 +860,26 @@ "version": "v1", "kind": "ObjectType" }, + { + "definition": { + "name": "Genre", + "fields": [ + { + "name": "GenreId", + "type": "Int" + }, + { + "name": "Name", + "type": "String" + } + ], + "graphql": { + "typeName": "Genre" + } + }, + "version": "v1", + "kind": "ObjectType" + }, { "kind": "ObjectType", "version": "v1", @@ -1018,7 +1084,6 @@ } } }, - { "kind": "CommandPermissions", "version": "v1", @@ -2231,6 +2296,9 @@ }, "AlbumId": { "column": "AlbumId" + }, + "GenreId": { + "column": "GenreId" } } } @@ -2269,6 +2337,12 @@ "operators": { "enableAll": true } + }, + { + "fieldName": "GenreId", + "operators": { + "enableAll": true + } } ], "orderableFields": [ @@ -2289,6 +2363,12 @@ "orderByDirections": { "enableAll": true } + }, + { + "fieldName": "GenreId", + "orderByDirections": { + "enableAll": true + } } ] }, @@ -2347,6 +2427,73 @@ "version": "v1", "kind": "Model" }, + { + "definition": { + "name": "Genres", + "objectType": "Genre", + "source": { + "dataConnectorName": "db", + "collection": "Genre", + "typeMapping": { + "Genre": { + "fieldMapping": { + "GenreId": { + "column": "GenreId" + }, + "Name": { + "column": "Name" + } + } + } + } + }, + "graphql": { + "selectUniques": [ + { + "queryRootField": "GenreByID", + "uniqueIdentifier": [ + "GenreId" + ] + } + ], + "selectMany": { + "queryRootField": "Genre" + }, + "filterExpressionType": "Genre_Where_Exp", + "orderByExpressionType": "Genre_Order_By" + }, + "filterableFields": [ + { + "fieldName": "GenreId", + "operators": { + "enableAll": true + } + }, + { + "fieldName": "Name", + "operators": { + "enableAll": true + } + } + ], + "orderableFields": [ + { + "fieldName": "GenreId", + "orderByDirections": { + "enableAll": true + } + }, + { + "fieldName": "Name", + "orderByDirections": { + "enableAll": true + } + } + ] + }, + "version": "v1", + "kind": "Model" + }, { "definition": { "typeName": "Artist", @@ -2378,6 +2525,16 @@ "ArtistId" ] } + }, + { + "role": "user_1", + "output": { + "allowedFields": [ + "AlbumId", + "Title", + "ArtistId" + ] + } } ] }, @@ -2394,7 +2551,19 @@ "allowedFields": [ "TrackId", "Name", - "AlbumId" + "AlbumId", + "GenreId" + ] + } + }, + { + "role": "user_1", + "output": { + "allowedFields": [ + "TrackId", + "Name", + "AlbumId", + "GenreId" ] } } @@ -2421,6 +2590,33 @@ "version": "v1", "kind": "TypePermissions" }, + { + "definition": { + "typeName": "Genre", + "permissions": [ + { + "role": "admin", + "output": { + "allowedFields": [ + "GenreId", + "Name" + ] + } + }, + { + "role": "user_1", + "output": { + "allowedFields": [ + "GenreId", + "Name" + ] + } + } + ] + }, + "version": "v1", + "kind": "TypePermissions" + }, { "definition": { "modelName": "Artists", @@ -2445,6 +2641,48 @@ "select": { "filter": null } + }, + { + "role": "user_1", + "select": { + "filter": { + "relationship": { + "name": "Tracks", + "predicate": { + "and": [ + { + "relationship": { + "name": "Album", + "predicate": { + "fieldComparison": { + "field": "AlbumId", + "operator": "_eq", + "value": { + "sessionVariable": "x-hasura-user-id" + } + } + } + } + }, + { + "relationship": { + "name": "Genre", + "predicate": { + "fieldComparison": { + "field": "GenreId", + "operator": "_eq", + "value": { + "sessionVariable": "x-hasura-user-id" + } + } + } + } + } + ] + } + } + } + } } ] }, @@ -2460,6 +2698,12 @@ "select": { "filter": null } + }, + { + "role": "user_1", + "select": { + "filter": null + } } ] }, @@ -2481,6 +2725,27 @@ "version": "v1", "kind": "ModelPermissions" }, + { + "definition": { + "modelName": "Genres", + "permissions": [ + { + "role": "admin", + "select": { + "filter": null + } + }, + { + "role": "user_1", + "select": { + "filter": null + } + } + ] + }, + "version": "v1", + "kind": "ModelPermissions" + }, { "definition": { "source": "Artist", @@ -2638,6 +2903,38 @@ "version": "v1", "kind": "Relationship" }, + { + "definition": { + "source": "Track", + "name": "Genre", + "target": { + "model": { + "name": "Genres", + "relationshipType": "Object" + } + }, + "mapping": [ + { + "source": { + "fieldPath": [ + { + "fieldName": "GenreId" + } + ] + }, + "target": { + "modelField": [ + { + "fieldName": "GenreId" + } + ] + } + } + ] + }, + "version": "v1", + "kind": "Relationship" + }, { "kind": "DataConnectorScalarRepresentation", "version": "v1", @@ -2662,4 +2959,4 @@ ] } ] -} +} \ No newline at end of file