Separate models metadata resolve step (#519)

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

## 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
This commit is contained in:
Daniel Harvey 2024-04-29 13:53:37 +01:00 committed by hasura-bot
parent 0eb062d168
commit 8a9bfaaa6b
29 changed files with 1379 additions and 1292 deletions

View File

@ -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<CustomTypeName>,
model_source: &'s resolved::model::ModelSource,
model_source: &'s resolved::ModelSource,
arguments: BTreeMap<String, ndc_models::Argument>,
mut filter_clauses: ResolvedFilterExpression<'s>,
permissions_predicate: &'s resolved::model::FilterPermission,
permissions_predicate: &'s resolved::FilterPermission,
limit: Option<u32>,
offset: Option<u32>,
order_by: Option<ResolvedOrderBy<'s>>,
@ -61,8 +61,8 @@ pub(crate) fn model_selection_ir<'s>(
usage_counts: &mut UsagesCounts,
) -> Result<ModelSelection<'s>, 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(

View File

@ -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<NDCRelationshipName>,
relationships: &mut BTreeMap<NDCRelationshipName, LocalModelRelationshipInfo<'s>>,
usage_counts: &mut UsagesCounts,
) -> Result<ndc_models::Expression, Error> {
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::<Result<Vec<_>, 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::<Result<Vec<_>, Error>>()?;
Ok(ndc_models::Expression::Or { expressions: exprs })
}
resolved::model::ModelPredicate::Relationship {
resolved::ModelPredicate::Relationship {
relationship_info,
predicate,
} => {

View File

@ -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<resolved::model::ModelSource>,
source: &'s Option<resolved::ModelSource>,
data_type: &subgraph::Qualified<CustomTypeName>,
kind: &RootFieldKind,
field: &'n gql::normalized_ast::Field<'s, GDS>,

View File

@ -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<Qualified<CustomTypeName>, resolved::model::FilterPermission>,
&'s HashMapWithJsonKey<Qualified<CustomTypeName>, 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<CustomTypeName>,
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 {

View File

@ -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<Qualified<CustomTypeName>, resolved::model::FilterPermission>,
&'s HashMapWithJsonKey<Qualified<CustomTypeName>, 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<CustomTypeName>,
resolved::model::FilterPermission,
> = &get_relay_node_namespace_typename_mappings(field_call)?.0;
let typename_permissions: &'s HashMap<Qualified<CustomTypeName>, 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(),

View File

@ -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<open_dds::types::CustomTypeName>,
model_source: &'s resolved::model::ModelSource,
model_source: &'s resolved::ModelSource,
session_variables: &SessionVariables,
model_name: &'s Qualified<open_dds::models::ModelName>,
) -> Result<ModelSelectMany<'n, 's>, error::Error> {

View File

@ -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<open_dds::types::CustomTypeName>,
model_source: &'s resolved::model::ModelSource,
model_source: &'s resolved::ModelSource,
session_variables: &SessionVariables,
model_name: &'s Qualified<open_dds::models::ModelName>,
) -> Result<ModelSelectOne<'n, 's>, error::Error> {

View File

@ -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;

View File

@ -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<CustomTypeName>,
data_connector_field_mappings: &BTreeMap<FieldName, data_connector_type_mappings::FieldMapping>,
data_connector_name: &Qualified<DataConnectorName>,
subgraph: &str,
data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars,
fields: &IndexMap<FieldName, data_connector_type_mappings::FieldDefinition>,
) -> Result<models::ModelPredicate, Error> {
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<CustomTypeName>,
data_connector: &Qualified<DataConnectorName>,
field_name: &FieldName,
fields: &IndexMap<FieldName, data_connector_type_mappings::FieldDefinition>,
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)?,
)),
}
}

View File

