Skip to content

Commit

Permalink
Add root fragment support to relay resolvers
Browse files Browse the repository at this point in the history
Summary: This diff allows Flow based resolvers to use fragments as their input, mirroring the "root fragment" in docblock resolvers. It pulls out the relevant features from the graphql string matching the name of the argument to the resolver. It then produces the equivalent docblock IR as if the features were parsed from the docblock.

Reviewed By: tyao1

Differential Revision: D57033324

fbshipit-source-id: c1ae5e4ab0a7e965efd4a595baba5e41b5f3ff64
  • Loading branch information
evanyeung authored and facebook-github-bot committed May 10, 2024
1 parent 35f9763 commit 77a87be
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 10 deletions.
2 changes: 2 additions & 0 deletions compiler/crates/relay-schema-generation/Cargo.toml
Expand Up @@ -25,11 +25,13 @@ hermes_parser = { git = "https://github.com/facebook/hermes.git" }
intern = { path = "../intern" }
relay-docblock = { path = "../relay-docblock" }
rustc-hash = "1.1.0"
schema = { path = "../schema" }
schema-extractor = { path = "../schema-extractor" }
serde = { version = "1.0.185", features = ["derive", "rc"] }
thiserror = "1.0.49"

[dev-dependencies]
extract-graphql = { path = "../extract-graphql" }
fixture-tests = { path = "../fixture-tests" }
graphql-cli = { path = "../graphql-cli" }
graphql-test-helpers = { path = "../graphql-test-helpers" }
Expand Down
7 changes: 6 additions & 1 deletion compiler/crates/relay-schema-generation/src/errors.rs
Expand Up @@ -6,13 +6,13 @@
*/

use intern::string_key::StringKey;
use schema::suggestion_list::did_you_mean;
use thiserror::Error;

use crate::JSImportType;

