support apollo federation (part 1) (#332)

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

## Description

This PR adds the capability to expose _entities and _service fields.
Apollo Federation uses these fields to extend types in other subgraphs
using the types from a subgraph. We can now use Hasura v3 as a subgraph
in an Apollo federated supergraph.

To make this possible, we had to do the following metadata changes:

- Add `apolloFederation` in `ObjectType.graphql`:
  ```yaml
  apolloFederation:
      keys:
        - fields:
          - id
  ```
- Add `apolloFederation` in `Model.graphql`:
  ```yaml
  apolloFederation:
      entitySource: true
  ```
- Add `apolloFederation` in `GraphqlConfig`:
  ```yaml
  apolloFederation:
      enableRootFields: true
  ```

https://github.com/hasura/v3-engine/assets/85472423/b3223c61-360a-4ed6-b8ab-b394469957ce

RFC:
https://github.com/hasura/graphql-engine/pull/10141/files#diff-a9f74850aca6ecad1556723e4b3c7395e59f26cd40d9de20bfef83cb9c64f028

## Changelog

- Add a changelog entry (in the "Changelog entry" section below) if the
changes in this PR have any user-facing impact. See [changelog
guide](https://github.com/hasura/graphql-engine-mono/wiki/Changelog-Guide).
- If no changelog is required ignore/remove this section and add a
`no-changelog-required` label to the PR.

### Product
_(Select all products this will be available in)_
- [ ] community-edition
- [ ] cloud
<!-- product : end : DO NOT REMOVE -->

### Type
<!-- See changelog structure:
https://github.com/hasura/graphql-engine-mono/wiki/Changelog-Guide#structure-of-our-changelog
-->
_(Select only one. In case of multiple, choose the most appropriate)_
- [ ] highlight
- [ ] enhancement
- [ ] bugfix
- [ ] behaviour-change
- [ ] performance-enhancement
- [ ] security-fix
<!-- type : end : DO NOT REMOVE -->

### Changelog entry
<!--
  - Add a user understandable changelog entry
- Include all details needed to understand the change. Try including
links to docs or issues if relevant
  - For Highlights start with a H4 heading (#### <entry title>)
  - Get the changelog entry reviewed by your team
-->

_Replace with changelog entry_

<!-- changelog-entry : end : DO NOT REMOVE -->

<!-- changelog : end : DO NOT REMOVE -->

V3_GIT_ORIGIN_REV_ID: 73cb9e6c8ef4bfe64d0f0cd9ce3ccbd32e208712
This commit is contained in:
paritosh-08 2024-03-22 07:59:03 +05:30 committed by hasura-bot
parent e931a391eb
commit 0642cbadaa
58 changed files with 2116 additions and 131 deletions

View File

@ -44,8 +44,11 @@ pub enum InternalDeveloperError {
#[error("Typecasting to array is not supported.")]
VariableArrayTypeCast,
#[error("Mapping for the Global ID typename {type_name:} not found")]
GlobalIDTypenameMappingNotFound { type_name: ast::TypeName },
#[error("Mapping for the {mapping_kind} typename {type_name:} not found")]
TypenameMappingNotFound {
type_name: ast::TypeName,
mapping_kind: &'static str,
},
#[error("Type mapping not found for the type name {type_name:} while executing the relationship {relationship_name:}")]
TypeMappingNotFoundForRelationship {
@ -173,6 +176,10 @@ pub enum Error {
ProjectIdConversionError(InvalidHeaderValue),
#[error("ndc validation error: {0}")]
NDCValidationError(NDCValidationError),
#[error("field '{field_name:} not found in entity representation")]
FieldNotFoundInEntityRepresentation { field_name: String },
#[error("field '{field_name:} not found in _Service")]
FieldNotFoundInService { field_name: String },
}
impl Error {

View File

@ -1,6 +1,8 @@
use super::remote_joins::types::{JoinNode, RemoteJoinType};
use super::ExecuteOrExplainResponse;
use crate::execute::plan::{NodeMutationPlan, NodeQueryPlan, ProcessResponseAs};
use crate::execute::plan::{
ApolloFederationSelect, NodeMutationPlan, NodeQueryPlan, ProcessResponseAs,
};
use crate::execute::remote_joins::types::{JoinId, JoinLocations, RemoteJoin};
use crate::execute::{error, plan};
use crate::metadata::resolved;
@ -86,17 +88,37 @@ pub(crate) async fn explain_query_plan(
.await;
parallel_root_steps.push(Box::new(types::Step::Sequence(sequence_steps)));
}
NodeQueryPlan::TypeName { .. } => {
return Err(error::Error::ExplainError(
"cannot explain introspection queries".to_string(),
));
NodeQueryPlan::ApolloFederationSelect(ApolloFederationSelect::EntitiesSelect(
parallel_ndc_query_executions,
)) => {
let mut parallel_steps = Vec::new();
for ndc_query_execution in parallel_ndc_query_executions {
let sequence_steps = get_execution_steps(
http_client,
alias.clone(),
&ndc_query_execution.process_response_as,
ndc_query_execution.execution_tree.remote_executions,
types::NDCRequest::Query(
ndc_query_execution.execution_tree.root_node.query,
),
ndc_query_execution.execution_tree.root_node.data_connector,
)
.await;
parallel_steps.push(Box::new(types::Step::Sequence(sequence_steps)));
}
match NonEmpty::from_vec(parallel_steps) {
None => {}
Some(parallel_steps) => {
parallel_root_steps.push(Box::new(types::Step::Parallel(parallel_steps)));
}
}
}
NodeQueryPlan::SchemaField { .. } => {
return Err(error::Error::ExplainError(
"cannot explain introspection queries".to_string(),
));
}
NodeQueryPlan::TypeField { .. } => {
NodeQueryPlan::TypeName { .. }
| NodeQueryPlan::SchemaField { .. }
| NodeQueryPlan::TypeField { .. }
| NodeQueryPlan::ApolloFederationSelect(ApolloFederationSelect::ServiceField {
..
}) => {
return Err(error::Error::ExplainError(
"cannot explain introspection queries".to_string(),
));

View File

@ -32,6 +32,7 @@ pub(crate) fn get_select_filter_predicate<'s>(
.and_then(|annotation| match annotation {
types::NamespaceAnnotation::Model { filter, .. } => Some(filter),
types::NamespaceAnnotation::NodeFieldTypeMappings(_) => None,
types::NamespaceAnnotation::EntityTypeMappings(_) => None,
types::NamespaceAnnotation::Command(_) => None,
})
// If we're hitting this case, it means that the caller of this

View File

@ -13,7 +13,9 @@ use super::root_field;
use crate::execute::error;
use crate::metadata::resolved::subgraph::QualifiedTypeReference;
use crate::metadata::resolved::{self, subgraph};
use crate::schema::types::ApolloFederationRootFields;
use crate::schema::types::CommandSourceDetail;
use crate::schema::types::EntityFieldTypeNameMapping;
use crate::schema::types::RootFieldKind;
use crate::schema::types::TypeKind;
use crate::schema::types::{
@ -21,6 +23,7 @@ use crate::schema::types::{
};
use crate::schema::{mk_typename, GDS};
pub mod apollo_federation;
pub mod node_field;
pub mod select_many;
pub mod select_one;
@ -95,6 +98,26 @@ pub fn generate_ir<'n, 's>(
)?;
Ok(ir)
}
RootFieldAnnotation::ApolloFederation(
ApolloFederationRootFields::Entities { typename_mappings },
) => {
let ir = generate_entities_ir(
field,
field_call,
typename_mappings,
session,
)?;
Ok(ir)
}
RootFieldAnnotation::ApolloFederation(
ApolloFederationRootFields::Service,
) => Ok(root_field::QueryRootField::ApolloFederation(
root_field::ApolloFederationRootFields::ServiceField {
selection_set: &field.selection_set,
schema,
role: session.role.clone(),
},
)),
_ => Err(error::Error::from(
error::InternalEngineError::UnexpectedAnnotation {
annotation: annotation.clone(),
@ -244,3 +267,20 @@ fn generate_nodefield_ir<'n, 's>(
)?);
Ok(ir)
}
fn generate_entities_ir<'n, 's>(
field: &'n gql::normalized_ast::Field<'s, GDS>,
field_call: &'n gql::normalized_ast::FieldCall<'s, GDS>,
typename_mappings: &'s HashMap<ast::TypeName, EntityFieldTypeNameMapping>,
session: &Session,
) -> Result<root_field::QueryRootField<'n, 's>, error::Error> {
let ir = root_field::QueryRootField::ApolloFederation(
root_field::ApolloFederationRootFields::EntitiesSelect(apollo_federation::entities_ir(
field,
field_call,
typename_mappings,
&session.variables,
)?),
);
Ok(ir)
}

View File

@ -0,0 +1,189 @@
use hasura_authn_core::SessionVariables;
use lang_graphql::{ast::common as ast, normalized_ast};
use ndc_client as ndc;
use open_dds::types::CustomTypeName;
use serde::Serialize;
use std::collections::{BTreeMap, HashMap};
use crate::execute::error;
use crate::execute::ir::filter::ResolvedFilterExpression;
use crate::execute::ir::model_selection;
use crate::execute::model_tracking::UsagesCounts;
use crate::metadata::resolved;
use crate::metadata::resolved::subgraph::Qualified;
use crate::metadata::resolved::types::mk_name;
use crate::schema::types::{EntityFieldTypeNameMapping, NamespaceAnnotation};
use crate::schema::GDS;
use crate::utils::HashMapWithJsonKey;
/// IR for the '_entities' operation for a model
#[derive(Serialize, Debug)]
pub struct EntitySelect<'n, 's> {
// The name of the field as published in the schema
pub field_name: &'n ast::Name,
/// Model Selection IR fragment
pub model_selection: model_selection::ModelSelection<'s>,
// We need this for validating the response from the data connector. This is not a reference as it is constructed
// from the original selection set by filtering fields that are relevant.
pub selection_set: normalized_ast::SelectionSet<'s, GDS>,
// All the models/commands used in this operation. This includes the models/commands used via relationships. And in
// future, the models/commands used in the filter clause
pub(crate) usage_counts: UsagesCounts,
}
fn get_entity_namespace_typename_mappings<'s>(
field_call: &normalized_ast::FieldCall<'s, GDS>,
) -> Result<
&'s HashMapWithJsonKey<Qualified<CustomTypeName>, resolved::model::FilterPermission>,
error::Error,
> {
field_call
.info
.namespaced
.as_ref()
.and_then(|annotation| match annotation {
NamespaceAnnotation::EntityTypeMappings(type_mappings) => Some(type_mappings),
_ => None,
})
.ok_or(error::Error::InternalError(error::InternalError::Engine(
error::InternalEngineError::ExpectedNamespaceAnnotationNotFound {
namespace_annotation_type: "Entity type mappings".to_string(),
},
)))
}
/// Generate the NDC IR for the entities root field.
///
/// This function generates the NDC IR for the entities root field. The entities query looks something like:
///
/// ```graphql
/// query MyQuery($representations: [_Any!]!) {
/// _entities(representations: $representations) {
/// ... on album {
/// AlbumId
/// }
/// ... on Article {
/// id
/// }
/// }
/// }
/// ```
/// The `representations` argument is a list of objects with a `__typename` field and the fields that are used to filter
/// the entities. The `__typename` field is used to determine the type of the entity and the fields are used to filter
/// the entities.
pub(crate) fn entities_ir<'n, 's>(
field: &'n normalized_ast::Field<'s, GDS>,
field_call: &'n normalized_ast::FieldCall<'s, GDS>,
typename_mappings: &'s HashMap<ast::TypeName, EntityFieldTypeNameMapping>,
session_variables: &SessionVariables,
) -> Result<Vec<EntitySelect<'n, 's>>, error::Error> {
let representations = field_call
.expected_argument(&lang_graphql::mk_name!("representations"))?
.value
.as_list()?;
let mut entity_selects = vec![];
for representation in representations {
let json_representation = representation.as_json();
let representation = json_representation.as_object().ok_or(
lang_graphql::normalized_ast::Error::UnexpectedValue {
expected_kind: "OBJECT",
found: serde_json::to_value(representation),
},
)?;
// The __typename field is used to determine the type of the entity
let typename_str = representation
.get("__typename")
.ok_or(error::Error::FieldNotFoundInEntityRepresentation {
field_name: "__typename".to_string(),
})?
.as_str()
.ok_or(lang_graphql::normalized_ast::Error::UnexpectedValue {
expected_kind: "OBJECT",
found: serde_json::to_value(representation),
})?;
let typename = ast::TypeName(mk_name(typename_str).map_err(|_| {
error::Error::TypeFieldInvalidGraphQlName {
name: typename_str.to_string(),
}
})?);
// Get the permissions for the typename
let typename_permissions: &'s HashMap<
Qualified<CustomTypeName>,
resolved::model::FilterPermission,
> = &get_entity_namespace_typename_mappings(field_call)?.0;
let typename_mapping = typename_mappings.get(&typename).ok_or(
error::InternalDeveloperError::TypenameMappingNotFound {
type_name: typename.clone(),
mapping_kind: "entity key field",
},
)?;
let role_model_select_permission = typename_permissions.get(&typename_mapping.type_name);
// If the typename has a permission, then we can proceed to generate the NDC IR for the entity
if let Some(role_model_select_permission) = role_model_select_permission {
// Get the model source for the entity
let model_source = typename_mapping.model_source.as_ref().ok_or(
error::InternalDeveloperError::NoSourceDataConnector {
type_name: typename.clone(),
field_name: lang_graphql::mk_name!("_entities"),
},
)?;
// Generate the filter clause for the entity
let filter_clause_expressions = typename_mapping
.key_fields_ndc_mapping
.iter()
.map(|(field_name, field_mapping)| {
// Get the value of the field from the representation
let val = representation.get(&field_name.0 .0).ok_or(
error::Error::FieldNotFoundInEntityRepresentation {
field_name: field_name.0 .0.clone(),
},
)?;
Ok(ndc::models::Expression::BinaryComparisonOperator {
column: ndc::models::ComparisonTarget::Column {
name: field_mapping.column.clone(),
path: vec![], // We don't support nested fields in the key fields, so the path is empty
},
operator: field_mapping.equal_operator.clone(),
value: ndc::models::ComparisonValue::Scalar { value: val.clone() },
})
})
.collect::<Result<_, error::Error>>()?;
// Filter the selection set to only include fields that are relevant to the entity
let new_selection_set = field.selection_set.filter_field_calls_by_typename(typename);
let filter_clauses = ResolvedFilterExpression {
expressions: filter_clause_expressions,
relationships: BTreeMap::new(),
};
let mut usage_counts = UsagesCounts::new();
let model_selection = model_selection::model_selection_ir(
&new_selection_set,
&typename_mapping.type_name,
model_source,
BTreeMap::new(),
filter_clauses,
role_model_select_permission,
None, // limit
None, // offset
None, // order_by
session_variables,
// Get all the models/commands that were used as relationships
&mut usage_counts,
)?;
entity_selects.push(EntitySelect {
field_name: &field_call.name,
model_selection,
selection_set: new_selection_set,
usage_counts,
});
}
}
Ok(entity_selects)
}

View File

@ -94,8 +94,9 @@ pub(crate) fn relay_node_ir<'n, 's>(
resolved::model::FilterPermission,
> = &get_relay_node_namespace_typename_mappings(field_call)?.0;
let typename_mapping = typename_mappings.get(&global_id.typename).ok_or(
error::InternalDeveloperError::GlobalIDTypenameMappingNotFound {
error::InternalDeveloperError::TypenameMappingNotFound {
type_name: global_id.typename.clone(),
mapping_kind: "Global ID",
},
)?;
let role_model_select_permission = typename_permissions.get(&typename_mapping.type_name);

View File

@ -6,7 +6,7 @@ use serde::Serialize;
use super::{
commands,
query_root::{node_field, select_many, select_one},
query_root::{apollo_federation, node_field, select_many, select_one},
};
use crate::schema::GDS;
@ -54,6 +54,20 @@ pub enum QueryRootField<'n, 's> {
selection_set: &'n gql::normalized_ast::SelectionSet<'s, GDS>,
ir: commands::FunctionBasedCommand<'s>,
},
// Apollo Federation related root fields
ApolloFederation(ApolloFederationRootFields<'n, 's>),
}
#[derive(Serialize, Debug)]
pub enum ApolloFederationRootFields<'n, 's> {
// Operation that selects entities according to the Apollo Federation spec
EntitiesSelect(Vec<apollo_federation::EntitySelect<'n, 's>>),
// Operation for the _service field (returns the schema in SDL format)
ServiceField {
selection_set: &'n gql::normalized_ast::SelectionSet<'s, GDS>,
schema: &'s gql::schema::Schema<GDS>,
role: Role,
},
}
/// IR of a mutation root field

View File

@ -55,6 +55,9 @@ pub fn get_all_usage_counts_in_query(ir: &IndexMap<Alias, RootField<'_, '_>>) ->
root_field::QueryRootField::TypeName { .. } => {}
root_field::QueryRootField::SchemaField { .. } => {}
root_field::QueryRootField::TypeField { .. } => {}
root_field::QueryRootField::ApolloFederation(
root_field::ApolloFederationRootFields::ServiceField { .. },
) => {}
root_field::QueryRootField::ModelSelectOne { ir, .. } => {
let usage_counts = ir.usage_counts.clone();
extend_usage_count(usage_counts, &mut all_usage_counts);
@ -74,6 +77,14 @@ pub fn get_all_usage_counts_in_query(ir: &IndexMap<Alias, RootField<'_, '_>>) ->
let usage_counts = ir.command_info.usage_counts.clone();
extend_usage_count(usage_counts, &mut all_usage_counts);
}
root_field::QueryRootField::ApolloFederation(
root_field::ApolloFederationRootFields::EntitiesSelect(irs),
) => {
for ir in irs {
let usage_counts = ir.usage_counts.clone();
extend_usage_count(usage_counts, &mut all_usage_counts);
}
}
},
root_field::RootField::MutationRootField(rf) => match rf {
root_field::MutationRootField::TypeName { .. } => {}

View File

@ -57,6 +57,8 @@ pub enum NodeQueryPlan<'n, 's, 'ir> {
NDCQueryExecution(NDCQueryExecution<'s, 'ir>),
/// NDC query for Relay 'node' to be executed
RelayNodeSelect(Option<NDCQueryExecution<'s, 'ir>>),
/// Apollo Federation query to be executed
ApolloFederationSelect(ApolloFederationSelect<'n, 's, 'ir>),
}
/// Mutation plan of individual root field or node.
@ -80,6 +82,16 @@ pub struct NDCQueryExecution<'s, 'ir> {
pub selection_set: &'ir normalized_ast::SelectionSet<'s, GDS>,
}
#[derive(Debug)]
pub enum ApolloFederationSelect<'n, 's, 'ir> {
/// NDC queries for Apollo Federation '_entities' to be executed
EntitiesSelect(Vec<NDCQueryExecution<'s, 'ir>>),
ServiceField {
sdl: String,
selection_set: &'n normalized_ast::SelectionSet<'s, GDS>,
},
}
#[derive(Debug)]
pub struct NDCMutationExecution<'n, 's, 'ir> {
pub query: ndc_client::models::MutationRequest,
@ -298,6 +310,37 @@ fn plan_query<'n, 's, 'ir>(
},
})
}
root_field::QueryRootField::ApolloFederation(
root_field::ApolloFederationRootFields::EntitiesSelect(irs),
) => {
let mut ndc_query_executions = Vec::new();
for ir in irs {
let execution_tree = generate_execution_tree(&ir.model_selection)?;
ndc_query_executions.push(NDCQueryExecution {
execution_tree,
selection_set: &ir.selection_set,
execution_span_attribute: "execute_entity".into(),
field_span_attribute: "entity".into(),
process_response_as: ProcessResponseAs::Object { is_nullable: true },
});
}
NodeQueryPlan::ApolloFederationSelect(ApolloFederationSelect::EntitiesSelect(
ndc_query_executions,
))
}
root_field::QueryRootField::ApolloFederation(
root_field::ApolloFederationRootFields::ServiceField {
schema,
selection_set,
role,
},
) => {
let sdl = schema.generate_sdl(role);
NodeQueryPlan::ApolloFederationSelect(ApolloFederationSelect::ServiceField {
sdl,
selection_set,
})
}
};
Ok(query_plan)
}
@ -552,9 +595,82 @@ async fn execute_query_field_plan<'n, 's, 'ir>(
&optional_query.as_ref().map_or(true, |ndc_query| {
ndc_query.process_response_as.is_nullable()
}),
resolve_relay_node_select(http_client, optional_query, project_id)
resolve_optional_ndc_select(http_client, optional_query, project_id)
.await,
),
NodeQueryPlan::ApolloFederationSelect(
ApolloFederationSelect::EntitiesSelect(entity_execution_plans),
) => {
let mut tasks: Vec<_> =
Vec::with_capacity(entity_execution_plans.capacity());
for query in entity_execution_plans.into_iter() {
// We are not running the field plans parallely here, we are just running them concurrently on a single thread.
// To run the field plans parallely, we will need to use tokio::spawn for each field plan.
let task = async {
(resolve_optional_ndc_select(
http_client,
Some(query),
project_id.clone(),
)
.await,)
};
tasks.push(task);
}
let executed_entities = futures::future::join_all(tasks).await;
let mut entities_result = Vec::new();
for result in executed_entities {
match result {
(Ok(value),) => entities_result.push(value),
(Err(e),) => {
return RootFieldResult::new(&true, Err(e));
}
}
}
RootFieldResult::new(&true, Ok(json::Value::Array(entities_result)))
}
NodeQueryPlan::ApolloFederationSelect(
ApolloFederationSelect::ServiceField { sdl, selection_set },
) => {
let service_result = {
let mut object_fields = Vec::new();
for (alias, field) in &selection_set.fields {
let field_call = match field.field_call() {
Ok(field_call) => field_call,
Err(e) => {
return RootFieldResult::new(&true, Err(e.into()))
}
};
match field_call.name.as_str() {
"sdl" => {
let extended_sdl = "extend schema\n @link(url: \"https://specs.apollo.dev/federation/v2.0\", import: [\"@key\", \"@extends\", \"@external\", \"@shareable\"])\n\n".to_string() + &sdl;
object_fields.push((
alias.to_string(),
json::Value::String(extended_sdl),
));
}
"__typename" => {
object_fields.push((
alias.to_string(),
json::Value::String("_Service".to_string()),
));
}
field_name => {
return RootFieldResult::new(
&true,
Err(error::Error::FieldNotFoundInService {
field_name: field_name.to_string(),
}),
)
}
};
}
Ok(json::Value::Object(object_fields.into_iter().collect()))
};
RootFieldResult::new(&true, service_result)
}
}
})
},
@ -764,7 +880,7 @@ async fn resolve_ndc_mutation_execution(
Ok(json::to_value(response)?)
}
async fn resolve_relay_node_select(
async fn resolve_optional_ndc_select(
http_client: &reqwest::Client,
optional_query: Option<NDCQueryExecution<'_, '_>>,
project_id: Option<ProjectId>,

View File

@ -101,8 +101,9 @@ where
}
OutputAnnotation::RelayNodeInterfaceID { typename_mappings } => {
let global_id_fields = typename_mappings.get(type_name).ok_or(
error::InternalDeveloperError::GlobalIDTypenameMappingNotFound {
error::InternalDeveloperError::TypenameMappingNotFound {
type_name: type_name.clone(),
mapping_kind: "Global ID",
},
)?;

View File

@ -45,6 +45,10 @@ pub enum Error {
GlobalIdSourceNotDefined {
object_type: Qualified<CustomTypeName>,
},
#[error("'apolloFederation.keys' for type {object_type:} found, but no model found with 'apolloFederation.entitySource: true' for type {object_type:}")]
ApolloFederationEntitySourceNotDefined {
object_type: Qualified<CustomTypeName>,
},
#[error("the data type {data_type:} for model {model_name:} has not been defined")]
UnknownModelDataType {
model_name: Qualified<ModelName>,
@ -205,6 +209,25 @@ pub enum Error {
model_name: Qualified<ModelName>,
field_name: FieldName,
},
#[error("unknown field {field_name:} in apollo federation keys defined for the object type {object_type:}")]
UnknownFieldInApolloFederationKey {
field_name: FieldName,
object_type: Qualified<CustomTypeName>,
},
#[error(
"empty keys in apollo federation configuration defined for the object type {object_type:}"
)]
EmptyKeysInApolloFederationConfigForObject {
object_type: Qualified<CustomTypeName>,
},
#[error("empty fields in apollo federation keys defined for the object type {object_type:}")]
EmptyFieldsInApolloFederationConfigForObject {
object_type: Qualified<CustomTypeName>,
},
#[error("multiple models are marked as entity source for the object type {type_name:}")]
MultipleEntitySourcesForType {
type_name: Qualified<CustomTypeName>,
},
#[error("duplicate field {field_name:} in unique identifier defined for model {model_name:}")]
DuplicateFieldInUniqueIdentifier {
model_name: Qualified<ModelName>,
@ -468,6 +491,11 @@ pub enum Error {
type_name: Qualified<CustomTypeName>,
model_name: ModelName,
},
#[error("Model {model_name:} is marked as an Apollo Federation entity source but there are no keys fields present in the related object type {type_name:}")]
NoKeysFieldsPresentInEntitySource {
type_name: Qualified<CustomTypeName>,
model_name: ModelName,
},
#[error("A field named `id` cannot be present in the object type {type_name} when global_id fields are non-empty.")]
IdFieldConflictingGlobalId {
type_name: Qualified<CustomTypeName>,
@ -549,6 +577,10 @@ pub enum Error {
},
#[error("model {model_name:} with arguments is unsupported as a global ID source")]
ModelWithArgumentsAsGlobalIdSource { model_name: Qualified<ModelName> },
#[error(
"model {model_name:} with arguments is unsupported as an Apollo Federation entity source"
)]
ModelWithArgumentsAsApolloFederationEntitySource { model_name: Qualified<ModelName> },
#[error("An error occurred while mapping arguments in the model {model_name:} to the collection {collection_name:} in the data connector {data_connector_name:}: {error:}")]
ModelCollectionArgumentMappingError {
data_connector_name: Qualified<DataConnectorName>,

View File

@ -39,7 +39,8 @@ lazy_static::lazy_static! {
},
mutation: graphql_config::MutationGraphqlConfig{
root_operation_type_name: "Mutation".to_string(),
}
},
apollo_federation: None,
});
}
@ -89,6 +90,7 @@ pub struct GlobalGraphqlConfig {
pub query_root_type_name: ast::TypeName,
pub mutation_root_type_name: ast::TypeName,
pub order_by_input: Option<OrderByInputGraphqlConfig>,
pub enable_apollo_federation_fields: bool,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
@ -221,6 +223,12 @@ pub fn resolve_graphql_config(
}
};
let enable_apollo_federation_fields = graphql_config_metadata
.apollo_federation
.as_ref()
.map(|federation_config| federation_config.enable_root_fields)
.unwrap_or_default();
Ok(GraphqlConfig {
query: QueryGraphqlConfig {
arguments_field_name,
@ -233,6 +241,7 @@ pub fn resolve_graphql_config(
query_root_type_name,
mutation_root_type_name,
order_by_input,
enable_apollo_federation_fields,
},
})
}