@ -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<Qualified<CustomTypeName>, type_permissions::ObjectTypeWithPermissions>,
pub scalar_types: HashMap<Qualified<CustomTypeName>, scalar_types::ScalarTypeRepresentation>,
pub models: IndexMap<Qualified<ModelName>, Model>,
pub models: IndexMap<Qualified<ModelName>, models::Model>,
pub commands: IndexMap<Qualified<CommandName>, command::Command>,
pub boolean_expression_types:
HashMap<Qualified<CustomTypeName>, 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<ast::TypeName>,
mut global_id_enabled_types: HashMap<Qualified<CustomTypeName>, Vec<Qualified<ModelName>>>,
mut apollo_federation_entity_enabled_types: HashMap<
global_id_enabled_types: HashMap<Qualified<CustomTypeName>, Vec<Qualified<ModelName>>>,
apollo_federation_entity_enabled_types: HashMap<
Qualified<CustomTypeName>,
Option<Qualified<open_dds::models::ModelName>>,
>,
@ -56,22 +50,8 @@ pub fn resolve_metadata(
boolean_expressions::ObjectBooleanExpressionType,
>,
data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars,
mut models: IndexMap<Qualified<ModelName>, models::Model>,
) -> Result<Metadata, Error> {
// 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<Qualified<CustomTypeName>, type_permissions::ObjectTypeWithPermissions>,
models: &IndexMap<Qualified<ModelName>, Model>,
models: &IndexMap<Qualified<ModelName>, models::Model>,
commands: &IndexMap<Qualified<CommandName>, command::Command>,
) -> Vec<Role> {
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<Qualified<CustomTypeName>, type_permissions::ObjectTypeWithPermissions>,
scalar_types: &HashMap<Qualified<CustomTypeName>, scalar_types::ScalarTypeRepresentation>,
existing_graphql_types: &mut HashSet<ast::TypeName>,
global_id_enabled_types: &mut HashMap<Qualified<CustomTypeName>, Vec<Qualified<ModelName>>>,
apollo_federation_entity_enabled_types: &mut HashMap<
Qualified<CustomTypeName>,
Option<Qualified<open_dds::models::ModelName>>,
>,
boolean_expression_types: &HashMap<
Qualified<CustomTypeName>,
boolean_expressions::ObjectBooleanExpressionType,
>,
graphql_config: &graphql_config::GraphqlConfig,
) -> Result<IndexMap<Qualified<ModelName>, 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<CustomTypeName>,
type_permissions::ObjectTypeWithPermissions,
>,
models: &IndexMap<Qualified<ModelName>, Model>,
models: &IndexMap<Qualified<ModelName>, models::Model>,
commands: &IndexMap<Qualified<CommandName>, command::Command>,
) -> Result<HashMap<Qualified<CustomTypeName>, 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<Qualified<CustomTypeName>, type_permissions::ObjectTypeWithPermissions>,
models: &mut IndexMap<Qualified<ModelName>, Model>,
models: &mut IndexMap<Qualified<ModelName>, models::Model>,
boolean_expression_types: &HashMap<
Qualified<CustomTypeName>,
boolean_expressions::ObjectBooleanExpressionType,

File diff suppressed because it is too large Load Diff

View File

@ -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<ModelName>,
model: &Model,
model: &models::Model,
schema: &ndc_models::SchemaResponse,
) -> std::result::Result<(), NDCValidationError> {
let model_source = match &model.source {

View File

@ -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<ModelPredicate>),
BooleanExpression(Box<models::ModelPredicate>),
}

View File

@ -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<CustomTypeName>,
source_type: &type_permissions::ObjectTypeWithPermissions,
target_model: &Model,
target_model: &models::Model,
data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars,
) -> Result<Vec<RelationshipModelMapping>, 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<Qualified<ModelName>, Model>,
models: &IndexMap<Qualified<ModelName>, models::Model>,
commands: &IndexMap<Qualified<CommandName>, Command>,
data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars,
source_type: &type_permissions::ObjectTypeWithPermissions,

View File

@ -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,

View File

@ -24,7 +24,7 @@ pub(crate) fn resolve(
) -> Result<DataConnectorTypeMappingsOutput, Error> {
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,
})

View File

