Skip to content

Commit

Permalink
Add directives to the base project resolvers (types/fields)
Browse files Browse the repository at this point in the history
Summary:
We need this directive to correctly skip fragment generation with
multi-projects where projects have a dependency on a `base` project.

When a `project` depends on the `base`, the assumption is that
all schema types and fields from the `base` are available for the project.

For resolvers, it means we're adding all "base" resolvers
to the current project. This leads to a situation where the transforms,
such as `generate_relay_resolvers_model_fragments.rs`, that are responsible for
creating additional documents for resolvers, are generating documents
for both the current project and the `base` project.

To prevent generation for `base` project, we will mark `base` project resolver-types,
and fields with the directive `@__belongs_to_base_schema`.
This directive will instruct us to skip adding fragments for such fields and objects.

Reviewed By: captbaritone

Differential Revision: D49014147

fbshipit-source-id: b787702024bbaab4be48ce3067e35db34a06b1b3
  • Loading branch information
alunyov authored and facebook-github-bot committed Sep 8, 2023
1 parent 3837d37 commit c93322b
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,13 +39,36 @@ pub(crate) fn extend_schema_with_resolvers(
project_config: &ProjectConfig,
graphql_asts_map: &FnvHashMap<ProjectName, GraphQLAsts>,
) -> 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(())
}
Expand All @@ -41,27 +77,33 @@ 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)?;

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)| {
Expand All @@ -76,7 +118,11 @@ fn extend_schema_with_fields<'a>(
.map::<DiagnosticsResult<()>, _>(|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,
)?;
Expand All @@ -96,52 +142,45 @@ struct TypeAsts(Vec<DocblockAST>);
struct FieldAstsAndDefinitions<'a>(Vec<(Vec<DocblockAST>, Option<&'a Vec<ExecutableDefinition>>)>);

fn extract_schema_documents_for_resolvers<'a>(
project_name: &'a ProjectName,
compiler_state: &'a CompilerState,
project_config: &'a ProjectConfig,
graphql_asts_map: &'a FnvHashMap<ProjectName, GraphQLAsts>,
) -> DiagnosticsResult<ResolverSchemaDocuments<'a>> {
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),
Expand All @@ -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<List<FieldDefinition>>) -> Option<List<FieldDefinition>> {
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<ConstantDirective> {
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,
},
}
}
2 changes: 1 addition & 1 deletion compiler/crates/relay-docblock/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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."
),
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

use std::sync::Arc;

use common::DirectiveName;
use common::NamedItem;
use common::WithLocation;
use docblock_shared::ResolverSourceHash;
Expand All @@ -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.
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions compiler/crates/relay-transforms/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit c93322b

Please sign in to comment.