#[derive(
Clone,
Copy,
Debug,
Error,
Eq,
Expand Down Expand Up @@ -57,4 +57,9 @@ pub enum SchemaGenerationError {
},
#[error("Not yet implemented")]
TODO,
#[error("Fragment `{fragment_name}` not found.{suggestions}", suggestions = did_you_mean(suggestions))]
FragmentNotFound {
fragment_name: StringKey,
suggestions: Vec<StringKey>,
},
}
76 changes: 71 additions & 5 deletions compiler/crates/relay-schema-generation/src/lib.rs
Expand Up @@ -31,6 +31,7 @@ use errors::SchemaGenerationError;
use graphql_ir::FragmentDefinitionName;
use graphql_syntax::ExecutableDefinition;
use graphql_syntax::FieldDefinition;
use graphql_syntax::FragmentDefinition;
use graphql_syntax::Identifier;
use graphql_syntax::ListTypeAnnotation;
use graphql_syntax::NamedTypeAnnotation;
Expand Down Expand Up @@ -328,7 +329,7 @@ impl RelayResolverExtractor {
if key.module_name.lookup().ends_with(".graphql")
&& field_definition.entity_name.item.lookup().ends_with("$key")
{
self.add_fragment_field_definition(fragment_definitions, field_definition)
self.add_fragment_field_definition(fragment_definitions, field_definition)?
} else {
self.unresolved_field_definitions
.push((key.clone(), field_definition));
Expand All @@ -338,10 +339,75 @@ impl RelayResolverExtractor {

fn add_fragment_field_definition(
&mut self,
_fragment_definitions: &Option<&Vec<ExecutableDefinition>>,
_field_definition: UnresolvedFieldDefinition,
) {
// TODO
fragment_definitions: &Option<&Vec<ExecutableDefinition>>,
field: UnresolvedFieldDefinition,
) -> DiagnosticsResult<()> {
let field_definition = FieldDefinition {
name: string_key_to_identifier(field.field_name),
type_: return_type_to_type_annotation(field.return_type)?,
arguments: None,
directives: vec![],
description: None,
hack_source: None,
span: field.field_name.location.span(),
};
let fragment_definition =
self.parse_fragment_definition(&field.entity_name, fragment_definitions)?;
let fragment_type_condition = WithLocation::from_span(
fragment_definition.location.source_location(),
fragment_definition.type_condition.span,
fragment_definition.type_condition.type_.value,
);
let live = field
.is_live
.map(|loc| UnpopulatedIrField { key_location: loc });
self.resolved_field_definitions.push(TerseRelayResolverIr {
field: field_definition,
type_: fragment_type_condition,
root_fragment: Some(field.entity_name.map(FragmentDefinitionName)), // this includes the $key
location: field.field_name.location,
deprecated: None,
live,
fragment_arguments: None, // We don't support arguments right now
source_hash: field.source_hash,
semantic_non_null: None,
});
Ok(())
}

// Most of this code is copied from docblock.ir
// find a way to share it?
fn parse_fragment_definition<'a>(
&self,
fragment_key: &WithLocation<StringKey>,
definitions: &Option<&'a Vec<ExecutableDefinition>>,
) -> DiagnosticsResult<&'a FragmentDefinition> {
let fragment_name = fragment_key.item.lookup().strip_suffix("$key").unwrap();
let fragment_definition = definitions.and_then(|defs| {
defs.iter().find(|item| {
if let ExecutableDefinition::Fragment(fragment) = item {
fragment.name.value.lookup() == fragment_name
} else {
false
}
})
});

if let Some(ExecutableDefinition::Fragment(fragment_definition)) = fragment_definition {
Ok(fragment_definition)
} else {
let suggestions = definitions
.map(|defs| defs.iter().filter_map(|def| def.name()).collect::<Vec<_>>())
.unwrap_or_default();

Err(vec![Diagnostic::error(
SchemaGenerationError::FragmentNotFound {
fragment_name: fragment_key.item,
suggestions,
},
fragment_key.location,
)])
}
}

fn add_type_definition(
Expand Down
32 changes: 30 additions & 2 deletions compiler/crates/relay-schema-generation/tests/docblock.rs
Expand Up @@ -11,8 +11,10 @@ use std::sync::Arc;
use common::Diagnostic;
use common::SourceLocationKey;
use common::TextSource;
use extract_graphql::JavaScriptSourceFeature;
use fixture_tests::Fixture;
use graphql_cli::DiagnosticPrinter;
use graphql_syntax::ExecutableDefinition;
use graphql_test_helpers::ProjectFixture;
use intern::Lookup;
use relay_config::ProjectName;
Expand All @@ -31,8 +33,12 @@ pub async fn transform_fixture(fixture: &Fixture<'_>) -> Result<String, String>
let project_fixture = ProjectFixture::deserialize(fixture.content);

project_fixture.files().iter().for_each(|(path, content)| {
if let Err(err) = extractor.parse_document(content, path.to_string_lossy().as_ref(), &None)
{
let gql_operations = parse_document_definitions(content, path);
if let Err(err) = extractor.parse_document(
content,
path.to_string_lossy().as_ref(),
&Some(&gql_operations),
) {
errors.extend(err);
}
});
Expand Down Expand Up @@ -86,6 +92,28 @@ pub async fn transform_fixture(fixture: &Fixture<'_>) -> Result<String, String>
Ok(out.join("\n\n") + "\n\n" + &err)
}

fn parse_document_definitions(content: &str, path: &Path) -> Vec<ExecutableDefinition> {
let features = extract_graphql::extract(content);
features
.into_iter()
.filter_map(|feature| {
if let JavaScriptSourceFeature::GraphQL(graphql_source) = feature {
Some(graphql_source.to_text_source().text)
} else {
None
}
})
.flat_map(|query_text| {
graphql_syntax::parse_executable(
&query_text,
SourceLocationKey::standalone(path.to_str().unwrap()),
)
.unwrap()
.definitions
})
.collect()
}

fn diagnostics_to_sorted_string(fixtures: &ProjectFixture, diagnostics: &[Diagnostic]) -> String {
let printer = DiagnosticPrinter::new(|source_location| match source_location {
SourceLocationKey::Standalone { path } => {
Expand Down
Expand Up @@ -8,10 +8,18 @@

//- module.js

import type Cat from 'Cat.js';
import type {CatIsHungryFragment$key} from 'CatIsHungryFragment.graphql';

import {graphql} from 'relay-runtime';
import {readFragment} from 'relay-runtime/store/ResolverFragments';

import type {CatIsHungryFragment$key} from 'CatIsHungryFragment.graphql';
/**
* @RelayResolver
*/
export function Cat(id: DataID): Cat {
return {}
}

/**
* @RelayResolver
Expand All @@ -27,3 +35,100 @@ export function full_name(key: CatIsHungryFragment$key): string {
return `${first_name} ${last_name}`;
}
==================================== OUTPUT ===================================
StrongObjectResolver(
StrongObjectIr {
type_name: Identifier {
span: 449:452,
token: Token {
span: 449:452,
kind: Identifier,
},
value: "Cat",
},
rhs_location: module.js:449:452,
root_fragment: WithLocation {
location: module.js:449:452,
item: FragmentDefinitionName(
"Cat__id",
),
},
description: None,
deprecated: None,
live: None,
semantic_non_null: None,
location: module.js:449:452,
implements_interfaces: [],
source_hash: ResolverSourceHash(
"97c71c89ad8ccb1e0ff10de8887a25e0",
),
},
)
type Cat @__RelayResolverModel {
id: ID!
__relay_model_instance: RelayResolverValue! @relay_resolver(import_path: "module.js", fragment_name: "Cat__id", generated_fragment: true, inject_fragment_data: "id", import_name: "Cat") @resolver_source_hash(value: "97c71c89ad8ccb1e0ff10de8887a25e0") @unselectable(reason: "This field is intended only for Relay's internal use")
}


TerseRelayResolver(
TerseRelayResolverIr {
field: FieldDefinition {
name: Identifier {
span: 529:538,
token: Token {
span: 529:538,
kind: Identifier,
},
value: "full_name",
},
type_: NonNull(
NonNullTypeAnnotation {
span: 570:576,
type_: Named(
NamedTypeAnnotation {
name: Identifier {
span: 570:576,
token: Token {
span: 570:576,
kind: Identifier,
},
value: "String",
},
},
),
exclamation: Token {
span: 0:0,
kind: Empty,
},
},
),
arguments: None,
directives: [],
description: None,
hack_source: None,
span: 529:538,
},
type_: WithLocation {
location: module.js:34:40,
item: "Cat",
},
root_fragment: Some(
WithLocation {
location: module.js:544:567,
item: FragmentDefinitionName(
"CatIsHungryFragment$key",
),
},
),
deprecated: None,
semantic_non_null: None,
live: None,
location: module.js:529:538,
fragment_arguments: None,
source_hash: ResolverSourceHash(
"97c71c89ad8ccb1e0ff10de8887a25e0",
),
},
)
extend type Cat {
full_name: String! @relay_resolver(import_path: "module.js", fragment_name: "CatIsHungryFragment$key", has_output_type: true, import_name: "full_name") @resolver_source_hash(value: "97c71c89ad8ccb1e0ff10de8887a25e0")
}
Expand Up @@ -7,10 +7,18 @@

//- module.js

import type Cat from 'Cat.js';
import type {CatIsHungryFragment$key} from 'CatIsHungryFragment.graphql';

import {graphql} from 'relay-runtime';
import {readFragment} from 'relay-runtime/store/ResolverFragments';

import type {CatIsHungryFragment$key} from 'CatIsHungryFragment.graphql';
/**
* @RelayResolver
*/
export function Cat(id: DataID): Cat {
return {}
}

/**
* @RelayResolver
Expand Down

0 comments on commit 77a87be

Please sign in to comment.