@ -80,7 +80,7 @@ impl DataConnectorTypeMappings {
/// output of `data_connector_type_mappings` step
pub struct DataConnectorTypeMappingsOutput {
pub existing_graphql_types: HashSet<ast::TypeName>,
pub graphql_types: HashSet<ast::TypeName>,
pub global_id_enabled_types: HashMap<Qualified<CustomTypeName>, Vec<Qualified<ModelName>>>,
pub apollo_federation_entity_enabled_types:
HashMap<Qualified<CustomTypeName>, Option<Qualified<open_dds::models::ModelName>>>,

View File

@ -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<Metadata, Error> {
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<Metadata, Error> {
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<Metadata, Error> {
&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<Metadata, Error> {
&scalar_types,
&boolean_expression_types,
&data_connectors,
models,
)
}

View File

@ -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<ast::TypeName>,
global_id_enabled_types: &HashMap<Qualified<CustomTypeName>, Vec<Qualified<ModelName>>>,
apollo_federation_entity_enabled_types: &HashMap<
Qualified<CustomTypeName>,
Option<Qualified<open_dds::models::ModelName>>,
>,
object_types: &HashMap<Qualified<CustomTypeName>, type_permissions::ObjectTypeWithPermissions>,
scalar_types: &HashMap<Qualified<CustomTypeName>, scalar_types::ScalarTypeRepresentation>,
boolean_expression_types: &HashMap<
Qualified<CustomTypeName>,
boolean_expressions::ObjectBooleanExpressionType,
>,
graphql_config: &graphql_config::GraphqlConfig,
) -> Result<ModelsOutput, Error> {
// 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<CustomTypeName>,
subgraph: &str,
boolean_expression_types: &HashMap<
Qualified<CustomTypeName>,
boolean_expressions::ObjectBooleanExpressionType,
>,
) -> Result<Option<boolean_expressions::ObjectBooleanExpressionType>, 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<FieldName, data_connector_type_mappings::FieldDefinition>,
) -> Result<Vec<OrderableField>, 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<Qualified<CustomTypeName>, type_permissions::ObjectTypeWithPermissions>,
global_id_enabled_types: &mut HashMap<Qualified<CustomTypeName>, Vec<Qualified<ModelName>>>,
apollo_federation_entity_enabled_types: &mut HashMap<
Qualified<CustomTypeName>,
Option<Qualified<ModelName>>,
>,
boolean_expression_types: &HashMap<
Qualified<CustomTypeName>,
boolean_expressions::ObjectBooleanExpressionType,
>,
) -> Result<Model, Error> {
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<ast::TypeName>,
data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars,
model_description: &Option<String>,
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<Option<ModelOrderByExpression>, 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<Qualified<CustomTypeName>, type_permissions::ObjectTypeWithPermissions>,
scalar_types: &HashMap<Qualified<CustomTypeName>, scalar_types::ScalarTypeRepresentation>,
data_connector_type_mappings: &data_connector_type_mappings::DataConnectorTypeMappings,
boolean_expression_types: &HashMap<
Qualified<CustomTypeName>,
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<CustomTypeName>,
type_permissions::ObjectTypeWithPermissions,
>,
data_type: &Qualified<CustomTypeName>,
model_name: &Qualified<ModelName>,
) -> 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<F: Fn() -> String>(
model_name: &Qualified<ModelName>,
model_data_type: &Qualified<CustomTypeName>,
model_source: &ModelSource,
field: &FieldName,
data_connectors: &data_connector_scalar_types::DataConnectorsWithScalars,
comparison_location: F,
) -> Result<NdcColumnForComparison, Error> {
// 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(),
})
}

View File

@ -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<Qualified<ModelName>, Model>,
pub graphql_types: HashSet<ast::TypeName>,
pub global_id_enabled_types: HashMap<Qualified<CustomTypeName>, Vec<Qualified<ModelName>>>,
pub apollo_federation_entity_enabled_types:
HashMap<Qualified<CustomTypeName>, Option<Qualified<open_dds::models::ModelName>>>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct UniqueIdentifierField {
pub field_type: QualifiedTypeReference,
pub ndc_column: Option<NdcColumnForComparison>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct SelectUniqueGraphQlDefinition {
pub query_root_field: ast::Name,
pub unique_identifier: IndexMap<FieldName, UniqueIdentifierField>,
pub description: Option<String>,
pub deprecated: Option<Deprecated>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct SelectManyGraphQlDefinition {
pub query_root_field: ast::Name,
pub description: Option<String>,
pub deprecated: Option<Deprecated>,
}
// 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<DataConnectorName>,
pub order_by_type_name: ast::TypeName,
pub order_by_fields: HashMap<FieldName, OrderByExpressionInfo>,
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<ModelGraphqlApiArgumentsConfig>,
pub select_uniques: Vec<SelectUniqueGraphQlDefinition>,
pub select_many: Option<SelectManyGraphQlDefinition>,
pub order_by_expression: Option<ModelOrderByExpression>,
pub limit_field: Option<LimitFieldGraphqlConfig>,
pub offset_field: Option<OffsetFieldGraphqlConfig>,
}
#[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<Qualified<CustomTypeName>, data_connector_type_mappings::TypeMapping>,
pub argument_mappings: HashMap<ArgumentName, String>,
}
#[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<ArgumentName, (QualifiedTypeReference, ValueExpression)>,
}
#[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<ModelPredicate>,
},
And(Vec<ModelPredicate>),
Or(Vec<ModelPredicate>),
Not(Box<ModelPredicate>),
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Model {
pub name: Qualified<ModelName>,
pub data_type: Qualified<CustomTypeName>,
pub type_fields: IndexMap<FieldName, data_connector_type_mappings::FieldDefinition>,
pub global_id_fields: Vec<FieldName>,
pub arguments: IndexMap<ArgumentName, ArgumentInfo>,
pub graphql_api: ModelGraphQlApi,
pub source: Option<ModelSource>,
pub select_permissions: Option<HashMap<Role, SelectPermission>>,
pub global_id_source: Option<NDCFieldSourceMapping>,
pub apollo_federation_key_source: Option<NDCFieldSourceMapping>,
pub filter_expression_type: Option<boolean_expressions::ObjectBooleanExpressionType>,
pub orderable_fields: Vec<OrderableField>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct NDCFieldSourceMapping {
pub ndc_mapping: HashMap<FieldName, NdcColumnForComparison>,
}

View File

@ -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<GDS>,
model: &resolved::model::Model,
model: &resolved::Model,
) -> Result<gql_schema::InputField<GDS>, 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<GDS>,
model: &resolved::model::Model,
model: &resolved::Model,
) -> Result<
BTreeMap<ast::Name, gql_schema::Namespaced<GDS, gql_schema::InputField<GDS>>>,
crate::schema::Error,