View File

@ -140,6 +140,10 @@ pub fn resolve_metadata(metadata: open_dds::Metadata) -> Result<Metadata, Error>
// later for validation, such that a type with global id field must have atleast one model with global id source
let mut global_id_enabled_types: HashMap<Qualified<CustomTypeName>, Vec<Qualified<ModelName>>> =
HashMap::new();
let mut apollo_federation_entity_enabled_types: HashMap<
Qualified<CustomTypeName>,
Option<Qualified<ModelName>>,
> = HashMap::new();
let mut data_connector_type_mappings = DataConnectorTypeMappings::new();
@ -157,6 +161,7 @@ pub fn resolve_metadata(metadata: open_dds::Metadata) -> Result<Metadata, Error>
&qualified_object_type_name,
subgraph,
&mut global_id_enabled_types,
&mut apollo_federation_entity_enabled_types,
)?;
// resolve object types' type mappings
@ -362,6 +367,7 @@ pub fn resolve_metadata(metadata: open_dds::Metadata) -> Result<Metadata, Error>
model,
&types,
&mut global_id_enabled_types,
&mut apollo_federation_entity_enabled_types,
&boolean_expression_types,
)?;
if resolved_model.global_id_source.is_some() {
@ -420,6 +426,15 @@ pub fn resolve_metadata(metadata: open_dds::Metadata) -> Result<Metadata, Error>
}
}
// To check if apollo federation entity keys are defined in object type but no model has
// apollo_federation_entity_source set to true:
// - Throw an error if no model with apolloFederation.entitySource:true is found for the object type.
for (object_type, model_name_list) in apollo_federation_entity_enabled_types {
if model_name_list.is_none() {
return Err(Error::ApolloFederationEntitySourceNotDefined { object_type });
}
}
// resolve commands
let mut commands: IndexMap<Qualified<CommandName>, Command> = IndexMap::new();
for open_dds::accessor::QualifiedObject {

View File

@ -185,13 +185,14 @@ pub struct Model {
pub graphql_api: ModelGraphQlApi,
pub source: Option<ModelSource>,
pub select_permissions: Option<HashMap<Role, SelectPermission>>,
pub global_id_source: Option<GlobalIdSource>,
pub global_id_source: Option<NDCFieldSourceMapping>,
pub apollo_federation_key_source: Option<NDCFieldSourceMapping>,
pub filter_expression_type: Option<ObjectBooleanExpressionType>,
pub orderable_fields: Vec<OrderableField>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct GlobalIdSource {
pub struct NDCFieldSourceMapping {
pub ndc_mapping: HashMap<FieldName, NdcColumnForComparison>,
}
@ -271,6 +272,10 @@ pub fn resolve_model(
model: &ModelV1,
types: &HashMap<Qualified<CustomTypeName>, TypeRepresentation>,
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>, ObjectBooleanExpressionType>,
) -> Result<Model, Error> {
let qualified_object_type_name =
@ -310,10 +315,57 @@ pub fn resolve_model(
model_names.push(qualified_model_name.clone());
}
}
global_id_source = Some(GlobalIdSource {
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 let Some(_apollo_federation_config) =
&object_type_representation.apollo_federation_config
{
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 {
@ -351,6 +403,7 @@ pub fn resolve_model(
source: None,
select_permissions: None,
global_id_source,
apollo_federation_key_source,
filter_expression_type,
orderable_fields: resolve_orderable_fields(model, &object_type_representation.fields)?,
})
@ -1309,6 +1362,31 @@ pub fn resolve_model_source(
}
}
if let Some(apollo_federation_key_source) = &mut model.apollo_federation_key_source {
if let Some(apollo_federation_config) = &model_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.schema)?;
Ok(())

View File

@ -45,6 +45,7 @@ pub struct ObjectTypeRepresentation {
pub relationships: IndexMap<ast::Name, Relationship>,
pub type_permissions: HashMap<Role, TypeOutputPermission>,
pub global_id_fields: Vec<FieldName>,
pub apollo_federation_config: Option<ResolvedObjectApolloFederationConfig>,
pub graphql_output_type_name: Option<ast::TypeName>,
pub graphql_input_type_name: Option<ast::TypeName>,
pub description: Option<String>,
@ -132,6 +133,18 @@ pub struct ObjectBooleanExpressionTypeGraphQlConfiguration {
pub type_name: ast::TypeName,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, derive_more::Display)]
#[display(fmt = "Display")]
pub struct ResolvedObjectApolloFederationConfig {
pub keys: nonempty::NonEmpty<ResolvedApolloFederationObjectKey>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, derive_more::Display)]
#[display(fmt = "Display")]
pub struct ResolvedApolloFederationObjectKey {
pub fields: nonempty::NonEmpty<FieldName>,
}
pub fn check_conflicting_graphql_types(
existing_graphql_types: &mut HashSet<ast::TypeName>,
new_graphql_type: Option<&ast::TypeName>,
@ -167,6 +180,10 @@ pub fn resolve_object_type(
Qualified<CustomTypeName>,
Vec<Qualified<open_dds::models::ModelName>>,
>,
apollo_federation_entity_enabled_types: &mut HashMap<
Qualified<CustomTypeName>,
Option<Qualified<open_dds::models::ModelName>>,
>,
) -> Result<TypeRepresentation, Error> {
let mut resolved_fields = IndexMap::new();
let mut resolved_global_id_fields = Vec::new();
@ -211,23 +228,70 @@ pub fn resolve_object_type(
}
None => {}
}
let (graphql_type_name, graphql_input_type_name) = match object_type_definition.graphql.as_ref()
{
None => Ok::<_, Error>((None, None)),
Some(graphql) => {
let graphql_type_name = graphql
.type_name
.as_ref()
.map(|type_name| mk_name(type_name.0.as_ref()).map(ast::TypeName))
.transpose()?;
let graphql_input_type_name = graphql
.input_type_name
.as_ref()
.map(|input_type_name| mk_name(input_type_name.0.as_ref()).map(ast::TypeName))
.transpose()?;
Ok((graphql_type_name, graphql_input_type_name))
}
}?;
let (graphql_type_name, graphql_input_type_name, apollo_federation_config) =
match object_type_definition.graphql.as_ref() {
None => Ok::<_, Error>((None, None, None)),
Some(graphql) => {
let graphql_type_name = graphql
.type_name
.as_ref()
.map(|type_name| mk_name(type_name.0.as_ref()).map(ast::TypeName))
.transpose()?;
let graphql_input_type_name = graphql
.input_type_name
.as_ref()
.map(|input_type_name| mk_name(input_type_name.0.as_ref()).map(ast::TypeName))
.transpose()?;
// To check if apolloFederation.keys are defined in object type but no model has
// apollo_federation_entity_source set to true:
// - If the object type has apolloFederation.keys configured, add the object type to the
// apollo_federation_entity_enabled_types map.
let resolved_apollo_federation_config = match &graphql.apollo_federation {
None => Ok(None),
Some(apollo_federation) => {
// Validate that the fields in the apollo federation keys are defined in the object type
let mut resolved_keys: Vec<ResolvedApolloFederationObjectKey> = Vec::new();
for key in &apollo_federation.keys {
let mut resolved_key_fields = Vec::new();
for field in &key.fields {
if !resolved_fields.contains_key(field) {
return Err(Error::UnknownFieldInApolloFederationKey {
field_name: field.clone(),
object_type: qualified_type_name.clone(),
});
}
resolved_key_fields.push(field.clone());
}
let resolved_key =
match nonempty::NonEmpty::from_vec(resolved_key_fields) {
None => {
return Err(
Error::EmptyFieldsInApolloFederationConfigForObject {
object_type: qualified_type_name.clone(),
},
)
}
Some(fields) => ResolvedApolloFederationObjectKey { fields },
};
resolved_keys.push(resolved_key);
}
apollo_federation_entity_enabled_types
.insert(qualified_type_name.clone(), None);
match nonempty::NonEmpty::from_vec(resolved_keys) {
None => Err(Error::EmptyKeysInApolloFederationConfigForObject {
object_type: qualified_type_name.clone(),
}),
Some(keys) => Ok(Some(ResolvedObjectApolloFederationConfig { keys })),
}
}
}?;
Ok((
graphql_type_name,
graphql_input_type_name,
resolved_apollo_federation_config,
))
}
}?;
check_conflicting_graphql_types(existing_graphql_types, graphql_type_name.as_ref())?;
check_conflicting_graphql_types(existing_graphql_types, graphql_input_type_name.as_ref())?;
@ -239,6 +303,7 @@ pub fn resolve_object_type(
graphql_output_type_name: graphql_type_name,
graphql_input_type_name,
description: object_type_definition.description.clone(),
apollo_federation_config,
}))
}

View File

@ -1,5 +1,5 @@
use lang_graphql::ast::common as ast;
use lang_graphql::schema as gql_schema;
use lang_graphql::{ast::common as ast, mk_name};
use open_dds::{
commands::CommandName,
models::ModelName,
@ -17,8 +17,9 @@ use crate::metadata::{
resolved::subgraph::Qualified,
};
use self::types::RootFieldAnnotation;
use self::types::{PossibleApolloFederationTypes, RootFieldAnnotation};
pub mod apollo_federation;
pub mod commands;
pub mod model_arguments;
pub mod model_filter;
@ -147,6 +148,23 @@ impl gql_schema::SchemaContext for GDS {
types::TypeId::OrderByEnumType { graphql_type_name } => {
model_order_by::build_order_by_enum_type_schema(self, builder, graphql_type_name)
}
types::TypeId::ApolloFederationType(PossibleApolloFederationTypes::Entity) => {
Ok(gql_schema::TypeInfo::Union(
apollo_federation::apollo_federation_entities_schema(builder, self)?,
))
}
types::TypeId::ApolloFederationType(PossibleApolloFederationTypes::Any) => {
Ok(gql_schema::TypeInfo::Scalar(gql_schema::Scalar {
name: ast::TypeName(mk_name!("_Any")),
description: None,
directives: Vec::new(),
}))
}
types::TypeId::ApolloFederationType(PossibleApolloFederationTypes::Service) => {
Ok(gql_schema::TypeInfo::Object(
apollo_federation::apollo_federation_service_schema(builder)?,
))
}
}
}
@ -190,6 +208,10 @@ pub enum Error {
"internal error: duplicate models with global id implementing the same type {type_name} are found"
)]
InternalErrorDuplicateGlobalIdSourceFound { type_name: ast::TypeName },
#[error(
"internal error: duplicate models with entity source for the same type {type_name} are found"
)]
InternalErrorDuplicateEntitySourceFound { type_name: ast::TypeName },
#[error("internal error while building schema, model not found: {model_name}")]
InternalModelNotFound { model_name: Qualified<ModelName> },
#[error(

