Skip to content

Commit 4517542

Browse files
committed
Experimental support for @semanticNonNull
1 parent 8e557f9 commit 4517542

File tree

52 files changed

+1282
-25
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1282
-25
lines changed

compiler/crates/relay-config/src/typegen_config.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,14 @@ pub struct TypegenConfig {
111111
/// of an union with the raw type, null and undefined.
112112
#[serde(default)]
113113
pub typescript_exclude_undefined_from_nullable_union: bool,
114+
115+
/// If your environment is configured to handles errors out of band, either via
116+
/// a network layer which discards responses with errors, or via enabling strict
117+
/// error handling in the runtime, you can enable this flag to have Relay generate
118+
/// non-null types for fields which are marked as semantically non-null in
119+
/// the schema.
120+
#[serde(default)]
121+
pub emit_semantic_nullability_types: bool,
114122
}
115123

116124
impl Default for TypegenConfig {

compiler/crates/relay-test-schema/src/testschema.graphql

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1191,3 +1191,17 @@ type Settings {
11911191
type WithWrongViewer {
11921192
actor_key: Viewer
11931193
}
1194+
1195+
extend type Query {
1196+
opera: Opera
1197+
}
1198+
1199+
type Opera {
1200+
composer: User @semanticNonNull
1201+
cast: [Portrayal] @semanticNonNull(levels: [0, 1])
1202+
}
1203+
1204+
type Portrayal {
1205+
singer: User @semanticNonNull
1206+
character: String @semanticNonNull
1207+
}

compiler/crates/relay-typegen/src/visit.rs

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -154,19 +154,19 @@ pub(crate) fn visit_selections(
154154
enclosing_linked_field_concrete_type,
155155
),
156156
Selection::LinkedField(linked_field) => {
157-
let linked_field_type = typegen_context
158-
.schema
159-
.field(linked_field.definition.item)
160-
.type_
161-
.inner();
157+
let linked_field_type = field_type(
158+
typegen_context.schema.field(linked_field.definition.item),
159+
typegen_context,
160+
)
161+
.inner();
162162
let nested_enclosing_linked_field_concrete_type =
163163
if linked_field_type.is_abstract_type() {
164164
None
165165
} else {
166166
Some(linked_field_type)
167167
};
168168
gen_visit_linked_field(
169-
typegen_context.schema,
169+
typegen_context,
170170
&mut type_selections,
171171
linked_field,
172172
|selections| {
@@ -330,7 +330,7 @@ fn generate_resolver_type(
330330
if is_relay_resolver_type(typegen_context, schema_field) {
331331
AST::Mixed
332332
} else {
333-
let type_ = &schema_field.type_.inner();
333+
let type_ = &field_type(schema_field, typegen_context).inner();
334334
expect_scalar_type(typegen_context, encountered_enums, custom_scalars, type_)
335335
}
336336
}
@@ -340,22 +340,25 @@ fn generate_resolver_type(
340340
Some(normalization_info.normalization_operation.location),
341341
);
342342

343-
if let Some(field_type) = normalization_info.weak_object_instance_field {
344-
let type_ = &typegen_context.schema.field(field_type).type_.inner();
343+
if let Some(field_id) = normalization_info.weak_object_instance_field {
344+
let type_ =
345+
&field_type(typegen_context.schema.field(field_id), typegen_context).inner();
345346
expect_scalar_type(typegen_context, encountered_enums, custom_scalars, type_)
346347
} else {
347348
AST::RawType(normalization_info.normalization_operation.item.0)
348349
}
349350
}
350351
ResolverOutputTypeInfo::EdgeTo => create_edge_to_return_type_ast(
351-
&schema_field.type_.inner(),
352+
&field_type(schema_field, typegen_context).inner(),
352353
typegen_context.schema,
353354
runtime_imports,
354355
),
355356
ResolverOutputTypeInfo::Legacy => AST::Mixed,
356357
};
357358

358-
let ast = transform_type_reference_into_ast(&schema_field.type_, |_| inner_ast);
359+
let ast = transform_type_reference_into_ast(&field_type(schema_field, typegen_context), |_| {
360+
inner_ast
361+
});
359362

360363
let return_type = if matches!(
361364
typegen_context.project_config.typegen_config.language,
@@ -545,11 +548,12 @@ fn relay_resolver_field_type(
545548
};
546549

547550
if let Some(field) = maybe_scalar_field {
548-
let inner_value = transform_type_reference_into_ast(&field.type_, |type_| {
551+
let type_ = field_type(field, typegen_context);
552+
let inner_value = transform_type_reference_into_ast(&type_, |type_| {
549553
expect_scalar_type(typegen_context, encountered_enums, custom_scalars, type_)
550554
});
551555
if required {
552-
if field.type_.is_non_null() {
556+
if type_.is_non_null() {
553557
inner_value
554558
} else {
555559
AST::NonNullable(Box::new(inner_value))
@@ -956,12 +960,12 @@ fn raw_response_visit_inline_fragment(
956960
}
957961

958962
fn gen_visit_linked_field(
959-
schema: &SDLSchema,
963+
typegen_context: &'_ TypegenContext<'_>,
960964
type_selections: &mut Vec<TypeSelection>,
961965
linked_field: &LinkedField,
962966
mut visit_selections_fn: impl FnMut(&[Selection]) -> Vec<TypeSelection>,
963967
) {
964-
let field = schema.field(linked_field.definition.item);
968+
let field = typegen_context.schema.field(linked_field.definition.item);
965969
let schema_name = field.name.item;
966970
let key = if let Some(alias) = linked_field.alias {
967971
alias.item
@@ -970,7 +974,10 @@ fn gen_visit_linked_field(
970974
};
971975
let selections = visit_selections_fn(&linked_field.selections);
972976

973-
let node_type = apply_required_directive_nullability(&field.type_, &linked_field.directives);
977+
let node_type = apply_required_directive_nullability(
978+
&field_type(field, typegen_context),
979+
&linked_field.directives,
980+
);
974981

975982
type_selections.push(TypeSelection::LinkedField(TypeSelectionLinkedField {
976983
field_name_or_alias: key,
@@ -996,7 +1003,10 @@ fn visit_scalar_field(
9961003
} else {
9971004
schema_name
9981005
};
999-
let field_type = apply_required_directive_nullability(&field.type_, &scalar_field.directives);
1006+
let field_type = apply_required_directive_nullability(
1007+
&field_type(field, typegen_context),
1008+
&scalar_field.directives,
1009+
);
10001010
let special_field = ScalarFieldSpecialSchemaField::from_schema_name(
10011011
schema_name,
10021012
&typegen_context.project_config.schema_config,
@@ -1919,19 +1929,28 @@ pub(crate) fn raw_response_visit_selections(
19191929
enclosing_linked_field_concrete_type,
19201930
),
19211931
Selection::LinkedField(linked_field) => {
1922-
let linked_field_type = typegen_context
1923-
.schema
1924-
.field(linked_field.definition.item)
1925-
.type_
1926-
.inner();
1932+
// Note: We intentionally use the semantic field type here
1933+
// despite the fact that we are generating a raw response type,
1934+
// which should model the _server's_ return type.
1935+
//
1936+
// While it's true that the server may return null for a semantic non-null field,
1937+
// it should only do so if that field also has an error in the errors array. Since
1938+
// raw response type is generally used to construct payloads for apis which do not
1939+
// allow the user to provide additional field level error data, we must ensure that
1940+
// only semantically valid values are allowed in the raw response type.
1941+
let linked_field_type = field_type(
1942+
typegen_context.schema.field(linked_field.definition.item),
1943+
typegen_context,
1944+
)
1945+
.inner();
19271946
let nested_enclosing_linked_field_concrete_type =
19281947
if linked_field_type.is_abstract_type() {
19291948
None
19301949
} else {
19311950
Some(linked_field_type)
19321951
};
19331952
gen_visit_linked_field(
1934-
typegen_context.schema,
1953+
typegen_context,
19351954
&mut type_selections,
19361955
linked_field,
19371956
|selections| {
@@ -2408,3 +2427,17 @@ fn return_ast_in_object_case(
24082427
Type::Interface(_) | Type::Object(_) | Type::Union(_) => ast_in_object_case,
24092428
}
24102429
}
2430+
2431+
/// Returns the type of the field, potentially wrapping the field or list items in a non-null type
2432+
/// to reflect the semantic nullability of the field if that feature is enabled.
2433+
fn field_type(field: &Field, typegen_options: &'_ TypegenContext<'_>) -> TypeReference<Type> {
2434+
if typegen_options
2435+
.project_config
2436+
.typegen_config
2437+
.emit_semantic_nullability_types
2438+
{
2439+
field.semantic_type()
2440+
} else {
2441+
field.type_.clone()
2442+
}
2443+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
==================================== INPUT ====================================
2+
# relay:emit_semantic_nullability_types
3+
query MyQuery @raw_response_type {
4+
opera {
5+
composer {
6+
name
7+
}
8+
cast {
9+
singer {
10+
name
11+
}
12+
character
13+
}
14+
}
15+
}
16+
==================================== OUTPUT ===================================
17+
export type MyQuery$variables = {||};
18+
export type MyQuery$data = {|
19+
+opera: ?{|
20+
+cast: $ReadOnlyArray<{|
21+
+character: string,
22+
+singer: {|
23+
+name: ?string,
24+
|},
25+
|}>,
26+
+composer: {|
27+
+name: ?string,
28+
|},
29+
|},
30+
|};
31+
export type MyQuery$rawResponse = {|
32+
+opera?: ?{|
33+
+cast: $ReadOnlyArray<{|
34+
+character: string,
35+
+singer: {|
36+
+id: string,
37+
+name: ?string,
38+
|},
39+
|}>,
40+
+composer: {|
41+
+id: string,
42+
+name: ?string,
43+
|},
44+
|},
45+
|};
46+
export type MyQuery = {|
47+
rawResponse: MyQuery$rawResponse,
48+
response: MyQuery$data,
49+
variables: MyQuery$variables,
50+
|};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# relay:emit_semantic_nullability_types
2+
query MyQuery @raw_response_type {
3+
opera {
4+
composer {
5+
name
6+
}
7+
cast {
8+
singer {
9+
name
10+
}
11+
character
12+
}
13+
}
14+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
==================================== INPUT ====================================
2+
# relay:emit_semantic_nullability_types
3+
fragment MyFragment on Screen {
4+
pixels
5+
}
6+
7+
%extensions%
8+
9+
type Screen {
10+
pixels: [[Int]] @semanticNonNull(levels: [2])
11+
}
12+
==================================== OUTPUT ===================================
13+
import type { FragmentType } from "relay-runtime";
14+
declare export opaque type MyFragment$fragmentType: FragmentType;
15+
export type MyFragment$data = {|
16+
+pixels: ?$ReadOnlyArray<?$ReadOnlyArray<number>>,
17+
+$fragmentType: MyFragment$fragmentType,
18+
|};
19+
export type MyFragment$key = {
20+
+$data?: MyFragment$data,
21+
+$fragmentSpreads: MyFragment$fragmentType,
22+
...
23+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# relay:emit_semantic_nullability_types
2+
fragment MyFragment on Screen {
3+
pixels
4+
}
5+
6+
%extensions%
7+
8+
type Screen {
9+
pixels: [[Int]] @semanticNonNull(levels: [2])
10+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
==================================== INPUT ====================================
2+
# relay:emit_semantic_nullability_types
3+
fragment MyFragment on ClientUser {
4+
best_friend @waterfall {
5+
name
6+
}
7+
}
8+
9+
%extensions%
10+
11+
type ClientUser {
12+
best_friend: User @semanticNonNull @relay_resolver(
13+
import_path: "./foo/bar.js"
14+
)
15+
}
16+
==================================== OUTPUT ===================================
17+
import type { RefetchableClientEdgeQuery_MyFragment_best_friend$fragmentType } from "RefetchableClientEdgeQuery_MyFragment_best_friend.graphql";
18+
export type ClientEdgeQuery_MyFragment_best_friend$variables = {|
19+
id: string,
20+
|};
21+
export type ClientEdgeQuery_MyFragment_best_friend$data = {|
22+
+node: ?{|
23+
+$fragmentSpreads: RefetchableClientEdgeQuery_MyFragment_best_friend$fragmentType,
24+
|},
25+
|};
26+
export type ClientEdgeQuery_MyFragment_best_friend = {|
27+
response: ClientEdgeQuery_MyFragment_best_friend$data,
28+
variables: ClientEdgeQuery_MyFragment_best_friend$variables,
29+
|};
30+
-------------------------------------------------------------------------------
31+
import type { FragmentType, DataID } from "relay-runtime";
32+
import clientUserBestFriendResolverType from "bar";
33+
// Type assertion validating that `clientUserBestFriendResolverType` resolver is correctly implemented.
34+
// A type error here indicates that the type signature of the resolver module is incorrect.
35+
(clientUserBestFriendResolverType: () => {|
36+
+id: DataID,
37+
|});
38+
declare export opaque type MyFragment$fragmentType: FragmentType;
39+
export type MyFragment$data = {|
40+
+best_friend: {|
41+
+name: ?string,
42+
|},
43+
+$fragmentType: MyFragment$fragmentType,
44+
|};
45+
export type MyFragment$key = {
46+
+$data?: MyFragment$data,
47+
+$fragmentSpreads: MyFragment$fragmentType,
48+
...
49+
};
50+
-------------------------------------------------------------------------------
51+
import type { FragmentType } from "relay-runtime";
52+
declare export opaque type RefetchableClientEdgeQuery_MyFragment_best_friend$fragmentType: FragmentType;
53+
import type { ClientEdgeQuery_MyFragment_best_friend$variables } from "ClientEdgeQuery_MyFragment_best_friend.graphql";
54+
export type RefetchableClientEdgeQuery_MyFragment_best_friend$data = {|
55+
+id: string,
56+
+name: ?string,
57+
+$fragmentType: RefetchableClientEdgeQuery_MyFragment_best_friend$fragmentType,
58+
|};
59+
export type RefetchableClientEdgeQuery_MyFragment_best_friend$key = {
60+
+$data?: RefetchableClientEdgeQuery_MyFragment_best_friend$data,
61+
+$fragmentSpreads: RefetchableClientEdgeQuery_MyFragment_best_friend$fragmentType,
62+
...
63+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# relay:emit_semantic_nullability_types
2+
fragment MyFragment on ClientUser {
3+
best_friend @waterfall {
4+
name
5+
}
6+
}
7+
8+
%extensions%
9+
10+
type ClientUser {
11+
best_friend: User @semanticNonNull @relay_resolver(
12+
import_path: "./foo/bar.js"
13+
)
14+
}

0 commit comments

Comments
 (0)