From 8a9bfaaa6b4019e0928c148fc786709791aeafc5 Mon Sep 17 00:00:00 2001 From: Daniel Harvey Date: Mon, 29 Apr 2024 13:53:37 +0100 Subject: [PATCH] Separate `models` metadata resolve step (#519) ## Description As per https://github.com/hasura/v3-engine/pull/483 and all PRs proceeding it, this moves the resolving of Models into a discreet metadata stage. The `resolve::model` module was a bit of a dumping ground, so I have tried to find more sensible homes for a lot of things, but some things remain (model permissions) and will be homed when sorting later stages. Functional no-op. V3_GIT_ORIGIN_REV_ID: b41bcc9f413a867f21dd72b5d7affee8d55e02df --- .../engine/src/execute/ir/model_selection.rs | 8 +- .../engine/src/execute/ir/permissions.rs | 16 +- v3/crates/engine/src/execute/ir/query_root.rs | 2 +- .../ir/query_root/apollo_federation.rs | 4 +- .../src/execute/ir/query_root/node_field.rs | 8 +- .../src/execute/ir/query_root/select_many.rs | 2 +- .../src/execute/ir/query_root/select_one.rs | 2 +- v3/crates/engine/src/metadata/resolved.rs | 4 + .../engine/src/metadata/resolved/argument.rs | 227 +++- .../engine/src/metadata/resolved/metadata.rs | 128 +- .../engine/src/metadata/resolved/model.rs | 1163 +---------------- .../src/metadata/resolved/ndc_validation.rs | 4 +- .../src/metadata/resolved/permission.rs | 4 +- .../src/metadata/resolved/relationship.rs | 11 +- .../stages/boolean_expressions/mod.rs | 3 +- .../data_connector_type_mappings/mod.rs | 6 +- .../data_connector_type_mappings/types.rs | 2 +- .../src/metadata/resolved/stages/mod.rs | 25 +- .../metadata/resolved/stages/models/mod.rs | 832 ++++++++++++ .../metadata/resolved/stages/models/types.rs | 162 +++ .../engine/src/schema/model_arguments.rs | 4 +- v3/crates/engine/src/schema/model_order_by.rs | 2 +- v3/crates/engine/src/schema/permissions.rs | 19 +- .../schema/query_root/apollo_federation.rs | 2 +- .../src/schema/query_root/node_field.rs | 2 +- .../src/schema/query_root/select_many.rs | 6 +- .../src/schema/query_root/select_one.rs | 4 +- v3/crates/engine/src/schema/types.rs | 12 +- .../schema/types/output_type/relationship.rs | 7 +- 29 files changed, 1379 insertions(+), 1292 deletions(-) create mode 100644 v3/crates/engine/src/metadata/resolved/stages/models/mod.rs create mode 100644 v3/crates/engine/src/metadata/resolved/stages/models/types.rs diff --git a/v3/crates/engine/src/execute/ir/model_selection.rs b/v3/crates/engine/src/execute/ir/model_selection.rs index 4f1c9f92551..aed348c7f09 100644 --- a/v3/crates/engine/src/execute/ir/model_selection.rs +++ b/v3/crates/engine/src/execute/ir/model_selection.rs @@ -50,10 +50,10 @@ pub struct ModelSelection<'s> { pub(crate) fn model_selection_ir<'s>( selection_set: &normalized_ast::SelectionSet<'s, GDS>, data_type: &Qualified, - model_source: &'s resolved::model::ModelSource, + model_source: &'s resolved::ModelSource, arguments: BTreeMap, mut filter_clauses: ResolvedFilterExpression<'s>, - permissions_predicate: &'s resolved::model::FilterPermission, + permissions_predicate: &'s resolved::FilterPermission, limit: Option, offset: Option, order_by: Option>, @@ -61,8 +61,8 @@ pub(crate) fn model_selection_ir<'s>( usage_counts: &mut UsagesCounts, ) -> Result, error::Error> { match permissions_predicate { - resolved::model::FilterPermission::AllowAll => {} - resolved::model::FilterPermission::Filter(predicate) => { + resolved::FilterPermission::AllowAll => {} + resolved::FilterPermission::Filter(predicate) => { let permissions_predicate_relationship_paths = Vec::new(); let mut permissions_predicate_relationships = BTreeMap::new(); let processed_model_perdicate = permissions::process_model_predicate( diff --git a/v3/crates/engine/src/execute/ir/permissions.rs b/v3/crates/engine/src/execute/ir/permissions.rs index d7766d06050..de646ab0435 100644 --- a/v3/crates/engine/src/execute/ir/permissions.rs +++ b/v3/crates/engine/src/execute/ir/permissions.rs @@ -24,7 +24,7 @@ use super::selection_set::NDCRelationshipName; /// is not found, then an error will be thrown. pub(crate) fn get_select_filter_predicate<'s>( field_call: &normalized_ast::FieldCall<'s, GDS>, -) -> Result<&'s resolved::model::FilterPermission, Error> { +) -> Result<&'s resolved::FilterPermission, Error> { field_call .info .namespaced @@ -75,14 +75,14 @@ pub(crate) fn get_argument_presets( } pub(crate) fn process_model_predicate<'s>( - model_predicate: &'s resolved::model::ModelPredicate, + model_predicate: &'s resolved::ModelPredicate, session_variables: &SessionVariables, mut relationship_paths: Vec, relationships: &mut BTreeMap>, usage_counts: &mut UsagesCounts, ) -> Result { match model_predicate { - resolved::model::ModelPredicate::UnaryFieldComparison { + resolved::ModelPredicate::UnaryFieldComparison { field: _, ndc_column, operator, @@ -91,7 +91,7 @@ pub(crate) fn process_model_predicate<'s>( *operator, &relationship_paths, )?), - resolved::model::ModelPredicate::BinaryFieldComparison { + resolved::ModelPredicate::BinaryFieldComparison { field: _, ndc_column, argument_type, @@ -105,7 +105,7 @@ pub(crate) fn process_model_predicate<'s>( session_variables, &relationship_paths, )?), - resolved::model::ModelPredicate::Not(predicate) => { + resolved::ModelPredicate::Not(predicate) => { let expr = process_model_predicate( predicate, session_variables, @@ -117,7 +117,7 @@ pub(crate) fn process_model_predicate<'s>( expression: Box::new(expr), }) } - resolved::model::ModelPredicate::And(predicates) => { + resolved::ModelPredicate::And(predicates) => { let exprs = predicates .iter() .map(|p| { @@ -132,7 +132,7 @@ pub(crate) fn process_model_predicate<'s>( .collect::, Error>>()?; Ok(ndc_models::Expression::And { expressions: exprs }) } - resolved::model::ModelPredicate::Or(predicates) => { + resolved::ModelPredicate::Or(predicates) => { let exprs = predicates .iter() .map(|p| { @@ -147,7 +147,7 @@ pub(crate) fn process_model_predicate<'s>( .collect::, Error>>()?; Ok(ndc_models::Expression::Or { expressions: exprs }) } - resolved::model::ModelPredicate::Relationship { + resolved::ModelPredicate::Relationship { relationship_info, predicate, } => { diff --git a/v3/crates/engine/src/execute/ir/query_root.rs b/v3/crates/engine/src/execute/ir/query_root.rs index dfe9fb9c440..99feb4f8dd5 100644 --- a/v3/crates/engine/src/execute/ir/query_root.rs +++ b/v3/crates/engine/src/execute/ir/query_root.rs @@ -169,7 +169,7 @@ fn generate_type_field_ir<'n, 's>( #[allow(clippy::too_many_arguments)] fn generate_model_rootfield_ir<'n, 's>( type_name: &ast::TypeName, - source: &'s Option, + source: &'s Option, data_type: &subgraph::Qualified, kind: &RootFieldKind, field: &'n gql::normalized_ast::Field<'s, GDS>, diff --git a/v3/crates/engine/src/execute/ir/query_root/apollo_federation.rs b/v3/crates/engine/src/execute/ir/query_root/apollo_federation.rs index 01517502579..6a6880711f0 100644 --- a/v3/crates/engine/src/execute/ir/query_root/apollo_federation.rs +++ b/v3/crates/engine/src/execute/ir/query_root/apollo_federation.rs @@ -38,7 +38,7 @@ pub struct EntitySelect<'n, 's> { fn get_entity_namespace_typename_mappings<'s>( field_call: &normalized_ast::FieldCall<'s, GDS>, ) -> Result< - &'s HashMapWithJsonKey, resolved::model::FilterPermission>, + &'s HashMapWithJsonKey, resolved::FilterPermission>, error::Error, > { field_call @@ -115,7 +115,7 @@ pub(crate) fn entities_ir<'n, 's>( // Get the permissions for the typename let typename_permissions: &'s HashMap< Qualified, - resolved::model::FilterPermission, + resolved::FilterPermission, > = &get_entity_namespace_typename_mappings(field_call)?.0; let typename_mapping = typename_mappings.get(&typename).ok_or( error::InternalDeveloperError::TypenameMappingNotFound { diff --git a/v3/crates/engine/src/execute/ir/query_root/node_field.rs b/v3/crates/engine/src/execute/ir/query_root/node_field.rs index 0060cac3705..3ff5c687926 100644 --- a/v3/crates/engine/src/execute/ir/query_root/node_field.rs +++ b/v3/crates/engine/src/execute/ir/query_root/node_field.rs @@ -42,7 +42,7 @@ pub struct NodeSelect<'n, 's> { fn get_relay_node_namespace_typename_mappings<'s>( field_call: &normalized_ast::FieldCall<'s, GDS>, ) -> Result< - &'s HashMapWithJsonKey, resolved::model::FilterPermission>, + &'s HashMapWithJsonKey, resolved::FilterPermission>, error::Error, > { field_call @@ -89,10 +89,8 @@ pub(crate) fn relay_node_ir<'n, 's>( decoding_error: e.to_string(), })?; let global_id: GlobalID = serde_json::from_slice(decoded_id_value.as_slice())?; - let typename_permissions: &'s HashMap< - Qualified, - resolved::model::FilterPermission, - > = &get_relay_node_namespace_typename_mappings(field_call)?.0; + let typename_permissions: &'s HashMap, resolved::FilterPermission> = + &get_relay_node_namespace_typename_mappings(field_call)?.0; let typename_mapping = typename_mappings.get(&global_id.typename).ok_or( error::InternalDeveloperError::TypenameMappingNotFound { type_name: global_id.typename.clone(), diff --git a/v3/crates/engine/src/execute/ir/query_root/select_many.rs b/v3/crates/engine/src/execute/ir/query_root/select_many.rs index ed95e9837a1..5c0dcd23f04 100644 --- a/v3/crates/engine/src/execute/ir/query_root/select_many.rs +++ b/v3/crates/engine/src/execute/ir/query_root/select_many.rs @@ -46,7 +46,7 @@ pub(crate) fn select_many_generate_ir<'n, 's>( field: &'n normalized_ast::Field<'s, GDS>, field_call: &'n normalized_ast::FieldCall<'s, GDS>, data_type: &Qualified, - model_source: &'s resolved::model::ModelSource, + model_source: &'s resolved::ModelSource, session_variables: &SessionVariables, model_name: &'s Qualified, ) -> Result, error::Error> { diff --git a/v3/crates/engine/src/execute/ir/query_root/select_one.rs b/v3/crates/engine/src/execute/ir/query_root/select_one.rs index 7a28358bf5a..99d283b429c 100644 --- a/v3/crates/engine/src/execute/ir/query_root/select_one.rs +++ b/v3/crates/engine/src/execute/ir/query_root/select_one.rs @@ -45,7 +45,7 @@ pub(crate) fn select_one_generate_ir<'n, 's>( field: &'n normalized_ast::Field<'s, GDS>, field_call: &'s normalized_ast::FieldCall<'s, GDS>, data_type: &Qualified, - model_source: &'s resolved::model::ModelSource, + model_source: &'s resolved::ModelSource, session_variables: &SessionVariables, model_name: &'s Qualified, ) -> Result, error::Error> { diff --git a/v3/crates/engine/src/metadata/resolved.rs b/v3/crates/engine/src/metadata/resolved.rs index 572f6eda659..13258b581e1 100644 --- a/v3/crates/engine/src/metadata/resolved.rs +++ b/v3/crates/engine/src/metadata/resolved.rs @@ -18,4 +18,8 @@ pub use stages::boolean_expressions::{ BooleanExpressionInfo, ComparisonExpressionInfo, ObjectBooleanExpressionType, }; pub use stages::data_connector_type_mappings::{FieldMapping, TypeMapping}; +pub use stages::models::{ + FilterPermission, Model, ModelOrderByExpression, ModelPredicate, ModelSource, + SelectManyGraphQlDefinition, SelectUniqueGraphQlDefinition, +}; pub use stages::resolve; diff --git a/v3/crates/engine/src/metadata/resolved/argument.rs b/v3/crates/engine/src/metadata/resolved/argument.rs index 00cb8ae67ed..92f7dda0a03 100644 --- a/v3/crates/engine/src/metadata/resolved/argument.rs +++ b/v3/crates/engine/src/metadata/resolved/argument.rs @@ -1,13 +1,15 @@ -use crate::metadata::resolved::error::{Error, TypeError, TypeMappingValidationError}; +use crate::metadata::resolved::error::{ + Error, TypeError, TypeMappingValidationError, TypePredicateError, +}; +use crate::metadata::resolved::model::resolve_ndc_type; use crate::metadata::resolved::ndc_validation; +use crate::metadata::resolved::permission::ValueExpression; use crate::metadata::resolved::stages::{ - boolean_expressions, data_connector_scalar_types, data_connector_type_mappings, scalar_types, - type_permissions, + boolean_expressions, data_connector_scalar_types, data_connector_type_mappings, models, + scalar_types, type_permissions, }; use crate::metadata::resolved::subgraph::{ArgumentInfo, Qualified}; - -use crate::metadata::resolved::permission::ValueExpression; -use crate::metadata::resolved::subgraph::QualifiedTypeReference; +use crate::metadata::resolved::subgraph::{QualifiedBaseType, QualifiedTypeReference}; use crate::metadata::resolved::types::{ get_object_type_for_boolean_expression, get_type_representation, unwrap_custom_type_name, TypeMappingToCollect, TypeRepresentation, @@ -15,7 +17,9 @@ use crate::metadata::resolved::types::{ use indexmap::IndexMap; use ndc_models; use open_dds::arguments::ArgumentName; -use open_dds::types::CustomTypeName; +use open_dds::data_connector::DataConnectorName; +use open_dds::permissions; +use open_dds::types::{CustomTypeName, FieldName, OperatorName}; use std::collections::{BTreeMap, HashMap}; use thiserror::Error; @@ -223,7 +227,7 @@ pub(crate) fn resolve_value_expression_for_argument( }, })?; - let resolved_model_predicate = super::model::resolve_model_predicate_with_type( + let resolved_model_predicate = resolve_model_predicate_with_type( bool_exp, base_type, data_connector_field_mappings, @@ -239,3 +243,210 @@ pub(crate) fn resolve_value_expression_for_argument( } } } + +/// a simplified version of resolve_model_predicate that only requires a type rather than an entire +/// Model for context. It skips relationships for simplicity, this should be simple enough to +/// re-add in future. Because this function takes the `data_connector_field_mappings` as an input, +/// many of the errors thrown in `resolve_model_predicate` are pushed out. +pub(crate) fn resolve_model_predicate_with_type( + model_predicate: &permissions::ModelPredicate, + type_name: &Qualified, + data_connector_field_mappings: &BTreeMap, + data_connector_name: &Qualified, + subgraph: &str, + data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, + fields: &IndexMap, +) -> Result { + match model_predicate { + permissions::ModelPredicate::FieldComparison(permissions::FieldComparisonPredicate { + field, + operator, + value, + }) => { + // TODO: (anon) typecheck the value expression with the field + // TODO: resolve the "in" operator too (ndc_models::BinaryArrayComparisonOperator) + + // Determine field_mapping for the predicate field + let field_mapping = data_connector_field_mappings.get(field).ok_or_else(|| { + Error::TypePredicateError { + type_predicate_error: TypePredicateError::UnknownFieldInTypePredicate { + field_name: field.clone(), + type_name: type_name.clone(), + }, + } + })?; + // Determine ndc type of the field + let field_ndc_type = &field_mapping.column_type; + + // Determine whether the ndc type is a simple scalar + // Get available scalars defined in the data connector + let scalars = &data_connectors + .data_connectors_with_scalars + .get(data_connector_name) + .ok_or(Error::TypePredicateError { + type_predicate_error: TypePredicateError::UnknownTypeDataConnector { + type_name: type_name.clone(), + data_connector: data_connector_name.clone(), + }, + })? + .scalars; + + // Get scalar type info from the data connector + let (_, scalar_type_info) = + data_connector_scalar_types::get_simple_scalar(field_ndc_type.clone(), scalars) + .ok_or_else(|| Error::TypePredicateError { + type_predicate_error: TypePredicateError::UnsupportedFieldInTypePredicate { + field_name: field.clone(), + type_name: type_name.clone(), + }, + })?; + + let (resolved_operator, argument_type) = resolve_binary_operator_for_type( + operator, + type_name, + data_connector_name, + field, + fields, + scalars, + scalar_type_info.scalar_type, + subgraph, + )?; + + let value_expression = match value { + open_dds::permissions::ValueExpression::Literal(json_value) => { + Ok(ValueExpression::Literal(json_value.clone())) + } + open_dds::permissions::ValueExpression::SessionVariable(session_variable) => { + Ok(ValueExpression::SessionVariable(session_variable.clone())) + } + open_dds::permissions::ValueExpression::BooleanExpression( + _inner_model_predicate, + ) => Err(Error::TypePredicateError { + type_predicate_error: TypePredicateError::NestedPredicateInTypePredicate { + type_name: type_name.clone(), + }, + }), + }?; + + Ok(models::ModelPredicate::BinaryFieldComparison { + field: field.clone(), + ndc_column: field_mapping.column.clone(), + operator: resolved_operator, + argument_type, + value: value_expression, + }) + } + permissions::ModelPredicate::FieldIsNull(permissions::FieldIsNullPredicate { field }) => { + // Determine field_mapping for the predicate field + let field_mapping = data_connector_field_mappings.get(field).ok_or_else(|| { + Error::TypePredicateError { + type_predicate_error: TypePredicateError::UnknownFieldInTypePredicate { + field_name: field.clone(), + type_name: type_name.clone(), + }, + } + })?; + + Ok(models::ModelPredicate::UnaryFieldComparison { + field: field.clone(), + ndc_column: field_mapping.column.clone(), + operator: ndc_models::UnaryComparisonOperator::IsNull, + }) + } + permissions::ModelPredicate::Relationship(permissions::RelationshipPredicate { + .. + }) => Err(Error::UnsupportedFeature { + message: "relationships not supported in type predicates".to_string(), + }), + permissions::ModelPredicate::Not(predicate) => { + let resolved_predicate = resolve_model_predicate_with_type( + predicate, + type_name, + data_connector_field_mappings, + data_connector_name, + subgraph, + data_connectors, + fields, + )?; + Ok(models::ModelPredicate::Not(Box::new(resolved_predicate))) + } + permissions::ModelPredicate::And(predicates) => { + let mut resolved_predicates = Vec::new(); + for predicate in predicates { + resolved_predicates.push(resolve_model_predicate_with_type( + predicate, + type_name, + data_connector_field_mappings, + data_connector_name, + subgraph, + data_connectors, + fields, + )?); + } + Ok(models::ModelPredicate::And(resolved_predicates)) + } + permissions::ModelPredicate::Or(predicates) => { + let mut resolved_predicates = Vec::new(); + for predicate in predicates { + resolved_predicates.push(resolve_model_predicate_with_type( + predicate, + type_name, + data_connector_field_mappings, + data_connector_name, + subgraph, + data_connectors, + fields, + )?); + } + Ok(models::ModelPredicate::Or(resolved_predicates)) + } + } +} + +#[allow(clippy::too_many_arguments)] +fn resolve_binary_operator_for_type( + operator: &OperatorName, + type_name: &Qualified, + data_connector: &Qualified, + field_name: &FieldName, + fields: &IndexMap, + scalars: &HashMap<&str, data_connector_scalar_types::ScalarTypeWithRepresentationInfo>, + ndc_scalar_type: &ndc_models::ScalarType, + subgraph: &str, +) -> Result<(String, QualifiedTypeReference), Error> { + let field_definition = fields + .get(field_name) + .ok_or_else(|| Error::TypePredicateError { + type_predicate_error: TypePredicateError::UnknownFieldInTypePredicate { + field_name: field_name.clone(), + type_name: type_name.clone(), + }, + })?; + let comparison_operator_definition = &ndc_scalar_type + .comparison_operators + .get(&operator.0) + .ok_or_else(|| Error::TypePredicateError { + type_predicate_error: TypePredicateError::InvalidOperatorInTypePredicate { + type_name: type_name.clone(), + operator_name: operator.clone(), + }, + })?; + match comparison_operator_definition { + ndc_models::ComparisonOperatorDefinition::Equal => { + Ok((operator.0.clone(), field_definition.field_type.clone())) + } + ndc_models::ComparisonOperatorDefinition::In => Ok(( + operator.0.clone(), + QualifiedTypeReference { + underlying_type: QualifiedBaseType::List(Box::new( + field_definition.field_type.clone(), + )), + nullable: true, + }, + )), + ndc_models::ComparisonOperatorDefinition::Custom { argument_type } => Ok(( + operator.0.clone(), + resolve_ndc_type(data_connector, argument_type, scalars, subgraph)?, + )), + } +} diff --git a/v3/crates/engine/src/metadata/resolved/metadata.rs b/v3/crates/engine/src/metadata/resolved/metadata.rs index 794c84ee3c8..744762c62ad 100644 --- a/v3/crates/engine/src/metadata/resolved/metadata.rs +++ b/v3/crates/engine/src/metadata/resolved/metadata.rs @@ -1,25 +1,20 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use hasura_authn_core::Role; use indexmap::IndexMap; -use lang_graphql::ast::common as ast; use serde::{Deserialize, Serialize}; use open_dds::{commands::CommandName, models::ModelName, types::CustomTypeName}; use crate::metadata::resolved::command; - use crate::metadata::resolved::error::Error; -use crate::metadata::resolved::model::{ - resolve_model, resolve_model_graphql_api, resolve_model_select_permissions, - resolve_model_source, Model, -}; +use crate::metadata::resolved::model::resolve_model_select_permissions; use crate::metadata::resolved::relationship::resolve_relationship; use crate::metadata::resolved::subgraph::Qualified; use crate::metadata::resolved::stages::{ boolean_expressions, data_connector_scalar_types, data_connector_type_mappings, graphql_config, - scalar_types, type_permissions, + models, scalar_types, type_permissions, }; /// Resolved and validated metadata for a project. Used internally in the v3 server. @@ -28,7 +23,7 @@ pub struct Metadata { pub object_types: HashMap, type_permissions::ObjectTypeWithPermissions>, pub scalar_types: HashMap, scalar_types::ScalarTypeRepresentation>, - pub models: IndexMap, Model>, + pub models: IndexMap, models::Model>, pub commands: IndexMap, command::Command>, pub boolean_expression_types: HashMap, boolean_expressions::ObjectBooleanExpressionType>, @@ -42,9 +37,8 @@ pub struct Metadata { pub fn resolve_metadata( metadata_accessor: &open_dds::accessor::MetadataAccessor, graphql_config: &graphql_config::GraphqlConfig, - mut existing_graphql_types: HashSet, - mut global_id_enabled_types: HashMap, Vec>>, - mut apollo_federation_entity_enabled_types: HashMap< + global_id_enabled_types: HashMap, Vec>>, + apollo_federation_entity_enabled_types: HashMap< Qualified, Option>, >, @@ -56,22 +50,8 @@ pub fn resolve_metadata( boolean_expressions::ObjectBooleanExpressionType, >, data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, + mut models: IndexMap, models::Model>, ) -> Result { - // resolve models - // TODO: validate types - let mut models = resolve_models( - metadata_accessor, - data_connectors, - data_connector_type_mappings, - &object_types, - scalar_types, - &mut existing_graphql_types, - &mut global_id_enabled_types, - &mut apollo_federation_entity_enabled_types, - boolean_expression_types, - graphql_config, - )?; - // To check if global_id_fields are defined in object type but no model has global_id_source set to true: // - Throw an error if no model with globalIdSource:true is found for the object type. for (object_type, model_name_list) in global_id_enabled_types { @@ -149,7 +129,7 @@ pub fn resolve_metadata( /// Gather all roles from various permission objects. fn collect_all_roles( object_types: &HashMap, type_permissions::ObjectTypeWithPermissions>, - models: &IndexMap, Model>, + models: &IndexMap, models::Model>, commands: &IndexMap, command::Command>, ) -> Vec { let mut roles = Vec::new(); @@ -228,94 +208,6 @@ fn resolve_commands( Ok(commands) } -/// resolve models -fn resolve_models( - metadata_accessor: &open_dds::accessor::MetadataAccessor, - data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, - data_connector_type_mappings: &data_connector_type_mappings::DataConnectorTypeMappings, - object_types: &HashMap, type_permissions::ObjectTypeWithPermissions>, - scalar_types: &HashMap, scalar_types::ScalarTypeRepresentation>, - existing_graphql_types: &mut HashSet, - global_id_enabled_types: &mut HashMap, Vec>>, - apollo_federation_entity_enabled_types: &mut HashMap< - Qualified, - Option>, - >, - boolean_expression_types: &HashMap< - Qualified, - boolean_expressions::ObjectBooleanExpressionType, - >, - graphql_config: &graphql_config::GraphqlConfig, -) -> Result, Model>, Error> { - // resolve models - // TODO: validate types - let mut models = IndexMap::new(); - let mut global_id_models = HashMap::new(); - - for open_dds::accessor::QualifiedObject { - subgraph, - object: model, - } in &metadata_accessor.models - { - let mut resolved_model = resolve_model( - subgraph, - model, - object_types, - global_id_enabled_types, - apollo_federation_entity_enabled_types, - boolean_expression_types, - )?; - if resolved_model.global_id_source.is_some() { - match global_id_models.insert( - resolved_model.data_type.clone(), - resolved_model.name.clone(), - ) { - None => {} - Some(duplicate_model_name) => { - return Err(Error::DuplicateModelGlobalIdSource { - model_1: resolved_model.name, - model_2: duplicate_model_name, - object_type: resolved_model.data_type, - }) - } - } - } - - if let Some(model_source) = &model.source { - resolve_model_source( - model_source, - &mut resolved_model, - subgraph, - data_connectors, - object_types, - scalar_types, - data_connector_type_mappings, - boolean_expression_types, - )?; - } - if let Some(model_graphql_definition) = &model.graphql { - resolve_model_graphql_api( - model_graphql_definition, - &mut resolved_model, - existing_graphql_types, - data_connectors, - &model.description, - graphql_config, - )?; - } - let qualified_model_name = Qualified::new(subgraph.to_string(), model.name.clone()); - if models - .insert(qualified_model_name.clone(), resolved_model) - .is_some() - { - return Err(Error::DuplicateModelDefinition { - name: qualified_model_name, - }); - } - } - Ok(models) -} - /// resolve relationships /// returns updated `types` value fn resolve_relationships( @@ -325,7 +217,7 @@ fn resolve_relationships( Qualified, type_permissions::ObjectTypeWithPermissions, >, - models: &IndexMap, Model>, + models: &IndexMap, models::Model>, commands: &IndexMap, command::Command>, ) -> Result, type_permissions::ObjectTypeWithPermissions>, Error> { @@ -423,7 +315,7 @@ fn resolve_model_permissions( metadata_accessor: &open_dds::accessor::MetadataAccessor, data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, object_types: &HashMap, type_permissions::ObjectTypeWithPermissions>, - models: &mut IndexMap, Model>, + models: &mut IndexMap, models::Model>, boolean_expression_types: &HashMap< Qualified, boolean_expressions::ObjectBooleanExpressionType, diff --git a/v3/crates/engine/src/metadata/resolved/model.rs b/v3/crates/engine/src/metadata/resolved/model.rs index 2cbd78b970a..4d082aa4e4a 100644 --- a/v3/crates/engine/src/metadata/resolved/model.rs +++ b/v3/crates/engine/src/metadata/resolved/model.rs @@ -1,411 +1,32 @@ use super::permission::ValueExpression; use super::relationship::RelationshipTarget; use super::stages::{ - boolean_expressions, data_connector_scalar_types, data_connector_type_mappings, - data_connectors, graphql_config, scalar_types, type_permissions, + boolean_expressions, data_connector_scalar_types, data_connector_type_mappings, models, + type_permissions, }; use super::typecheck; -use super::types::{collect_type_mapping_for_source, NdcColumnForComparison, TypeMappingToCollect}; -use crate::metadata::resolved::argument::{ - get_argument_mappings, resolve_value_expression_for_argument, -}; -use crate::metadata::resolved::error::{ - BooleanExpressionError, Error, GraphqlConfigError, RelationshipError, TypePredicateError, -}; -use crate::metadata::resolved::ndc_validation; +use crate::metadata::resolved::argument::resolve_value_expression_for_argument; +use crate::metadata::resolved::error::{Error, RelationshipError}; + use crate::metadata::resolved::subgraph::{ - deserialize_qualified_btreemap, mk_qualified_type_name, mk_qualified_type_reference, - serialize_qualified_btreemap, ArgumentInfo, Qualified, QualifiedBaseType, - QualifiedTypeReference, + mk_qualified_type_name, Qualified, QualifiedBaseType, QualifiedTypeReference, }; -use crate::metadata::resolved::types::{mk_name, store_new_graphql_type}; +use crate::metadata::resolved::types::mk_name; use crate::schema::types::output_type::relationship::{ ModelTargetSource, PredicateRelationshipAnnotation, }; use indexmap::IndexMap; -use lang_graphql::ast::common::{self as ast, Name}; + use ndc_models; use open_dds::permissions::{FieldIsNullPredicate, NullableModelPredicate, RelationshipPredicate}; -use open_dds::types::Deprecated; use open_dds::{ - arguments::ArgumentName, data_connector::DataConnectorName, - models::{ - self, EnableAllOrSpecific, ModelGraphQlDefinition, ModelName, ModelV1, OrderableField, - }, + models::ModelName, permissions::{self, ModelPermissionsV1, Role}, types::{CustomTypeName, FieldName, OperatorName}, }; -use serde::{Deserialize, Serialize}; -use std::collections::{BTreeMap, HashMap, HashSet}; -use std::iter; - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct UniqueIdentifierField { - pub field_type: QualifiedTypeReference, - pub ndc_column: Option, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct SelectUniqueGraphQlDefinition { - pub query_root_field: ast::Name, - pub unique_identifier: IndexMap, - pub description: Option, - pub deprecated: Option, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct SelectManyGraphQlDefinition { - pub query_root_field: ast::Name, - pub description: Option, - pub deprecated: Option, -} - -// TODO: add support for aggregates -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct OrderByExpressionInfo { - pub ndc_column: String, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct ModelOrderByExpression { - pub data_connector_name: Qualified, - pub order_by_type_name: ast::TypeName, - pub order_by_fields: HashMap, - pub order_by_field_name: ast::Name, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct ModelGraphqlApiArgumentsConfig { - pub field_name: Name, - pub type_name: ast::TypeName, -} -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct LimitFieldGraphqlConfig { - pub field_name: Name, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct OffsetFieldGraphqlConfig { - pub field_name: Name, -} -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)] -pub struct ModelGraphQlApi { - pub arguments_input_config: Option, - pub select_uniques: Vec, - pub select_many: Option, - pub order_by_expression: Option, - pub limit_field: Option, - pub offset_field: Option, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct ModelSource { - pub data_connector: data_connectors::DataConnectorLink, - pub collection: String, - #[serde( - serialize_with = "serialize_qualified_btreemap", - deserialize_with = "deserialize_qualified_btreemap" - )] - pub type_mappings: - BTreeMap, data_connector_type_mappings::TypeMapping>, - pub argument_mappings: HashMap, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub enum FilterPermission { - AllowAll, - Filter(ModelPredicate), -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct SelectPermission { - pub filter: FilterPermission, - // pub allow_aggregations: bool, - pub argument_presets: BTreeMap, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub enum ModelPredicate { - UnaryFieldComparison { - field: FieldName, - ndc_column: String, - operator: ndc_models::UnaryComparisonOperator, - }, - BinaryFieldComparison { - field: FieldName, - ndc_column: String, - operator: String, - argument_type: QualifiedTypeReference, - value: ValueExpression, - }, - Relationship { - relationship_info: PredicateRelationshipAnnotation, - predicate: Box, - }, - And(Vec), - Or(Vec), - Not(Box), -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct Model { - pub name: Qualified, - pub data_type: Qualified, - pub type_fields: IndexMap, - pub global_id_fields: Vec, - pub arguments: IndexMap, - pub graphql_api: ModelGraphQlApi, - pub source: Option, - pub select_permissions: Option>, - pub global_id_source: Option, - pub apollo_federation_key_source: Option, - pub filter_expression_type: Option, - pub orderable_fields: Vec, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct NDCFieldSourceMapping { - pub ndc_mapping: HashMap, -} - -fn resolve_filter_expression_type( - model: &ModelV1, - model_data_type: &Qualified, - subgraph: &str, - boolean_expression_types: &HashMap< - Qualified, - boolean_expressions::ObjectBooleanExpressionType, - >, -) -> Result, Error> { - model - .filter_expression_type - .as_ref() - .map(|filter_expression_type| { - let boolean_expression_type_name = - Qualified::new(subgraph.to_string(), filter_expression_type.clone()); - let boolean_expression_type = boolean_expression_types - .get(&boolean_expression_type_name) - .ok_or_else(|| { - Error::from( - BooleanExpressionError::UnknownBooleanExpressionTypeInModel { - name: boolean_expression_type_name.clone(), - model: Qualified::new(subgraph.to_string(), model.name.clone()), - }, - ) - })?; - if boolean_expression_type.object_type != *model_data_type { - return Err(Error::from( - BooleanExpressionError::BooleanExpressionTypeForInvalidObjectTypeInModel { - name: boolean_expression_type_name.clone(), - boolean_expression_object_type: boolean_expression_type.object_type.clone(), - model: Qualified::new(subgraph.to_string(), model.name.clone()), - model_object_type: model_data_type.clone(), - }, - )); - } - // This is also checked in resolve_model_graphql_api, but we want to disallow this even - // if the model is not used in the graphql layer. - if model.source.is_none() { - return Err(Error::CannotUseFilterExpressionsWithoutSource { - model: Qualified::new(subgraph.to_string(), model.name.clone()), - }); - // TODO: Compatibility of model source and the boolean expression type is checked in - // resolve_model_source. Figure out a way to make this logic not scattered. - } - Ok(boolean_expression_type.clone()) - }) - .transpose() -} - -fn resolve_orderable_fields( - model: &ModelV1, - type_fields: &IndexMap, -) -> Result, Error> { - for field in &model.orderable_fields { - // Check for unknown orderable field - if !type_fields.contains_key(&field.field_name) { - return Err(Error::UnknownFieldInOrderableFields { - model_name: model.name.clone(), - field_name: field.field_name.clone(), - }); - } - match &field.order_by_directions { - EnableAllOrSpecific::EnableAll(true) => {} - _ => { - return Err(Error::UnsupportedFeature { - message: "Field level order by configuration is not fully supported yet. Please use \"enableAll\":true.".to_string(), - }) - } - } - } - - // Model orderable fields should have all type fields - if model.orderable_fields.len() != type_fields.len() { - return Err(Error::UnsupportedFeature { - message: "Field level order by configuration is not fully supported yet. Please add all fields in orderable_fields.".to_string(), - }); - } - Ok(model.orderable_fields.clone()) -} - -pub fn resolve_model( - subgraph: &str, - model: &ModelV1, - object_types: &HashMap, type_permissions::ObjectTypeWithPermissions>, - global_id_enabled_types: &mut HashMap, Vec>>, - apollo_federation_entity_enabled_types: &mut HashMap< - Qualified, - Option>, - >, - boolean_expression_types: &HashMap< - Qualified, - boolean_expressions::ObjectBooleanExpressionType, - >, -) -> Result { - 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 = get_model_object_type_representation( - object_types, - &qualified_object_type_name, - &qualified_model_name, - )?; - let mut global_id_source = None; - 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. - if object_type_representation - .object_type - .global_id_fields - .is_empty() - { - return Err(Error::NoGlobalFieldsPresentInGlobalIdSource { - type_name: qualified_object_type_name, - model_name: model.name.clone(), - }); - } - if !model.arguments.is_empty() { - return Err(Error::ModelWithArgumentsAsGlobalIdSource { - model_name: qualified_model_name, - }); - } - // model has `global_id_source`; insert into the hashmap of `global_id_enabled_types` - match global_id_enabled_types.get_mut(&qualified_object_type_name) { - None => { - // this shouldn't happen; but for some reason the object type - // containing globalIdFields is not inserted. Insert it now - global_id_enabled_types.insert( - qualified_object_type_name.clone(), - vec![qualified_model_name.clone()], - ); - } - Some(model_names) => { - model_names.push(qualified_model_name.clone()); - } - } - global_id_source = Some(NDCFieldSourceMapping { - ndc_mapping: HashMap::new(), - }); - }; - let mut apollo_federation_key_source = None; - if model - .graphql - .as_ref() - .and_then(|g| g.apollo_federation.as_ref().map(|a| a.entity_source)) - .unwrap_or_default() - { - // Check if there are any apollo federation keys present in the related - // object type, if the model is marked as an apollo federation entity source. - if object_type_representation - .object_type - .apollo_federation_config - .is_some() - { - if !model.arguments.is_empty() { - return Err(Error::ModelWithArgumentsAsApolloFederationEntitySource { - model_name: qualified_model_name, - }); - } - // model has `apollo_federation_entity_source`; insert into the hashmap of - // `apollo_federation_entity_enabled_types` - match apollo_federation_entity_enabled_types.get_mut(&qualified_object_type_name) { - None => { - // the model's graphql configuration has `apollo_federation.entitySource` but the object type - // of the model doesn't have any apollo federation keys - return Err(Error::NoKeysFieldsPresentInEntitySource { - type_name: qualified_object_type_name, - model_name: model.name.clone(), - }); - } - Some(type_name) => { - match type_name { - None => { - *type_name = Some(qualified_model_name.clone()); - } - // Multiple models are marked as apollo federation entity source - Some(_) => { - return Err(Error::MultipleEntitySourcesForType { - type_name: qualified_object_type_name, - }); - } - } - } - } - apollo_federation_key_source = Some(NDCFieldSourceMapping { - ndc_mapping: HashMap::new(), - }); - } - } - - let mut arguments = IndexMap::new(); - for argument in &model.arguments { - if arguments - .insert( - argument.name.clone(), - ArgumentInfo { - argument_type: mk_qualified_type_reference(&argument.argument_type, subgraph), - description: argument.description.clone(), - }, - ) - .is_some() - { - return Err(Error::DuplicateModelArgumentDefinition { - model_name: qualified_model_name, - argument_name: argument.name.clone(), - }); - } - } - - let filter_expression_type = resolve_filter_expression_type( - model, - &qualified_object_type_name, - subgraph, - boolean_expression_types, - )?; - - Ok(Model { - name: qualified_model_name, - data_type: qualified_object_type_name, - type_fields: object_type_representation.object_type.fields.clone(), - global_id_fields: object_type_representation - .object_type - .global_id_fields - .clone(), - arguments, - graphql_api: ModelGraphQlApi::default(), - source: None, - select_permissions: None, - global_id_source, - apollo_federation_key_source, - filter_expression_type, - orderable_fields: resolve_orderable_fields( - model, - &object_type_representation.object_type.fields, - )?, - }) -} +use std::collections::{BTreeMap, HashMap}; // helper function to resolve ndc types to dds type based on scalar type representations pub(crate) fn resolve_ndc_type( @@ -457,271 +78,16 @@ pub(crate) fn resolve_ndc_type( } } -#[allow(clippy::too_many_arguments)] -fn resolve_binary_operator_for_type( - operator: &OperatorName, - type_name: &Qualified, - data_connector: &Qualified, - field_name: &FieldName, - fields: &IndexMap, - scalars: &HashMap<&str, data_connector_scalar_types::ScalarTypeWithRepresentationInfo>, - ndc_scalar_type: &ndc_models::ScalarType, - subgraph: &str, -) -> Result<(String, QualifiedTypeReference), Error> { - let field_definition = fields - .get(field_name) - .ok_or_else(|| Error::TypePredicateError { - type_predicate_error: TypePredicateError::UnknownFieldInTypePredicate { - field_name: field_name.clone(), - type_name: type_name.clone(), - }, - })?; - let comparison_operator_definition = &ndc_scalar_type - .comparison_operators - .get(&operator.0) - .ok_or_else(|| Error::TypePredicateError { - type_predicate_error: TypePredicateError::InvalidOperatorInTypePredicate { - type_name: type_name.clone(), - operator_name: operator.clone(), - }, - })?; - match comparison_operator_definition { - ndc_models::ComparisonOperatorDefinition::Equal => { - Ok((operator.0.clone(), field_definition.field_type.clone())) - } - ndc_models::ComparisonOperatorDefinition::In => Ok(( - operator.0.clone(), - QualifiedTypeReference { - underlying_type: QualifiedBaseType::List(Box::new( - field_definition.field_type.clone(), - )), - nullable: true, - }, - )), - ndc_models::ComparisonOperatorDefinition::Custom { argument_type } => Ok(( - operator.0.clone(), - resolve_ndc_type(data_connector, argument_type, scalars, subgraph)?, - )), - } -} - -#[allow(clippy::too_many_arguments)] -/// this is mostly the same code as `resolve_binary_operator_for_type`, they could probably be -/// recombined if we nest our error types better, so we don't need to specify the model name this deep -/// into the code -fn resolve_binary_operator_for_model( - operator: &OperatorName, - model_name: &Qualified, - data_connector: &Qualified, - field_name: &FieldName, - fields: &IndexMap, - scalars: &HashMap<&str, data_connector_scalar_types::ScalarTypeWithRepresentationInfo>, - ndc_scalar_type: &ndc_models::ScalarType, - subgraph: &str, -) -> Result<(String, QualifiedTypeReference), Error> { - let field_definition = - fields - .get(field_name) - .ok_or_else(|| Error::UnknownFieldInSelectPermissionsDefinition { - field_name: field_name.clone(), - model_name: model_name.clone(), - })?; - let comparison_operator_definition = &ndc_scalar_type - .comparison_operators - .get(&operator.0) - .ok_or_else(|| Error::InvalidOperatorInModelSelectPermission { - model_name: model_name.clone(), - operator_name: operator.clone(), - })?; - match comparison_operator_definition { - ndc_models::ComparisonOperatorDefinition::Equal => { - Ok((operator.0.clone(), field_definition.field_type.clone())) - } - ndc_models::ComparisonOperatorDefinition::In => Ok(( - operator.0.clone(), - QualifiedTypeReference { - underlying_type: QualifiedBaseType::List(Box::new( - field_definition.field_type.clone(), - )), - nullable: true, - }, - )), - ndc_models::ComparisonOperatorDefinition::Custom { argument_type } => Ok(( - operator.0.clone(), - resolve_ndc_type(data_connector, argument_type, scalars, subgraph)?, - )), - } -} - -/// a simplified version of resolve_model_predicate that only requires a type rather than an entire -/// Model for context. It skips relationships for simplicity, this should be simple enough to -/// re-add in future. Because this function takes the `data_connector_field_mappings` as an input, -/// many of the errors thrown in `resolve_model_predicate` are pushed out. -pub(crate) fn resolve_model_predicate_with_type( - model_predicate: &permissions::ModelPredicate, - type_name: &Qualified, - data_connector_field_mappings: &BTreeMap, - data_connector_name: &Qualified, - subgraph: &str, - data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, - fields: &IndexMap, -) -> Result { - match model_predicate { - permissions::ModelPredicate::FieldComparison(permissions::FieldComparisonPredicate { - field, - operator, - value, - }) => { - // TODO: (anon) typecheck the value expression with the field - // TODO: resolve the "in" operator too (ndc_models::BinaryArrayComparisonOperator) - - // Determine field_mapping for the predicate field - let field_mapping = data_connector_field_mappings.get(field).ok_or_else(|| { - Error::TypePredicateError { - type_predicate_error: TypePredicateError::UnknownFieldInTypePredicate { - field_name: field.clone(), - type_name: type_name.clone(), - }, - } - })?; - // Determine ndc type of the field - let field_ndc_type = &field_mapping.column_type; - - // Determine whether the ndc type is a simple scalar - // Get available scalars defined in the data connector - let scalars = &data_connectors - .data_connectors_with_scalars - .get(data_connector_name) - .ok_or(Error::TypePredicateError { - type_predicate_error: TypePredicateError::UnknownTypeDataConnector { - type_name: type_name.clone(), - data_connector: data_connector_name.clone(), - }, - })? - .scalars; - - // Get scalar type info from the data connector - let (_, scalar_type_info) = - data_connector_scalar_types::get_simple_scalar(field_ndc_type.clone(), scalars) - .ok_or_else(|| Error::TypePredicateError { - type_predicate_error: TypePredicateError::UnsupportedFieldInTypePredicate { - field_name: field.clone(), - type_name: type_name.clone(), - }, - })?; - - let (resolved_operator, argument_type) = resolve_binary_operator_for_type( - operator, - type_name, - data_connector_name, - field, - fields, - scalars, - scalar_type_info.scalar_type, - subgraph, - )?; - - let value_expression = match value { - open_dds::permissions::ValueExpression::Literal(json_value) => { - Ok(ValueExpression::Literal(json_value.clone())) - } - open_dds::permissions::ValueExpression::SessionVariable(session_variable) => { - Ok(ValueExpression::SessionVariable(session_variable.clone())) - } - open_dds::permissions::ValueExpression::BooleanExpression( - _inner_model_predicate, - ) => Err(Error::TypePredicateError { - type_predicate_error: TypePredicateError::NestedPredicateInTypePredicate { - type_name: type_name.clone(), - }, - }), - }?; - - Ok(ModelPredicate::BinaryFieldComparison { - field: field.clone(), - ndc_column: field_mapping.column.clone(), - operator: resolved_operator, - argument_type, - value: value_expression, - }) - } - permissions::ModelPredicate::FieldIsNull(FieldIsNullPredicate { field }) => { - // Determine field_mapping for the predicate field - let field_mapping = data_connector_field_mappings.get(field).ok_or_else(|| { - Error::TypePredicateError { - type_predicate_error: TypePredicateError::UnknownFieldInTypePredicate { - field_name: field.clone(), - type_name: type_name.clone(), - }, - } - })?; - - Ok(ModelPredicate::UnaryFieldComparison { - field: field.clone(), - ndc_column: field_mapping.column.clone(), - operator: ndc_models::UnaryComparisonOperator::IsNull, - }) - } - permissions::ModelPredicate::Relationship(RelationshipPredicate { .. }) => { - Err(Error::UnsupportedFeature { - message: "relationships not supported in type predicates".to_string(), - }) - } - permissions::ModelPredicate::Not(predicate) => { - let resolved_predicate = resolve_model_predicate_with_type( - predicate, - type_name, - data_connector_field_mappings, - data_connector_name, - subgraph, - data_connectors, - fields, - )?; - Ok(ModelPredicate::Not(Box::new(resolved_predicate))) - } - permissions::ModelPredicate::And(predicates) => { - let mut resolved_predicates = Vec::new(); - for predicate in predicates { - resolved_predicates.push(resolve_model_predicate_with_type( - predicate, - type_name, - data_connector_field_mappings, - data_connector_name, - subgraph, - data_connectors, - fields, - )?); - } - Ok(ModelPredicate::And(resolved_predicates)) - } - permissions::ModelPredicate::Or(predicates) => { - let mut resolved_predicates = Vec::new(); - for predicate in predicates { - resolved_predicates.push(resolve_model_predicate_with_type( - predicate, - type_name, - data_connector_field_mappings, - data_connector_name, - subgraph, - data_connectors, - fields, - )?); - } - Ok(ModelPredicate::Or(resolved_predicates)) - } - } -} - fn resolve_model_predicate( model_predicate: &permissions::ModelPredicate, - model: &Model, + model: &models::Model, subgraph: &str, data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, fields: &IndexMap, object_types: &HashMap, type_permissions::ObjectTypeWithPermissions>, - models: &IndexMap, Model>, + models: &IndexMap, models::Model>, // type_representation: &TypeRepresentation, -) -> Result { +) -> Result { match model_predicate { permissions::ModelPredicate::FieldComparison(permissions::FieldComparisonPredicate { field, @@ -795,7 +161,7 @@ fn resolve_model_predicate( } }?; - Ok(ModelPredicate::BinaryFieldComparison { + Ok(models::ModelPredicate::BinaryFieldComparison { field: field.clone(), ndc_column: field_mapping.column.clone(), operator: resolved_operator, @@ -827,7 +193,7 @@ fn resolve_model_predicate( } })?; - Ok(ModelPredicate::UnaryFieldComparison { + Ok(models::ModelPredicate::UnaryFieldComparison { field: field.clone(), ndc_column: field_mapping.column.clone(), operator: ndc_models::UnaryComparisonOperator::IsNull, @@ -921,7 +287,7 @@ fn resolve_model_predicate( models, )?; - Ok(ModelPredicate::Relationship { + Ok(models::ModelPredicate::Relationship { relationship_info: annotation, predicate: Box::new(target_model_predicate), }) @@ -958,7 +324,7 @@ fn resolve_model_predicate( object_types, models, )?; - Ok(ModelPredicate::Not(Box::new(resolved_predicate))) + Ok(models::ModelPredicate::Not(Box::new(resolved_predicate))) } permissions::ModelPredicate::And(predicates) => { let mut resolved_predicates = Vec::new(); @@ -973,7 +339,7 @@ fn resolve_model_predicate( models, )?); } - Ok(ModelPredicate::And(resolved_predicates)) + Ok(models::ModelPredicate::And(resolved_predicates)) } permissions::ModelPredicate::Or(predicates) => { let mut resolved_predicates = Vec::new(); @@ -988,24 +354,24 @@ fn resolve_model_predicate( models, )?); } - Ok(ModelPredicate::Or(resolved_predicates)) + Ok(models::ModelPredicate::Or(resolved_predicates)) } } } pub fn resolve_model_select_permissions( - model: &Model, + model: &models::Model, subgraph: &str, model_permissions: &ModelPermissionsV1, data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, object_types: &HashMap, type_permissions::ObjectTypeWithPermissions>, - models: &IndexMap, Model>, + models: &IndexMap, models::Model>, boolean_expression_types: &HashMap< Qualified, boolean_expressions::ObjectBooleanExpressionType, >, data_connector_type_mappings: &data_connector_type_mappings::DataConnectorTypeMappings, -) -> Result, Error> { +) -> Result, Error> { let mut validated_permissions = HashMap::new(); for model_permission in &model_permissions.permissions { if let Some(select) = &model_permission.select { @@ -1019,8 +385,8 @@ pub fn resolve_model_select_permissions( object_types, models, ) - .map(FilterPermission::Filter)?, - NullableModelPredicate::Null(()) => FilterPermission::AllowAll, + .map(models::FilterPermission::Filter)?, + NullableModelPredicate::Null(()) => models::FilterPermission::AllowAll, }; let mut argument_presets = BTreeMap::new(); @@ -1075,7 +441,7 @@ pub fn resolve_model_select_permissions( } } - let resolved_permission = SelectPermission { + let resolved_permission = models::SelectPermission { filter: resolved_predicate.clone(), argument_presets, }; @@ -1085,453 +451,58 @@ pub fn resolve_model_select_permissions( Ok(validated_permissions) } -pub(crate) fn get_ndc_column_for_comparison String>( +#[allow(clippy::too_many_arguments)] +/// this is mostly the same code as `resolve_binary_operator_for_type`, they could probably be +/// recombined if we nest our error types better, so we don't need to specify the model name this deep +/// into the code +fn resolve_binary_operator_for_model( + operator: &OperatorName, model_name: &Qualified, - model_data_type: &Qualified, - model_source: &ModelSource, - field: &FieldName, - data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, - comparison_location: F, -) -> Result { - // Get field mappings of model data type - let data_connector_type_mappings::TypeMapping::Object { field_mappings, .. } = model_source - .type_mappings - .get(model_data_type) - .ok_or(Error::TypeMappingRequired { - model_name: model_name.clone(), - type_name: model_data_type.clone(), - data_connector: model_source.data_connector.name.clone(), - })?; - - // Determine field_mapping for the given field - let field_mapping = - field_mappings - .get(field) - .ok_or_else(|| Error::NoFieldMappingForComparedField { - comparison_location: comparison_location(), - field_name: field.clone(), - model_name: model_name.clone(), - })?; - - // Determine ndc type of the field - let field_ndc_type = &field_mapping.column_type; - - // Get available scalars defined in the data connector - let scalars = &data_connectors - .data_connectors_with_scalars - .get(&model_source.data_connector.name) - .ok_or(Error::UnknownModelDataConnector { - model_name: model_name.clone(), - data_connector: model_source.data_connector.name.clone(), - })? - .scalars; - // Determine whether the ndc type is a simple scalar and get scalar type info - let (_field_ndc_type_scalar, scalar_type_info) = - data_connector_scalar_types::get_simple_scalar(field_ndc_type.clone(), scalars) - .ok_or_else(|| Error::UncomparableNonScalarFieldType { - comparison_location: comparison_location(), - field_name: field.clone(), - model_name: model_name.clone(), - })?; - - let equal_operator = match scalar_type_info - .comparison_operators - .equal_operators - .as_slice() - { - [] => { - return Err(Error::NoEqualOperatorForComparedField { - comparison_location: comparison_location(), - field_name: field.clone(), - model_name: model_name.clone(), - }); - } - [equal_operator] => equal_operator, - _ => { - return Err(Error::MultipleEqualOperatorsForComparedField { - comparison_location: comparison_location(), - field_name: field.clone(), - model_name: model_name.clone(), - }); - } - }; - - Ok(NdcColumnForComparison { - column: field_mapping.column.clone(), - equal_operator: equal_operator.clone(), - }) -} - -pub fn resolve_model_graphql_api( - model_graphql_definition: &ModelGraphQlDefinition, - model: &mut Model, - existing_graphql_types: &mut HashSet, - data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, - model_description: &Option, - graphql_config: &graphql_config::GraphqlConfig, -) -> Result<(), Error> { - let model_name = &model.name; - for select_unique in &model_graphql_definition.select_uniques { - let mut unique_identifier_fields = IndexMap::new(); - for field_name in &select_unique.unique_identifier { - let field_type = &model - .type_fields - .get(field_name) - .ok_or_else(|| Error::UnknownFieldInUniqueIdentifier { - model_name: model_name.clone(), - field_name: field_name.clone(), - })? - .field_type; - let ndc_column = model - .source - .as_ref() - .map(|model_source| { - get_ndc_column_for_comparison( - &model.name, - &model.data_type, - model_source, - field_name, - data_connectors, - || "the unique identifier for select unique".to_string(), - ) - }) - .transpose()?; - let unique_identifier_field = UniqueIdentifierField { - field_type: field_type.clone(), - ndc_column, - }; - if unique_identifier_fields - .insert(field_name.clone(), unique_identifier_field) - .is_some() - { - return Err(Error::DuplicateFieldInUniqueIdentifier { - model_name: model_name.clone(), - field_name: field_name.clone(), - }); - } - } - let select_unique_field_name = mk_name(&select_unique.query_root_field.0)?; - let select_unique_description = if select_unique.description.is_some() { - select_unique.description.clone() - } else { - model_description.as_ref().map(|description| { - format!( - "Selects a single object from the model. Model description: {}", - description - ) - }) - }; - model - .graphql_api - .select_uniques - .push(SelectUniqueGraphQlDefinition { - query_root_field: select_unique_field_name, - unique_identifier: unique_identifier_fields, - description: select_unique_description, - deprecated: select_unique.deprecated.clone(), - }); - } - - model.graphql_api.order_by_expression = model - .source - .as_ref() - .map( - |model_source: &ModelSource| -> Result, Error> { - let order_by_expression_type_name = match &model_graphql_definition - .order_by_expression_type - { - None => Ok(None), - Some(type_name) => mk_name(type_name.0.as_str()).map(ast::TypeName).map(Some), - }?; - // TODO: (paritosh) should we check for conflicting graphql types for default order_by type name as well? - store_new_graphql_type( - existing_graphql_types, - order_by_expression_type_name.as_ref(), - )?; - order_by_expression_type_name - .map(|order_by_type_name| { - let data_connector_type_mappings::TypeMapping::Object { - field_mappings, - .. - } = model_source.type_mappings.get(&model.data_type).ok_or( - Error::TypeMappingRequired { - model_name: model_name.clone(), - type_name: model.data_type.clone(), - data_connector: model_source.data_connector.name.clone(), - }, - )?; - - let mut order_by_fields = HashMap::new(); - for (field_name, field_mapping) in field_mappings.iter() { - order_by_fields.insert( - field_name.clone(), - OrderByExpressionInfo { - ndc_column: field_mapping.column.clone(), - }, - ); - } - - match &graphql_config.query.order_by_field_name { - None => Err(Error::GraphqlConfigError { - graphql_config_error: - GraphqlConfigError::MissingOrderByInputFieldInGraphqlConfig, - }), - Some(order_by_field_name) => Ok(ModelOrderByExpression { - data_connector_name: model_source.data_connector.name.clone(), - order_by_type_name, - order_by_fields, - order_by_field_name: order_by_field_name.clone(), - }), - } - }) - .transpose() - }, - ) - .transpose()? - .flatten(); - - // record select_many root field - model.graphql_api.select_many = match &model_graphql_definition.select_many { - None => Ok(None), - Some(gql_definition) => mk_name(&gql_definition.query_root_field.0).map(|f: ast::Name| { - let select_many_description = if gql_definition.description.is_some() { - gql_definition.description.clone() - } else { - model_description.as_ref().map(|description| { - format!( - "Selects multiple objects from the model. Model description: {}", - description - ) - }) - }; - Some(SelectManyGraphQlDefinition { - query_root_field: f, - description: select_many_description, - deprecated: gql_definition.deprecated.clone(), - }) - }), - }?; - - // record limit and offset field names - model.graphql_api.limit_field = - graphql_config - .query - .limit_field_name - .as_ref() - .map(|limit_field| LimitFieldGraphqlConfig { - field_name: limit_field.clone(), - }); - - model.graphql_api.offset_field = - graphql_config - .query - .offset_field_name - .as_ref() - .map(|offset_field| OffsetFieldGraphqlConfig { - field_name: offset_field.clone(), - }); - - if model.arguments.is_empty() { - if model_graphql_definition.arguments_input_type.is_some() { - return Err(Error::UnnecessaryModelArgumentsGraphQlInputConfiguration { - model_name: model_name.clone(), - }); - } - } else { - let arguments_input_type_name = match &model_graphql_definition.arguments_input_type { - None => Ok(None), - Some(type_name) => mk_name(type_name.0.as_str()).map(ast::TypeName).map(Some), - }?; - store_new_graphql_type(existing_graphql_types, arguments_input_type_name.as_ref())?; - - if let Some(type_name) = arguments_input_type_name { - let argument_input_field_name = graphql_config - .query - .arguments_field_name - .as_ref() - .ok_or_else(|| Error::GraphqlConfigError { - graphql_config_error: - GraphqlConfigError::MissingArgumentsInputFieldInGraphqlConfig, - })?; - model.graphql_api.arguments_input_config = Some(ModelGraphqlApiArgumentsConfig { - field_name: argument_input_field_name.clone(), - type_name, - }); - } - } - - Ok(()) -} - -pub fn resolve_model_source( - model_source: &models::ModelSource, - model: &mut Model, + data_connector: &Qualified, + field_name: &FieldName, + fields: &IndexMap, + scalars: &HashMap<&str, data_connector_scalar_types::ScalarTypeWithRepresentationInfo>, + ndc_scalar_type: &ndc_models::ScalarType, subgraph: &str, - data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, - object_types: &HashMap, type_permissions::ObjectTypeWithPermissions>, - scalar_types: &HashMap, scalar_types::ScalarTypeRepresentation>, - data_connector_type_mappings: &data_connector_type_mappings::DataConnectorTypeMappings, - boolean_expression_types: &HashMap< - Qualified, - boolean_expressions::ObjectBooleanExpressionType, - >, -) -> Result<(), Error> { - if model.source.is_some() { - return Err(Error::DuplicateModelSourceDefinition { - model_name: model.name.clone(), - }); - } - let qualified_data_connector_name = Qualified::new( - subgraph.to_string(), - model_source.data_connector_name.clone(), - ); - let data_connector_context = data_connectors - .data_connectors_with_scalars - .get(&qualified_data_connector_name) - .ok_or_else(|| Error::UnknownModelDataConnector { - model_name: model.name.clone(), - data_connector: qualified_data_connector_name.clone(), +) -> Result<(String, QualifiedTypeReference), Error> { + let field_definition = + fields + .get(field_name) + .ok_or_else(|| Error::UnknownFieldInSelectPermissionsDefinition { + field_name: field_name.clone(), + model_name: model_name.clone(), + })?; + let comparison_operator_definition = &ndc_scalar_type + .comparison_operators + .get(&operator.0) + .ok_or_else(|| Error::InvalidOperatorInModelSelectPermission { + model_name: model_name.clone(), + operator_name: operator.clone(), })?; - - let source_collection = data_connector_context - .inner - .schema - .collections - .iter() - .find(|collection_info| collection_info.name == *model_source.collection) - .ok_or_else(|| Error::UnknownModelCollection { - model_name: model.name.clone(), - data_connector: qualified_data_connector_name.clone(), - collection: model_source.collection.clone(), - })?; - - // Get the mappings of arguments and any type mappings that need resolving from the arguments - let (argument_mappings, argument_type_mappings_to_collect) = get_argument_mappings( - &model.arguments, - &model_source.argument_mapping, - &source_collection.arguments, - object_types, - scalar_types, - boolean_expression_types, - ) - .map_err(|err| Error::ModelCollectionArgumentMappingError { - data_connector_name: qualified_data_connector_name.clone(), - model_name: model.name.clone(), - collection_name: model_source.collection.clone(), - error: err, - })?; - - // Collect type mappings. - let mut type_mappings = BTreeMap::new(); - let source_collection_type_mapping_to_collect = TypeMappingToCollect { - type_name: &model.data_type, - ndc_object_type_name: source_collection.collection_type.as_str(), - }; - for type_mapping_to_collect in iter::once(&source_collection_type_mapping_to_collect) - .chain(argument_type_mappings_to_collect.iter()) - { - collect_type_mapping_for_source( - type_mapping_to_collect, - data_connector_type_mappings, - &qualified_data_connector_name, - object_types, - scalar_types, - &mut type_mappings, - ) - .map_err(|error| Error::ModelTypeMappingCollectionError { - model_name: model.name.clone(), - error, - })?; - } - - if let Some(filter_expression) = &model.filter_expression_type { - if filter_expression.data_connector_name != qualified_data_connector_name { - return Err(Error::DifferentDataConnectorInFilterExpression { - model: model.name.clone(), - model_data_connector: qualified_data_connector_name.clone(), - filter_expression_type: filter_expression.name.clone(), - filter_expression_data_connector: filter_expression.data_connector_name.clone(), - }); - } - - if filter_expression.data_connector_object_type != source_collection.collection_type { - return Err(Error::DifferentDataConnectorObjectTypeInFilterExpression { - model: model.name.clone(), - model_data_connector_object_type: source_collection.collection_type.clone(), - filter_expression_type: filter_expression.name.clone(), - filter_expression_data_connector_object_type: filter_expression - .data_connector_object_type - .clone(), - }); + match comparison_operator_definition { + ndc_models::ComparisonOperatorDefinition::Equal => { + Ok((operator.0.clone(), field_definition.field_type.clone())) } + ndc_models::ComparisonOperatorDefinition::In => Ok(( + operator.0.clone(), + QualifiedTypeReference { + underlying_type: QualifiedBaseType::List(Box::new( + field_definition.field_type.clone(), + )), + nullable: true, + }, + )), + ndc_models::ComparisonOperatorDefinition::Custom { argument_type } => Ok(( + operator.0.clone(), + resolve_ndc_type(data_connector, argument_type, scalars, subgraph)?, + )), } - - let resolved_model_source = ModelSource { - data_connector: data_connectors::DataConnectorLink::new( - qualified_data_connector_name, - data_connector_context.inner.url.clone(), - data_connector_context.inner.headers, - )?, - collection: model_source.collection.clone(), - type_mappings, - argument_mappings, - }; - - let model_object_type = - get_model_object_type_representation(object_types, &model.data_type, &model.name)?; - - if let Some(global_id_source) = &mut model.global_id_source { - for global_id_field in &model_object_type.object_type.global_id_fields { - global_id_source.ndc_mapping.insert( - global_id_field.clone(), - get_ndc_column_for_comparison( - &model.name, - &model.data_type, - &resolved_model_source, - global_id_field, - data_connectors, - || format!("the global ID fields of type {}", model.data_type), - )?, - ); - } - } - - if let Some(apollo_federation_key_source) = &mut model.apollo_federation_key_source { - if let Some(apollo_federation_config) = - &model_object_type.object_type.apollo_federation_config - { - for key in &apollo_federation_config.keys { - for field in &key.fields { - apollo_federation_key_source.ndc_mapping.insert( - field.clone(), - get_ndc_column_for_comparison( - &model.name, - &model.data_type, - &resolved_model_source, - field, - data_connectors, - || { - format!( - "the apollo federation key fields of type {}", - model.data_type - ) - }, - )?, - ); - } - } - } - } - - model.source = Some(resolved_model_source); - ndc_validation::validate_ndc(&model.name, model, data_connector_context.inner.schema)?; - Ok(()) } /// Gets the `type_permissions::ObjectTypeWithPermissions` 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>( +fn get_model_object_type_representation<'s>( object_types: &'s HashMap< Qualified, type_permissions::ObjectTypeWithPermissions, diff --git a/v3/crates/engine/src/metadata/resolved/ndc_validation.rs b/v3/crates/engine/src/metadata/resolved/ndc_validation.rs index f428d4de710..7010f474828 100644 --- a/v3/crates/engine/src/metadata/resolved/ndc_validation.rs +++ b/v3/crates/engine/src/metadata/resolved/ndc_validation.rs @@ -1,3 +1,4 @@ +use crate::metadata::resolved::stages::models; use ndc_models; use open_dds::{ commands::{CommandName, DataConnectorCommand, FunctionName, ProcedureName}, @@ -9,7 +10,6 @@ use thiserror::Error; use super::{ command::Command, - model::Model, subgraph::{Qualified, QualifiedBaseType, QualifiedTypeName, QualifiedTypeReference}, }; @@ -131,7 +131,7 @@ fn get_underlying_type_name(output_type: &QualifiedTypeReference) -> &QualifiedT pub fn validate_ndc( model_name: &Qualified, - model: &Model, + model: &models::Model, schema: &ndc_models::SchemaResponse, ) -> std::result::Result<(), NDCValidationError> { let model_source = match &model.source { diff --git a/v3/crates/engine/src/metadata/resolved/permission.rs b/v3/crates/engine/src/metadata/resolved/permission.rs index 93d1fdce4bf..736e495fb41 100644 --- a/v3/crates/engine/src/metadata/resolved/permission.rs +++ b/v3/crates/engine/src/metadata/resolved/permission.rs @@ -1,9 +1,9 @@ -use super::model::ModelPredicate; +use crate::metadata::resolved::stages::models; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub enum ValueExpression { Literal(serde_json::Value), SessionVariable(open_dds::session_variables::SessionVariable), - BooleanExpression(Box), + BooleanExpression(Box), } diff --git a/v3/crates/engine/src/metadata/resolved/relationship.rs b/v3/crates/engine/src/metadata/resolved/relationship.rs index 95e2ebdbfe6..23cdf3a0a22 100644 --- a/v3/crates/engine/src/metadata/resolved/relationship.rs +++ b/v3/crates/engine/src/metadata/resolved/relationship.rs @@ -1,9 +1,7 @@ use super::command::Command; use super::error::{Error, RelationshipError}; -use super::model::get_ndc_column_for_comparison; -use super::model::Model; -use super::stages::{data_connector_scalar_types, data_connectors}; +use super::stages::{data_connector_scalar_types, data_connectors, models, type_permissions}; use super::subgraph::Qualified; use super::subgraph::QualifiedTypeReference; use super::types::mk_name; @@ -13,7 +11,6 @@ use lang_graphql::ast::common as ast; use open_dds::arguments::ArgumentName; use open_dds::commands::CommandName; -use crate::metadata::resolved::stages::type_permissions; use open_dds::models::ModelName; use open_dds::relationships::{ self, FieldAccess, RelationshipName, RelationshipType, RelationshipV1, @@ -155,7 +152,7 @@ fn resolve_relationship_mappings_model( relationship: &RelationshipV1, source_type_name: &Qualified, source_type: &type_permissions::ObjectTypeWithPermissions, - target_model: &Model, + target_model: &models::Model, data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, ) -> Result, Error> { let mut resolved_relationship_mappings = Vec::new(); @@ -219,7 +216,7 @@ fn resolve_relationship_mappings_model( .source .as_ref() .map(|target_model_source| { - get_ndc_column_for_comparison( + models::get_ndc_column_for_comparison( &target_model.name, &target_model.data_type, target_model_source, @@ -382,7 +379,7 @@ fn get_relationship_capabilities( pub fn resolve_relationship( relationship: &RelationshipV1, subgraph: &str, - models: &IndexMap, Model>, + models: &IndexMap, models::Model>, commands: &IndexMap, Command>, data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, source_type: &type_permissions::ObjectTypeWithPermissions, diff --git a/v3/crates/engine/src/metadata/resolved/stages/boolean_expressions/mod.rs b/v3/crates/engine/src/metadata/resolved/stages/boolean_expressions/mod.rs index 0b5d7746170..c5b4dd1ec21 100644 --- a/v3/crates/engine/src/metadata/resolved/stages/boolean_expressions/mod.rs +++ b/v3/crates/engine/src/metadata/resolved/stages/boolean_expressions/mod.rs @@ -5,6 +5,7 @@ use crate::metadata::resolved::stages::{ scalar_types, type_permissions, }; +use crate::metadata::resolved::model::resolve_ndc_type; use crate::metadata::resolved::subgraph::Qualified; use crate::metadata::resolved::types::{ collect_type_mapping_for_source, mk_name, store_new_graphql_type, TypeMappingToCollect, @@ -317,7 +318,7 @@ pub fn resolve_boolean_expression_info( { operators.insert( op_name.clone(), - crate::metadata::resolved::model::resolve_ndc_type( + resolve_ndc_type( data_connector_name, &get_argument_type(op_definition, &field_mapping.column_type), scalar_types, diff --git a/v3/crates/engine/src/metadata/resolved/stages/data_connector_type_mappings/mod.rs b/v3/crates/engine/src/metadata/resolved/stages/data_connector_type_mappings/mod.rs index 2d25ed84c30..9ba73c7ee52 100644 --- a/v3/crates/engine/src/metadata/resolved/stages/data_connector_type_mappings/mod.rs +++ b/v3/crates/engine/src/metadata/resolved/stages/data_connector_type_mappings/mod.rs @@ -24,7 +24,7 @@ pub(crate) fn resolve( ) -> Result { let mut data_connector_type_mappings = DataConnectorTypeMappings::new(); let mut object_types = HashMap::new(); - let mut existing_graphql_types = HashSet::new(); + let mut graphql_types = HashSet::new(); let mut global_id_enabled_types = HashMap::new(); let mut apollo_federation_entity_enabled_types = HashMap::new(); @@ -38,7 +38,7 @@ pub(crate) fn resolve( let resolved_object_type = resolve_object_type( object_type_definition, - &mut existing_graphql_types, + &mut graphql_types, &qualified_object_type_name, subgraph, &mut global_id_enabled_types, @@ -85,7 +85,7 @@ pub(crate) fn resolve( Ok(DataConnectorTypeMappingsOutput { data_connector_type_mappings, object_types, - existing_graphql_types, + graphql_types, global_id_enabled_types, apollo_federation_entity_enabled_types, }) diff --git a/v3/crates/engine/src/metadata/resolved/stages/data_connector_type_mappings/types.rs b/v3/crates/engine/src/metadata/resolved/stages/data_connector_type_mappings/types.rs index 9ae415db028..071486b935e 100644 --- a/v3/crates/engine/src/metadata/resolved/stages/data_connector_type_mappings/types.rs +++ b/v3/crates/engine/src/metadata/resolved/stages/data_connector_type_mappings/types.rs @@ -80,7 +80,7 @@ impl DataConnectorTypeMappings { /// output of `data_connector_type_mappings` step pub struct DataConnectorTypeMappingsOutput { - pub existing_graphql_types: HashSet, + pub graphql_types: HashSet, pub global_id_enabled_types: HashMap, Vec>>, pub apollo_federation_entity_enabled_types: HashMap, Option>>, diff --git a/v3/crates/engine/src/metadata/resolved/stages/mod.rs b/v3/crates/engine/src/metadata/resolved/stages/mod.rs index 7850533861b..0c0e4e9013e 100644 --- a/v3/crates/engine/src/metadata/resolved/stages/mod.rs +++ b/v3/crates/engine/src/metadata/resolved/stages/mod.rs @@ -4,6 +4,7 @@ pub mod data_connector_scalar_types; pub mod data_connector_type_mappings; pub mod data_connectors; pub mod graphql_config; +pub mod models; pub mod scalar_types; pub mod type_permissions; @@ -24,7 +25,7 @@ pub fn resolve(metadata: open_dds::Metadata) -> Result { let data_connector_type_mappings::DataConnectorTypeMappingsOutput { data_connector_type_mappings, - existing_graphql_types, + graphql_types, global_id_enabled_types, apollo_federation_entity_enabled_types, object_types, @@ -33,7 +34,7 @@ pub fn resolve(metadata: open_dds::Metadata) -> Result { let scalar_types::ScalarTypesOutput { scalar_types, graphql_types, - } = scalar_types::resolve(&metadata_accessor, &existing_graphql_types)?; + } = scalar_types::resolve(&metadata_accessor, &graphql_types)?; let data_connector_scalar_types::DataConnectorWithScalarsOutput { data_connectors, @@ -61,10 +62,27 @@ pub fn resolve(metadata: open_dds::Metadata) -> Result { &graphql_config, )?; + let models::ModelsOutput { + models, + global_id_enabled_types, + apollo_federation_entity_enabled_types, + graphql_types: _graphql_types, + } = models::resolve( + &metadata_accessor, + &data_connectors, + &data_connector_type_mappings, + &graphql_types, + &global_id_enabled_types, + &apollo_federation_entity_enabled_types, + &object_types_with_permissions, + &scalar_types, + &boolean_expression_types, + &graphql_config, + )?; + resolve_metadata( &metadata_accessor, &graphql_config, - graphql_types, global_id_enabled_types, apollo_federation_entity_enabled_types, &data_connector_type_mappings, @@ -72,5 +90,6 @@ pub fn resolve(metadata: open_dds::Metadata) -> Result { &scalar_types, &boolean_expression_types, &data_connectors, + models, ) } diff --git a/v3/crates/engine/src/metadata/resolved/stages/models/mod.rs b/v3/crates/engine/src/metadata/resolved/stages/models/mod.rs new file mode 100644 index 00000000000..49f859d6608 --- /dev/null +++ b/v3/crates/engine/src/metadata/resolved/stages/models/mod.rs @@ -0,0 +1,832 @@ +pub use types::{ + FilterPermission, LimitFieldGraphqlConfig, Model, ModelGraphQlApi, + ModelGraphqlApiArgumentsConfig, ModelOrderByExpression, ModelPredicate, ModelSource, + ModelsOutput, NDCFieldSourceMapping, OffsetFieldGraphqlConfig, OrderByExpressionInfo, + SelectManyGraphQlDefinition, SelectPermission, SelectUniqueGraphQlDefinition, + UniqueIdentifierField, +}; +mod types; + +use crate::metadata::resolved::argument::get_argument_mappings; +use crate::metadata::resolved::error::{BooleanExpressionError, Error, GraphqlConfigError}; +use crate::metadata::resolved::ndc_validation; + +use crate::metadata::resolved::stages::{ + boolean_expressions, data_connector_scalar_types, data_connector_type_mappings, + data_connectors, graphql_config, scalar_types, type_permissions, +}; +use crate::metadata::resolved::subgraph::{mk_qualified_type_reference, ArgumentInfo, Qualified}; +use crate::metadata::resolved::types::{ + collect_type_mapping_for_source, NdcColumnForComparison, TypeMappingToCollect, +}; +use crate::metadata::resolved::types::{mk_name, store_new_graphql_type}; + +use indexmap::IndexMap; +use lang_graphql::ast::common::{self as ast}; + +use open_dds::{ + models::{ + self, EnableAllOrSpecific, ModelGraphQlDefinition, ModelName, ModelV1, OrderableField, + }, + types::{CustomTypeName, FieldName}, +}; + +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::iter; + +/// resolve models +pub fn resolve( + metadata_accessor: &open_dds::accessor::MetadataAccessor, + data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, + data_connector_type_mappings: &data_connector_type_mappings::DataConnectorTypeMappings, + existing_graphql_types: &HashSet, + global_id_enabled_types: &HashMap, Vec>>, + apollo_federation_entity_enabled_types: &HashMap< + Qualified, + Option>, + >, + object_types: &HashMap, type_permissions::ObjectTypeWithPermissions>, + scalar_types: &HashMap, scalar_types::ScalarTypeRepresentation>, + boolean_expression_types: &HashMap< + Qualified, + boolean_expressions::ObjectBooleanExpressionType, + >, + graphql_config: &graphql_config::GraphqlConfig, +) -> Result { + // resolve models + // TODO: validate types + let mut models = IndexMap::new(); + let mut global_id_models = HashMap::new(); + let mut graphql_types = existing_graphql_types.clone(); + let mut global_id_enabled_types = global_id_enabled_types.clone(); + let mut apollo_federation_entity_enabled_types = apollo_federation_entity_enabled_types.clone(); + + for open_dds::accessor::QualifiedObject { + subgraph, + object: model, + } in &metadata_accessor.models + { + let mut resolved_model = resolve_model( + subgraph, + model, + object_types, + &mut global_id_enabled_types, + &mut apollo_federation_entity_enabled_types, + boolean_expression_types, + )?; + if resolved_model.global_id_source.is_some() { + match global_id_models.insert( + resolved_model.data_type.clone(), + resolved_model.name.clone(), + ) { + None => {} + Some(duplicate_model_name) => { + return Err(Error::DuplicateModelGlobalIdSource { + model_1: resolved_model.name, + model_2: duplicate_model_name, + object_type: resolved_model.data_type, + }) + } + } + } + + if let Some(model_source) = &model.source { + resolve_model_source( + model_source, + &mut resolved_model, + subgraph, + data_connectors, + object_types, + scalar_types, + data_connector_type_mappings, + boolean_expression_types, + )?; + } + if let Some(model_graphql_definition) = &model.graphql { + resolve_model_graphql_api( + model_graphql_definition, + &mut resolved_model, + &mut graphql_types, + data_connectors, + &model.description, + graphql_config, + )?; + } + let qualified_model_name = Qualified::new(subgraph.to_string(), model.name.clone()); + if models + .insert(qualified_model_name.clone(), resolved_model) + .is_some() + { + return Err(Error::DuplicateModelDefinition { + name: qualified_model_name, + }); + } + } + Ok(ModelsOutput { + models, + graphql_types, + apollo_federation_entity_enabled_types, + global_id_enabled_types, + }) +} + +fn resolve_filter_expression_type( + model: &ModelV1, + model_data_type: &Qualified, + subgraph: &str, + boolean_expression_types: &HashMap< + Qualified, + boolean_expressions::ObjectBooleanExpressionType, + >, +) -> Result, Error> { + model + .filter_expression_type + .as_ref() + .map(|filter_expression_type| { + let boolean_expression_type_name = + Qualified::new(subgraph.to_string(), filter_expression_type.clone()); + let boolean_expression_type = boolean_expression_types + .get(&boolean_expression_type_name) + .ok_or_else(|| { + Error::from( + BooleanExpressionError::UnknownBooleanExpressionTypeInModel { + name: boolean_expression_type_name.clone(), + model: Qualified::new(subgraph.to_string(), model.name.clone()), + }, + ) + })?; + if boolean_expression_type.object_type != *model_data_type { + return Err(Error::from( + BooleanExpressionError::BooleanExpressionTypeForInvalidObjectTypeInModel { + name: boolean_expression_type_name.clone(), + boolean_expression_object_type: boolean_expression_type.object_type.clone(), + model: Qualified::new(subgraph.to_string(), model.name.clone()), + model_object_type: model_data_type.clone(), + }, + )); + } + // This is also checked in resolve_model_graphql_api, but we want to disallow this even + // if the model is not used in the graphql layer. + if model.source.is_none() { + return Err(Error::CannotUseFilterExpressionsWithoutSource { + model: Qualified::new(subgraph.to_string(), model.name.clone()), + }); + // TODO: Compatibility of model source and the boolean expression type is checked in + // resolve_model_source. Figure out a way to make this logic not scattered. + } + Ok(boolean_expression_type.clone()) + }) + .transpose() +} + +fn resolve_orderable_fields( + model: &ModelV1, + type_fields: &IndexMap, +) -> Result, Error> { + for field in &model.orderable_fields { + // Check for unknown orderable field + if !type_fields.contains_key(&field.field_name) { + return Err(Error::UnknownFieldInOrderableFields { + model_name: model.name.clone(), + field_name: field.field_name.clone(), + }); + } + match &field.order_by_directions { + EnableAllOrSpecific::EnableAll(true) => {} + _ => { + return Err(Error::UnsupportedFeature { + message: "Field level order by configuration is not fully supported yet. Please use \"enableAll\":true.".to_string(), + }) + } + } + } + + // Model orderable fields should have all type fields + if model.orderable_fields.len() != type_fields.len() { + return Err(Error::UnsupportedFeature { + message: "Field level order by configuration is not fully supported yet. Please add all fields in orderable_fields.".to_string(), + }); + } + Ok(model.orderable_fields.clone()) +} + +fn resolve_model( + subgraph: &str, + model: &ModelV1, + object_types: &HashMap, type_permissions::ObjectTypeWithPermissions>, + global_id_enabled_types: &mut HashMap, Vec>>, + apollo_federation_entity_enabled_types: &mut HashMap< + Qualified, + Option>, + >, + boolean_expression_types: &HashMap< + Qualified, + boolean_expressions::ObjectBooleanExpressionType, + >, +) -> Result { + 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 = get_model_object_type_representation( + object_types, + &qualified_object_type_name, + &qualified_model_name, + )?; + let mut global_id_source = None; + 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. + if object_type_representation + .object_type + .global_id_fields + .is_empty() + { + return Err(Error::NoGlobalFieldsPresentInGlobalIdSource { + type_name: qualified_object_type_name, + model_name: model.name.clone(), + }); + } + if !model.arguments.is_empty() { + return Err(Error::ModelWithArgumentsAsGlobalIdSource { + model_name: qualified_model_name, + }); + } + // model has `global_id_source`; insert into the hashmap of `global_id_enabled_types` + match global_id_enabled_types.get_mut(&qualified_object_type_name) { + None => { + // this shouldn't happen; but for some reason the object type + // containing globalIdFields is not inserted. Insert it now + global_id_enabled_types.insert( + qualified_object_type_name.clone(), + vec![qualified_model_name.clone()], + ); + } + Some(model_names) => { + model_names.push(qualified_model_name.clone()); + } + } + global_id_source = Some(NDCFieldSourceMapping { + ndc_mapping: HashMap::new(), + }); + }; + let mut apollo_federation_key_source = None; + if model + .graphql + .as_ref() + .and_then(|g| g.apollo_federation.as_ref().map(|a| a.entity_source)) + .unwrap_or_default() + { + // Check if there are any apollo federation keys present in the related + // object type, if the model is marked as an apollo federation entity source. + if object_type_representation + .object_type + .apollo_federation_config + .is_some() + { + if !model.arguments.is_empty() { + return Err(Error::ModelWithArgumentsAsApolloFederationEntitySource { + model_name: qualified_model_name, + }); + } + // model has `apollo_federation_entity_source`; insert into the hashmap of + // `apollo_federation_entity_enabled_types` + match apollo_federation_entity_enabled_types.get_mut(&qualified_object_type_name) { + None => { + // the model's graphql configuration has `apollo_federation.entitySource` but the object type + // of the model doesn't have any apollo federation keys + return Err(Error::NoKeysFieldsPresentInEntitySource { + type_name: qualified_object_type_name, + model_name: model.name.clone(), + }); + } + Some(type_name) => { + match type_name { + None => { + *type_name = Some(qualified_model_name.clone()); + } + // Multiple models are marked as apollo federation entity source + Some(_) => { + return Err(Error::MultipleEntitySourcesForType { + type_name: qualified_object_type_name, + }); + } + } + } + } + apollo_federation_key_source = Some(NDCFieldSourceMapping { + ndc_mapping: HashMap::new(), + }); + } + } + + let mut arguments = IndexMap::new(); + for argument in &model.arguments { + if arguments + .insert( + argument.name.clone(), + ArgumentInfo { + argument_type: mk_qualified_type_reference(&argument.argument_type, subgraph), + description: argument.description.clone(), + }, + ) + .is_some() + { + return Err(Error::DuplicateModelArgumentDefinition { + model_name: qualified_model_name, + argument_name: argument.name.clone(), + }); + } + } + + let filter_expression_type = resolve_filter_expression_type( + model, + &qualified_object_type_name, + subgraph, + boolean_expression_types, + )?; + + Ok(Model { + name: qualified_model_name, + data_type: qualified_object_type_name, + type_fields: object_type_representation.object_type.fields.clone(), + global_id_fields: object_type_representation + .object_type + .global_id_fields + .clone(), + arguments, + graphql_api: ModelGraphQlApi::default(), + source: None, + select_permissions: None, + global_id_source, + apollo_federation_key_source, + filter_expression_type, + orderable_fields: resolve_orderable_fields( + model, + &object_type_representation.object_type.fields, + )?, + }) +} + +fn resolve_model_graphql_api( + model_graphql_definition: &ModelGraphQlDefinition, + model: &mut Model, + existing_graphql_types: &mut HashSet, + data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, + model_description: &Option, + graphql_config: &graphql_config::GraphqlConfig, +) -> Result<(), Error> { + let model_name = &model.name; + for select_unique in &model_graphql_definition.select_uniques { + let mut unique_identifier_fields = IndexMap::new(); + for field_name in &select_unique.unique_identifier { + let field_type = &model + .type_fields + .get(field_name) + .ok_or_else(|| Error::UnknownFieldInUniqueIdentifier { + model_name: model_name.clone(), + field_name: field_name.clone(), + })? + .field_type; + let ndc_column = model + .source + .as_ref() + .map(|model_source| { + get_ndc_column_for_comparison( + &model.name, + &model.data_type, + model_source, + field_name, + data_connectors, + || "the unique identifier for select unique".to_string(), + ) + }) + .transpose()?; + let unique_identifier_field = UniqueIdentifierField { + field_type: field_type.clone(), + ndc_column, + }; + if unique_identifier_fields + .insert(field_name.clone(), unique_identifier_field) + .is_some() + { + return Err(Error::DuplicateFieldInUniqueIdentifier { + model_name: model_name.clone(), + field_name: field_name.clone(), + }); + } + } + let select_unique_field_name = mk_name(&select_unique.query_root_field.0)?; + let select_unique_description = if select_unique.description.is_some() { + select_unique.description.clone() + } else { + model_description.as_ref().map(|description| { + format!( + "Selects a single object from the model. Model description: {}", + description + ) + }) + }; + model + .graphql_api + .select_uniques + .push(SelectUniqueGraphQlDefinition { + query_root_field: select_unique_field_name, + unique_identifier: unique_identifier_fields, + description: select_unique_description, + deprecated: select_unique.deprecated.clone(), + }); + } + + model.graphql_api.order_by_expression = model + .source + .as_ref() + .map( + |model_source: &ModelSource| -> Result, Error> { + let order_by_expression_type_name = match &model_graphql_definition + .order_by_expression_type + { + None => Ok(None), + Some(type_name) => mk_name(type_name.0.as_str()).map(ast::TypeName).map(Some), + }?; + // TODO: (paritosh) should we check for conflicting graphql types for default order_by type name as well? + store_new_graphql_type( + existing_graphql_types, + order_by_expression_type_name.as_ref(), + )?; + order_by_expression_type_name + .map(|order_by_type_name| { + let data_connector_type_mappings::TypeMapping::Object { + field_mappings, + .. + } = model_source.type_mappings.get(&model.data_type).ok_or( + Error::TypeMappingRequired { + model_name: model_name.clone(), + type_name: model.data_type.clone(), + data_connector: model_source.data_connector.name.clone(), + }, + )?; + + let mut order_by_fields = HashMap::new(); + for (field_name, field_mapping) in field_mappings.iter() { + order_by_fields.insert( + field_name.clone(), + OrderByExpressionInfo { + ndc_column: field_mapping.column.clone(), + }, + ); + } + + match &graphql_config.query.order_by_field_name { + None => Err(Error::GraphqlConfigError { + graphql_config_error: + GraphqlConfigError::MissingOrderByInputFieldInGraphqlConfig, + }), + Some(order_by_field_name) => Ok(ModelOrderByExpression { + data_connector_name: model_source.data_connector.name.clone(), + order_by_type_name, + order_by_fields, + order_by_field_name: order_by_field_name.clone(), + }), + } + }) + .transpose() + }, + ) + .transpose()? + .flatten(); + + // record select_many root field + model.graphql_api.select_many = match &model_graphql_definition.select_many { + None => Ok(None), + Some(gql_definition) => mk_name(&gql_definition.query_root_field.0).map(|f: ast::Name| { + let select_many_description = if gql_definition.description.is_some() { + gql_definition.description.clone() + } else { + model_description.as_ref().map(|description| { + format!( + "Selects multiple objects from the model. Model description: {}", + description + ) + }) + }; + Some(SelectManyGraphQlDefinition { + query_root_field: f, + description: select_many_description, + deprecated: gql_definition.deprecated.clone(), + }) + }), + }?; + + // record limit and offset field names + model.graphql_api.limit_field = + graphql_config + .query + .limit_field_name + .as_ref() + .map(|limit_field| LimitFieldGraphqlConfig { + field_name: limit_field.clone(), + }); + + model.graphql_api.offset_field = + graphql_config + .query + .offset_field_name + .as_ref() + .map(|offset_field| OffsetFieldGraphqlConfig { + field_name: offset_field.clone(), + }); + + if model.arguments.is_empty() { + if model_graphql_definition.arguments_input_type.is_some() { + return Err(Error::UnnecessaryModelArgumentsGraphQlInputConfiguration { + model_name: model_name.clone(), + }); + } + } else { + let arguments_input_type_name = match &model_graphql_definition.arguments_input_type { + None => Ok(None), + Some(type_name) => mk_name(type_name.0.as_str()).map(ast::TypeName).map(Some), + }?; + store_new_graphql_type(existing_graphql_types, arguments_input_type_name.as_ref())?; + + if let Some(type_name) = arguments_input_type_name { + let argument_input_field_name = graphql_config + .query + .arguments_field_name + .as_ref() + .ok_or_else(|| Error::GraphqlConfigError { + graphql_config_error: + GraphqlConfigError::MissingArgumentsInputFieldInGraphqlConfig, + })?; + model.graphql_api.arguments_input_config = Some(ModelGraphqlApiArgumentsConfig { + field_name: argument_input_field_name.clone(), + type_name, + }); + } + } + + Ok(()) +} + +fn resolve_model_source( + model_source: &models::ModelSource, + model: &mut Model, + subgraph: &str, + data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, + object_types: &HashMap, type_permissions::ObjectTypeWithPermissions>, + scalar_types: &HashMap, scalar_types::ScalarTypeRepresentation>, + data_connector_type_mappings: &data_connector_type_mappings::DataConnectorTypeMappings, + boolean_expression_types: &HashMap< + Qualified, + boolean_expressions::ObjectBooleanExpressionType, + >, +) -> Result<(), Error> { + if model.source.is_some() { + return Err(Error::DuplicateModelSourceDefinition { + model_name: model.name.clone(), + }); + } + let qualified_data_connector_name = Qualified::new( + subgraph.to_string(), + model_source.data_connector_name.clone(), + ); + let data_connector_context = data_connectors + .data_connectors_with_scalars + .get(&qualified_data_connector_name) + .ok_or_else(|| Error::UnknownModelDataConnector { + model_name: model.name.clone(), + data_connector: qualified_data_connector_name.clone(), + })?; + + let source_collection = data_connector_context + .inner + .schema + .collections + .iter() + .find(|collection_info| collection_info.name == *model_source.collection) + .ok_or_else(|| Error::UnknownModelCollection { + model_name: model.name.clone(), + data_connector: qualified_data_connector_name.clone(), + collection: model_source.collection.clone(), + })?; + + // Get the mappings of arguments and any type mappings that need resolving from the arguments + let (argument_mappings, argument_type_mappings_to_collect) = get_argument_mappings( + &model.arguments, + &model_source.argument_mapping, + &source_collection.arguments, + object_types, + scalar_types, + boolean_expression_types, + ) + .map_err(|err| Error::ModelCollectionArgumentMappingError { + data_connector_name: qualified_data_connector_name.clone(), + model_name: model.name.clone(), + collection_name: model_source.collection.clone(), + error: err, + })?; + + // Collect type mappings. + let mut type_mappings = BTreeMap::new(); + let source_collection_type_mapping_to_collect = TypeMappingToCollect { + type_name: &model.data_type, + ndc_object_type_name: source_collection.collection_type.as_str(), + }; + for type_mapping_to_collect in iter::once(&source_collection_type_mapping_to_collect) + .chain(argument_type_mappings_to_collect.iter()) + { + collect_type_mapping_for_source( + type_mapping_to_collect, + data_connector_type_mappings, + &qualified_data_connector_name, + object_types, + scalar_types, + &mut type_mappings, + ) + .map_err(|error| Error::ModelTypeMappingCollectionError { + model_name: model.name.clone(), + error, + })?; + } + + if let Some(filter_expression) = &model.filter_expression_type { + if filter_expression.data_connector_name != qualified_data_connector_name { + return Err(Error::DifferentDataConnectorInFilterExpression { + model: model.name.clone(), + model_data_connector: qualified_data_connector_name.clone(), + filter_expression_type: filter_expression.name.clone(), + filter_expression_data_connector: filter_expression.data_connector_name.clone(), + }); + } + + if filter_expression.data_connector_object_type != source_collection.collection_type { + return Err(Error::DifferentDataConnectorObjectTypeInFilterExpression { + model: model.name.clone(), + model_data_connector_object_type: source_collection.collection_type.clone(), + filter_expression_type: filter_expression.name.clone(), + filter_expression_data_connector_object_type: filter_expression + .data_connector_object_type + .clone(), + }); + } + } + + let resolved_model_source = ModelSource { + data_connector: data_connectors::DataConnectorLink::new( + qualified_data_connector_name, + data_connector_context.inner.url.clone(), + data_connector_context.inner.headers, + )?, + collection: model_source.collection.clone(), + type_mappings, + argument_mappings, + }; + + let model_object_type = + get_model_object_type_representation(object_types, &model.data_type, &model.name)?; + + if let Some(global_id_source) = &mut model.global_id_source { + for global_id_field in &model_object_type.object_type.global_id_fields { + global_id_source.ndc_mapping.insert( + global_id_field.clone(), + get_ndc_column_for_comparison( + &model.name, + &model.data_type, + &resolved_model_source, + global_id_field, + data_connectors, + || format!("the global ID fields of type {}", model.data_type), + )?, + ); + } + } + + if let Some(apollo_federation_key_source) = &mut model.apollo_federation_key_source { + if let Some(apollo_federation_config) = + &model_object_type.object_type.apollo_federation_config + { + for key in &apollo_federation_config.keys { + for field in &key.fields { + apollo_federation_key_source.ndc_mapping.insert( + field.clone(), + get_ndc_column_for_comparison( + &model.name, + &model.data_type, + &resolved_model_source, + field, + data_connectors, + || { + format!( + "the apollo federation key fields of type {}", + model.data_type + ) + }, + )?, + ); + } + } + } + } + + model.source = Some(resolved_model_source); + ndc_validation::validate_ndc(&model.name, model, data_connector_context.inner.schema)?; + Ok(()) +} + +/// Gets the `type_permissions::ObjectTypeWithPermissions` 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. +fn get_model_object_type_representation<'s>( + object_types: &'s HashMap< + Qualified, + type_permissions::ObjectTypeWithPermissions, + >, + data_type: &Qualified, + model_name: &Qualified, +) -> Result<&'s type_permissions::ObjectTypeWithPermissions, crate::metadata::resolved::error::Error> +{ + match object_types.get(data_type) { + Some(object_type_representation) => Ok(object_type_representation), + None => Err(Error::UnknownModelDataType { + model_name: model_name.clone(), + data_type: data_type.clone(), + }), + } +} + +pub(crate) fn get_ndc_column_for_comparison String>( + model_name: &Qualified, + model_data_type: &Qualified, + model_source: &ModelSource, + field: &FieldName, + data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, + comparison_location: F, +) -> Result { + // Get field mappings of model data type + let data_connector_type_mappings::TypeMapping::Object { field_mappings, .. } = model_source + .type_mappings + .get(model_data_type) + .ok_or(Error::TypeMappingRequired { + model_name: model_name.clone(), + type_name: model_data_type.clone(), + data_connector: model_source.data_connector.name.clone(), + })?; + + // Determine field_mapping for the given field + let field_mapping = + field_mappings + .get(field) + .ok_or_else(|| Error::NoFieldMappingForComparedField { + comparison_location: comparison_location(), + field_name: field.clone(), + model_name: model_name.clone(), + })?; + + // Determine ndc type of the field + let field_ndc_type = &field_mapping.column_type; + + // Get available scalars defined in the data connector + let scalars = &data_connectors + .data_connectors_with_scalars + .get(&model_source.data_connector.name) + .ok_or(Error::UnknownModelDataConnector { + model_name: model_name.clone(), + data_connector: model_source.data_connector.name.clone(), + })? + .scalars; + // Determine whether the ndc type is a simple scalar and get scalar type info + let (_field_ndc_type_scalar, scalar_type_info) = + data_connector_scalar_types::get_simple_scalar(field_ndc_type.clone(), scalars) + .ok_or_else(|| Error::UncomparableNonScalarFieldType { + comparison_location: comparison_location(), + field_name: field.clone(), + model_name: model_name.clone(), + })?; + + let equal_operator = match scalar_type_info + .comparison_operators + .equal_operators + .as_slice() + { + [] => { + return Err(Error::NoEqualOperatorForComparedField { + comparison_location: comparison_location(), + field_name: field.clone(), + model_name: model_name.clone(), + }); + } + [equal_operator] => equal_operator, + _ => { + return Err(Error::MultipleEqualOperatorsForComparedField { + comparison_location: comparison_location(), + field_name: field.clone(), + model_name: model_name.clone(), + }); + } + }; + + Ok(NdcColumnForComparison { + column: field_mapping.column.clone(), + equal_operator: equal_operator.clone(), + }) +} diff --git a/v3/crates/engine/src/metadata/resolved/stages/models/types.rs b/v3/crates/engine/src/metadata/resolved/stages/models/types.rs new file mode 100644 index 00000000000..1ef8c42a941 --- /dev/null +++ b/v3/crates/engine/src/metadata/resolved/stages/models/types.rs @@ -0,0 +1,162 @@ +use crate::metadata::resolved::permission::ValueExpression; +use crate::metadata::resolved::stages::{ + boolean_expressions, data_connector_type_mappings, data_connectors, +}; +use crate::metadata::resolved::subgraph::{ + deserialize_qualified_btreemap, serialize_qualified_btreemap, ArgumentInfo, Qualified, + QualifiedTypeReference, +}; +use crate::metadata::resolved::types::NdcColumnForComparison; + +use crate::schema::types::output_type::relationship::PredicateRelationshipAnnotation; +use indexmap::IndexMap; +use lang_graphql::ast::common::{self as ast, Name}; +use ndc_models; + +use open_dds::types::Deprecated; +use open_dds::{ + arguments::ArgumentName, + data_connector::DataConnectorName, + models::{ModelName, OrderableField}, + permissions::Role, + types::{CustomTypeName, FieldName}, +}; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, HashMap, HashSet}; + +pub struct ModelsOutput { + pub models: IndexMap, Model>, + pub graphql_types: HashSet, + pub global_id_enabled_types: HashMap, Vec>>, + pub apollo_federation_entity_enabled_types: + HashMap, Option>>, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct UniqueIdentifierField { + pub field_type: QualifiedTypeReference, + pub ndc_column: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct SelectUniqueGraphQlDefinition { + pub query_root_field: ast::Name, + pub unique_identifier: IndexMap, + pub description: Option, + pub deprecated: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct SelectManyGraphQlDefinition { + pub query_root_field: ast::Name, + pub description: Option, + pub deprecated: Option, +} + +// TODO: add support for aggregates +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct OrderByExpressionInfo { + pub ndc_column: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct ModelOrderByExpression { + pub data_connector_name: Qualified, + pub order_by_type_name: ast::TypeName, + pub order_by_fields: HashMap, + pub order_by_field_name: ast::Name, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct ModelGraphqlApiArgumentsConfig { + pub field_name: Name, + pub type_name: ast::TypeName, +} +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct LimitFieldGraphqlConfig { + pub field_name: Name, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct OffsetFieldGraphqlConfig { + pub field_name: Name, +} +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)] +pub struct ModelGraphQlApi { + pub arguments_input_config: Option, + pub select_uniques: Vec, + pub select_many: Option, + pub order_by_expression: Option, + pub limit_field: Option, + pub offset_field: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct ModelSource { + pub data_connector: data_connectors::DataConnectorLink, + pub collection: String, + #[serde( + serialize_with = "serialize_qualified_btreemap", + deserialize_with = "deserialize_qualified_btreemap" + )] + pub type_mappings: + BTreeMap, data_connector_type_mappings::TypeMapping>, + pub argument_mappings: HashMap, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub enum FilterPermission { + AllowAll, + Filter(ModelPredicate), +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct SelectPermission { + pub filter: FilterPermission, + // pub allow_aggregations: bool, + pub argument_presets: BTreeMap, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub enum ModelPredicate { + UnaryFieldComparison { + field: FieldName, + ndc_column: String, + operator: ndc_models::UnaryComparisonOperator, + }, + BinaryFieldComparison { + field: FieldName, + ndc_column: String, + operator: String, + argument_type: QualifiedTypeReference, + value: ValueExpression, + }, + Relationship { + relationship_info: PredicateRelationshipAnnotation, + predicate: Box, + }, + And(Vec), + Or(Vec), + Not(Box), +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Model { + pub name: Qualified, + pub data_type: Qualified, + pub type_fields: IndexMap, + pub global_id_fields: Vec, + pub arguments: IndexMap, + pub graphql_api: ModelGraphQlApi, + pub source: Option, + pub select_permissions: Option>, + pub global_id_source: Option, + pub apollo_federation_key_source: Option, + pub filter_expression_type: Option, + pub orderable_fields: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct NDCFieldSourceMapping { + pub ndc_mapping: HashMap, +} diff --git a/v3/crates/engine/src/schema/model_arguments.rs b/v3/crates/engine/src/schema/model_arguments.rs index 027aff14750..d069412b194 100644 --- a/v3/crates/engine/src/schema/model_arguments.rs +++ b/v3/crates/engine/src/schema/model_arguments.rs @@ -14,7 +14,7 @@ use crate::metadata::resolved::subgraph::Qualified; /// arguments fields will live. pub fn get_model_arguments_input_field( builder: &mut gql_schema::Builder, - model: &resolved::model::Model, + model: &resolved::Model, ) -> Result, crate::schema::Error> { model .graphql_api @@ -49,7 +49,7 @@ pub fn get_model_arguments_input_field( pub fn build_model_argument_fields( gds: &GDS, builder: &mut gql_schema::Builder, - model: &resolved::model::Model, + model: &resolved::Model, ) -> Result< BTreeMap>>, crate::schema::Error, diff --git a/v3/crates/engine/src/schema/model_order_by.rs b/v3/crates/engine/src/schema/model_order_by.rs index 6b076055427..483c8c15001 100644 --- a/v3/crates/engine/src/schema/model_order_by.rs +++ b/v3/crates/engine/src/schema/model_order_by.rs @@ -82,7 +82,7 @@ pub fn build_order_by_enum_type_schema( pub fn get_order_by_expression_input_field( builder: &mut gql_schema::Builder, model_name: Qualified, - order_by_expression_info: &resolved::model::ModelOrderByExpression, + order_by_expression_info: &resolved::ModelOrderByExpression, ) -> gql_schema::InputField { gql_schema::InputField::new( order_by_expression_info.order_by_field_name.clone(), diff --git a/v3/crates/engine/src/schema/permissions.rs b/v3/crates/engine/src/schema/permissions.rs index 495c3655740..e59f2ed7230 100644 --- a/v3/crates/engine/src/schema/permissions.rs +++ b/v3/crates/engine/src/schema/permissions.rs @@ -1,9 +1,8 @@ use open_dds::types::{CustomTypeName, FieldName}; use std::collections::{BTreeMap, HashMap}; -use crate::metadata::resolved::model::FilterPermission; use crate::metadata::resolved::permission::ValueExpression; -use crate::metadata::resolved::stages::{data_connector_type_mappings, type_permissions}; +use crate::metadata::resolved::stages::{data_connector_type_mappings, models, type_permissions}; use crate::metadata::resolved::subgraph::{Qualified, QualifiedTypeReference}; use crate::metadata::resolved::types::{object_type_exists, unwrap_custom_type_name}; use crate::metadata::resolved::{self}; @@ -14,7 +13,7 @@ use super::types::ArgumentNameAndPath; /// Build namespace annotation for select permissions pub(crate) fn get_select_permissions_namespace_annotations( - model: &resolved::model::Model, + model: &models::Model, object_types: &HashMap, type_permissions::ObjectTypeWithPermissions>, ) -> Result>, schema::Error> { let mut permissions: HashMap> = model @@ -97,9 +96,9 @@ pub(crate) fn get_select_permissions_namespace_annotations( /// This is different from generating permissions for select_many etc, /// as we need to check the permissions of the arguments used in the selection. pub(crate) fn get_select_one_namespace_annotations( - model: &resolved::model::Model, + model: &models::Model, object_type_representation: &type_permissions::ObjectTypeWithPermissions, - select_unique: &resolved::model::SelectUniqueGraphQlDefinition, + select_unique: &models::SelectUniqueGraphQlDefinition, object_types: &HashMap, type_permissions::ObjectTypeWithPermissions>, ) -> Result>, schema::Error> { let select_permissions = get_select_permissions_namespace_annotations(model, object_types)?; @@ -120,7 +119,7 @@ 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( - target_model: &resolved::model::Model, + target_model: &models::Model, source_object_type_representation: &type_permissions::ObjectTypeWithPermissions, target_object_type_representation: &type_permissions::ObjectTypeWithPermissions, mappings: &[resolved::relationship::RelationshipModelMapping], @@ -442,8 +441,8 @@ pub(crate) fn get_allowed_roles_for_field<'a>( /// Builds namespace annotations for the `node` field. pub(crate) fn get_node_field_namespace_permissions( object_type_representation: &type_permissions::ObjectTypeWithPermissions, - model: &resolved::model::Model, -) -> HashMap { + model: &models::Model, +) -> HashMap { let mut permissions = HashMap::new(); match &model.select_permissions { @@ -481,8 +480,8 @@ pub(crate) fn get_node_field_namespace_permissions( /// Builds namespace annotations for the `_entities` field. pub(crate) fn get_entities_field_namespace_permissions( object_type_representation: &type_permissions::ObjectTypeWithPermissions, - model: &resolved::model::Model, -) -> HashMap { + model: &models::Model, +) -> HashMap { let mut permissions = HashMap::new(); match &model.select_permissions { diff --git a/v3/crates/engine/src/schema/query_root/apollo_federation.rs b/v3/crates/engine/src/schema/query_root/apollo_federation.rs index 54575f3e9b1..eea72e22ce6 100644 --- a/v3/crates/engine/src/schema/query_root/apollo_federation.rs +++ b/v3/crates/engine/src/schema/query_root/apollo_federation.rs @@ -36,7 +36,7 @@ pub(crate) fn apollo_federation_field( ) -> Result { let mut roles_type_permissions: HashMap< Role, - HashMap, resolved::model::FilterPermission>, + HashMap, resolved::FilterPermission>, > = HashMap::new(); let mut typename_mappings = HashMap::new(); for model in gds.metadata.models.values() { diff --git a/v3/crates/engine/src/schema/query_root/node_field.rs b/v3/crates/engine/src/schema/query_root/node_field.rs index c6720aa2141..03b4ad6a5d4 100644 --- a/v3/crates/engine/src/schema/query_root/node_field.rs +++ b/v3/crates/engine/src/schema/query_root/node_field.rs @@ -53,7 +53,7 @@ pub(crate) fn relay_node_field( let mut roles_type_permissions: HashMap< Role, - HashMap, resolved::model::FilterPermission>, + HashMap, resolved::FilterPermission>, > = HashMap::new(); for model in gds.metadata.models.values() { if let Some(global_id_source) = &model.global_id_source { diff --git a/v3/crates/engine/src/schema/query_root/select_many.rs b/v3/crates/engine/src/schema/query_root/select_many.rs index 3637cd190aa..fe0f8d4b1e6 100644 --- a/v3/crates/engine/src/schema/query_root/select_many.rs +++ b/v3/crates/engine/src/schema/query_root/select_many.rs @@ -22,7 +22,7 @@ use crate::schema::{ /// limit, offset, order_by and where. pub(crate) fn generate_select_many_arguments( builder: &mut gql_schema::Builder, - model: &resolved::model::Model, + model: &resolved::Model, ) -> Result< BTreeMap>>, crate::schema::Error, @@ -97,8 +97,8 @@ pub(crate) fn generate_select_many_arguments( pub(crate) fn select_many_field( gds: &GDS, builder: &mut gql_schema::Builder, - model: &resolved::model::Model, - select_many: &resolved::model::SelectManyGraphQlDefinition, + model: &resolved::Model, + select_many: &resolved::SelectManyGraphQlDefinition, parent_type: &ast::TypeName, ) -> Result< ( diff --git a/v3/crates/engine/src/schema/query_root/select_one.rs b/v3/crates/engine/src/schema/query_root/select_one.rs index fc30825d256..b69d7339047 100644 --- a/v3/crates/engine/src/schema/query_root/select_one.rs +++ b/v3/crates/engine/src/schema/query_root/select_one.rs @@ -21,8 +21,8 @@ use crate::schema::{ pub(crate) fn select_one_field( gds: &GDS, builder: &mut gql_schema::Builder, - model: &resolved::model::Model, - select_unique: &resolved::model::SelectUniqueGraphQlDefinition, + model: &resolved::Model, + select_unique: &resolved::SelectUniqueGraphQlDefinition, parent_type: &ast::TypeName, ) -> Result< ( diff --git a/v3/crates/engine/src/schema/types.rs b/v3/crates/engine/src/schema/types.rs index 27a1471358f..567c0bd393f 100644 --- a/v3/crates/engine/src/schema/types.rs +++ b/v3/crates/engine/src/schema/types.rs @@ -64,7 +64,7 @@ pub struct NodeFieldTypeNameMapping { pub type_name: Qualified, // `model_source` is are optional because we allow building schema without specifying a data source // In such a case, `global_id_fields_ndc_mapping` will also be empty - pub model_source: Option, + pub model_source: Option, pub global_id_fields_ndc_mapping: HashMap, } @@ -73,7 +73,7 @@ pub struct EntityFieldTypeNameMapping { pub type_name: Qualified, // `model_source` is are optional because we allow building schema without specifying a data source // In such a case, `global_id_fields_ndc_mapping` will also be empty - pub model_source: Option, + pub model_source: Option, pub key_fields_ndc_mapping: HashMap, } @@ -126,7 +126,7 @@ pub enum RootFieldAnnotation { }, Model { data_type: Qualified, - source: Option, + source: Option, // select_permissions: HashMap, kind: RootFieldKind, name: Qualified, @@ -307,7 +307,7 @@ pub enum NamespaceAnnotation { Command(ArgumentPresets), /// any filter and arguments for selecting from a model Model { - filter: resolved::model::FilterPermission, + filter: resolved::FilterPermission, argument_presets: ArgumentPresets, }, /// The `NodeFieldTypeMappings` contains a Hashmap of typename to the filter permission. @@ -315,13 +315,13 @@ pub enum NamespaceAnnotation { /// decoding, a typename will be obtained. We need to use that typename to look up the /// Hashmap to get the appropriate `resolved::model::FilterPermission`. NodeFieldTypeMappings( - HashMapWithJsonKey, resolved::model::FilterPermission>, + HashMapWithJsonKey, resolved::FilterPermission>, ), /// `EntityTypeMappings` is similar to the `NodeFieldTypeMappings`. While executing the `_entities` field, the /// `representations` argument is used, which contains typename. We need to use that typename to look up the hashmap /// to get the appropriate `resolved::model::FilterPermission`. EntityTypeMappings( - HashMapWithJsonKey, resolved::model::FilterPermission>, + HashMapWithJsonKey, resolved::FilterPermission>, ), } diff --git a/v3/crates/engine/src/schema/types/output_type/relationship.rs b/v3/crates/engine/src/schema/types/output_type/relationship.rs index 45aec74cc7a..b328314faa9 100644 --- a/v3/crates/engine/src/schema/types/output_type/relationship.rs +++ b/v3/crates/engine/src/schema/types/output_type/relationship.rs @@ -12,6 +12,7 @@ use serde::{Deserialize, Serialize}; use crate::{ metadata::resolved::{ self, + stages::models, subgraph::{ deserialize_qualified_btreemap, serialize_qualified_btreemap, Qualified, QualifiedTypeReference, @@ -87,13 +88,13 @@ pub struct PredicateRelationshipAnnotation { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct ModelTargetSource { - pub(crate) model: resolved::model::ModelSource, + pub(crate) model: models::ModelSource, pub(crate) capabilities: resolved::relationship::RelationshipCapabilities, } impl ModelTargetSource { pub fn new( - model: &resolved::model::Model, + model: &models::Model, relationship: &resolved::relationship::Relationship, ) -> Result, schema::Error> { model @@ -104,7 +105,7 @@ impl ModelTargetSource { } pub fn from_model_source( - model_source: &resolved::model::ModelSource, + model_source: &models::ModelSource, relationship: &resolved::relationship::Relationship, ) -> Result { Ok(Self {