View File

@ -0,0 +1,69 @@
//! Schema of the apollo federation according to <https://www.apollographql.com/docs/federation/subgraph-spec>
use lang_graphql::{
mk_name,
schema::{self as gql_schema, RegisteredTypeName},
};
use std::collections::BTreeMap;
use super::{
permissions,
types::{self, output_type::get_object_type_representation, Annotation},
};
use crate::schema::{mk_typename, GDS};
use lang_graphql::ast::common as ast;
use super::types::output_type::get_custom_output_type;
pub fn apollo_federation_entities_schema(
builder: &mut gql_schema::Builder<GDS>,
gds: &GDS,
) -> Result<gql_schema::Union<GDS>, crate::schema::Error> {
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)?;
let object_typename = get_custom_output_type(gds, builder, &model.data_type)?;
let entity_union_permissions =
permissions::get_entity_union_permissions(object_type_representation);
entity_members.insert(
object_typename.clone(),
builder.conditional_namespaced((), entity_union_permissions),
);
}
}
Ok(gql_schema::Union::new(
builder,
entity_typename,
None,
entity_members,
Vec::new(),
))
}
pub fn apollo_federation_service_schema(
builder: &mut gql_schema::Builder<GDS>,
) -> Result<gql_schema::Object<GDS>, crate::schema::Error> {
let service_typename = mk_typename("_Service")?;
let sdl_name = mk_name!("sdl");
let sdl_field = gql_schema::Field::new(
sdl_name.clone(),
None,
Annotation::Output(types::OutputAnnotation::SDL),
ast::TypeContainer::named_non_null(RegisteredTypeName::string()),
BTreeMap::new(),
gql_schema::DeprecationStatus::NotDeprecated,
);
let service_fields =
BTreeMap::from([(sdl_name, builder.allow_all_namespaced(sdl_field, None))]);
Ok(gql_schema::Object::new(
builder,
service_typename,
None,
service_fields,
BTreeMap::new(),
Vec::new(),
))
}

View File

