Skip to content

Commit 6a6ac7e

Browse files
authored
feat: allow loadables on the root types (#690)
- `@loadable` fields on root types would previously cause the compiler to panic - however, they can be refetched by just refetching from the root. - also, include a somewhat contrived demo showing that this works with mutations, too
1 parent 1e8042a commit 6a6ac7e

32 files changed

+848
-104
lines changed

crates/generate_artifacts/src/entrypoint_artifact.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ pub(crate) fn generate_entrypoint_artifacts<TNetworkProtocol: NetworkProtocol>(
8989
entrypoint
9090
.variable_definitions
9191
.iter()
92-
.map(|variable_definition| &variable_definition.item),
92+
.map(|variable_definition| &variable_definition.item)
93+
.collect(),
9394
&schema.find_mutation(),
9495
file_extensions,
9596
persisted_documents,
@@ -98,7 +99,6 @@ pub(crate) fn generate_entrypoint_artifacts<TNetworkProtocol: NetworkProtocol>(
9899

99100
#[allow(clippy::too_many_arguments)]
100101
pub(crate) fn generate_entrypoint_artifacts_with_client_field_traversal_result<
101-
'a,
102102
TNetworkProtocol: NetworkProtocol,
103103
>(
104104
schema: &Schema<TNetworkProtocol>,
@@ -107,7 +107,7 @@ pub(crate) fn generate_entrypoint_artifacts_with_client_field_traversal_result<
107107
merged_selection_map: &MergedSelectionMap,
108108
traversal_state: &ScalarClientFieldTraversalState,
109109
encountered_client_type_map: &FieldToCompletedMergeTraversalStateMap,
110-
variable_definitions: impl Iterator<Item = &'a ValidatedVariableDefinition> + Clone + 'a,
110+
variable_definitions: Vec<&ValidatedVariableDefinition>,
111111
default_root_operation: &Option<(&ServerObjectEntityName, &RootOperationName)>,
112112
file_extensions: GenerateFileExtensionsOption,
113113
persisted_documents: &mut Option<PersistedDocuments>,
@@ -144,7 +144,7 @@ pub(crate) fn generate_entrypoint_artifacts_with_client_field_traversal_result<
144144
query_name,
145145
schema,
146146
merged_selection_map,
147-
variable_definitions.clone(),
147+
variable_definitions.iter().copied(),
148148
root_operation_name,
149149
Format::Pretty,
150150
);
@@ -230,7 +230,7 @@ pub(crate) fn generate_entrypoint_artifacts_with_client_field_traversal_result<
230230
query_name,
231231
schema,
232232
merged_selection_map,
233-
variable_definitions,
233+
variable_definitions.iter().copied(),
234234
root_operation_name,
235235
concrete_type.name.item,
236236
persisted_documents,

crates/generate_artifacts/src/generate_artifacts.rs

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ use isograph_lang_types::{
1717
};
1818
use isograph_schema::{
1919
ClientFieldVariant, ClientScalarSelectable, ClientSelectableId, FieldMapItem,
20-
FieldTraversalResult, NameAndArguments, NetworkProtocol, NormalizationKey, ScalarSelectableId,
21-
Schema, ServerEntityName, ServerObjectSelectableVariant, UserWrittenClientTypeInfo,
22-
ValidatedSelection, ValidatedVariableDefinition, WrappedSelectionMapSelection,
23-
accessible_client_fields, description, inline_fragment_reader_selection_set,
24-
output_type_annotation, selection_map_wrapped,
20+
FieldTraversalResult, NameAndArguments, NetworkProtocol, NormalizationKey, RefetchStrategy,
21+
ScalarSelectableId, Schema, ServerEntityName, ServerObjectSelectableVariant,
22+
UserWrittenClientTypeInfo, ValidatedSelection, ValidatedVariableDefinition,
23+
WrappedSelectionMapSelection, accessible_client_fields, description,
24+
inline_fragment_reader_selection_set, output_type_annotation, selection_map_wrapped,
2525
};
2626
use lazy_static::lazy_static;
2727
use std::{
@@ -271,26 +271,11 @@ fn get_artifact_path_and_content_impl<TNetworkProtocol: NetworkProtocol>(
271271
This is indicative of a bug in Isograph.",
272272
);
273273

274-
if schema
275-
.fetchable_types
276-
.contains_key(&client_scalar_selectable.parent_object_entity_name)
277-
{
278-
panic!("Loadable fields on root objects are not yet supported");
279-
}
280-
281-
let wrapped_map = selection_map_wrapped(
282-
merged_selection_map.clone(),
283-
vec![
284-
WrappedSelectionMapSelection::InlineFragment(
285-
type_to_refine_to.name.item,
286-
),
287-
WrappedSelectionMapSelection::LinkedField {
288-
server_object_selectable_name: "node".intern().into(),
289-
arguments: vec![id_arg.clone()],
290-
concrete_type: None,
291-
},
292-
],
293-
);
274+
let variable_definitions_iter = client_scalar_selectable
275+
.variable_definitions
276+
.iter()
277+
.map(|variable_definition| &variable_definition.item);
278+
294279
let id_var = ValidatedVariableDefinition {
295280
name: WithLocation::new("id".intern().into(), Location::Generated),
296281
type_: GraphQLTypeAnnotation::NonNull(Box::new(
@@ -305,11 +290,42 @@ fn get_artifact_path_and_content_impl<TNetworkProtocol: NetworkProtocol>(
305290
)),
306291
default_value: None,
307292
};
308-
let variable_definitions_iter = client_scalar_selectable
309-
.variable_definitions
310-
.iter()
311-
.map(|variable_definition| &variable_definition.item)
312-
.chain(std::iter::once(&id_var));
293+
294+
let (wrapped_map, variable_definitions_iter): (_, Vec<_>) =
295+
match client_scalar_selectable.refetch_strategy.as_ref().expect(
296+
"Expected a refetch strategy. \
297+
This is indicative of a bug in Isograph.",
298+
) {
299+
RefetchStrategy::RefetchFromRoot => (
300+
selection_map_wrapped(merged_selection_map.clone(), vec![]),
301+
variable_definitions_iter.collect(),
302+
),
303+
RefetchStrategy::UseRefetchField(_) => {
304+
let wrapped_map = selection_map_wrapped(
305+
merged_selection_map.clone(),
306+
vec![
307+
WrappedSelectionMapSelection::InlineFragment(
308+
type_to_refine_to.name.item,
309+
),
310+
WrappedSelectionMapSelection::LinkedField {
311+
server_object_selectable_name: "node"
312+
.intern()
313+
.into(),
314+
arguments: vec![id_arg.clone()],
315+
concrete_type: None,
316+
},
317+
],
318+
);
319+
320+
(
321+
wrapped_map,
322+
variable_definitions_iter
323+
.chain(std::iter::once(&id_var))
324+
.collect(),
325+
)
326+
}
327+
};
328+
313329
let mut traversal_state = traversal_state.clone();
314330
traversal_state.refetch_paths = traversal_state
315331
.refetch_paths

crates/generate_artifacts/src/reader_ast.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,7 @@ fn loadably_selected_field_ast_node<TNetworkProtocol: NetworkProtocol>(
525525
indentation_level + 1,
526526
);
527527

528+
let empty_selection_set = vec![];
528529
let (reader_ast, additional_reader_imports) = generate_reader_ast(
529530
schema,
530531
client_field
@@ -534,7 +535,8 @@ fn loadably_selected_field_ast_node<TNetworkProtocol: NetworkProtocol>(
534535
"Expected refetch strategy. \
535536
This is indicative of a bug in Isograph.",
536537
)
537-
.refetch_selection_set(),
538+
.refetch_selection_set()
539+
.unwrap_or(&empty_selection_set),
538540
indentation_level + 1,
539541
// This is weird!
540542
&Default::default(),

crates/generate_artifacts/src/refetch_reader_artifact.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ pub(crate) fn generate_refetch_reader_artifact<TNetworkProtocol: NetworkProtocol
3434
This is indicative of a bug in Isograph.",
3535
);
3636

37+
let empty_selection_set = vec![];
38+
3739
let (reader_ast, reader_imports) = generate_reader_ast(
3840
schema,
3941
if was_selected_loadably {
@@ -46,6 +48,7 @@ pub(crate) fn generate_refetch_reader_artifact<TNetworkProtocol: NetworkProtocol
4648
This is indicative of a bug in Isograph.",
4749
)
4850
.refetch_selection_set()
51+
.unwrap_or(&empty_selection_set)
4952
} else {
5053
client_field.selection_set_for_parent_query()
5154
},

crates/isograph_compiler/src/add_selection_sets.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,7 @@ fn get_validated_refetch_strategy<TNetworkProtocol: NetworkProtocol + 'static>(
481481
generate_refetch_query: use_refetch_field_strategy.generate_refetch_query,
482482
}),
483483
)),
484+
Some(RefetchStrategy::RefetchFromRoot) => Ok(Some(RefetchStrategy::RefetchFromRoot)),
484485
None => Ok(None),
485486
}
486487
}

