From 9046cc45b490216529c8b0b0d6807e4821314b20 Mon Sep 17 00:00:00 2001 From: Daniel Harvey Date: Tue, 30 Apr 2024 11:34:15 +0100 Subject: [PATCH] Separate `model_permissions` resolve step (#526) ## Description Following work in https://github.com/hasura/v3-engine/pull/523 and friends, this resolves `model_permissions` in a fresh step, creating a new `ModelWithPermissions` type to differentiate from the regular `Model` which no longer has any notion of permissions. Functional no-op. V3_GIT_ORIGIN_REV_ID: fdf928ae12159a6c4ef87effc4704193c5306e46 --- v3/crates/engine/src/metadata/resolved.rs | 5 +- .../engine/src/metadata/resolved/argument.rs | 18 +- .../engine/src/metadata/resolved/metadata.rs | 83 +-- .../engine/src/metadata/resolved/model.rs | 468 +------------- .../src/metadata/resolved/permission.rs | 4 +- .../src/metadata/resolved/stages/mod.rs | 15 +- .../resolved/stages/model_permissions/mod.rs | 589 ++++++++++++++++++ .../stages/model_permissions/types.rs | 55 ++ .../metadata/resolved/stages/models/mod.rs | 10 +- .../metadata/resolved/stages/models/types.rs | 41 -- .../src/metadata/resolved/stages/roles/mod.rs | 4 +- .../engine/src/schema/apollo_federation.rs | 7 +- .../engine/src/schema/boolean_expression.rs | 18 +- .../engine/src/schema/model_arguments.rs | 11 +- v3/crates/engine/src/schema/model_order_by.rs | 10 +- v3/crates/engine/src/schema/permissions.rs | 15 +- v3/crates/engine/src/schema/query_root.rs | 4 +- .../schema/query_root/apollo_federation.rs | 14 +- .../src/schema/query_root/node_field.rs | 14 +- .../src/schema/query_root/select_many.rs | 24 +- .../src/schema/query_root/select_one.rs | 12 +- v3/crates/engine/src/schema/relay.rs | 9 +- .../engine/src/schema/types/output_type.rs | 4 +- .../schema/types/output_type/relationship.rs | 3 +- 24 files changed, 763 insertions(+), 674 deletions(-) create mode 100644 v3/crates/engine/src/metadata/resolved/stages/model_permissions/mod.rs create mode 100644 v3/crates/engine/src/metadata/resolved/stages/model_permissions/types.rs diff --git a/v3/crates/engine/src/metadata/resolved.rs b/v3/crates/engine/src/metadata/resolved.rs index e54432eca9b..ff5920d287f 100644 --- a/v3/crates/engine/src/metadata/resolved.rs +++ b/v3/crates/engine/src/metadata/resolved.rs @@ -18,9 +18,10 @@ pub use stages::boolean_expressions::{ pub use stages::command_permissions::CommandWithPermissions; pub use stages::commands::Command; pub use stages::data_connector_type_mappings::{FieldMapping, TypeMapping}; +pub use stages::model_permissions::{FilterPermission, ModelPredicate, ModelWithPermissions}; pub use stages::models::{ - FilterPermission, Model, ModelOrderByExpression, ModelPredicate, ModelSource, - SelectManyGraphQlDefinition, SelectUniqueGraphQlDefinition, + Model, ModelOrderByExpression, ModelSource, SelectManyGraphQlDefinition, + SelectUniqueGraphQlDefinition, }; /// we seem to be exporting functions. perhaps these would be better served as methods on the data /// types we export? diff --git a/v3/crates/engine/src/metadata/resolved/argument.rs b/v3/crates/engine/src/metadata/resolved/argument.rs index c08ef238ab2..96e01f3c981 100644 --- a/v3/crates/engine/src/metadata/resolved/argument.rs +++ b/v3/crates/engine/src/metadata/resolved/argument.rs @@ -5,8 +5,8 @@ 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, models, - relationships, scalar_types, type_permissions, + boolean_expressions, data_connector_scalar_types, data_connector_type_mappings, + model_permissions, relationships, scalar_types, type_permissions, }; use crate::metadata::resolved::subgraph::{ArgumentInfo, Qualified}; use crate::metadata::resolved::subgraph::{QualifiedBaseType, QualifiedTypeReference}; @@ -256,7 +256,7 @@ pub(crate) fn resolve_model_predicate_with_type( subgraph: &str, data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, fields: &IndexMap, -) -> Result { +) -> Result { match model_predicate { permissions::ModelPredicate::FieldComparison(permissions::FieldComparisonPredicate { field, @@ -328,7 +328,7 @@ pub(crate) fn resolve_model_predicate_with_type( }), }?; - Ok(models::ModelPredicate::BinaryFieldComparison { + Ok(model_permissions::ModelPredicate::BinaryFieldComparison { field: field.clone(), ndc_column: field_mapping.column.clone(), operator: resolved_operator, @@ -347,7 +347,7 @@ pub(crate) fn resolve_model_predicate_with_type( } })?; - Ok(models::ModelPredicate::UnaryFieldComparison { + Ok(model_permissions::ModelPredicate::UnaryFieldComparison { field: field.clone(), ndc_column: field_mapping.column.clone(), operator: ndc_models::UnaryComparisonOperator::IsNull, @@ -368,7 +368,9 @@ pub(crate) fn resolve_model_predicate_with_type( data_connectors, fields, )?; - Ok(models::ModelPredicate::Not(Box::new(resolved_predicate))) + Ok(model_permissions::ModelPredicate::Not(Box::new( + resolved_predicate, + ))) } permissions::ModelPredicate::And(predicates) => { let mut resolved_predicates = Vec::new(); @@ -383,7 +385,7 @@ pub(crate) fn resolve_model_predicate_with_type( fields, )?); } - Ok(models::ModelPredicate::And(resolved_predicates)) + Ok(model_permissions::ModelPredicate::And(resolved_predicates)) } permissions::ModelPredicate::Or(predicates) => { let mut resolved_predicates = Vec::new(); @@ -398,7 +400,7 @@ pub(crate) fn resolve_model_predicate_with_type( fields, )?); } - Ok(models::ModelPredicate::Or(resolved_predicates)) + Ok(model_permissions::ModelPredicate::Or(resolved_predicates)) } } } diff --git a/v3/crates/engine/src/metadata/resolved/metadata.rs b/v3/crates/engine/src/metadata/resolved/metadata.rs index 28c043a98b1..030430e6238 100644 --- a/v3/crates/engine/src/metadata/resolved/metadata.rs +++ b/v3/crates/engine/src/metadata/resolved/metadata.rs @@ -7,12 +7,11 @@ use serde::{Deserialize, Serialize}; use open_dds::{commands::CommandName, models::ModelName, types::CustomTypeName}; use crate::metadata::resolved::error::Error; -use crate::metadata::resolved::model::resolve_model_select_permissions; use crate::metadata::resolved::subgraph::Qualified; use crate::metadata::resolved::stages::{ - boolean_expressions, command_permissions, data_connector_scalar_types, - data_connector_type_mappings, graphql_config, models, relationships, roles, scalar_types, + boolean_expressions, command_permissions, graphql_config, model_permissions, relationships, + roles, scalar_types, }; /// Resolved and validated metadata for a project. Used internally in the v3 server. @@ -21,7 +20,7 @@ pub struct Metadata { pub object_types: HashMap, relationships::ObjectTypeWithRelationships>, pub scalar_types: HashMap, scalar_types::ScalarTypeRepresentation>, - pub models: IndexMap, models::Model>, + pub models: IndexMap, model_permissions::ModelWithPermissions>, pub commands: IndexMap, command_permissions::CommandWithPermissions>, pub boolean_expression_types: HashMap, boolean_expressions::ObjectBooleanExpressionType>, @@ -33,33 +32,16 @@ pub struct Metadata { Functions to validate and resolve OpenDD spec to internal metadata *******************/ pub fn resolve_metadata( - metadata_accessor: &open_dds::accessor::MetadataAccessor, graphql_config: &graphql_config::GraphqlConfig, - data_connector_type_mappings: &data_connector_type_mappings::DataConnectorTypeMappings, object_types: HashMap, relationships::ObjectTypeWithRelationships>, scalar_types: &HashMap, scalar_types::ScalarTypeRepresentation>, boolean_expression_types: &HashMap< Qualified, boolean_expressions::ObjectBooleanExpressionType, >, - data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, - mut models: IndexMap, models::Model>, + models: IndexMap, model_permissions::ModelWithPermissions>, commands: IndexMap, command_permissions::CommandWithPermissions>, ) -> Result { - // resolve model permissions - // Note: Model permissions's predicate can include the relationship field, - // hence Model permissions should be resolved after the relationships of a - // model is resolved. - // TODO: make this return values rather than blindly mutating it's inputs - resolve_model_permissions( - metadata_accessor, - data_connectors, - &object_types, - &mut models, - boolean_expression_types, - data_connector_type_mappings, - )?; - let roles = roles::resolve(&object_types, &models, &commands); Ok(Metadata { @@ -72,60 +54,3 @@ pub fn resolve_metadata( roles, }) } - -/// resolve model permissions -/// this currently works by mutating `models`, let's change it to -/// return new values instead where possible -fn resolve_model_permissions( - metadata_accessor: &open_dds::accessor::MetadataAccessor, - data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, - object_types: &HashMap, relationships::ObjectTypeWithRelationships>, - models: &mut IndexMap, models::Model>, - boolean_expression_types: &HashMap< - Qualified, - boolean_expressions::ObjectBooleanExpressionType, - >, - data_connector_type_mappings: &data_connector_type_mappings::DataConnectorTypeMappings, -) -> Result<(), Error> { - // Note: Model permissions's predicate can include the relationship field, - // hence Model permissions should be resolved after the relationships of a - // model is resolved. - for open_dds::accessor::QualifiedObject { - subgraph, - object: permissions, - } in &metadata_accessor.model_permissions - { - let model_name = Qualified::new(subgraph.to_string(), permissions.model_name.clone()); - let model = - models - .get(&model_name) - .ok_or_else(|| Error::UnknownModelInModelSelectPermissions { - model_name: model_name.clone(), - })?; - - if model.select_permissions.is_none() { - let select_permissions = Some(resolve_model_select_permissions( - model, - subgraph, - permissions, - data_connectors, - object_types, - models, // This is required to get the model for the relationship target - boolean_expression_types, - data_connector_type_mappings, - )?); - - let model = models.get_mut(&model_name).ok_or_else(|| { - Error::UnknownModelInModelSelectPermissions { - model_name: model_name.clone(), - } - })?; - model.select_permissions = select_permissions; - } else { - return Err(Error::DuplicateModelSelectPermission { - model_name: model_name.clone(), - }); - } - } - Ok(()) -} diff --git a/v3/crates/engine/src/metadata/resolved/model.rs b/v3/crates/engine/src/metadata/resolved/model.rs index 0ba228e2a3d..568b91ff0fc 100644 --- a/v3/crates/engine/src/metadata/resolved/model.rs +++ b/v3/crates/engine/src/metadata/resolved/model.rs @@ -1,31 +1,15 @@ -use super::permission::ValueExpression; -use super::stages::{ - boolean_expressions, data_connector_scalar_types, data_connector_type_mappings, models, - relationships, -}; -use super::typecheck; +use super::stages::data_connector_scalar_types; -use crate::metadata::resolved::argument::resolve_value_expression_for_argument; -use crate::metadata::resolved::error::{Error, RelationshipError}; +use crate::metadata::resolved::error::Error; use crate::metadata::resolved::subgraph::{ mk_qualified_type_name, Qualified, QualifiedBaseType, QualifiedTypeReference, }; -use crate::metadata::resolved::types::mk_name; -use crate::schema::types::output_type::relationship::{ - ModelTargetSource, PredicateRelationshipAnnotation, -}; -use indexmap::IndexMap; use ndc_models; -use open_dds::permissions::{FieldIsNullPredicate, NullableModelPredicate, RelationshipPredicate}; -use open_dds::{ - data_connector::DataConnectorName, - models::ModelName, - permissions::{self, ModelPermissionsV1, Role}, - types::{CustomTypeName, FieldName, OperatorName}, -}; -use std::collections::{BTreeMap, HashMap}; + +use open_dds::data_connector::DataConnectorName; +use std::collections::HashMap; // helper function to resolve ndc types to dds type based on scalar type representations pub(crate) fn resolve_ndc_type( @@ -76,445 +60,3 @@ pub(crate) fn resolve_ndc_type( ndc_models::Type::Predicate { .. } => Err(Error::PredicateTypesUnsupported), } } - -fn resolve_model_predicate( - model_predicate: &permissions::ModelPredicate, - model: &models::Model, - subgraph: &str, - data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, - fields: &IndexMap, - object_types: &HashMap, relationships::ObjectTypeWithRelationships>, - models: &IndexMap, models::Model>, -) -> 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) - if let Some(model_source) = &model.source { - // 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 predicate field - let field_mapping = field_mappings.get(field).ok_or_else(|| { - Error::UnknownFieldInSelectPermissionsDefinition { - field_name: field.clone(), - model_name: model.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(&model_source.data_connector.name) - .ok_or(Error::UnknownModelDataConnector { - model_name: model.name.clone(), - data_connector: model_source.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::UnsupportedFieldInSelectPermissionsPredicate { - field_name: field.clone(), - model_name: model.name.clone(), - })?; - - let (resolved_operator, argument_type) = resolve_binary_operator_for_model( - operator, - &model.name, - &model_source.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(_model_predicate) => { - Err(Error::NestedPredicateInSelectPermissionPredicate { - model_name: model.name.clone(), - }) - } - }?; - - Ok(models::ModelPredicate::BinaryFieldComparison { - field: field.clone(), - ndc_column: field_mapping.column.clone(), - operator: resolved_operator, - argument_type, - value: value_expression, - }) - } else { - Err(Error::ModelSourceRequiredForPredicate { - model_name: model.name.clone(), - }) - } - } - permissions::ModelPredicate::FieldIsNull(FieldIsNullPredicate { field }) => { - if let Some(model_source) = &model.source { - // 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 predicate field - let field_mapping = field_mappings.get(field).ok_or_else(|| { - Error::UnknownFieldInSelectPermissionsDefinition { - field_name: field.clone(), - model_name: model.name.clone(), - } - })?; - - Ok(models::ModelPredicate::UnaryFieldComparison { - field: field.clone(), - ndc_column: field_mapping.column.clone(), - operator: ndc_models::UnaryComparisonOperator::IsNull, - }) - } else { - Err(Error::ModelSourceRequiredForPredicate { - model_name: model.name.clone(), - }) - } - } - permissions::ModelPredicate::Relationship(RelationshipPredicate { name, predicate }) => { - if let Some(nested_predicate) = predicate { - let object_type_representation = get_model_object_type_representation( - object_types, - &model.data_type, - &model.name, - )?; - let relationship_field_name = mk_name(&name.0)?; - let relationship = &object_type_representation - .relationships - .get(&relationship_field_name) - .ok_or_else(|| Error::UnknownRelationshipInSelectPermissionsPredicate { - relationship_name: name.clone(), - model_name: model.name.clone(), - type_name: model.data_type.clone(), - })?; - - match &relationship.target { - relationships::RelationshipTarget::Command { .. } => { - Err(Error::UnsupportedFeature { - message: "Predicate cannot be built using command relationships" - .to_string(), - }) - } - relationships::RelationshipTarget::Model { - model_name, - relationship_type, - target_typename, - mappings, - } => { - let target_model = models.get(model_name).ok_or_else(|| { - Error::UnknownModelUsedInRelationshipSelectPermissionsPredicate { - model_name: model.name.clone(), - target_model_name: model_name.clone(), - relationship_name: name.clone(), - } - })?; - - // predicates with relationships is currently only supported for local relationships - if let (Some(target_model_source), Some(model_source)) = - (&target_model.source, &model.source) - { - if target_model_source.data_connector.name - == model_source.data_connector.name - { - let target_source = ModelTargetSource::from_model_source( - target_model_source, - relationship, - ) - .map_err(|_| Error::RelationshipError { - relationship_error: - RelationshipError::NoRelationshipCapabilitiesDefined { - relationship_name: relationship.name.clone(), - type_name: model.data_type.clone(), - data_connector_name: target_model_source - .data_connector - .name - .clone(), - }, - })?; - - let annotation = PredicateRelationshipAnnotation { - source_type: relationship.source.clone(), - relationship_name: relationship.name.clone(), - target_model_name: model_name.clone(), - target_source: target_source.clone(), - target_type: target_typename.clone(), - relationship_type: relationship_type.clone(), - mappings: mappings.clone(), - source_data_connector: model_source.data_connector.clone(), - source_type_mappings: model_source.type_mappings.clone(), - }; - - let target_model_predicate = resolve_model_predicate( - nested_predicate, - target_model, - // local relationships exists in the same subgraph as the source model - subgraph, - data_connectors, - &target_model.type_fields, - object_types, - models, - )?; - - Ok(models::ModelPredicate::Relationship { - relationship_info: annotation, - predicate: Box::new(target_model_predicate), - }) - } else { - Err(Error::UnsupportedFeature { - message: "Predicate cannot be built using remote relationships" - .to_string(), - }) - } - } else { - Err( - Error::ModelAndTargetSourceRequiredForRelationshipPredicate { - source_model_name: model.name.clone(), - target_model_name: target_model.name.clone(), - }, - ) - } - } - } - } else { - Err(Error::NoPredicateDefinedForRelationshipPredicate { - model_name: model.name.clone(), - relationship_name: name.clone(), - }) - } - } - permissions::ModelPredicate::Not(predicate) => { - let resolved_predicate = resolve_model_predicate( - predicate, - model, - subgraph, - data_connectors, - fields, - object_types, - models, - )?; - 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( - predicate, - model, - subgraph, - data_connectors, - fields, - object_types, - models, - )?); - } - 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( - predicate, - model, - subgraph, - data_connectors, - fields, - object_types, - models, - )?); - } - Ok(models::ModelPredicate::Or(resolved_predicates)) - } - } -} - -pub fn resolve_model_select_permissions( - model: &models::Model, - subgraph: &str, - model_permissions: &ModelPermissionsV1, - data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, - object_types: &HashMap, relationships::ObjectTypeWithRelationships>, - models: &IndexMap, models::Model>, - boolean_expression_types: &HashMap< - Qualified, - boolean_expressions::ObjectBooleanExpressionType, - >, - data_connector_type_mappings: &data_connector_type_mappings::DataConnectorTypeMappings, -) -> Result, Error> { - let mut validated_permissions = HashMap::new(); - for model_permission in &model_permissions.permissions { - if let Some(select) = &model_permission.select { - let resolved_predicate = match &select.filter { - NullableModelPredicate::NotNull(model_predicate) => resolve_model_predicate( - model_predicate, - model, - subgraph, - data_connectors, - &model.type_fields, - object_types, - models, - ) - .map(models::FilterPermission::Filter)?, - NullableModelPredicate::Null(()) => models::FilterPermission::AllowAll, - }; - - let mut argument_presets = BTreeMap::new(); - - for argument_preset in &select.argument_presets { - if argument_presets.contains_key(&argument_preset.argument) { - return Err(Error::DuplicateModelArgumentPreset { - model_name: model.name.clone(), - argument_name: argument_preset.argument.clone(), - }); - } - - match model.arguments.get(&argument_preset.argument) { - Some(argument) => { - let value_expression = resolve_value_expression_for_argument( - &argument_preset.argument, - &argument_preset.value, - &argument.argument_type, - subgraph, - object_types, - boolean_expression_types, - data_connectors, - data_connector_type_mappings, - )?; - - // additionally typecheck literals - // we do this outside the argument resolve so that we can emit a model-specific error - // on typechecking failure - typecheck::typecheck_value_expression( - &argument.argument_type, - &argument_preset.value, - ) - .map_err(|type_error| { - Error::ModelArgumentPresetTypeError { - model_name: model.name.clone(), - argument_name: argument_preset.argument.clone(), - type_error, - } - })?; - - argument_presets.insert( - argument_preset.argument.clone(), - (argument.argument_type.clone(), value_expression), - ); - } - None => { - return Err(Error::ModelArgumentPresetMismatch { - model_name: model.name.clone(), - argument_name: argument_preset.argument.clone(), - }); - } - } - } - - let resolved_permission = models::SelectPermission { - filter: resolved_predicate.clone(), - argument_presets, - }; - validated_permissions.insert(model_permission.role.clone(), resolved_permission); - } - } - Ok(validated_permissions) -} - -#[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)?, - )), - } -} - -/// 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, - relationships::ObjectTypeWithRelationships, - >, - data_type: &Qualified, - model_name: &Qualified, -) -> Result<&'s relationships::ObjectTypeWithRelationships, 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(), - }), - } -} diff --git a/v3/crates/engine/src/metadata/resolved/permission.rs b/v3/crates/engine/src/metadata/resolved/permission.rs index 736e495fb41..6d90379b11f 100644 --- a/v3/crates/engine/src/metadata/resolved/permission.rs +++ b/v3/crates/engine/src/metadata/resolved/permission.rs @@ -1,9 +1,9 @@ -use crate::metadata::resolved::stages::models; +use crate::metadata::resolved::stages::model_permissions; 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/stages/mod.rs b/v3/crates/engine/src/metadata/resolved/stages/mod.rs index 81e0951950d..3e8b798e76e 100644 --- a/v3/crates/engine/src/metadata/resolved/stages/mod.rs +++ b/v3/crates/engine/src/metadata/resolved/stages/mod.rs @@ -7,6 +7,7 @@ pub mod data_connector_scalar_types; pub mod data_connector_type_mappings; pub mod data_connectors; pub mod graphql_config; +pub mod model_permissions; pub mod models; pub mod relationships; pub mod roles; @@ -116,15 +117,21 @@ pub fn resolve(metadata: open_dds::Metadata) -> Result { &data_connector_type_mappings, )?; - resolve_metadata( + let models_with_permissions = model_permissions::resolve( &metadata_accessor, - &graphql_config, + &data_connectors, + &object_types_with_relationships, + &models, + &boolean_expression_types, &data_connector_type_mappings, + )?; + + resolve_metadata( + &graphql_config, object_types_with_relationships, &scalar_types, &boolean_expression_types, - &data_connectors, - models, + models_with_permissions, commands_with_permissions, ) } diff --git a/v3/crates/engine/src/metadata/resolved/stages/model_permissions/mod.rs b/v3/crates/engine/src/metadata/resolved/stages/model_permissions/mod.rs new file mode 100644 index 00000000000..60d47cea29e --- /dev/null +++ b/v3/crates/engine/src/metadata/resolved/stages/model_permissions/mod.rs @@ -0,0 +1,589 @@ +mod types; +use crate::metadata::resolved::permission::ValueExpression; +use crate::metadata::resolved::stages::{ + boolean_expressions, data_connector_scalar_types, data_connector_type_mappings, models, + relationships, +}; +use crate::metadata::resolved::typecheck; +use indexmap::IndexMap; +use open_dds::{models::ModelName, types::CustomTypeName}; +use std::collections::{BTreeMap, HashMap}; +pub use types::{FilterPermission, ModelPredicate, ModelWithPermissions, SelectPermission}; + +use crate::metadata::resolved::argument::resolve_value_expression_for_argument; +use crate::metadata::resolved::error::{Error, RelationshipError}; + +use crate::metadata::resolved::subgraph::{ + mk_qualified_type_name, Qualified, QualifiedBaseType, QualifiedTypeReference, +}; +use crate::metadata::resolved::types::mk_name; +use crate::schema::types::output_type::relationship::{ + ModelTargetSource, PredicateRelationshipAnnotation, +}; + +use ndc_models; +use open_dds::permissions::{FieldIsNullPredicate, NullableModelPredicate, RelationshipPredicate}; +use open_dds::{ + data_connector::DataConnectorName, + permissions::{ModelPermissionsV1, Role}, + types::{FieldName, OperatorName}, +}; + +/// resolve model permissions +pub fn resolve( + metadata_accessor: &open_dds::accessor::MetadataAccessor, + data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, + object_types: &HashMap, relationships::ObjectTypeWithRelationships>, + models: &IndexMap, models::Model>, + boolean_expression_types: &HashMap< + Qualified, + boolean_expressions::ObjectBooleanExpressionType, + >, + data_connector_type_mappings: &data_connector_type_mappings::DataConnectorTypeMappings, +) -> Result, ModelWithPermissions>, Error> { + let mut models_with_permissions: IndexMap, ModelWithPermissions> = models + .iter() + .map(|(model_name, model)| { + ( + model_name.clone(), + ModelWithPermissions { + model: model.clone(), + select_permissions: None, + }, + ) + }) + .collect(); + + // Note: Model permissions's predicate can include the relationship field, + // hence Model permissions should be resolved after the relationships of a + // model is resolved. + for open_dds::accessor::QualifiedObject { + subgraph, + object: permissions, + } in &metadata_accessor.model_permissions + { + let model_name = Qualified::new(subgraph.to_string(), permissions.model_name.clone()); + let model = models_with_permissions + .get_mut(&model_name) + .ok_or_else(|| Error::UnknownModelInModelSelectPermissions { + model_name: model_name.clone(), + })?; + + if model.select_permissions.is_none() { + let select_permissions = Some(resolve_model_select_permissions( + &model.model, + subgraph, + permissions, + data_connectors, + object_types, + models, // This is required to get the model for the relationship target + boolean_expression_types, + data_connector_type_mappings, + )?); + + model.select_permissions = select_permissions; + } else { + return Err(Error::DuplicateModelSelectPermission { + model_name: model_name.clone(), + }); + } + } + Ok(models_with_permissions) +} + +// helper function to resolve ndc types to dds type based on scalar type representations +pub(crate) fn resolve_ndc_type( + data_connector: &Qualified, + source_type: &ndc_models::Type, + scalars: &HashMap<&str, data_connector_scalar_types::ScalarTypeWithRepresentationInfo>, + subgraph: &str, +) -> Result { + match source_type { + ndc_models::Type::Named { name } => { + let scalar_type = + scalars + .get(name.as_str()) + .ok_or(Error::UnknownScalarTypeInDataConnector { + data_connector: data_connector.clone(), + scalar_type: name.clone(), + })?; + scalar_type + .representation + .clone() + .ok_or(Error::DataConnectorScalarRepresentationRequired { + data_connector: data_connector.clone(), + scalar_type: name.clone(), + }) + .map(|ty| QualifiedTypeReference { + underlying_type: QualifiedBaseType::Named(mk_qualified_type_name( + &ty, subgraph, + )), + nullable: false, + }) + } + ndc_models::Type::Nullable { underlying_type } => { + resolve_ndc_type(data_connector, underlying_type, scalars, subgraph).map(|ty| { + QualifiedTypeReference { + underlying_type: ty.underlying_type, + nullable: true, + } + }) + } + ndc_models::Type::Array { element_type } => { + resolve_ndc_type(data_connector, element_type, scalars, subgraph).map(|ty| { + QualifiedTypeReference { + underlying_type: QualifiedBaseType::List(Box::new(ty)), + nullable: false, + } + }) + } + ndc_models::Type::Predicate { .. } => Err(Error::PredicateTypesUnsupported), + } +} + +fn resolve_model_predicate( + model_predicate: &open_dds::permissions::ModelPredicate, + model: &models::Model, + subgraph: &str, + data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, + fields: &IndexMap, + object_types: &HashMap, relationships::ObjectTypeWithRelationships>, + models: &IndexMap, models::Model>, +) -> Result { + match model_predicate { + open_dds::permissions::ModelPredicate::FieldComparison( + open_dds::permissions::FieldComparisonPredicate { + field, + operator, + value, + }, + ) => { + // TODO: (anon) typecheck the value expression with the field + // TODO: resolve the "in" operator too (ndc_models::BinaryArrayComparisonOperator) + if let Some(model_source) = &model.source { + // 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 predicate field + let field_mapping = field_mappings.get(field).ok_or_else(|| { + Error::UnknownFieldInSelectPermissionsDefinition { + field_name: field.clone(), + model_name: model.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(&model_source.data_connector.name) + .ok_or(Error::UnknownModelDataConnector { + model_name: model.name.clone(), + data_connector: model_source.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::UnsupportedFieldInSelectPermissionsPredicate { + field_name: field.clone(), + model_name: model.name.clone(), + })?; + + let (resolved_operator, argument_type) = resolve_binary_operator_for_model( + operator, + &model.name, + &model_source.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(_model_predicate) => { + Err(Error::NestedPredicateInSelectPermissionPredicate { + model_name: model.name.clone(), + }) + } + }?; + + Ok(ModelPredicate::BinaryFieldComparison { + field: field.clone(), + ndc_column: field_mapping.column.clone(), + operator: resolved_operator, + argument_type, + value: value_expression, + }) + } else { + Err(Error::ModelSourceRequiredForPredicate { + model_name: model.name.clone(), + }) + } + } + open_dds::permissions::ModelPredicate::FieldIsNull(FieldIsNullPredicate { field }) => { + if let Some(model_source) = &model.source { + // 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 predicate field + let field_mapping = field_mappings.get(field).ok_or_else(|| { + Error::UnknownFieldInSelectPermissionsDefinition { + field_name: field.clone(), + model_name: model.name.clone(), + } + })?; + + Ok(ModelPredicate::UnaryFieldComparison { + field: field.clone(), + ndc_column: field_mapping.column.clone(), + operator: ndc_models::UnaryComparisonOperator::IsNull, + }) + } else { + Err(Error::ModelSourceRequiredForPredicate { + model_name: model.name.clone(), + }) + } + } + open_dds::permissions::ModelPredicate::Relationship(RelationshipPredicate { + name, + predicate, + }) => { + if let Some(nested_predicate) = predicate { + let object_type_representation = get_model_object_type_representation( + object_types, + &model.data_type, + &model.name, + )?; + let relationship_field_name = mk_name(&name.0)?; + let relationship = &object_type_representation + .relationships + .get(&relationship_field_name) + .ok_or_else(|| Error::UnknownRelationshipInSelectPermissionsPredicate { + relationship_name: name.clone(), + model_name: model.name.clone(), + type_name: model.data_type.clone(), + })?; + + match &relationship.target { + relationships::RelationshipTarget::Command { .. } => { + Err(Error::UnsupportedFeature { + message: "Predicate cannot be built using command relationships" + .to_string(), + }) + } + relationships::RelationshipTarget::Model { + model_name, + relationship_type, + target_typename, + mappings, + } => { + let target_model = models.get(model_name).ok_or_else(|| { + Error::UnknownModelUsedInRelationshipSelectPermissionsPredicate { + model_name: model.name.clone(), + target_model_name: model_name.clone(), + relationship_name: name.clone(), + } + })?; + + // predicates with relationships is currently only supported for local relationships + if let (Some(target_model_source), Some(model_source)) = + (&target_model.source, &model.source) + { + if target_model_source.data_connector.name + == model_source.data_connector.name + { + let target_source = ModelTargetSource::from_model_source( + target_model_source, + relationship, + ) + .map_err(|_| Error::RelationshipError { + relationship_error: + RelationshipError::NoRelationshipCapabilitiesDefined { + relationship_name: relationship.name.clone(), + type_name: model.data_type.clone(), + data_connector_name: target_model_source + .data_connector + .name + .clone(), + }, + })?; + + let annotation = PredicateRelationshipAnnotation { + source_type: relationship.source.clone(), + relationship_name: relationship.name.clone(), + target_model_name: model_name.clone(), + target_source: target_source.clone(), + target_type: target_typename.clone(), + relationship_type: relationship_type.clone(), + mappings: mappings.clone(), + source_data_connector: model_source.data_connector.clone(), + source_type_mappings: model_source.type_mappings.clone(), + }; + + let target_model_predicate = resolve_model_predicate( + nested_predicate, + target_model, + // local relationships exists in the same subgraph as the source model + subgraph, + data_connectors, + &target_model.type_fields, + object_types, + models, + )?; + + Ok(ModelPredicate::Relationship { + relationship_info: annotation, + predicate: Box::new(target_model_predicate), + }) + } else { + Err(Error::UnsupportedFeature { + message: "Predicate cannot be built using remote relationships" + .to_string(), + }) + } + } else { + Err( + Error::ModelAndTargetSourceRequiredForRelationshipPredicate { + source_model_name: model.name.clone(), + target_model_name: target_model.name.clone(), + }, + ) + } + } + } + } else { + Err(Error::NoPredicateDefinedForRelationshipPredicate { + model_name: model.name.clone(), + relationship_name: name.clone(), + }) + } + } + open_dds::permissions::ModelPredicate::Not(predicate) => { + let resolved_predicate = resolve_model_predicate( + predicate, + model, + subgraph, + data_connectors, + fields, + object_types, + models, + )?; + Ok(ModelPredicate::Not(Box::new(resolved_predicate))) + } + open_dds::permissions::ModelPredicate::And(predicates) => { + let mut resolved_predicates = Vec::new(); + for predicate in predicates { + resolved_predicates.push(resolve_model_predicate( + predicate, + model, + subgraph, + data_connectors, + fields, + object_types, + models, + )?); + } + Ok(ModelPredicate::And(resolved_predicates)) + } + open_dds::permissions::ModelPredicate::Or(predicates) => { + let mut resolved_predicates = Vec::new(); + for predicate in predicates { + resolved_predicates.push(resolve_model_predicate( + predicate, + model, + subgraph, + data_connectors, + fields, + object_types, + models, + )?); + } + Ok(ModelPredicate::Or(resolved_predicates)) + } + } +} + +#[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)?, + )), + } +} + +/// 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, + relationships::ObjectTypeWithRelationships, + >, + data_type: &Qualified, + model_name: &Qualified, +) -> Result<&'s relationships::ObjectTypeWithRelationships, 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 fn resolve_model_select_permissions( + model: &models::Model, + subgraph: &str, + model_permissions: &ModelPermissionsV1, + data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars, + object_types: &HashMap, relationships::ObjectTypeWithRelationships>, + models: &IndexMap, models::Model>, + boolean_expression_types: &HashMap< + Qualified, + boolean_expressions::ObjectBooleanExpressionType, + >, + data_connector_type_mappings: &data_connector_type_mappings::DataConnectorTypeMappings, +) -> Result, Error> { + let mut validated_permissions = HashMap::new(); + for model_permission in &model_permissions.permissions { + if let Some(select) = &model_permission.select { + let resolved_predicate = match &select.filter { + NullableModelPredicate::NotNull(model_predicate) => resolve_model_predicate( + model_predicate, + model, + subgraph, + data_connectors, + &model.type_fields, + object_types, + models, + ) + .map(FilterPermission::Filter)?, + NullableModelPredicate::Null(()) => FilterPermission::AllowAll, + }; + + let mut argument_presets = BTreeMap::new(); + + for argument_preset in &select.argument_presets { + if argument_presets.contains_key(&argument_preset.argument) { + return Err(Error::DuplicateModelArgumentPreset { + model_name: model.name.clone(), + argument_name: argument_preset.argument.clone(), + }); + } + + match model.arguments.get(&argument_preset.argument) { + Some(argument) => { + let value_expression = resolve_value_expression_for_argument( + &argument_preset.argument, + &argument_preset.value, + &argument.argument_type, + subgraph, + object_types, + boolean_expression_types, + data_connectors, + data_connector_type_mappings, + )?; + + // additionally typecheck literals + // we do this outside the argument resolve so that we can emit a model-specific error + // on typechecking failure + typecheck::typecheck_value_expression( + &argument.argument_type, + &argument_preset.value, + ) + .map_err(|type_error| { + Error::ModelArgumentPresetTypeError { + model_name: model.name.clone(), + argument_name: argument_preset.argument.clone(), + type_error, + } + })?; + + argument_presets.insert( + argument_preset.argument.clone(), + (argument.argument_type.clone(), value_expression), + ); + } + None => { + return Err(Error::ModelArgumentPresetMismatch { + model_name: model.name.clone(), + argument_name: argument_preset.argument.clone(), + }); + } + } + } + + let resolved_permission = SelectPermission { + filter: resolved_predicate.clone(), + argument_presets, + }; + validated_permissions.insert(model_permission.role.clone(), resolved_permission); + } + } + Ok(validated_permissions) +} diff --git a/v3/crates/engine/src/metadata/resolved/stages/model_permissions/types.rs b/v3/crates/engine/src/metadata/resolved/stages/model_permissions/types.rs new file mode 100644 index 00000000000..9b67580770b --- /dev/null +++ b/v3/crates/engine/src/metadata/resolved/stages/model_permissions/types.rs @@ -0,0 +1,55 @@ +use crate::metadata::resolved::permission::ValueExpression; +use crate::metadata::resolved::stages::models; + +use std::collections::{BTreeMap, HashMap}; + +use crate::metadata::resolved::subgraph::QualifiedTypeReference; + +use crate::schema::types::output_type::relationship::PredicateRelationshipAnnotation; + +use ndc_models; + +use open_dds::{arguments::ArgumentName, permissions::Role, types::FieldName}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct ModelWithPermissions { + pub model: models::Model, + pub select_permissions: Option>, +} + +#[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), +} diff --git a/v3/crates/engine/src/metadata/resolved/stages/models/mod.rs b/v3/crates/engine/src/metadata/resolved/stages/models/mod.rs index 49f859d6608..e1f6d390522 100644 --- a/v3/crates/engine/src/metadata/resolved/stages/models/mod.rs +++ b/v3/crates/engine/src/metadata/resolved/stages/models/mod.rs @@ -1,9 +1,8 @@ pub use types::{ - FilterPermission, LimitFieldGraphqlConfig, Model, ModelGraphQlApi, - ModelGraphqlApiArgumentsConfig, ModelOrderByExpression, ModelPredicate, ModelSource, - ModelsOutput, NDCFieldSourceMapping, OffsetFieldGraphqlConfig, OrderByExpressionInfo, - SelectManyGraphQlDefinition, SelectPermission, SelectUniqueGraphQlDefinition, - UniqueIdentifierField, + LimitFieldGraphqlConfig, Model, ModelGraphQlApi, ModelGraphqlApiArgumentsConfig, + ModelOrderByExpression, ModelSource, ModelsOutput, NDCFieldSourceMapping, + OffsetFieldGraphqlConfig, OrderByExpressionInfo, SelectManyGraphQlDefinition, + SelectUniqueGraphQlDefinition, UniqueIdentifierField, }; mod types; @@ -356,7 +355,6 @@ fn resolve_model( arguments, graphql_api: ModelGraphQlApi::default(), source: None, - select_permissions: None, global_id_source, apollo_federation_key_source, filter_expression_type, diff --git a/v3/crates/engine/src/metadata/resolved/stages/models/types.rs b/v3/crates/engine/src/metadata/resolved/stages/models/types.rs index 1ef8c42a941..7d6f93a9bcf 100644 --- a/v3/crates/engine/src/metadata/resolved/stages/models/types.rs +++ b/v3/crates/engine/src/metadata/resolved/stages/models/types.rs @@ -1,4 +1,3 @@ -use crate::metadata::resolved::permission::ValueExpression; use crate::metadata::resolved::stages::{ boolean_expressions, data_connector_type_mappings, data_connectors, }; @@ -8,17 +7,14 @@ use crate::metadata::resolved::subgraph::{ }; 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}; @@ -104,42 +100,6 @@ pub struct ModelSource { 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, @@ -149,7 +109,6 @@ pub struct Model { 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, diff --git a/v3/crates/engine/src/metadata/resolved/stages/roles/mod.rs b/v3/crates/engine/src/metadata/resolved/stages/roles/mod.rs index 514300c4016..376f3812816 100644 --- a/v3/crates/engine/src/metadata/resolved/stages/roles/mod.rs +++ b/v3/crates/engine/src/metadata/resolved/stages/roles/mod.rs @@ -7,12 +7,12 @@ use open_dds::{commands::CommandName, models::ModelName, types::CustomTypeName}; use crate::metadata::resolved::subgraph::Qualified; -use crate::metadata::resolved::stages::{command_permissions, models, relationships}; +use crate::metadata::resolved::stages::{command_permissions, model_permissions, relationships}; /// Gather all roles from various permission objects. pub fn resolve( object_types: &HashMap, relationships::ObjectTypeWithRelationships>, - models: &IndexMap, models::Model>, + models: &IndexMap, model_permissions::ModelWithPermissions>, commands: &IndexMap, command_permissions::CommandWithPermissions>, ) -> Vec { let mut roles = Vec::new(); diff --git a/v3/crates/engine/src/schema/apollo_federation.rs b/v3/crates/engine/src/schema/apollo_federation.rs index a88f006dc42..711e3aca7e0 100644 --- a/v3/crates/engine/src/schema/apollo_federation.rs +++ b/v3/crates/engine/src/schema/apollo_federation.rs @@ -22,10 +22,11 @@ pub fn apollo_federation_entities_schema( let entity_typename = mk_typename("_Entity")?; let mut entity_members = BTreeMap::new(); for model in gds.metadata.models.values() { - if model.apollo_federation_key_source.is_some() { - let object_type_representation = get_object_type_representation(gds, &model.data_type)?; + if model.model.apollo_federation_key_source.is_some() { + let object_type_representation = + get_object_type_representation(gds, &model.model.data_type)?; - let object_typename = get_custom_output_type(gds, builder, &model.data_type)?; + let object_typename = get_custom_output_type(gds, builder, &model.model.data_type)?; let entity_union_permissions = permissions::get_entity_union_permissions(object_type_representation); entity_members.insert( diff --git a/v3/crates/engine/src/schema/boolean_expression.rs b/v3/crates/engine/src/schema/boolean_expression.rs index 0daad04c320..4d72d7312c7 100644 --- a/v3/crates/engine/src/schema/boolean_expression.rs +++ b/v3/crates/engine/src/schema/boolean_expression.rs @@ -155,11 +155,11 @@ pub fn build_boolean_expression_input_schema( })?; let target_object_type_representation = - get_object_type_representation(gds, &target_model.data_type)?; + get_object_type_representation(gds, &target_model.model.data_type)?; // Build relationship field in filter expression only when // the target_model is backed by a source - if let Some(target_source) = &target_model.source { + if let Some(target_source) = &target_model.model.source { let target_model_source = ModelTargetSource::from_model_source(target_source, relationship)?; @@ -176,12 +176,12 @@ pub fn build_boolean_expression_input_schema( { // If the relationship target model does not have filterExpressionType do not include // it in the source model filter expression input type. - if let Some(ref target_model_filter_expression) = &target_model - .clone() - .filter_expression_type - .and_then(|ref boolean_expression_type| { - boolean_expression_type.clone().graphql - }) + if let Some(ref target_model_filter_expression) = + &target_model.model.clone().filter_expression_type.and_then( + |ref boolean_expression_type| { + boolean_expression_type.clone().graphql + }, + ) { let target_model_filter_expression_type_name = &target_model_filter_expression.type_name; @@ -191,7 +191,7 @@ pub fn build_boolean_expression_input_schema( relationship_name: relationship.name.clone(), target_source: target_model_source.clone(), target_type: target_typename.clone(), - target_model_name: target_model.name.clone(), + target_model_name: target_model.model.name.clone(), relationship_type: relationship_type.clone(), mappings: mappings.clone(), source_data_connector: boolean_expression_type diff --git a/v3/crates/engine/src/schema/model_arguments.rs b/v3/crates/engine/src/schema/model_arguments.rs index d069412b194..b579cc052b5 100644 --- a/v3/crates/engine/src/schema/model_arguments.rs +++ b/v3/crates/engine/src/schema/model_arguments.rs @@ -14,20 +14,21 @@ 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: &resolved::ModelWithPermissions, ) -> Result, crate::schema::Error> { model + .model .graphql_api .arguments_input_config .as_ref() .ok_or(crate::schema::Error::NoArgumentsInputConfigForSelectMany { - model_name: model.name.clone(), + model_name: model.model.name.clone(), }) .map(|arguments_input_config| { // This function call adds the model arguments to the // `args` input object builder.register_type(TypeId::ModelArgumentsInput { - model_name: model.name.clone(), + model_name: model.model.name.clone(), type_name: arguments_input_config.type_name.clone(), }); @@ -49,12 +50,13 @@ pub fn get_model_arguments_input_field( pub fn build_model_argument_fields( gds: &GDS, builder: &mut gql_schema::Builder, - model: &resolved::Model, + model: &resolved::ModelWithPermissions, ) -> Result< BTreeMap>>, crate::schema::Error, > { model + .model .arguments .iter() .map(|(argument_name, argument_type)| { @@ -68,6 +70,7 @@ pub fn build_model_argument_fields( ModelInputAnnotation::ModelArgument { argument_type: argument_type.argument_type.clone(), ndc_table_argument: model + .model .source .as_ref() .and_then(|model_source| { diff --git a/v3/crates/engine/src/schema/model_order_by.rs b/v3/crates/engine/src/schema/model_order_by.rs index a05ff90c728..a6b409d93ed 100644 --- a/v3/crates/engine/src/schema/model_order_by.rs +++ b/v3/crates/engine/src/schema/model_order_by.rs @@ -112,7 +112,7 @@ pub fn build_model_order_by_input_schema( model_name: model_name.clone(), })?; - let object_type_representation = get_object_type_representation(gds, &model.data_type)?; + let object_type_representation = get_object_type_representation(gds, &model.model.data_type)?; let mut fields = BTreeMap::new(); @@ -125,7 +125,7 @@ pub fn build_model_order_by_input_schema( model_name: model_name.clone(), })?; - if let Some(model_order_by_expression) = model.graphql_api.order_by_expression.as_ref() { + if let Some(model_order_by_expression) = model.model.graphql_api.order_by_expression.as_ref() { for (field_name, order_by_expression) in &model_order_by_expression.order_by_fields { let graphql_field_name = mk_name(field_name.clone().0.as_str())?; let input_type = @@ -171,12 +171,12 @@ pub fn build_model_order_by_input_schema( })?; let target_object_type_representation = - get_object_type_representation(gds, &target_model.data_type)?; + get_object_type_representation(gds, &target_model.model.data_type)?; // Build relationship field in filter expression only when both // the target_model and source model are backed by a source if let (Some(target_source), Some(model_source)) = - (&target_model.source, &model.source) + (&target_model.model.source, &model.model.source) { let target_model_source = ModelTargetSource::from_model_source(target_source, relationship)?; @@ -193,7 +193,7 @@ pub fn build_model_order_by_input_schema( // If the relationship target model does not have orderByExpressionType do not include // it in the source model order_by input type. if let Some(target_model_order_by_expression) = - target_model.graphql_api.order_by_expression.as_ref() + target_model.model.graphql_api.order_by_expression.as_ref() { let target_model_order_by_expression_type_name = &target_model_order_by_expression.order_by_type_name; diff --git a/v3/crates/engine/src/schema/permissions.rs b/v3/crates/engine/src/schema/permissions.rs index ea3f6146dac..de5d21766f7 100644 --- a/v3/crates/engine/src/schema/permissions.rs +++ b/v3/crates/engine/src/schema/permissions.rs @@ -15,7 +15,7 @@ use super::types::ArgumentNameAndPath; /// Build namespace annotation for select permissions pub(crate) fn get_select_permissions_namespace_annotations( - model: &models::Model, + model: &resolved::ModelWithPermissions, object_types: &HashMap, resolved::ObjectTypeWithRelationships>, ) -> Result>, schema::Error> { let mut permissions: HashMap> = model @@ -37,6 +37,7 @@ pub(crate) fn get_select_permissions_namespace_annotations( ( ArgumentNameAndPath { ndc_argument_name: model + .model .source .as_ref() .and_then(|model_source| { @@ -61,9 +62,9 @@ pub(crate) fn get_select_permissions_namespace_annotations( // them to model argument preset annotations as well. if there is no // source defined for the model, we don't generate these preset // annotations. - if let Some(model_source) = model.source.as_ref() { + if let Some(model_source) = model.model.source.as_ref() { let mut role_presets_map = HashMap::new(); - for (arg_name, arg_info) in &model.arguments { + for (arg_name, arg_info) in &model.model.arguments { // get the NDC argument name of this command source let ndc_argument_name = model_source.argument_mappings.get(arg_name).cloned(); @@ -98,7 +99,7 @@ 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: &models::Model, + model: &resolved::ModelWithPermissions, object_type_representation: &resolved::ObjectTypeWithRelationships, select_unique: &models::SelectUniqueGraphQlDefinition, object_types: &HashMap, resolved::ObjectTypeWithRelationships>, @@ -121,7 +122,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: &models::Model, + target_model: &resolved::ModelWithPermissions, source_object_type_representation: &resolved::ObjectTypeWithRelationships, target_object_type_representation: &resolved::ObjectTypeWithRelationships, mappings: &[resolved::RelationshipModelMapping], @@ -439,7 +440,7 @@ 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: &resolved::ObjectTypeWithRelationships, - model: &models::Model, + model: &resolved::ModelWithPermissions, ) -> HashMap { let mut permissions = HashMap::new(); @@ -478,7 +479,7 @@ 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: &resolved::ObjectTypeWithRelationships, - model: &models::Model, + model: &resolved::ModelWithPermissions, ) -> HashMap { let mut permissions = HashMap::new(); diff --git a/v3/crates/engine/src/schema/query_root.rs b/v3/crates/engine/src/schema/query_root.rs index 71adbf38013..0854be71a44 100644 --- a/v3/crates/engine/src/schema/query_root.rs +++ b/v3/crates/engine/src/schema/query_root.rs @@ -24,7 +24,7 @@ pub fn query_root_schema( ) -> Result, crate::schema::Error> { let mut fields = BTreeMap::new(); for model in gds.metadata.models.values() { - for select_unique in model.graphql_api.select_uniques.iter() { + for select_unique in model.model.graphql_api.select_uniques.iter() { let (field_name, field) = select_one::select_one_field( gds, builder, @@ -34,7 +34,7 @@ pub fn query_root_schema( )?; fields.insert(field_name, field); } - for select_many in model.graphql_api.select_many.iter() { + for select_many in model.model.graphql_api.select_many.iter() { let (field_name, field) = select_many::select_many_field( gds, builder, 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 eea72e22ce6..656d3ddeb08 100644 --- a/v3/crates/engine/src/schema/query_root/apollo_federation.rs +++ b/v3/crates/engine/src/schema/query_root/apollo_federation.rs @@ -40,25 +40,27 @@ pub(crate) fn apollo_federation_field( > = HashMap::new(); let mut typename_mappings = HashMap::new(); for model in gds.metadata.models.values() { - if let Some(apollo_federation_key_source) = &model.apollo_federation_key_source { - let output_typename = get_custom_output_type(gds, builder, &model.data_type)?; + if let Some(apollo_federation_key_source) = &model.model.apollo_federation_key_source { + let output_typename = get_custom_output_type(gds, builder, &model.model.data_type)?; - let object_type_representation = get_object_type_representation(gds, &model.data_type)?; + let object_type_representation = + get_object_type_representation(gds, &model.model.data_type)?; let entities_field_permissions = get_entities_field_namespace_permissions(object_type_representation, model); for (role, model_predicate) in entities_field_permissions.iter() { let role_type_permissions = roles_type_permissions.entry(role.clone()).or_default(); - role_type_permissions.insert(model.data_type.clone(), model_predicate.clone()); + role_type_permissions + .insert(model.model.data_type.clone(), model_predicate.clone()); } if typename_mappings .insert( output_typename.type_name().clone(), EntityFieldTypeNameMapping { - type_name: model.data_type.clone(), - model_source: model.source.clone(), + type_name: model.model.data_type.clone(), + model_source: model.model.source.clone(), key_fields_ndc_mapping: apollo_federation_key_source.ndc_mapping.clone(), }, ) 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 03b4ad6a5d4..d17bb79a289 100644 --- a/v3/crates/engine/src/schema/query_root/node_field.rs +++ b/v3/crates/engine/src/schema/query_root/node_field.rs @@ -56,25 +56,27 @@ pub(crate) fn relay_node_field( HashMap, resolved::FilterPermission>, > = HashMap::new(); for model in gds.metadata.models.values() { - if let Some(global_id_source) = &model.global_id_source { - let output_typename = get_custom_output_type(gds, builder, &model.data_type)?; + if let Some(global_id_source) = &model.model.global_id_source { + let output_typename = get_custom_output_type(gds, builder, &model.model.data_type)?; - let object_type_representation = get_object_type_representation(gds, &model.data_type)?; + let object_type_representation = + get_object_type_representation(gds, &model.model.data_type)?; let node_field_permissions = get_node_field_namespace_permissions(object_type_representation, model); for (role, model_predicate) in node_field_permissions.iter() { let role_type_permissions = roles_type_permissions.entry(role.clone()).or_default(); - role_type_permissions.insert(model.data_type.clone(), model_predicate.clone()); + role_type_permissions + .insert(model.model.data_type.clone(), model_predicate.clone()); } if typename_mappings .insert( output_typename.type_name().clone(), NodeFieldTypeNameMapping { - type_name: model.data_type.clone(), - model_source: model.source.clone(), + type_name: model.model.data_type.clone(), + model_source: model.model.source.clone(), global_id_fields_ndc_mapping: global_id_source.ndc_mapping.clone(), }, ) 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 fe0f8d4b1e6..81d8aca27f8 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: &resolved::ModelWithPermissions, ) -> Result< BTreeMap>>, crate::schema::Error, @@ -30,7 +30,7 @@ pub(crate) fn generate_select_many_arguments( let mut arguments = BTreeMap::new(); // insert limit argument - if let Some(limit_field) = &model.graphql_api.limit_field { + if let Some(limit_field) = &model.model.graphql_api.limit_field { let limit_argument = generate_int_input_argument( limit_field.field_name.as_str(), Annotation::Input(types::InputAnnotation::Model( @@ -44,7 +44,7 @@ pub(crate) fn generate_select_many_arguments( } // insert offset argument - if let Some(offset_field) = &model.graphql_api.offset_field { + if let Some(offset_field) = &model.model.graphql_api.offset_field { let offset_argument = generate_int_input_argument( offset_field.field_name.as_str(), Annotation::Input(types::InputAnnotation::Model( @@ -59,11 +59,11 @@ pub(crate) fn generate_select_many_arguments( } // generate and insert order_by argument - if let Some(order_by_expression_info) = &model.graphql_api.order_by_expression { + if let Some(order_by_expression_info) = &model.model.graphql_api.order_by_expression { let order_by_argument = { get_order_by_expression_input_field( builder, - model.name.clone(), + model.model.name.clone(), order_by_expression_info, ) }; @@ -75,7 +75,7 @@ pub(crate) fn generate_select_many_arguments( } // generate and insert where argument - if let Some(boolean_expression_type) = &model.filter_expression_type { + if let Some(boolean_expression_type) = &model.model.filter_expression_type { if let Some(boolean_expression) = &boolean_expression_type.graphql { let where_argument = get_where_expression_input_field( builder, @@ -97,7 +97,7 @@ pub(crate) fn generate_select_many_arguments( pub(crate) fn select_many_field( gds: &GDS, builder: &mut gql_schema::Builder, - model: &resolved::Model, + model: &resolved::ModelWithPermissions, select_many: &resolved::SelectManyGraphQlDefinition, parent_type: &ast::TypeName, ) -> Result< @@ -112,7 +112,7 @@ pub(crate) fn select_many_field( // Generate the `args` input object and add the model // arguments within it. - if !model.arguments.is_empty() { + if !model.model.arguments.is_empty() { let model_arguments_input = model_arguments::get_model_arguments_input_field(builder, model)?; @@ -136,7 +136,7 @@ pub(crate) fn select_many_field( } let field_type = ast::TypeContainer::list_null(ast::TypeContainer::named_non_null( - get_custom_output_type(gds, builder, &model.data_type)?, + get_custom_output_type(gds, builder, &model.model.data_type)?, )); let field = builder.conditional_namespaced( @@ -145,10 +145,10 @@ pub(crate) fn select_many_field( select_many.description.clone(), Annotation::Output(types::OutputAnnotation::RootField( types::RootFieldAnnotation::Model { - data_type: model.data_type.clone(), - source: model.source.clone(), + data_type: model.model.data_type.clone(), + source: model.model.source.clone(), kind: types::RootFieldKind::SelectMany, - name: model.name.clone(), + name: model.model.name.clone(), }, )), field_type, 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 b69d7339047..e53514a6d22 100644 --- a/v3/crates/engine/src/schema/query_root/select_one.rs +++ b/v3/crates/engine/src/schema/query_root/select_one.rs @@ -21,7 +21,7 @@ use crate::schema::{ pub(crate) fn select_one_field( gds: &GDS, builder: &mut gql_schema::Builder, - model: &resolved::Model, + model: &resolved::ModelWithPermissions, select_unique: &resolved::SelectUniqueGraphQlDefinition, parent_type: &ast::TypeName, ) -> Result< @@ -70,8 +70,8 @@ pub(crate) fn select_one_field( } } - let object_type_representation = get_object_type_representation(gds, &model.data_type)?; - let output_typename = get_custom_output_type(gds, builder, &model.data_type)?; + let object_type_representation = get_object_type_representation(gds, &model.model.data_type)?; + let output_typename = get_custom_output_type(gds, builder, &model.model.data_type)?; let field_annotations = permissions::get_select_one_namespace_annotations( model, @@ -86,10 +86,10 @@ pub(crate) fn select_one_field( select_unique.description.clone(), Annotation::Output(types::OutputAnnotation::RootField( types::RootFieldAnnotation::Model { - data_type: model.data_type.clone(), - source: model.source.clone(), + data_type: model.model.data_type.clone(), + source: model.model.source.clone(), kind: types::RootFieldKind::SelectOne, - name: model.name.clone(), + name: model.model.name.clone(), }, )), ast::TypeContainer::named_null(output_typename), diff --git a/v3/crates/engine/src/schema/relay.rs b/v3/crates/engine/src/schema/relay.rs index d704a7049ea..159c6d40dff 100644 --- a/v3/crates/engine/src/schema/relay.rs +++ b/v3/crates/engine/src/schema/relay.rs @@ -23,10 +23,11 @@ pub fn node_interface_schema( let mut roles_implementing_global_id: HashMap> = HashMap::new(); for model in gds.metadata.models.values() { - if model.global_id_source.is_some() { - let object_type_representation = get_object_type_representation(gds, &model.data_type)?; + if model.model.global_id_source.is_some() { + let object_type_representation = + get_object_type_representation(gds, &model.model.data_type)?; - let object_typename = get_custom_output_type(gds, builder, &model.data_type)?; + let object_typename = get_custom_output_type(gds, builder, &model.model.data_type)?; let node_interface_annotations = permissions::get_node_interface_annotations(object_type_representation); @@ -43,7 +44,7 @@ pub fn node_interface_schema( // Multiple models can be backed by the same type typename_global_id_mappings.insert( object_typename.type_name().clone(), - model.global_id_fields.clone(), + model.model.global_id_fields.clone(), ); } } diff --git a/v3/crates/engine/src/schema/types/output_type.rs b/v3/crates/engine/src/schema/types/output_type.rs index 885ddfbdc8c..32118e04920 100644 --- a/v3/crates/engine/src/schema/types/output_type.rs +++ b/v3/crates/engine/src/schema/types/output_type.rs @@ -316,7 +316,7 @@ fn object_type_fields( } })?; - if !model.arguments.is_empty() { + if !model.model.arguments.is_empty() { return Err(Error::InternalUnsupported { summary: "Relationships to models with arguments aren't supported".into(), }); @@ -330,7 +330,7 @@ fn object_type_fields( }; let target_object_type_representation = - get_object_type_representation(gds, &model.data_type)?; + get_object_type_representation(gds, &model.model.data_type)?; builder.conditional_namespaced( gql_schema::Field::::new( 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 6c0f2aa49d8..74be5b7ab97 100644 --- a/v3/crates/engine/src/schema/types/output_type/relationship.rs +++ b/v3/crates/engine/src/schema/types/output_type/relationship.rs @@ -94,10 +94,11 @@ pub struct ModelTargetSource { impl ModelTargetSource { pub fn new( - model: &models::Model, + model: &resolved::ModelWithPermissions, relationship: &resolved::Relationship, ) -> Result, schema::Error> { model + .model .source .as_ref() .map(|model_source| Self::from_model_source(model_source, relationship))