@ -14,7 +14,7 @@ use ndc_client as gdc;
use open_dds::arguments::ArgumentName;
use open_dds::commands::DataConnectorCommand;
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use super::types::output_type::get_type_kind;
@ -96,7 +96,7 @@ pub(crate) fn command_field(
> {
let output_typename = get_output_type(gds, builder, &command.output_type)?;
let mut arguments = HashMap::new();
let mut arguments = BTreeMap::new();
for (argument_name, argument_type) in &command.arguments {
let (field_name, input_field) =

View File

@ -4,7 +4,7 @@ use crate::metadata::resolved;
use crate::schema::GDS;
use lang_graphql::schema as gql_schema;
use open_dds::models::ModelName;
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use super::types::input_type::get_input_type;
use super::types::{Annotation, InputAnnotation, ModelInputAnnotation, TypeId};
@ -51,7 +51,7 @@ pub fn build_model_argument_fields(
builder: &mut gql_schema::Builder<GDS>,
model: &resolved::model::Model,
) -> Result<
HashMap<ast::Name, gql_schema::Namespaced<GDS, gql_schema::InputField<GDS>>>,
BTreeMap<ast::Name, gql_schema::Namespaced<GDS, gql_schema::InputField<GDS>>>,
crate::schema::Error,
> {
model
@ -123,6 +123,7 @@ pub fn build_model_arguments_input_schema(
type_name.clone(),
None,
build_model_argument_fields(gds, builder, model)?,
Vec::new(),
),
))
}

View File

@ -2,7 +2,7 @@ use hasura_authn_core::Role;
use lang_graphql::ast::common as ast;
use lang_graphql::schema::{self as gql_schema, InputField, Namespaced};
use open_dds::models::ModelName;
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use super::types::output_type::get_object_type_representation;
use super::types::output_type::relationship::{FilterRelationshipAnnotation, ModelTargetSource};
@ -57,7 +57,7 @@ pub fn build_model_filter_expression_input_schema(
}
})?;
if let Some(boolean_expression_info) = &model.graphql_api.filter_expression {
let mut input_fields = HashMap::new();
let mut input_fields = BTreeMap::new();
// `_and`, `_or` or `_not` fields are available for all roles
let not_field_name = &boolean_expression_info
@ -264,7 +264,7 @@ pub fn build_model_filter_expression_input_schema(
}
}
Ok(gql_schema::TypeInfo::InputObject(
gql_schema::InputObject::new(type_name.clone(), None, input_fields),
gql_schema::InputObject::new(type_name.clone(), None, input_fields, Vec::new()),
))
} else {
Err(
@ -302,7 +302,7 @@ pub fn build_scalar_comparison_input(
operators: &Vec<(ast::Name, QualifiedTypeReference)>,
is_null_operator_name: &ast::Name,
) -> Result<gql_schema::TypeInfo<GDS>, Error> {
let mut input_fields: HashMap<ast::Name, Namespaced<GDS, InputField<GDS>>> = HashMap::new();
let mut input_fields: BTreeMap<ast::Name, Namespaced<GDS, InputField<GDS>>> = BTreeMap::new();
// Add is_null field
let is_null_input_type = ast::TypeContainer {
@ -358,6 +358,6 @@ pub fn build_scalar_comparison_input(
}
Ok(gql_schema::TypeInfo::InputObject(
gql_schema::InputObject::new(type_name.clone(), None, input_fields),
gql_schema::InputObject::new(type_name.clone(), None, input_fields, Vec::new()),
))
}

View File

@ -3,7 +3,7 @@ use lang_graphql::ast::common as ast;
use lang_graphql::schema as gql_schema;
use open_dds::models::ModelName;
use open_dds::relationships::RelationshipType;
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use super::types::output_type::relationship::{ModelTargetSource, OrderByRelationshipAnnotation};
use super::types::{output_type::get_object_type_representation, Annotation, TypeId};
@ -25,7 +25,7 @@ pub fn build_order_by_enum_type_schema(
builder: &mut gql_schema::Builder<GDS>,
order_by_type_name: &ast::TypeName,
) -> Result<gql_schema::TypeInfo<GDS>, Error> {
let mut order_by_values = HashMap::new();
let mut order_by_values = BTreeMap::new();
let order_by_input_config = gds
.metadata
.graphql_config
@ -75,6 +75,7 @@ pub fn build_order_by_enum_type_schema(
name: order_by_type_name.clone(),
description: None,
values: order_by_values,
directives: Vec::new(),
}))
}
@ -116,7 +117,7 @@ pub fn build_model_order_by_input_schema(
let object_type_representation = get_object_type_representation(gds, &model.data_type)?;
let mut fields = HashMap::new();
let mut fields = BTreeMap::new();
let order_by_input_config = gds
.metadata
@ -243,7 +244,7 @@ pub fn build_model_order_by_input_schema(
}
Ok(gql_schema::TypeInfo::InputObject(
gql_schema::InputObject::new(type_name.clone(), None, fields),
gql_schema::InputObject::new(type_name.clone(), None, fields, Vec::new()),
))
} else {
Err(Error::NoOrderByExpression {

View File

@ -2,7 +2,7 @@
use lang_graphql::ast::common::{self as ast, TypeName};
use lang_graphql::schema as gql_schema;
use std::collections::HashMap;
use std::collections::BTreeMap;
use crate::schema::{commands, GDS};
@ -12,7 +12,7 @@ pub fn mutation_root_schema(
gds: &GDS,
mutation_root_type_name: &TypeName,
) -> Result<gql_schema::Object<GDS>, crate::schema::Error> {
let mut fields = HashMap::new();
let mut fields = BTreeMap::new();
// Add node field for only the commands which have a mutation root field
// defined, that is, they are based on procedures.
@ -42,6 +42,7 @@ pub fn mutation_root_schema(
mutation_root_type_name.clone(),
None,
fields,
HashMap::new(),
BTreeMap::new(),
Vec::new(),
))
}

View File

@ -148,6 +148,26 @@ pub(crate) fn get_node_interface_annotations(
permissions
}
/// Build namespace annotations for the _Entity union.
/// The key fields and the _Entity union will only be exposed
/// for a role if the role has access (select permissions)
/// to all the key fields.
pub(crate) fn get_entity_union_permissions(
object_type_representation: &ObjectTypeRepresentation,
) -> HashMap<Role, Option<types::NamespaceAnnotation>> {
let mut permissions = HashMap::new();
for (role, type_output_permission) in &object_type_representation.type_permissions {
let is_permitted = object_type_representation
.global_id_fields
.iter()
.all(|field_name| type_output_permission.allowed_fields.contains(field_name));
if is_permitted {
permissions.insert(role.clone(), None);
}
}
permissions
}
/// Build namespace annotations for each field based on the type permissions
pub(crate) fn get_allowed_roles_for_field<'a>(
object_type_representation: &'a ObjectTypeRepresentation,
@ -200,3 +220,46 @@ pub(crate) fn get_node_field_namespace_permissions(
permissions
}
/// Builds namespace annotations for the `_entities` field.
pub(crate) fn get_entities_field_namespace_permissions(
object_type_representation: &ObjectTypeRepresentation,
model: &resolved::model::Model,
) -> HashMap<Role, FilterPermission> {
let mut permissions = HashMap::new();
match &model.select_permissions {
// Model doesn't have any select permissions, so no `FilterPermission` can be obtained
None => {}
Some(select_permissions) => {
for (role, type_output_permission) in &object_type_representation.type_permissions {
if let Some(apollo_federation_config) =
&object_type_representation.apollo_federation_config
{
let is_all_keys_field_accessible =
apollo_federation_config.keys.iter().all(|key_fields| {
key_fields.fields.iter().all(|field_name| {
type_output_permission.allowed_fields.contains(field_name)
})
});
if is_all_keys_field_accessible {
let select_permission =
select_permissions.get(role).map(|s| s.filter.clone());
match select_permission {
// Select permission doesn't exist for the role, so no `FilterPermission` can
// be obtained.
None => {}
Some(select_permission) => {
permissions.insert(role.clone(), select_permission);
}
}
};
}
}
}
}
permissions
}

View File

@ -3,7 +3,7 @@
use lang_graphql::ast::common::TypeName;
use lang_graphql::schema as gql_schema;
use open_dds::commands::GraphQlRootFieldKind;
use std::collections::HashMap;
use std::collections::BTreeMap;
use crate::schema::commands;
use crate::schema::query_root::node_field::relay_node_field;
@ -11,6 +11,7 @@ use crate::schema::GDS;
use self::node_field::RelayNodeFieldOutput;
pub mod apollo_federation;
pub mod node_field;
pub mod select_many;
pub mod select_one;
@ -21,7 +22,7 @@ pub fn query_root_schema(
gds: &GDS,
query_root_type_name: &TypeName,
) -> Result<gql_schema::Object<GDS>, crate::schema::Error> {
let mut fields = HashMap::new();
let mut fields = BTreeMap::new();
for model in gds.metadata.models.values() {
for select_unique in model.graphql_api.select_uniques.iter() {
let (field_name, field) = select_one::select_one_field(
@ -86,11 +87,48 @@ pub fn query_root_schema(
});
};
// apollo federation field
if gds.metadata.graphql_config.enable_apollo_federation_fields {
let apollo_federation::ApolloFederationFieldOutput {
apollo_federation_entities_field,
apollo_federation_entities_field_permissions,
apollo_federation_service_field,
} = apollo_federation::apollo_federation_field(gds, builder)?;
if fields
.insert(
apollo_federation_entities_field.name.clone(),
builder.conditional_namespaced(
apollo_federation_entities_field.clone(),
apollo_federation_entities_field_permissions,
),
)
.is_some()
{
return Err(crate::schema::Error::DuplicateFieldInQueryRoot {
field_name: apollo_federation_entities_field.name,
});
};
if fields
.insert(
apollo_federation_service_field.name.clone(),
builder.allow_all_namespaced(apollo_federation_service_field.clone(), None),
)
.is_some()
{
return Err(crate::schema::Error::DuplicateFieldInQueryRoot {
field_name: apollo_federation_service_field.name,
});
};
}
Ok(gql_schema::Object::new(
builder,
query_root_type_name.clone(),
None,
fields,
HashMap::new(),
BTreeMap::new(),
Vec::new(),
))
}

View File

@ -0,0 +1,133 @@
use std::collections::{BTreeMap, HashMap};
use hasura_authn_core::Role;
use lang_graphql::ast::common as ast;
use lang_graphql::{mk_name, schema as gql_schema};
use open_dds::types::CustomTypeName;
use crate::metadata::resolved;
use crate::metadata::resolved::subgraph::Qualified;
use crate::schema::permissions::get_entities_field_namespace_permissions;
use crate::schema::types::output_type::{
apollo_federation_entities_type, apollo_federation_service_type, get_custom_output_type,
get_object_type_representation, representations_type_reference,
};
use crate::schema::types::EntityFieldTypeNameMapping;
use crate::schema::{
types::{self, Annotation},
GDS,
};
use crate::utils::HashMapWithJsonKey;
pub(crate) struct ApolloFederationFieldOutput {
/// The _entities field.
pub apollo_federation_entities_field: gql_schema::Field<GDS>,
/// Roles having access to the `_entities` field.
pub apollo_federation_entities_field_permissions:
HashMap<Role, Option<types::NamespaceAnnotation>>,
/// The _service field.
pub apollo_federation_service_field: gql_schema::Field<GDS>,
}
pub(crate) fn apollo_federation_field(
gds: &GDS,
builder: &mut gql_schema::Builder<GDS>,
) -> Result<ApolloFederationFieldOutput, crate::schema::Error> {
let mut roles_type_permissions: HashMap<
Role,
HashMap<Qualified<CustomTypeName>, resolved::model::FilterPermission>,
> = 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)?;
let object_type_representation = get_object_type_representation(gds, &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());
}
if typename_mappings
.insert(
output_typename.type_name().clone(),
EntityFieldTypeNameMapping {
type_name: model.data_type.clone(),
model_source: model.source.clone(),
key_fields_ndc_mapping: apollo_federation_key_source.ndc_mapping.clone(),
},
)
.is_some()
{
// This is declared as an internal error because this error should
// never happen, because this is validated while resolving the metadata.
return Err(
crate::schema::Error::InternalErrorDuplicateEntitySourceFound {
type_name: output_typename.type_name().clone(),
},
);
};
}
}
let mut apollo_federation_entities_field_permissions = HashMap::new();
for (role, role_type_permission) in roles_type_permissions {
apollo_federation_entities_field_permissions.insert(
role.clone(),
Some(types::NamespaceAnnotation::EntityTypeMappings(
HashMapWithJsonKey(role_type_permission),
)),
);
}
let representations_argument: gql_schema::InputField<GDS> = gql_schema::InputField::new(
lang_graphql::mk_name!("representations"),
None,
Annotation::Input(
types::InputAnnotation::ApolloFederationRepresentationsInput(
types::ApolloFederationInputAnnotation::AnyScalarInputAnnotation,
),
),
representations_type_reference(builder),
None,
gql_schema::DeprecationStatus::NotDeprecated,
);
let entities_arguments = BTreeMap::from([(
mk_name!("representations"),
builder.allow_all_namespaced(representations_argument, None),
)]);
let entity_field = gql_schema::Field::new(
mk_name!("_entities"),
None,
Annotation::Output(types::OutputAnnotation::RootField(
types::RootFieldAnnotation::ApolloFederation(
types::ApolloFederationRootFields::Entities { typename_mappings },
),
)),
ast::TypeContainer::named_non_null(apollo_federation_entities_type(builder)),
entities_arguments,
gql_schema::DeprecationStatus::NotDeprecated,
);
let service_field = gql_schema::Field::new(
mk_name!("_service"),
None,
Annotation::Output(types::OutputAnnotation::RootField(
types::RootFieldAnnotation::ApolloFederation(
types::ApolloFederationRootFields::Service,
),
)),
ast::TypeContainer::named_non_null(apollo_federation_service_type(builder)),
BTreeMap::new(),
gql_schema::DeprecationStatus::NotDeprecated,
);
Ok(ApolloFederationFieldOutput {
apollo_federation_service_field: service_field,
apollo_federation_entities_field: entity_field,
apollo_federation_entities_field_permissions,
})
}

View File