View File

@ -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<GDS>,
model_name: Qualified<ModelName>,
order_by_expression_info: &resolved::model::ModelOrderByExpression,
order_by_expression_info: &resolved::ModelOrderByExpression,
) -> gql_schema::InputField<GDS> {
gql_schema::InputField::new(
order_by_expression_info.order_by_field_name.clone(),

View File

@ -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<Qualified<CustomTypeName>, type_permissions::ObjectTypeWithPermissions>,
) -> Result<HashMap<Role, Option<types::NamespaceAnnotation>>, schema::Error> {
let mut permissions: HashMap<Role, Option<types::NamespaceAnnotation>> = 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<Qualified<CustomTypeName>, type_permissions::ObjectTypeWithPermissions>,
) -> Result<HashMap<Role, Option<types::NamespaceAnnotation>>, 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<Role, FilterPermission> {
model: &models::Model,
) -> HashMap<Role, resolved::FilterPermission> {
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<Role, FilterPermission> {
model: &models::Model,
) -> HashMap<Role, resolved::FilterPermission> {
let mut permissions = HashMap::new();
match &model.select_permissions {

View File

@ -36,7 +36,7 @@ pub(crate) fn apollo_federation_field(
) -> Result<ApolloFederationFieldOutput, crate::schema::Error> {
let mut roles_type_permissions: HashMap<
Role,
HashMap<Qualified<CustomTypeName>, resolved::model::FilterPermission>,
HashMap<Qualified<CustomTypeName>, resolved::FilterPermission>,
> = HashMap::new();
let mut typename_mappings = HashMap::new();
for model in gds.metadata.models.values() {

View File

@ -53,7 +53,7 @@ pub(crate) fn relay_node_field(
let mut roles_type_permissions: HashMap<
Role,
HashMap<Qualified<CustomTypeName>, resolved::model::FilterPermission>,
HashMap<Qualified<CustomTypeName>, resolved::FilterPermission>,
> = HashMap::new();
for model in gds.metadata.models.values() {
if let Some(global_id_source) = &model.global_id_source {

View File

@ -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<GDS>,
model: &resolved::model::Model,
model: &resolved::Model,
) -> Result<
BTreeMap<Name, gql_schema::Namespaced<GDS, gql_schema::InputField<GDS>>>,
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<GDS>,
model: &resolved::model::Model,
select_many: &resolved::model::SelectManyGraphQlDefinition,
model: &resolved::Model,
select_many: &resolved::SelectManyGraphQlDefinition,
parent_type: &ast::TypeName,
) -> Result<
(

View File

@ -21,8 +21,8 @@ use crate::schema::{
pub(crate) fn select_one_field(
gds: &GDS,
builder: &mut gql_schema::Builder<GDS>,
model: &resolved::model::Model,
select_unique: &resolved::model::SelectUniqueGraphQlDefinition,
model: &resolved::Model,
select_unique: &resolved::SelectUniqueGraphQlDefinition,
parent_type: &ast::TypeName,
) -> Result<
(

View File

@ -64,7 +64,7 @@ pub struct NodeFieldTypeNameMapping {
pub type_name: Qualified<types::CustomTypeName>,
// `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<resolved::model::ModelSource>,
pub model_source: Option<resolved::ModelSource>,
pub global_id_fields_ndc_mapping: HashMap<types::FieldName, NdcColumnForComparison>,
}
@ -73,7 +73,7 @@ pub struct EntityFieldTypeNameMapping {
pub type_name: Qualified<types::CustomTypeName>,
// `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<resolved::model::ModelSource>,
pub model_source: Option<resolved::ModelSource>,
pub key_fields_ndc_mapping: HashMap<types::FieldName, NdcColumnForComparison>,
}
@ -126,7 +126,7 @@ pub enum RootFieldAnnotation {
},
Model {
data_type: Qualified<types::CustomTypeName>,
source: Option<resolved::model::ModelSource>,
source: Option<resolved::ModelSource>,
// select_permissions: HashMap<Role, resolved::SelectPermission>,
kind: RootFieldKind,
name: Qualified<models::ModelName>,
@ -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<Qualified<types::CustomTypeName>, resolved::model::FilterPermission>,
HashMapWithJsonKey<Qualified<types::CustomTypeName>, 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<Qualified<types::CustomTypeName>, resolved::model::FilterPermission>,
HashMapWithJsonKey<Qualified<types::CustomTypeName>, resolved::FilterPermission>,
),
}

View File

@ -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<Option<Self>, 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<Self, schema::Error> {
Ok(Self {