diff --git a/compiler/crates/relay-compiler/src/build_project/build_resolvers_schema.rs b/compiler/crates/relay-compiler/src/build_project/build_resolvers_schema.rs index 5b9640a592d85..df038d3c0b30a 100644 --- a/compiler/crates/relay-compiler/src/build_project/build_resolvers_schema.rs +++ b/compiler/crates/relay-compiler/src/build_project/build_resolvers_schema.rs @@ -6,12 +6,25 @@ */ use common::DiagnosticsResult; +use common::Span; use docblock_syntax::DocblockAST; use errors::try_all; use fnv::FnvHashMap; +use graphql_syntax::ConstantDirective; use graphql_syntax::ExecutableDefinition; +use graphql_syntax::FieldDefinition; +use graphql_syntax::Identifier; +use graphql_syntax::InterfaceTypeExtension; +use graphql_syntax::List; +use graphql_syntax::ObjectTypeDefinition; +use graphql_syntax::ObjectTypeExtension; +use graphql_syntax::ScalarTypeDefinition; +use graphql_syntax::Token; +use graphql_syntax::TokenKind; +use graphql_syntax::TypeSystemDefinition; use relay_config::ProjectName; use relay_docblock::extend_schema_with_resolver_type_system_definition; +use relay_transforms::RESOLVER_BELONGS_TO_BASE_SCHEMA_DIRECTIVE; use schema::SDLSchema; use crate::compiler_state::CompilerState; @@ -26,13 +39,36 @@ pub(crate) fn extend_schema_with_resolvers( project_config: &ProjectConfig, graphql_asts_map: &FnvHashMap, ) -> DiagnosticsResult<()> { + // Get resolver types/fields for main project let ResolverSchemaDocuments { type_asts, field_asts_and_definitions, - } = extract_schema_documents_for_resolvers(compiler_state, project_config, graphql_asts_map)?; + } = extract_schema_documents_for_resolvers( + &project_config.name, + compiler_state, + graphql_asts_map, + )?; + + extend_schema_with_types(schema, project_config, type_asts, false)?; + extend_schema_with_fields(schema, project_config, field_asts_and_definitions, false)?; - extend_schema_with_types(schema, project_config, type_asts)?; - extend_schema_with_fields(schema, project_config, field_asts_and_definitions)?; + if let Some(base_project_name) = project_config.base { + // We also need to extend the schema with resolvers from base project. + // But we need to mark them with special directive, so we do not + // add any new documents (query/fragment) for them, + // when calling `apply_transform`. + let ResolverSchemaDocuments { + type_asts, + field_asts_and_definitions, + } = extract_schema_documents_for_resolvers( + &base_project_name, + compiler_state, + graphql_asts_map, + )?; + + extend_schema_with_types(schema, project_config, type_asts, true)?; + extend_schema_with_fields(schema, project_config, field_asts_and_definitions, true)?; + } Ok(()) } @@ -41,6 +77,7 @@ fn extend_schema_with_types( schema: &mut SDLSchema, project_config: &ProjectConfig, type_asts: TypeAsts, + is_base_project: bool, ) -> DiagnosticsResult<()> { let type_definitions = build_schema_documents_from_docblocks(&type_asts.0, project_config, schema, None)?; @@ -48,20 +85,25 @@ fn extend_schema_with_types( for schema_document in type_definitions { for definition in schema_document.definitions { extend_schema_with_resolver_type_system_definition( - definition, + if is_base_project { + mark_extension_as_base(definition) + } else { + definition + }, schema, schema_document.location, )?; } } - Ok(()) } +/// Extend the schema with resolver fields fn extend_schema_with_fields<'a>( schema: &mut SDLSchema, project_config: &ProjectConfig, field_asts_and_definitions: FieldAstsAndDefinitions<'a>, + is_base_project: bool, ) -> DiagnosticsResult<()> { let field_definitions = try_all(field_asts_and_definitions.0.into_iter().map( |(asts, definitions)| { @@ -76,7 +118,11 @@ fn extend_schema_with_fields<'a>( .map::, _>(|schema_document| { for definition in schema_document.definitions { extend_schema_with_resolver_type_system_definition( - definition, + if is_base_project { + mark_extension_as_base(definition) + } else { + definition + }, schema, schema_document.location, )?; @@ -96,52 +142,45 @@ struct TypeAsts(Vec); struct FieldAstsAndDefinitions<'a>(Vec<(Vec, Option<&'a Vec>)>); fn extract_schema_documents_for_resolvers<'a>( + project_name: &'a ProjectName, compiler_state: &'a CompilerState, - project_config: &'a ProjectConfig, graphql_asts_map: &'a FnvHashMap, ) -> DiagnosticsResult> { - let mut projects = vec![project_config.name]; - projects.extend(project_config.base); - - let docblock_ast_sources = projects.iter().map(|project_name| { - ( - compiler_state.docblocks.get(project_name), - graphql_asts_map.get(project_name), - ) - }); - + let docblock_ast_sources = ( + compiler_state.docblocks.get(project_name), + graphql_asts_map.get(project_name), + ); let mut errors = vec![]; let mut type_asts = vec![]; let mut field_asts_and_definitions = vec![]; - for docblock_ast in docblock_ast_sources { - if let (Some(docblocks), Some(graphql_asts)) = docblock_ast { - for (file_path, docblock_sources) in &docblocks.get_all() { - match parse_docblock_asts_from_sources(file_path, docblock_sources) { - Ok(result) => { - // Type resolvers should not rely on any fragments - // @rootFragment is not supported for them, so - // we don't need to extract any fragments from the `file_path` - type_asts.extend(result.types); - - // But for fields, we may need to validate the correctness - // of the @rootFragment. - // And here we're reading GraphQL asts for the file, - // and keeping them together with Docblock ASTs - if !result.fields.is_empty() { - field_asts_and_definitions.push(( - result.fields, - graphql_asts.get_executable_definitions_for_file(file_path), - )); - } + if let (Some(docblocks), Some(graphql_asts)) = docblock_ast_sources { + for (file_path, docblock_sources) in &docblocks.get_all() { + match parse_docblock_asts_from_sources(file_path, docblock_sources) { + Ok(result) => { + // Type resolvers should not rely on any fragments + // @rootFragment is not supported for them, so + // we don't need to extract any fragments from the `file_path` + type_asts.extend(result.types); + + // But for fields, we may need to validate the correctness + // of the @rootFragment. + // And here we're reading GraphQL asts for the file, + // and keeping them together with Docblock ASTs + if !result.fields.is_empty() { + field_asts_and_definitions.push(( + result.fields, + graphql_asts.get_executable_definitions_for_file(file_path), + )); } - Err(err) => errors.extend(err), } + Err(err) => errors.extend(err), } - } else { - panic!("Expected to have access to AST and docblock sources."); } + } else { + panic!("Expected to have access to AST and docblock sources."); } + if errors.is_empty() { Ok(ResolverSchemaDocuments { type_asts: TypeAsts(type_asts), @@ -151,3 +190,93 @@ fn extract_schema_documents_for_resolvers<'a>( Err(errors) } } + +/// Mark extension as base schema extension (add speical directive to the type/field) +fn mark_extension_as_base(definition: TypeSystemDefinition) -> TypeSystemDefinition { + match definition { + TypeSystemDefinition::ObjectTypeDefinition(def) => { + TypeSystemDefinition::ObjectTypeDefinition(ObjectTypeDefinition { + directives: merge_directives( + &def.directives, + &[belongs_to_base_schema_directive()], + ), + ..def + }) + } + TypeSystemDefinition::ScalarTypeDefinition(def) => { + TypeSystemDefinition::ScalarTypeDefinition(ScalarTypeDefinition { + directives: merge_directives( + &def.directives, + &[belongs_to_base_schema_directive()], + ), + ..def + }) + } + TypeSystemDefinition::ObjectTypeExtension(def) => { + TypeSystemDefinition::ObjectTypeExtension(ObjectTypeExtension { + fields: mark_fields_as_base(def.fields), + ..def + }) + } + TypeSystemDefinition::InterfaceTypeExtension(def) => { + TypeSystemDefinition::InterfaceTypeExtension(InterfaceTypeExtension { + fields: mark_fields_as_base(def.fields), + ..def + }) + } + _ => panic!( + "Expected docblocks to only expose object and scalar definitions, and object and interface extensions." + ), + } +} + +/// Mark fields as base schema extension fields +fn mark_fields_as_base(fields: Option>) -> Option> { + fields.map(|list| List { + items: list + .items + .iter() + .map(|item| FieldDefinition { + directives: merge_directives( + &item.directives, + &[belongs_to_base_schema_directive()], + ), + ..item.clone() + }) + .collect(), + ..list + }) +} + +/// Merge two lists of directives +fn merge_directives(a: &[ConstantDirective], b: &[ConstantDirective]) -> Vec { + if a.is_empty() { + b.to_vec() + } else if b.is_empty() { + a.to_vec() + } else { + let mut directives = a.to_vec(); + directives.extend(b.iter().cloned()); + directives + } +} + +/// Create special directive to mark types/fields as belonging to base schema +fn belongs_to_base_schema_directive() -> ConstantDirective { + ConstantDirective { + name: Identifier { + value: RESOLVER_BELONGS_TO_BASE_SCHEMA_DIRECTIVE.0, + span: Span::empty(), + token: Token { + span: Span::empty(), + kind: TokenKind::Empty, + }, + }, + arguments: None, + span: Span::empty(), + at: Token { + span: Span::empty(), + kind: TokenKind::Empty, + }, + } +} diff --git a/compiler/crates/relay-docblock/src/lib.rs b/compiler/crates/relay-docblock/src/lib.rs index db7651e68f539..32f7f8abfb9a9 100644 --- a/compiler/crates/relay-docblock/src/lib.rs +++ b/compiler/crates/relay-docblock/src/lib.rs @@ -92,7 +92,7 @@ pub fn extend_schema_with_resolver_type_system_definition( schema.add_interface_type_extension(extension, location.source_location())?; } _ => panic!( - "Expected docblocks to only expose object and scalar extensions, and object and interface definitions" + "Expected docblocks to only expose object and scalar definitions, and object and interface extensions." ), }) } diff --git a/compiler/crates/relay-transforms/src/generate_relay_resolvers_model_fragments.rs b/compiler/crates/relay-transforms/src/generate_relay_resolvers_model_fragments.rs index aec0382d987d9..b912ed1962072 100644 --- a/compiler/crates/relay-transforms/src/generate_relay_resolvers_model_fragments.rs +++ b/compiler/crates/relay-transforms/src/generate_relay_resolvers_model_fragments.rs @@ -7,6 +7,7 @@ use std::sync::Arc; +use common::DirectiveName; use common::NamedItem; use common::WithLocation; use docblock_shared::ResolverSourceHash; @@ -32,6 +33,7 @@ lazy_static! { // help us avoid potential collision with product code (__self, __instance can be used for something else) static ref RESOLVER_MODEL_INSTANCE_FIELD_NAME: StringKey = "__relay_model_instance".intern(); + pub static ref RESOLVER_BELONGS_TO_BASE_SCHEMA_DIRECTIVE: DirectiveName = DirectiveName("__belongs_to_base_schema".intern()); } /// Currently, this is a wrapper of the hash of the resolver source code. @@ -58,6 +60,16 @@ pub fn generate_relay_resolvers_model_fragments( .named(*RELAY_RESOLVER_MODEL_DIRECTIVE_NAME) .is_some() { + // For resolvers that belong to the base schema, we don't need to generate operations. + // These operations should be generated during compilcation of the base project. + if object + .directives + .named(*RESOLVER_BELONGS_TO_BASE_SCHEMA_DIRECTIVE) + .is_some() + { + continue; + } + let object_type = program.schema.get_type(object.name.item.0).unwrap(); let model_instance_field_id = program diff --git a/compiler/crates/relay-transforms/src/generate_relay_resolvers_operations_for_nested_objects.rs b/compiler/crates/relay-transforms/src/generate_relay_resolvers_operations_for_nested_objects.rs index fa2892bd8d10e..1fbeb516c3065 100644 --- a/compiler/crates/relay-transforms/src/generate_relay_resolvers_operations_for_nested_objects.rs +++ b/compiler/crates/relay-transforms/src/generate_relay_resolvers_operations_for_nested_objects.rs @@ -41,6 +41,7 @@ use crate::match_::RawResponseGenerationMode; use crate::relay_resolvers::get_bool_argument_is_true; use crate::SplitOperationMetadata; use crate::ValidationMessage; +use crate::RESOLVER_BELONGS_TO_BASE_SCHEMA_DIRECTIVE; fn generate_fat_selections_from_type( schema: &SDLSchema, @@ -472,6 +473,16 @@ pub fn generate_relay_resolvers_operations_for_nested_objects( } if let Some(directive) = field.directives.named(*RELAY_RESOLVER_DIRECTIVE_NAME) { + // For resolvers that belong to the base schema, we don't need to generate fragments. + // These fragments should be generated during compilcation of the base project. + if field + .directives + .named(*RESOLVER_BELONGS_TO_BASE_SCHEMA_DIRECTIVE) + .is_some() + { + continue; + } + let has_output_type = get_bool_argument_is_true(&directive.arguments, *HAS_OUTPUT_TYPE_ARGUMENT_NAME); if !has_output_type { diff --git a/compiler/crates/relay-transforms/src/lib.rs b/compiler/crates/relay-transforms/src/lib.rs index e9f18c20ed49c..6a551b5fb1d59 100644 --- a/compiler/crates/relay-transforms/src/lib.rs +++ b/compiler/crates/relay-transforms/src/lib.rs @@ -129,6 +129,7 @@ pub use generate_data_driven_dependency_metadata::RelayDataDrivenDependencyMetad pub use generate_id_field::generate_id_field; pub use generate_live_query_metadata::generate_live_query_metadata; pub use generate_relay_resolvers_model_fragments::ArtifactSourceKeyData; +pub use generate_relay_resolvers_model_fragments::RESOLVER_BELONGS_TO_BASE_SCHEMA_DIRECTIVE; pub use generate_relay_resolvers_operations_for_nested_objects::generate_relay_resolvers_operations_for_nested_objects; pub use generate_typename::generate_typename; pub use generate_typename::TYPE_DISCRIMINATOR_DIRECTIVE_NAME;