@ -2,7 +2,7 @@
use lang_graphql::{ast::common as ast, schema as gql_schema};
use open_dds::types::CustomTypeName;
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use crate::metadata::resolved;
use crate::metadata::resolved::subgraph::Qualified;
@ -35,7 +35,7 @@ pub(crate) fn relay_node_field(
gds: &GDS,
builder: &mut gql_schema::Builder<GDS>,
) -> Result<RelayNodeFieldOutput, crate::schema::Error> {
let mut arguments = HashMap::new();
let mut arguments = BTreeMap::new();
let mut typename_mappings = HashMap::new();
// The node field should only be accessible to a role, if
// atleast one object implements the global `id` field.

View File

@ -5,7 +5,7 @@
use lang_graphql::ast::common as ast;
use lang_graphql::ast::common::Name;
use lang_graphql::schema as gql_schema;
use std::collections::HashMap;
use std::collections::BTreeMap;
use crate::metadata::resolved;
use crate::schema::mk_deprecation_status;
@ -24,10 +24,10 @@ pub(crate) fn generate_select_many_arguments(
builder: &mut gql_schema::Builder<GDS>,
model: &resolved::model::Model,
) -> Result<
HashMap<Name, gql_schema::Namespaced<GDS, gql_schema::InputField<GDS>>>,
BTreeMap<Name, gql_schema::Namespaced<GDS, gql_schema::InputField<GDS>>>,
crate::schema::Error,
> {
let mut arguments = HashMap::new();
let mut arguments = BTreeMap::new();
// insert limit argument
if let Some(limit_field) = &model.graphql_api.limit_field {

View File

@ -3,7 +3,7 @@
//! A 'select_one' operation fetches zero or one row from a model
use lang_graphql::{ast::common as ast, schema as gql_schema};
use std::collections::HashMap;
use std::collections::BTreeMap;
use crate::metadata::resolved;
use crate::metadata::resolved::types::mk_name;
@ -33,7 +33,7 @@ pub(crate) fn select_one_field(
> {
let query_root_field = select_unique.query_root_field.clone();
let mut arguments = HashMap::new();
let mut arguments = BTreeMap::new();
for (field_name, field) in select_unique.unique_identifier.iter() {
let graphql_field_name = mk_name(field_name.0.as_str())?;
let argument = gql_schema::InputField::new(

View File

@ -1,7 +1,7 @@
//! Schema of the relay according to <https://relay.dev/graphql/objectidentification.htm>
use lang_graphql::schema as gql_schema;
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use super::permissions;
use crate::schema::types::{
@ -17,8 +17,8 @@ pub fn node_interface_schema(
builder: &mut gql_schema::Builder<GDS>,
gds: &GDS,
) -> Result<gql_schema::Interface<GDS>, crate::schema::Error> {
let mut fields = HashMap::new();
let mut implemented_by = HashMap::new();
let mut fields = BTreeMap::new();
let mut implemented_by = BTreeMap::new();
let mut typename_global_id_mappings = HashMap::new();
let mut roles_implementing_global_id: HashMap<Role, Option<types::NamespaceAnnotation>> =
HashMap::new();
@ -54,7 +54,7 @@ pub fn node_interface_schema(
typename_mappings: typename_global_id_mappings,
}),
get_output_type(gds, builder, &ID_TYPE_REFERENCE)?,
HashMap::new(),
BTreeMap::new(),
gql_schema::DeprecationStatus::NotDeprecated,
);
fields.insert(
@ -67,7 +67,8 @@ pub fn node_interface_schema(
node_typename,
None,
fields,
HashMap::new(),
BTreeMap::new(),
implemented_by,
Vec::new(),
))
}

View File

@ -60,6 +60,15 @@ pub struct NodeFieldTypeNameMapping {
pub global_id_fields_ndc_mapping: HashMap<types::FieldName, NdcColumnForComparison>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
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 key_fields_ndc_mapping: HashMap<types::FieldName, NdcColumnForComparison>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum RootFieldKind {
SelectOne,
@ -130,6 +139,15 @@ pub enum RootFieldAnnotation {
source: Option<CommandSourceDetail>,
procedure_name: Option<commands::ProcedureName>,
},
ApolloFederation(ApolloFederationRootFields),
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Display)]
pub enum ApolloFederationRootFields {
Entities {
typename_mappings: HashMap<ast::TypeName, EntityFieldTypeNameMapping>,
},
Service,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Display, Copy)]
@ -163,6 +181,7 @@ pub enum OutputAnnotation {
RelayNodeInterfaceID {
typename_mappings: HashMap<ast::TypeName, Vec<types::FieldName>>,
},
SDL,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Display)]
@ -204,6 +223,12 @@ pub enum RelayInputAnnotation {
NodeFieldIdArgument,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Display)]
/// Annotations for Apollo federation input arguments/types.
pub enum ApolloFederationInputAnnotation {
AnyScalarInputAnnotation,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Display)]
/// Annotations for GraphQL input arguments/types.
pub enum InputAnnotation {
@ -217,6 +242,7 @@ pub enum InputAnnotation {
ndc_func_proc_argument: Option<String>,
},
Relay(RelayInputAnnotation),
ApolloFederationRepresentationsInput(ApolloFederationInputAnnotation),
}
/// Contains the different possible entities that can be used to generate
@ -270,6 +296,12 @@ pub enum NamespaceAnnotation {
NodeFieldTypeMappings(
HashMapWithJsonKey<Qualified<types::CustomTypeName>, resolved::model::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>,
),
}
#[derive(Serialize, Clone, Debug, Hash, PartialEq, Eq)]
@ -314,6 +346,15 @@ pub enum TypeId {
OrderByEnumType {
graphql_type_name: ast::TypeName,
},
ApolloFederationType(PossibleApolloFederationTypes),
}
#[derive(Serialize, Clone, Debug, Hash, PartialEq, Eq)]
pub enum PossibleApolloFederationTypes {
Entity,
Any,
Service,
}
impl Display for TypeId {
@ -350,6 +391,15 @@ impl TypeId {
TypeId::OrderByEnumType {
graphql_type_name, ..
} => graphql_type_name.clone(),
TypeId::ApolloFederationType(PossibleApolloFederationTypes::Entity) => {
ast::TypeName(mk_name!("_Entity"))
}
TypeId::ApolloFederationType(PossibleApolloFederationTypes::Any) => {
ast::TypeName(mk_name!("_Any"))
}
TypeId::ApolloFederationType(PossibleApolloFederationTypes::Service) => {
ast::TypeName(mk_name!("_Service"))
}
}
}
}

View File

@ -9,7 +9,7 @@ use indexmap::IndexMap;
use lang_graphql::ast::common as ast;
use lang_graphql::schema as gql_schema;
use open_dds::types::{CustomTypeName, FieldName};
use std::collections::HashMap;
use std::collections::BTreeMap;
use super::inbuilt_type::base_type_container_for_inbuilt_type;
@ -108,7 +108,7 @@ fn input_object_type_input_fields(
gds: &GDS,
builder: &mut gql_schema::Builder<GDS>,
fields: &IndexMap<FieldName, FieldDefinition>,
) -> Result<HashMap<ast::Name, gql_schema::Namespaced<GDS, gql_schema::InputField<GDS>>>, Error> {
) -> Result<BTreeMap<ast::Name, gql_schema::Namespaced<GDS, gql_schema::InputField<GDS>>>, Error> {
fields
.iter()
.map(|(field_name, field_definition)| {
@ -129,7 +129,7 @@ fn input_object_type_input_fields(
let namespaced_input_field = builder.allow_all_namespaced(input_field, None);
Ok((graphql_field_name, namespaced_input_field))
})
.collect::<Result<HashMap<_, _>, _>>()
.collect::<Result<BTreeMap<_, _>, _>>()
}
pub fn input_object_type_schema(
@ -162,6 +162,6 @@ pub fn input_object_type_schema(
input_object_type_input_fields(gds, builder, &object_type_representation.fields)?;
Ok(gql_schema::TypeInfo::InputObject(
gql_schema::InputObject::new(graphql_type_name, None, input_fields),
gql_schema::InputObject::new(graphql_type_name, None, input_fields, Vec::new()),
))
}

View File

@ -1,22 +1,25 @@
use lang_graphql::ast::common as ast;
use lang_graphql::schema::{self as gql_schema};
use lang_graphql::ast::common::{self as ast, TypeContainer};
use lang_graphql::mk_name;
use lang_graphql::schema::{self as gql_schema, Directive, RegisteredType};
use open_dds::commands::DataConnectorCommand;
use open_dds::{
relationships,
types::{CustomTypeName, InbuiltType},
};
use std::collections::{HashMap, HashSet};
use std::collections::{BTreeMap, HashMap, HashSet};
use self::relationship::{
CommandRelationshipAnnotation, CommandTargetSource, ModelRelationshipAnnotation,
ModelTargetSource,
};
use super::inbuilt_type::base_type_container_for_inbuilt_type;
use super::Annotation;
use super::{Annotation, PossibleApolloFederationTypes, TypeId};
use crate::metadata::resolved::subgraph::{
Qualified, QualifiedBaseType, QualifiedTypeName, QualifiedTypeReference,
};
use crate::metadata::resolved::types::ObjectTypeRepresentation;
use crate::metadata::resolved::types::{
ObjectTypeRepresentation, ResolvedObjectApolloFederationConfig,
};
use crate::metadata::resolved::{
self,
types::{mk_name, TypeRepresentation},
@ -92,6 +95,30 @@ pub fn node_interface_type(
builder.register_type(super::TypeId::NodeRoot)
}
pub fn apollo_federation_entities_type(
builder: &mut gql_schema::Builder<GDS>,
) -> gql_schema::RegisteredTypeName {
builder.register_type(super::TypeId::ApolloFederationType(
super::PossibleApolloFederationTypes::Entity,
))
}
pub fn apollo_federation_service_type(
builder: &mut gql_schema::Builder<GDS>,
) -> gql_schema::RegisteredTypeName {
builder.register_type(super::TypeId::ApolloFederationType(
super::PossibleApolloFederationTypes::Service,
))
}
pub fn apollo_federation_any_scalar(
builder: &mut gql_schema::Builder<GDS>,
) -> gql_schema::RegisteredTypeName {
builder.register_type(super::TypeId::ApolloFederationType(
super::PossibleApolloFederationTypes::Any,
))
}
pub fn get_custom_output_type(
gds: &GDS,
builder: &mut gql_schema::Builder<GDS>,
@ -160,7 +187,7 @@ fn object_type_fields(
gds: &GDS,
builder: &mut gql_schema::Builder<GDS>,
object_type_representation: &ObjectTypeRepresentation,
) -> Result<HashMap<ast::Name, gql_schema::Namespaced<GDS, gql_schema::Field<GDS>>>, Error> {
) -> Result<BTreeMap<ast::Name, gql_schema::Namespaced<GDS, gql_schema::Field<GDS>>>, Error> {
let mut graphql_fields = object_type_representation
.fields
.iter()
@ -175,7 +202,7 @@ fn object_type_fields(
field_base_type_kind: get_type_kind(gds, &field_definition.field_type)?,
}),
get_output_type(gds, builder, &field_definition.field_type)?,
HashMap::new(),
BTreeMap::new(),
mk_deprecation_status(&field_definition.deprecated),
);
// if output permissions are defined for this type, we conditionally
@ -191,7 +218,7 @@ fn object_type_fields(
};
Ok((graphql_field_name, namespaced_field))
})
.collect::<Result<HashMap<_, _>, _>>()?;
.collect::<Result<BTreeMap<_, _>, _>>()?;
let graphql_relationship_fields = object_type_representation
.relationships
.iter()
@ -221,7 +248,7 @@ fn object_type_fields(
// generate argument fields for the command arguments which are not mapped to
// any type fields, so that they can be exposed in the relationship field schema
let mut arguments = HashMap::new();
let mut arguments = BTreeMap::new();
for (argument_name, argument_type) in &command.arguments {
if !arguments_with_mapping.contains(argument_name) {
let (field_name, input_field) = generate_command_argument(
@ -303,7 +330,7 @@ fn object_type_fields(
relationships::RelationshipType::Array => {
generate_select_many_arguments(builder, model)?
}
relationships::RelationshipType::Object => HashMap::new(),
relationships::RelationshipType::Object => BTreeMap::new(),
};
let target_object_type_representation =
@ -345,6 +372,33 @@ fn object_type_fields(
Ok(graphql_fields)
}
fn generate_apollo_federation_directives(
apollo_federation_config: &ResolvedObjectApolloFederationConfig,
) -> Vec<Directive> {
let mut directives = Vec::new();
for key in &apollo_federation_config.keys {
let fields = key
.fields
.iter()
.map(|f| f.0 .0.clone())
.collect::<Vec<_>>()
.join(" ");
let key_directive = gql_schema::Directive {
name: mk_name!("key"),
arguments: vec![(
mk_name!("fields"),
lang_graphql::ast::value::ConstValue::SimpleValue(
lang_graphql::ast::value::SimpleValue::String(fields),
),
)]
.into_iter()
.collect(),
};
directives.push(key_directive);
}
directives
}
pub fn output_type_schema(
gds: &GDS,
builder: &mut gql_schema::Builder<GDS>,
@ -365,18 +419,25 @@ pub fn output_type_schema(
resolved::types::TypeRepresentation::Object(object_type_representation) => {
let mut object_type_fields =
object_type_fields(gds, builder, object_type_representation)?;
let directives = match &object_type_representation.apollo_federation_config {
Some(apollo_federation_config) => {
generate_apollo_federation_directives(apollo_federation_config)
}
None => Vec::new(),
};
if object_type_representation.global_id_fields.is_empty() {
Ok(gql_schema::TypeInfo::Object(gql_schema::Object::new(
builder,
graphql_type_name,
object_type_representation.description.clone(),
object_type_fields,
HashMap::new(),
BTreeMap::new(),
directives,
)))
} else {
// Generate the Global object `id` field and insert it
// into the `object_type_fields`.
let mut interfaces = HashMap::new();
let mut interfaces = BTreeMap::new();
let global_id_field_name = lang_graphql::mk_name!("id");
let global_id_field = gql_schema::Field::<GDS>::new(
global_id_field_name.clone(),
@ -385,7 +446,7 @@ pub fn output_type_schema(
global_id_fields: object_type_representation.global_id_fields.to_vec(),
}),
get_output_type(gds, builder, &ID_TYPE_REFERENCE)?,
HashMap::new(),
BTreeMap::new(),
gql_schema::DeprecationStatus::NotDeprecated,
);
if object_type_fields
@ -411,12 +472,19 @@ pub fn output_type_schema(
builder.conditional_namespaced((), node_interface_annotations),
);
}
let directives = match &object_type_representation.apollo_federation_config {
Some(apollo_federation_config) => {
generate_apollo_federation_directives(apollo_federation_config)
}
None => Vec::new(),
};
Ok(gql_schema::TypeInfo::Object(gql_schema::Object::new(
builder,
graphql_type_name,
object_type_representation.description.clone(),
object_type_fields,
interfaces,
directives,
)))
}
}
@ -450,3 +518,11 @@ pub(crate) fn get_object_type_representation<'s>(
}
}
}
pub(crate) fn representations_type_reference(
builder: &mut gql_schema::Builder<GDS>,
) -> RegisteredType {
TypeContainer::list_non_null(TypeContainer::named_non_null(builder.register_type(
TypeId::ApolloFederationType(PossibleApolloFederationTypes::Any),
)))
}

View File

@ -36,6 +36,7 @@ pub fn scalar_type_schema(
Ok(gql_schema::TypeInfo::Scalar(gql_schema::Scalar {
name: graphql_type_name,
description: scalar_type_representation.description.clone(),
directives: Vec::new(),
}))
}
}

View File

@ -0,0 +1,185 @@
{
"version": "v2",
"supergraph": {
"objects": [
{
"kind": "GraphqlConfig",
"version": "v1",
"definition": {
"query": {
"rootOperationTypeName": "Query"
},
"mutation": {
"rootOperationTypeName": "Mutation"
},
"apolloFederation": {
"enableRootFields": true
}
}
}
]
},
"subgraphs": [
{
"name": "default",
"objects": [
{
"kind": "DataConnectorScalarRepresentation",
"version": "v1",
"definition": {
"dataConnectorName": "db",
"dataConnectorScalarType": "String",
"representation": "String",
"graphql": {
"comparisonExpressionTypeName": "String_Comparison_Exp"
}
}
},
{
"kind": "DataConnectorScalarRepresentation",
"version": "v1",
"definition": {
"dataConnectorName": "db",
"dataConnectorScalarType": "Int",
"representation": "Int"
}
},
{
"kind": "ObjectType",
"version": "v1",
"definition": {
"name": "author",
"fields": [
{
"name": "author_id",
"type": "Int!"
},
{
"name": "first_name",
"type": "String!"
},
{
"name": "last_name",
"type": "String!"
}
],
"graphql": {
"typeName": "Author",
"apolloFederation": {
"keys": [
{
"fields": [
"author_id"
]
}
]
}
},
"dataConnectorTypeMapping": [
{
"dataConnectorName": "db",
"dataConnectorObjectType": "author",
"fieldMapping": {
"author_id": {
"column": {
"name": "id"
}
},
"first_name": {
"column": {
"name": "first_name"
}
},
"last_name": {
"column": {
"name": "last_name"
}
}
}
}
]
}
},
{
"kind": "Model",
"version": "v1",
"definition": {
"name": "Authors",
"objectType": "author",
"source": {
"dataConnectorName": "db",
"collection": "authors"
},
"graphql": {
"selectUniques": [
{
"queryRootField": "AuthorByID",
"uniqueIdentifier": [
"author_id"
]
}
],
"apolloFederation": {
"entitySource": true
}
},
"orderableFields": [
{
"fieldName": "author_id",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "first_name",
"orderByDirections": {
"enableAll": true
}
},
{
"fieldName": "last_name",
"orderByDirections": {
"enableAll": true
}
}
]
}
},
{
"kind": "TypePermissions",
"version": "v1",
"definition": {
"typeName": "author",
"permissions": [
{
"role": "admin",
"output": {
"allowedFields": [
"author_id",
"first_name",
"last_name"
]
}
}
]
}
},
{
"kind": "ModelPermissions",
"version": "v1",
"definition": {
"modelName": "Authors",
"permissions": [
{
"role": "admin",
"select": {
"filter": null
}
}
]
}
}
]
}
]
}

View File

@ -0,0 +1,16 @@
{
"data": {
"_entities": [
{
"author_id": 1,
"first_name": "Peter",
"last_name": "Landin"
},
{
"author_id": 2,
"first_name": "John",
"last_name": "Hughes"
}
]
}
}

View File

@ -0,0 +1,9 @@
query {
_entities(representations: [{__typename: "Author", author_id: 1}, {__typename: "Author", author_id: 2}]) {
... on Author {
author_id
first_name
last_name
}
}
}

View File

@ -0,0 +1 @@
{ "x-hasura-role": "admin" }

View File

@ -0,0 +1,9 @@
{
"data": {
"__typename": "Query",
"_service": {
"__typename": "_Service",
"sdl": "extend schema\n @link(url: \"https://specs.apollo.dev/federation/v2.0\", import: [\"@key\", \"@extends\", \"@external\", \"@shareable\"])\n\nschema {\n query: Query \n}\n\ntype Author@key(fields: \"author_id\") {\n author_id: Int! \n first_name: String! \n last_name: String! \n}\n\nscalar Boolean\n\nscalar Float\n\nscalar ID\n\nscalar Int\n\ntype Query {\n AuthorByID: Author \n _entities: _Entity! \n _service: _Service! \n}\n\nscalar String\n\nscalar _Any\n\nunion _Entity = Author\n\ntype _Service {\n sdl: String! \n}"
}
}
}

View File

@ -0,0 +1,7 @@
query {
__typename
_service {
__typename
sdl
}
}

View File

@ -0,0 +1 @@
{ "x-hasura-role": "admin" }

View File

@ -944,3 +944,25 @@ fn test_graphql_descriptions() {
],
);
}
#[test]
fn test_apollo_federation_service_sdl() {
let test_path_string = "execute/apollo_federation_fields/service_sdl";
let common_apollo_metadata = "execute/apollo_federation_fields/common_metadata.json";
let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json";
common::test_execution_expectation_legacy(
test_path_string,
&[common_metadata_path_string, common_apollo_metadata],
);
}
#[test]
fn test_apollo_federation_entities() {
let test_path_string = "execute/apollo_federation_fields/entities";
let common_apollo_metadata = "execute/apollo_federation_fields/common_metadata.json";
let common_metadata_path_string = "execute/common_metadata/postgres_connector_schema.json";
common::test_execution_expectation_legacy(
test_path_string,
&[common_metadata_path_string, common_apollo_metadata],
);
}