crates/isograph_schema/src/create_merged_selection_set.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1177,6 +1177,8 @@ fn insert_imperative_field_into_refetch_paths<TNetworkProtocol: NetworkProtocol>
11771177
},
11781178
);
11791179

1180+
let empty_selection_set = vec![];
1181+
11801182
// Generate a merged selection set, but using the refetch strategy
11811183
create_merged_selection_map_for_field_and_insert_into_global_map(
11821184
schema,
@@ -1188,7 +1190,8 @@ fn insert_imperative_field_into_refetch_paths<TNetworkProtocol: NetworkProtocol>
11881190
"Expected refetch strategy. \
11891191
This is indicative of a bug in Isograph.",
11901192
)
1191-
.refetch_selection_set(),
1193+
.refetch_selection_set()
1194+
.unwrap_or(&empty_selection_set),
11921195
encountered_client_type_map,
11931196
DefinitionLocation::Client(SelectionType::Scalar((
11941197
parent_object_entity_name,

crates/isograph_schema/src/data_model/traits/client_selectables_trait.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ impl<TNetworkProtocol: NetworkProtocol> ClientScalarOrObjectSelectable
5858
ClientFieldVariant::ImperativelyLoadedField(_) => self
5959
.refetch_strategy
6060
.as_ref()
61-
.map(|strategy| strategy.refetch_selection_set())
61+
.and_then(|strategy| strategy.refetch_selection_set())
6262
.expect(
6363
"Expected imperatively loaded field to have refetch selection set. \
6464
This is indicative of a bug in Isograph.",

crates/isograph_schema/src/process_client_field_declaration.rs

Lines changed: 70 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -274,30 +274,39 @@ impl<TNetworkProtocol: NetworkProtocol> Schema<TNetworkProtocol> {
274274
);
275275

276276
let selections = client_field_declaration.item.selection_set;
277-
let id_field = self
278-
.server_entity_data
279-
.server_object_entity_extra_info
280-
.get(&parent_object_entity_name)
281-
.expect(
282-
"Expected parent_object_entity_name to exist in \
277+
278+
let refetch_strategy = if self
279+
.fetchable_types
280+
.contains_key(&parent_object_entity_name)
281+
{
282+
Some(RefetchStrategy::RefetchFromRoot)
283+
} else {
284+
let id_field = self
285+
.server_entity_data
286+
.server_object_entity_extra_info
287+
.get(&parent_object_entity_name)
288+
.expect(
289+
"Expected parent_object_entity_name to exist in \
283290
server_object_entity_available_selectables",
284-
)
285-
.id_field;
286-
let refetch_strategy = id_field.map(|_| {
287-
// Assume that if we have an id field, this implements Node
288-
RefetchStrategy::UseRefetchField(generate_refetch_field_strategy(
289-
vec![id_selection()],
290-
query_id,
291-
vec![
292-
WrappedSelectionMapSelection::InlineFragment(object.name.item),
293-
WrappedSelectionMapSelection::LinkedField {
294-
server_object_selectable_name: *NODE_FIELD_NAME,
295-
arguments: id_top_level_arguments(),
296-
concrete_type: None,
297-
},
298-
],
299-
))
300-
});
291+
)
292+
.id_field;
293+
294+
id_field.map(|_| {
295+
// Assume that if we have an id field, this implements Node
296+
RefetchStrategy::UseRefetchField(generate_refetch_field_strategy(
297+
vec![id_selection()],
298+
query_id,
299+
vec![
300+
WrappedSelectionMapSelection::InlineFragment(object.name.item),
301+
WrappedSelectionMapSelection::LinkedField {
302+
server_object_selectable_name: *NODE_FIELD_NAME,
303+
arguments: id_top_level_arguments(),
304+
concrete_type: None,
305+
},
306+
],
307+
))
308+
})
309+
};
301310

302311
Ok(UnprocessedClientFieldItem {
303312
parent_object_entity_name,
@@ -351,40 +360,45 @@ impl<TNetworkProtocol: NetworkProtocol> Schema<TNetworkProtocol> {
351360
let unprocessed_fields = client_pointer_declaration.item.selection_set;
352361

353362
// TODO extract this into a helper function, probably on TNetworkProtocol
354-
let id_field = self
355-
.server_entity_data
356-
.server_object_entity_extra_info
357-
.get(to_object_name.inner())
358-
.expect(
359-
"Expected parent_object_entity_name \
363+
let refetch_strategy = if self.fetchable_types.contains_key(to_object_name.inner()) {
364+
RefetchStrategy::RefetchFromRoot
365+
} else {
366+
let id_field = self
367+
.server_entity_data
368+
.server_object_entity_extra_info
369+
.get(to_object_name.inner())
370+
.expect(
371+
"Expected parent_object_entity_name \
360372
to exist in server_object_entity_available_selectables",
361-
)
362-
.id_field;
363-
let refetch_strategy = match id_field {
364-
None => Err(WithSpan::new(
365-
ProcessClientFieldDeclarationError::ClientPointerTargetTypeHasNoId {
366-
target_type_name: client_pointer_declaration.item.target_type.inner().0,
367-
},
368-
client_pointer_declaration.item.target_type.span(),
369-
)),
370-
Some(_) => {
371-
// Assume that if we have an id field, this implements Node
372-
Ok(RefetchStrategy::UseRefetchField(
373-
generate_refetch_field_strategy(
374-
vec![],
375-
query_id,
376-
vec![
377-
WrappedSelectionMapSelection::InlineFragment(to_object.name.item),
378-
WrappedSelectionMapSelection::LinkedField {
379-
server_object_selectable_name: *NODE_FIELD_NAME,
380-
arguments: id_top_level_arguments(),
381-
concrete_type: None,
382-
},
383-
],
384-
),
385-
))
386-
}
387-
}?;
373+
)
374+
.id_field;
375+
376+
match id_field {
377+
None => Err(WithSpan::new(
378+
ProcessClientFieldDeclarationError::ClientPointerTargetTypeHasNoId {
379+
target_type_name: client_pointer_declaration.item.target_type.inner().0,
380+
},
381+
client_pointer_declaration.item.target_type.span(),
382+
)),
383+
Some(_) => {
384+
// Assume that if we have an id field, this implements Node
385+
Ok(RefetchStrategy::UseRefetchField(
386+
generate_refetch_field_strategy(
387+
vec![],
388+
query_id,
389+
vec![
390+
WrappedSelectionMapSelection::InlineFragment(to_object.name.item),
391+
WrappedSelectionMapSelection::LinkedField {
392+
server_object_selectable_name: *NODE_FIELD_NAME,
393+
arguments: id_top_level_arguments(),
394+
concrete_type: None,
395+
},
396+
],
397+
),
398+
))
399+
}
400+
}?
401+
};
388402

389403
self.client_object_selectables.insert(
390404
(parent_object_name, *client_object_selectable_name),

crates/isograph_schema/src/refetch_strategy.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub enum RefetchStrategy<
2525
TSelectionTypeSelectionLinkedFieldAssociatedData,
2626
>,
2727
),
28-
// RefetchFromRoot
28+
RefetchFromRoot,
2929
}
3030

3131
impl<
@@ -39,18 +39,21 @@ impl<
3939
{
4040
pub fn refetch_selection_set(
4141
&self,
42-
) -> &Vec<
43-
WithSpan<
44-
SelectionTypeContainingSelections<
45-
TSelectionTypeSelectionScalarFieldAssociatedData,
46-
TSelectionTypeSelectionLinkedFieldAssociatedData,
42+
) -> Option<
43+
&Vec<
44+
WithSpan<
45+
SelectionTypeContainingSelections<
46+
TSelectionTypeSelectionScalarFieldAssociatedData,
47+
TSelectionTypeSelectionLinkedFieldAssociatedData,
48+
>,
4749
>,
4850
>,
4951
> {
5052
match self {
5153
RefetchStrategy::UseRefetchField(used_refetch_field) => {
52-
&used_refetch_field.refetch_selection_set
54+
Some(&used_refetch_field.refetch_selection_set)
5355
}
56+
RefetchStrategy::RefetchFromRoot => None,
5457
}
5558
}
5659
}

0 commit comments

Comments
 (0)