mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-09-11 10:46:25 +03:00
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:
parent
e931a391eb
commit
0642cbadaa
@ -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 {
|
||||
|
@ -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(),
|
||||
));
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
189
v3/crates/engine/src/execute/ir/query_root/apollo_federation.rs
Normal file
189
v3/crates/engine/src/execute/ir/query_root/apollo_federation.rs
Normal 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)
|
||||
}
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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 { .. } => {}
|
||||
|
@ -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>,
|
||||
|
@ -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",
|
||||
},
|
||||
)?;
|
||||
|
||||
|
@ -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>,
|
||||
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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(())
|
||||
|
@ -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,
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
69
v3/crates/engine/src/schema/apollo_federation.rs
Normal file
69
v3/crates/engine/src/schema/apollo_federation.rs
Normal 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(),
|
||||
))
|
||||
}
|
@ -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) =
|
||||
|
@ -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(),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
@ -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()),
|
||||
))
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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(),
|
||||
))
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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(),
|
||||
))
|
||||
}
|
||||
|
133
v3/crates/engine/src/schema/query_root/apollo_federation.rs
Normal file
133
v3/crates/engine/src/schema/query_root/apollo_federation.rs
Normal 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,
|
||||
})
|
||||
}
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
@ -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(
|
||||
|
@ -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(),
|
||||
))
|
||||
}
|
||||
|
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()),
|
||||
))
|
||||
}
|
||||
|
@ -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),
|
||||
)))
|
||||
}
|
||||
|
@ -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(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"data": {
|
||||
"_entities": [
|
||||
{
|
||||
"author_id": 1,
|
||||
"first_name": "Peter",
|
||||
"last_name": "Landin"
|
||||
},
|
||||
{
|
||||
"author_id": 2,
|
||||
"first_name": "John",
|
||||
"last_name": "Hughes"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
{}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
{ "x-hasura-role": "admin" }
|
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
{}
|
@ -0,0 +1,7 @@
|
||||
query {
|
||||
__typename
|
||||
_service {
|
||||
__typename
|
||||
sdl
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
{ "x-hasura-role": "admin" }
|
@ -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],
|
||||
);
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
441
v3/crates/lang-graphql/src/generate_sdl.rs
Normal file
441
v3/crates/lang-graphql/src/generate_sdl.rs
Normal 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")
|
||||
}
|
@ -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() {
|
||||
|
@ -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;
|
||||
|
@ -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()?;
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
))
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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>,
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user