View File

@ -45,6 +45,9 @@
},
"mutation": {
"rootOperationTypeName": "Mutation"
},
"apolloFederation": {
"enableRootFields": true
}
}
}
@ -2504,7 +2507,16 @@
"name": "Artist",
"graphql": {
"typeName": "artist",
"inputTypeName": "artistInput"
"inputTypeName": "artistInput",
"apolloFederation": {
"keys": [
{
"fields": [
"ArtistId"
]
}
]
}
},
"fields": [
{
@ -2587,7 +2599,10 @@
"selectMany": {
"queryRootField": "Artist"
},
"orderByExpressionType": "ArtistOrderBy"
"orderByExpressionType": "ArtistOrderBy",
"apolloFederation": {
"entitySource": true
}
},
"source": {
"collection": "Artist",

View File

@ -33,9 +33,12 @@ pub struct ConstDirective {
/// The name of the directive.
pub name: Spanning<Name>,
/// The arguments to the directive.
pub arguments: Option<Spanning<Vec<Spanning<InputValueDefinition>>>>,
pub arguments: Option<Spanning<Vec<ConstArgument>>>,
}
/// A const argument
pub type ConstArgument = Spanning<KeyValue<ConstValue>>;
/// The definition of the schema in a GraphQL service.
///
/// [Reference](https://spec.graphql.org/October2021/#SchemaDefinition). This also covers

View File

@ -0,0 +1,441 @@
/// This module is for generating schema in SDL (Schema Definition Language) format.
/// An example of schema in SDL format is:
/// ```graphql
/// schema {
/// query: Query
/// mutation: Mutation
/// subscription: Subscription
/// }
///
/// type Query {
/// allPersons(last: Int): [Person!]!
/// allPosts(last: Int): [Post!]!
/// }
///
/// type Mutation {
/// createPerson(name: String!, age: Int!): Person!
/// updatePerson(id: ID!, name: String!, age: Int!): Person!
/// deletePerson(id: ID!): Person!
/// }
///
/// type Subscription {
/// newPerson: Person!
/// }
///
/// type Person {
/// id: ID!
/// name: String!
/// age: Int!
/// posts: [Post!]!
/// }
///
/// type Post {
/// title: String!
/// author: Person!
/// }
/// ```
///
/// The schema can generated by calling `generate_sdl` method on the `Schema` struct.
///
use std::collections::BTreeMap;
use crate::{
ast::common as ast,
schema::{
DeprecationStatus, Directive, Enum, Field, InputObject, Interface, Namespaced, Object,
Scalar, Schema, SchemaContext, TypeInfo, Union,
},
};
impl<S: SchemaContext> Object<S> {
/// Generate SDL for the object type. An example of object type in SDL format is:
/// ```graphql
/// """Person Object Type"""
/// type Person @key(fields: "id") {
/// id: ID!
/// name: String!
/// age: Int!
/// posts: [Post!]!
/// }
/// ```
/// Please note that, if there are no fields in the object type, it will return `None`.
fn generate_sdl(&self, namespace: &S::Namespace) -> Option<String> {
let fields_sdl = generate_fields_sdl(&self.fields, namespace);
if fields_sdl.is_empty() {
None
} else {
Some(with_description(
&self.description,
format!(
"type {}{} {}",
&self.name,
generate_directives_sdl(&self.directives, None),
in_curly_braces(fields_sdl)
),
))
}
}
}
impl<S: SchemaContext> InputObject<S> {
/// Generate SDL for the input object type. An example of input object type in SDL format is:
/// ```graphql
/// """Insert Person Input Object Type"""
/// input InsertPersonInput @deprecated(reason: "Use `PersonInput` instead") {
/// name: String!
/// age: Int!
/// }
/// ```
/// Please note that, if there are no fields in the input object type, it will return `None`.
fn generate_sdl(&self, namespace: &S::Namespace) -> Option<String> {
let fields_sdl = self
.fields
.iter()
.filter_map(|(field_name, field)| {
field.get(namespace).map(|(data, _)| {
with_description(
&data.description,
format_field_with_type(
field_name,
&data.field_type,
Some(&data.deprecation_status),
),
)
})
})
.collect::<Vec<String>>();
if fields_sdl.is_empty() {
None
} else {
Some(with_description(
&self.description,
format!(
"input {}{} {}",
&self.name,
generate_directives_sdl(&self.directives, None),
in_curly_braces(fields_sdl)
),
))
}
}
}
impl Scalar {
/// Generate SDL for the scalar type. An example of scalar type in SDL format is:
/// ```graphql
/// """Custom Scalar Type for Date"""
/// scalar Date
/// ```
fn generate_sdl(&self) -> String {
with_description(
&self.description,
format!(
"scalar {}{}",
&self.name,
generate_directives_sdl(&self.directives, None)
),
)
}
}
impl<S: SchemaContext> Enum<S> {
/// Generate SDL for the enum type. An example of enum type in SDL format is:
/// ```graphql
/// """Custom Enum Type for application status"""
/// enum ApplicationStatus {
/// DRAFT @deprecated(reason: "Use `PENDING` instead")
/// PENDING
/// APPROVED
/// REJECTED
/// }
/// ```
/// Please note that, if there are no values in the enum type, it will return `None`.
fn generate_sdl(&self, namespace: &S::Namespace) -> Option<String> {
let fields_sdl = self
.values
.values()
.filter_map(|enum_val| {
enum_val.get(namespace).map(|(data, _)| {
with_description(
&data.description,
format!(
"{} {}",
data.value,
generate_directives_sdl(&[], Some(&data.deprecation_status))
),
)
})
})
.collect::<Vec<String>>();
if fields_sdl.is_empty() {
None
} else {
Some(with_description(
&self.description,
format!(
"enum {}{} {}",
self.name,
generate_directives_sdl(&self.directives, None),
in_curly_braces(fields_sdl)
),
))
}
}
}
impl<S: SchemaContext> Union<S> {
/// Generate SDL for the union type. An example of union type in SDL format is:
/// ```graphql
/// """Union Type for search results"""
/// union SearchResult = Human | Droid | Starship
/// ```
/// Please note that, if there are no members in the union type, it will return `None`.
fn generate_sdl(&self, namespace: &S::Namespace) -> Option<String> {
let members_sdl = &self
.members
.iter()
.filter_map(|(union_member, member_value)| {
if member_value.get(namespace).is_some() {
Some(union_member.to_string())
} else {
None
}
})
.collect::<Vec<String>>();
if members_sdl.is_empty() {
None
} else {
Some(with_description(
&self.description,
format!(
"union {}{} = {}",
self.name,
generate_directives_sdl(&self.directives, None),
members_sdl.join(" | ")
),
))
}
}
}
impl<S: SchemaContext> Interface<S> {
/// Generate SDL for the interface type. An example of interface type in SDL format is:
/// ```graphql
/// """Interface for Node"""
/// interface Node {
/// id: ID!
/// }
/// ```
/// Please note that, if there are no fields in the interface type, it will return `None`.
fn generate_sdl(&self, namespace: &S::Namespace) -> Option<String> {
let fields_sdl = generate_fields_sdl(&self.fields, namespace);
if fields_sdl.is_empty() {
None
} else {
Some(with_description(
&self.description,
format!(
"interface {}{} {}",
&self.name,
generate_directives_sdl(&self.directives, None),
in_curly_braces(fields_sdl)
),
))
}
}
}
impl<S: SchemaContext> TypeInfo<S> {
fn generate_sdl(&self, namespace: &S::Namespace) -> Option<String> {
match self {
TypeInfo::Scalar(scalar) => Some(scalar.generate_sdl()),
TypeInfo::Enum(enm) => enm.generate_sdl(namespace),
TypeInfo::Object(object) => object.generate_sdl(namespace),
TypeInfo::Interface(interface) => interface.generate_sdl(namespace),
TypeInfo::Union(union) => union.generate_sdl(namespace),
TypeInfo::InputObject(input_object) => input_object.generate_sdl(namespace),
}
}
}
impl<S: SchemaContext> Schema<S> {
pub fn generate_sdl(&self, namespace: &S::Namespace) -> String {
let schema_sdl = get_schema_sdl(self, namespace);
self.types
.iter()
.fold(schema_sdl, |mut acc, (type_name, type_info)| {
// Ignore schema related types
if !type_name.as_str().starts_with("__") {
if let Some(type_sdl) = type_info.generate_sdl(namespace) {
acc.push_str("\n\n");
acc.push_str(&type_sdl);
}
};
acc
})
}
}
/// Generate SDL for the schema. An example of schema in SDL format is:
/// ```graphql
/// schema {
/// query: Query
/// mutation: Mutation
/// subscription: Subscription
/// }
/// ```
///
/// Please note that query type will always be there in the schema.
///
/// If there is no mutation type, it will not include mutation in the schema. Also, if there is no
/// mutation field in the mutation type, it will not include mutation in the schema.
///
/// If there is no subscription type, it will not include subscription in the schema.
fn get_schema_sdl<S: SchemaContext>(schema: &Schema<S>, namespace: &S::Namespace) -> String {
let query_field = format!("query: {} ", &schema.query_type);
let mutation_field = schema.mutation_type.as_ref().and_then(|t| {
schema.types.get(t).and_then(|type_info| match type_info {
TypeInfo::Object(object) => {
// If there is only __typename in the mutation fields, ignore the mutation altogether
if object
.fields
.iter()
.all(|(k, v)| (k.as_str() == "__typename") || (v.get(namespace).is_none()))
{
None
} else {
Some(format!("mutation: {} ", t))
}
}
_ => None,
})
});
let subscription_field = schema
.subscription_type
.as_ref()
.map(|t| format!("subscription: {} ", t));
format!(
"schema {}",
in_curly_braces(
vec![Some(query_field), mutation_field, subscription_field]
.into_iter()
.flatten()
.collect()
)
)
}
/// Generate SDL for description. Descriptions are just wrapped in 3 double quotes.
fn generate_description_sdl(description: &Option<String>) -> String {
description
.as_ref()
.map(|d| format!("\"\"\"{}\"\"\"", d))
.unwrap_or_default()
}
/// Generate SDL for directives. An example of directives in SDL format is:
/// ```graphql
/// @deprecated(reason: "Use `PENDING` instead")
/// ```
fn generate_directives_sdl(
directives: &[Directive],
deprecation_status: Option<&DeprecationStatus>,
) -> String {
let other_directives = directives
.iter()
.map(|d| {
let args = d
.arguments
.iter()
.map(|(k, v)| format!("{}: {}", k, v.to_json()))
.collect::<Vec<String>>()
.join(", ");
format!(
"@{}{}",
d.name,
if args.is_empty() {
String::default()
} else {
format!("({})", args)
}
)
})
.collect::<Vec<String>>()
.join(" ");
match deprecation_status {
Some(DeprecationStatus::Deprecated { reason }) => {
let reason_arg = reason
.as_ref()
.map(|r| format!("(reason: {})", r))
.unwrap_or_default();
format!("@deprecated{} {}", reason_arg, other_directives)
}
_ => other_directives,
}
}
/// Generate SDL for fields. This will not include schema related fields (fields starting with __).
fn generate_fields_sdl<S: SchemaContext>(
fields: &BTreeMap<ast::Name, Namespaced<S, Field<S>>>,
namespace: &S::Namespace,
) -> Vec<String> {
let mut fields_sdl = Vec::new();
for (field_name, field) in fields {
// Ignore schema related fields
if !field_name.as_str().starts_with("__") {
if let Some((data, _)) = field.get(namespace) {
fields_sdl.push(with_description(
&data.description,
format_field_with_type(
field_name,
&data.field_type,
Some(&data.deprecation_status),
),
));
}
}
}
fields_sdl
}
fn format_field_with_type(
field_name: &ast::Name,
field_type: &ast::TypeContainer<ast::TypeName>,
deprecation_status: Option<&DeprecationStatus>,
) -> String {
let field_sdl = format!("{}: {}", field_name, field_type);
match deprecation_status {
Some(deprecation_status) => format!(
"{} {}",
field_sdl,
generate_directives_sdl(&[], Some(deprecation_status))
),
_ => field_sdl,
}
}
fn with_description(description: &Option<String>, sdl: String) -> String {
if description.is_some() {
format!("{}\n{}", generate_description_sdl(description), sdl)
} else {
sdl
}
}
fn in_curly_braces(strings: Vec<String>) -> String {
format!(
"{{\n{}\n}}",
strings
.into_iter()
.map(with_indent)
.collect::<Vec<String>>()
.join("\n")
)
}
fn with_indent(sdl: String) -> String {
sdl.lines()
.map(|l| format!(" {}", l))
.collect::<Vec<String>>()
.join("\n")
}

View File

@ -258,7 +258,7 @@ fn object_type<'s, S: schema::SchemaContext>(
None,
S::introspection_node(),
ast::TypeContainer::named_null(RegisteredTypeName::string()),
std::collections::HashMap::new(),
std::collections::BTreeMap::new(),
schema::DeprecationStatus::NotDeprecated,
);
if allowed_fields.is_empty() {

View File

@ -1,5 +1,6 @@
pub mod ast;
pub mod generate_graphql_schema;
pub mod generate_sdl;
pub mod http;
pub mod introspection;
pub mod lexer;

View File

@ -100,7 +100,7 @@ impl<'a> Parser<'a> {
))
}
fn parse_const_arguments(
fn parse_field_arguments(
&mut self,
) -> super::Result<Option<Spanning<Vec<Spanning<InputValueDefinition>>>>> {
self.parse_optional_nonempty_delimited_list(
@ -110,6 +110,14 @@ impl<'a> Parser<'a> {
)
}
fn parse_const_arguments(&mut self) -> super::Result<Option<Spanning<Vec<ConstArgument>>>> {
self.parse_optional_nonempty_delimited_list(
lexer::Punctuation::ParenL,
lexer::Punctuation::ParenR,
|s| s.parse_key_value(|s| s.parse_const_value()),
)
}
fn parse_input_value_definition(
&mut self,
) -> super::Result<Spanning<crate::ast::schema::InputValueDefinition>> {
@ -143,7 +151,7 @@ impl<'a> Parser<'a> {
fn parse_field_definition(&mut self) -> super::Result<Spanning<FieldDefinition>> {
let description = self.parse_optional_string()?;
let name = self.parse_name()?;
let arguments = self.parse_const_arguments()?;
let arguments = self.parse_field_arguments()?;
self.parse_punctuation(lexer::Punctuation::Colon)?;
let field_type = self.parse_type()?;
let directives = self.parse_const_directives()?;

View File

@ -14,7 +14,7 @@ pub mod sdl;
// A simple wrapper on top of ast::TypeName so that we can track the construction
// of TypeNames during the schema building phase.
#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone, Hash)]
#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone, Hash, PartialOrd, Ord)]
pub struct RegisteredTypeName(pub(super) ast::TypeName);
impl RegisteredTypeName {
@ -220,13 +220,20 @@ impl DeprecationStatus {
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct Directive {
pub name: ast::Name,
pub arguments: BTreeMap<ast::Name, gql::ConstValue>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct Object<S: SchemaContext> {
pub name: ast::TypeName,
pub description: Option<String>,
pub fields: HashMap<ast::Name, Namespaced<S, Field<S>>>,
pub fields: BTreeMap<ast::Name, Namespaced<S, Field<S>>>,
/// The set of interfaces that this object type implements
pub interfaces: HashMap<ast::TypeName, Namespaced<S, ()>>,
pub interfaces: BTreeMap<ast::TypeName, Namespaced<S, ()>>,
pub directives: Vec<Directive>,
}
fn build_typename_field<S: SchemaContext>(builder: &mut Builder<S>) -> Namespaced<S, Field<S>> {
@ -239,7 +246,7 @@ fn build_typename_field<S: SchemaContext>(builder: &mut Builder<S>) -> Namespace
base: ast::BaseType::Named(TypeName(mk_name!("String"))),
nullable: false,
},
arguments: HashMap::new(),
arguments: BTreeMap::new(),
deprecation_status: DeprecationStatus::NotDeprecated,
},
S::introspection_namespace_node(),
@ -251,8 +258,9 @@ impl<S: SchemaContext> Object<S> {
builder: &mut Builder<S>,
name: ast::TypeName,
description: Option<String>,
fields: HashMap<ast::Name, Namespaced<S, Field<S>>>,
interfaces: HashMap<RegisteredTypeName, Namespaced<S, ()>>,
fields: BTreeMap<ast::Name, Namespaced<S, Field<S>>>,
interfaces: BTreeMap<RegisteredTypeName, Namespaced<S, ()>>,
directives: Vec<Directive>,
) -> Self {
let interfaces = interfaces.into_iter().map(|(k, v)| (k.0, v)).collect();
let mut definition = Object {
@ -260,6 +268,7 @@ impl<S: SchemaContext> Object<S> {
description,
fields,
interfaces,
directives,
};
let typename_field = build_typename_field(builder);
definition
@ -281,7 +290,7 @@ pub struct Field<S: SchemaContext> {
pub description: Option<String>,
pub info: S::GenericNodeInfo,
pub field_type: ast::Type,
pub arguments: HashMap<ast::Name, Namespaced<S, InputField<S>>>,
pub arguments: BTreeMap<ast::Name, Namespaced<S, InputField<S>>>,
pub deprecation_status: DeprecationStatus,
}
@ -291,7 +300,7 @@ impl<S: SchemaContext> Field<S> {
description: Option<String>,
info: S::GenericNodeInfo,
field_type: RegisteredType,
arguments: HashMap<ast::Name, Namespaced<S, InputField<S>>>,
arguments: BTreeMap<ast::Name, Namespaced<S, InputField<S>>>,
deprecation_status: DeprecationStatus,
) -> Self {
Field {
@ -309,19 +318,22 @@ impl<S: SchemaContext> Field<S> {
pub struct InputObject<S: SchemaContext> {
pub name: ast::TypeName,
pub description: Option<String>,
pub fields: HashMap<ast::Name, Namespaced<S, InputField<S>>>,
pub fields: BTreeMap<ast::Name, Namespaced<S, InputField<S>>>,
pub directives: Vec<Directive>,
}
impl<S: SchemaContext> InputObject<S> {
pub fn new(
name: ast::TypeName,
description: Option<String>,
fields: HashMap<ast::Name, Namespaced<S, InputField<S>>>,
fields: BTreeMap<ast::Name, Namespaced<S, InputField<S>>>,
directives: Vec<Directive>,
) -> Self {
InputObject {
name,
description,
fields,
directives,
}
}
// TODO: we'll probably have to pre-compute this if required
@ -366,6 +378,7 @@ impl<S: SchemaContext> InputField<S> {
pub struct Scalar {
pub name: ast::TypeName,
pub description: Option<String>,
pub directives: Vec<Directive>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
@ -380,15 +393,17 @@ pub struct EnumValue<S: SchemaContext> {
pub struct Enum<S: SchemaContext> {
pub name: ast::TypeName,
pub description: Option<String>,
pub values: HashMap<ast::Name, Namespaced<S, EnumValue<S>>>,
pub values: BTreeMap<ast::Name, Namespaced<S, EnumValue<S>>>,
pub directives: Vec<Directive>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct Union<S: SchemaContext> {
pub name: ast::TypeName,
pub description: Option<String>,
fields: HashMap<ast::Name, Namespaced<S, Field<S>>>,
pub members: HashMap<ast::TypeName, Namespaced<S, ()>>,
fields: BTreeMap<ast::Name, Namespaced<S, Field<S>>>,
pub members: BTreeMap<ast::TypeName, Namespaced<S, ()>>,
pub directives: Vec<Directive>,
}
impl<S: SchemaContext> Union<S> {
@ -396,14 +411,16 @@ impl<S: SchemaContext> Union<S> {
builder: &mut Builder<S>,
name: ast::TypeName,
description: Option<String>,
members: HashMap<RegisteredTypeName, Namespaced<S, ()>>,
members: BTreeMap<RegisteredTypeName, Namespaced<S, ()>>,
directives: Vec<Directive>,
) -> Self {
let typename_field = build_typename_field(builder);
Union {
name,
description,
fields: HashMap::from_iter([(typename_field.data.name.clone(), typename_field)]),
fields: BTreeMap::from_iter([(typename_field.data.name.clone(), typename_field)]),
members: members.into_iter().map(|(k, v)| (k.0, v)).collect(),
directives,
}
}
@ -411,15 +428,20 @@ impl<S: SchemaContext> Union<S> {
// Note Clone of Name is constant
self.members.keys().collect()
}
pub fn get_fields(&self) -> &BTreeMap<ast::Name, Namespaced<S, Field<S>>> {
&self.fields
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct Interface<S: SchemaContext> {
pub name: ast::TypeName,
pub description: Option<String>,
pub fields: HashMap<ast::Name, Namespaced<S, Field<S>>>,
pub interfaces: HashMap<ast::TypeName, Namespaced<S, ()>>,
pub implemented_by: HashMap<ast::TypeName, Namespaced<S, ()>>,
pub fields: BTreeMap<ast::Name, Namespaced<S, Field<S>>>,
pub interfaces: BTreeMap<ast::TypeName, Namespaced<S, ()>>,
pub implemented_by: BTreeMap<ast::TypeName, Namespaced<S, ()>>,
pub directives: Vec<Directive>,
}
impl<S: SchemaContext> Interface<S> {
@ -427,9 +449,10 @@ impl<S: SchemaContext> Interface<S> {
builder: &mut Builder<S>,
name: ast::TypeName,
description: Option<String>,
fields: HashMap<ast::Name, Namespaced<S, Field<S>>>,
interfaces: HashMap<RegisteredTypeName, Namespaced<S, ()>>,
implemented_by: HashMap<RegisteredTypeName, Namespaced<S, ()>>,
fields: BTreeMap<ast::Name, Namespaced<S, Field<S>>>,
interfaces: BTreeMap<RegisteredTypeName, Namespaced<S, ()>>,
implemented_by: BTreeMap<RegisteredTypeName, Namespaced<S, ()>>,
directives: Vec<Directive>,
) -> Self {
let mut definition = Interface {
name,
@ -437,6 +460,7 @@ impl<S: SchemaContext> Interface<S> {
fields,
interfaces: interfaces.into_iter().map(|(k, v)| (k.0, v)).collect(),
implemented_by: implemented_by.into_iter().map(|(k, v)| (k.0, v)).collect(),
directives,
};
let typename_field = build_typename_field(builder);
definition

View File

@ -4,7 +4,9 @@ use thiserror::Error;
use crate::ast::common as ast;
use crate::ast::schema as sdl;
use crate::ast::schema::ConstDirective;
use crate::ast::spanning::Positioned;
use crate::ast::spanning::Spanning;
use crate::parser;
#[derive(Error, Debug, Clone)]
@ -30,7 +32,7 @@ where
.parse_schema_document()
.map_err(Error::InternalParseError)?;
let mut types = BTreeMap::new();
let mut introspection_root_fields = HashMap::new();
let mut introspection_root_fields = BTreeMap::new();
let mut builder = Builder {
registered_types: HashSet::new(),
registered_namespaces: HashSet::new(),
@ -151,14 +153,42 @@ fn convert_scalar_type_definition(definition: &sdl::ScalarTypeDefinition) -> Res
.description
.as_ref()
.map(|description| description.item.clone()),
directives: Vec::new(),
})
}
fn convert_directives(const_directives: &[Spanning<ConstDirective>]) -> Vec<Directive> {
let mut directives = Vec::new();
for directive in const_directives {
directives.push(Directive {
name: directive.item.name.item.clone(),
arguments: directive
.item
.arguments
.as_ref()
.map(|arguments| {
arguments
.item
.iter()
.map(|argument| {
(
argument.item.key.item.clone(),
argument.item.value.item.clone(),
)
})
.collect()
})
.unwrap_or(BTreeMap::new()),
});
}
directives
}
fn convert_enum_type_definition<S: SchemaContext>(
builder: &mut Builder<S>,
definition: &sdl::EnumTypeDefinition,
) -> Result<Enum<S>> {
let mut values = HashMap::new();
let mut values = BTreeMap::new();
for enum_value_definition in &definition.values {
let enum_value_definition = &enum_value_definition.item;
let enum_value = &enum_value_definition.value.item;
@ -188,6 +218,7 @@ fn convert_enum_type_definition<S: SchemaContext>(
.as_ref()
.map(|description| description.item.clone()),
values,
directives: convert_directives(&definition.directives),
})
}
@ -200,7 +231,7 @@ where
S: SchemaContext,
F: FnMut(&mut Builder<S>, ast::TypeName) -> RegisteredTypeName,
{
let mut arguments = HashMap::new();
let mut arguments = BTreeMap::new();
for field_definition in &definition.arguments {
let argument_name = &field_definition.item.name.item;
let normalized_argument_definition = convert_input_value_definition(
@ -248,7 +279,7 @@ where
S: SchemaContext,
F: FnMut(&mut Builder<S>, ast::TypeName) -> RegisteredTypeName,
{
let mut fields = HashMap::new();
let mut fields = BTreeMap::new();
for field_definition in &definition.fields {
let field_name = &field_definition.item.name.item;
let normalized_field_definition =
@ -267,7 +298,7 @@ where
}
}
let mut implements = HashMap::new();
let mut implements = BTreeMap::new();
for interface in &definition.implements {
if implements
.insert(
@ -288,6 +319,7 @@ where
.map(|description| description.item.clone()),
fields,
implements,
convert_directives(&definition.directives),
))
}
@ -300,7 +332,7 @@ where
S: SchemaContext,
F: FnMut(&mut Builder<S>, ast::TypeName) -> RegisteredTypeName,
{
let mut fields = HashMap::new();
let mut fields = BTreeMap::new();
for field_definition in &definition.fields {
let field_name = &field_definition.item.name.item;
let normalized_field_definition =
@ -318,7 +350,7 @@ where
// TODO, throw an error
}
}
let mut implements = HashMap::new();
let mut implements = BTreeMap::new();
for interface in &definition.implements {
if implements
@ -331,7 +363,7 @@ where
// TODO throw an error
}
}
let implemented_by = HashMap::new();
let implemented_by = BTreeMap::new();
Ok(Interface::new(
builder,
ast::TypeName(definition.name.item.clone()),
@ -342,6 +374,7 @@ where
fields,
implements,
implemented_by,
convert_directives(&definition.directives),
))
}
@ -354,7 +387,7 @@ where
S: SchemaContext,
F: FnMut(&mut Builder<S>, ast::TypeName) -> RegisteredTypeName,
{
let mut members = HashMap::new();
let mut members = BTreeMap::new();
for member in &definition.members {
if members
.insert(
@ -374,6 +407,7 @@ where
.as_ref()
.map(|description| description.item.clone()),
members,
convert_directives(&definition.directives),
))
}
@ -415,7 +449,7 @@ where
S: SchemaContext,
F: FnMut(&mut Builder<S>, ast::TypeName) -> RegisteredTypeName,
{
let mut fields = HashMap::new();
let mut fields = BTreeMap::new();
for field_definition in &definition.fields {
let field_name = &field_definition.item.name.item;
let normalized_field_definition = convert_input_value_definition(
@ -443,5 +477,6 @@ where
.as_ref()
.map(|description| description.item.clone()),
fields,
convert_directives(&definition.directives),
))
}

View File

@ -1,4 +1,5 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
@ -27,7 +28,7 @@ use crate::schema;
pub struct SelectableType<'s, S: schema::SchemaContext> {
pub(super) type_name: &'s ast::TypeName,
pub(super) possible_types: HashSet<&'s ast::TypeName>,
fields: Option<&'s HashMap<ast::Name, schema::Namespaced<S, schema::Field<S>>>>,
fields: Option<&'s BTreeMap<ast::Name, schema::Namespaced<S, schema::Field<S>>>>,
}
impl<'s, S: schema::SchemaContext> SelectableType<'s, S> {
@ -82,7 +83,7 @@ impl<S: schema::SchemaContext> schema::Union<S> {
fn to_selectable_type(&self) -> SelectableType<S> {
SelectableType {
type_name: &self.name,
fields: None,
fields: Some(self.get_fields()),
possible_types: self.possible_types(),
}
}

View File

@ -339,6 +339,7 @@ mod test {
let int_scalar = Scalar {
name: int_typename.clone(),
description: None,
directives: vec![],
};
let int_scalar_type_info = InputType::Scalar(&int_scalar);
let schema = fake_schema();
@ -368,6 +369,7 @@ mod test {
let int_scalar = Scalar {
name: int_typename.clone(),
description: None,
directives: vec![],
};
let int_scalar_type_info = InputType::Scalar(&int_scalar);
let schema = fake_schema();
@ -395,6 +397,7 @@ mod test {
let int_scalar = Scalar {
name: int_typename.clone(),
description: None,
directives: vec![],
};
let int_scalar_type_info = InputType::Scalar(&int_scalar);
let schema = fake_schema();
@ -420,6 +423,7 @@ mod test {
let int_scalar = Scalar {
name: int_typename.clone(),
description: None,
directives: vec![],
};
let int_scalar_type_info = InputType::Scalar(&int_scalar);
let schema = fake_schema();
@ -446,6 +450,7 @@ mod test {
let int_scalar = Scalar {
name: int_typename.clone(),
description: None,
directives: vec![],
};
let int_scalar_type_info = InputType::Scalar(&int_scalar);
let schema = fake_schema();
@ -475,6 +480,7 @@ mod test {
let int_scalar = Scalar {
name: int_typename.clone(),
description: None,
directives: vec![],
};
let int_scalar_type_info = InputType::Scalar(&int_scalar);
let schema = fake_schema();
@ -500,6 +506,7 @@ mod test {
let int_scalar = Scalar {
name: int_typename.clone(),
description: None,
directives: vec![],
};
let int_scalar_type_info = InputType::Scalar(&int_scalar);
let schema = fake_schema();
@ -527,6 +534,7 @@ mod test {
let int_scalar = Scalar {
name: int_typename.clone(),
description: None,
directives: vec![],
};
let int_scalar_type_info = InputType::Scalar(&int_scalar);
let schema = fake_schema();

View File

@ -1,5 +1,6 @@
use indexmap::IndexMap;
use nonempty::NonEmpty;
use std::collections::BTreeMap;
use std::collections::HashMap;
use super::collect;
@ -262,7 +263,7 @@ fn normalize_arguments<'q, 's, S: schema::SchemaContext>(
variables: &input::value::Variables<'q, 's, S>,
type_name: &ast::TypeName,
field_name: &ast::Name,
arguments_schema: &'s HashMap<ast::Name, schema::Namespaced<S, schema::InputField<S>>>,
arguments_schema: &'s BTreeMap<ast::Name, schema::Namespaced<S, schema::InputField<S>>>,
arguments: &'q Option<Spanning<Vec<executable::Argument>>>,
) -> Result<IndexMap<ast::Name, normalized::InputField<'s, S>>> {
let mut arguments_map = HashMap::new();

View File

@ -791,6 +791,17 @@
"type": "null"
}
]
},
"apolloFederation": {
"description": "Configuration for exposing apollo federation related types and directives.",
"anyOf": [
{
"$ref": "#/definitions/ObjectApolloFederationConfig"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
@ -800,6 +811,40 @@
"title": "GraphQlTypeName",
"type": "string"
},
"ObjectApolloFederationConfig": {
"$id": "https://hasura.io/jsonschemas/metadata/ObjectApolloFederationConfig",
"title": "ObjectApolloFederationConfig",
"type": "object",
"required": [
"keys"
],
"properties": {
"keys": {
"type": "array",
"items": {
"$ref": "#/definitions/ApolloFederationObjectKey"
}
}
},
"additionalProperties": false
},
"ApolloFederationObjectKey": {
"$id": "https://hasura.io/jsonschemas/metadata/ApolloFederationObjectKey",
"title": "ApolloFederationObjectKey",
"type": "object",
"required": [
"fields"
],
"properties": {
"fields": {
"type": "array",
"items": {
"$ref": "#/definitions/FieldName"
}
}
},
"additionalProperties": false
},
"DataConnectorTypeMapping": {
"$id": "https://hasura.io/jsonschemas/metadata/DataConnectorTypeMapping",
"title": "DataConnectorTypeMapping",
@ -1242,7 +1287,10 @@
"queryRootField": "ArticleMany",
"description": "Description for the select many ArticleMany"
},
"orderByExpressionType": "Article_Order_By"
"orderByExpressionType": "Article_Order_By",
"apolloFederation": {
"entitySource": true
}
},
"description": "Description for the model Articles"
}
@ -1535,6 +1583,17 @@
"type": "null"
}
]
},
"apolloFederation": {
"description": "Apollo Federation configuration",
"anyOf": [
{
"$ref": "#/definitions/ModelApolloFederationConfiguration"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
@ -1629,6 +1688,21 @@
},
"additionalProperties": false
},
"ModelApolloFederationConfiguration": {
"$id": "https://hasura.io/jsonschemas/metadata/ModelApolloFederationConfiguration",
"title": "ModelApolloFederationConfiguration",
"type": "object",
"required": [
"entitySource"
],
"properties": {
"entitySource": {
"description": "Whether this model should be used as the source for fetching _entity for object of its type.",
"type": "boolean"
}
},
"additionalProperties": false
},
"CommandV1": {
"$id": "https://hasura.io/jsonschemas/metadata/CommandV1",
"title": "CommandV1",
@ -3064,6 +3138,16 @@
},
"mutation": {
"$ref": "#/definitions/MutationGraphqlConfig"
},
"apolloFederation": {
"anyOf": [
{
"$ref": "#/definitions/GraphqlApolloFederationConfig"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
@ -3305,6 +3389,20 @@
},
"additionalProperties": false
},
"GraphqlApolloFederationConfig": {
"$id": "https://hasura.io/jsonschemas/metadata/GraphqlApolloFederationConfig",
"title": "GraphqlApolloFederationConfig",
"type": "object",
"required": [
"enableRootFields"
],
"properties": {
"enableRootFields": {
"type": "boolean"
}
},
"additionalProperties": false
},
"OpenDdSubgraph": {
"$id": "https://hasura.io/jsonschemas/metadata/OpenDdSubgraph",
"title": "OpenDdSubgraph",

View File

@ -20,6 +20,7 @@ pub enum GraphqlConfig {
pub struct GraphqlConfigV1 {
pub query: QueryGraphqlConfig,
pub mutation: MutationGraphqlConfig,
pub apollo_federation: Option<GraphqlApolloFederationConfig>,
}
#[derive(Serialize, Clone, Debug, PartialEq, opendds_derive::OpenDd)]
@ -123,3 +124,11 @@ pub struct OrderByEnumTypeName {
pub struct MutationGraphqlConfig {
pub root_operation_type_name: String,
}
#[derive(Serialize, Clone, Debug, PartialEq, opendds_derive::OpenDd)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
#[opendd(json_schema(title = "GraphqlApolloFederationConfig"))]
pub struct GraphqlApolloFederationConfig {
pub enable_root_fields: bool,
}

View File

@ -122,7 +122,10 @@ impl ModelV1 {
"queryRootField": "ArticleMany",
"description": "Description for the select many ArticleMany"
},
"orderByExpressionType": "Article_Order_By"
"orderByExpressionType": "Article_Order_By",
"apolloFederation": {
"entitySource": true
}
},
"description": "Description for the model Articles"
})
@ -174,6 +177,8 @@ pub struct ModelGraphQlDefinition {
pub arguments_input_type: Option<GraphQlTypeName>,
/// The type name of the order by expression input type.
pub order_by_expression_type: Option<GraphQlTypeName>,
/// Apollo Federation configuration
pub apollo_federation: Option<ModelApolloFederationConfiguration>,
}
impl ModelGraphQlDefinition {
@ -275,3 +280,12 @@ pub enum OrderByDirection {
Asc,
Desc,
}
#[derive(Serialize, Clone, Debug, PartialEq, opendds_derive::OpenDd)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
#[opendd(json_schema(title = "ModelApolloFederationConfiguration"))]
pub struct ModelApolloFederationConfiguration {
/// Whether this model should be used as the source for fetching _entity for object of its type.
pub entity_source: bool,
}

View File

@ -253,6 +253,8 @@ pub struct ObjectTypeGraphQLConfiguration {
pub type_name: Option<GraphQlTypeName>,
/// The name to use for the GraphQL type representation of this object type when used in an input context.
pub input_type_name: Option<GraphQlTypeName>,
/// Configuration for exposing apollo federation related types and directives.
pub apollo_federation: Option<ObjectApolloFederationConfig>,
// TODO: Add type_kind if we want to allow making objects interfaces.
}
@ -654,3 +656,19 @@ pub struct Deprecated {
}
impl_OpenDd_default_for!(Deprecated);
#[derive(Serialize, Clone, Debug, PartialEq, JsonSchema, opendds_derive::OpenDd, Eq)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
#[opendd(json_schema(title = "ObjectApolloFederationConfig"))]
pub struct ObjectApolloFederationConfig {
pub keys: Vec<ApolloFederationObjectKey>,
}
#[derive(Serialize, Clone, Debug, PartialEq, JsonSchema, opendds_derive::OpenDd, Eq)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
#[opendd(json_schema(title = "ApolloFederationObjectKey"))]
pub struct ApolloFederationObjectKey {
pub fields: Vec<FieldName>,
}