Skip to content

Commit

Permalink
add magic link field (#282)
Browse files Browse the repository at this point in the history
- Add a magic `link` field that can be selected on any item, whose type is `Link`
- This is analogous to Relay's `__id` field
- This field is used internally by e.g. `asUser`, whose resolver is implemented as `data.__typename === 'User' ? data.link : null`
- Within the runtime store APIs, a `Link` is the data structure that is used to navigate between two items. e.g. the store might contain `{ Query: { pets: [{ __link: '0', __typename: 'Pet' }] }`. `{ __link: '0', __typename: 'Pet' }` is a `Link`
  • Loading branch information
PatrykWalach authored Dec 28, 2024
1 parent de7554c commit 3a405a0
Show file tree
Hide file tree
Showing 18 changed files with 291 additions and 98 deletions.
15 changes: 12 additions & 3 deletions crates/graphql_artifact_generation/src/eager_reader_artifact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,18 +156,18 @@ pub(crate) fn generate_eager_reader_condition_artifact(
reader_imports_to_import_statement(&reader_imports, file_extensions);

let reader_param_type = "{ data: any, parameters: Record<PropertyKey, never> }";
let reader_output_type = "boolean";
let reader_output_type = "Link | null";

let reader_content = format!(
"import type {{ EagerReaderArtifact, ReaderAst }} from '@isograph/react';\n\
"import type {{ EagerReaderArtifact, ReaderAst, Link }} from '@isograph/react';\n\
{reader_import_statement}\n\
const readerAst: ReaderAst<{reader_param_type}> = {reader_ast};\n\n\
const artifact: EagerReaderArtifact<\n\
{}{reader_param_type},\n\
{}{reader_output_type}\n\
> = {{\n\
{}kind: \"EagerReaderArtifact\",\n\
{}resolver: ({{ data }}) => data.__typename === \"{concrete_type}\",\n\
{}resolver: ({{ data }}) => data.__typename === \"{concrete_type}\" ? data.link : null,\n\
{}readerAst,\n\
}};\n\n\
export default artifact;\n",
Expand Down Expand Up @@ -196,19 +196,27 @@ pub(crate) fn generate_eager_reader_param_type_artifact(

let mut param_type_imports = BTreeSet::new();
let mut loadable_fields = BTreeSet::new();
let mut link_fields = false;
let client_field_parameter_type = generate_client_field_parameter_type(
schema,
client_field.selection_set_for_parent_query(),
parent_type,
&mut param_type_imports,
&mut loadable_fields,
1,
&mut link_fields,
);

let param_type_import_statement =
param_type_imports_to_import_statement(&param_type_imports, file_extensions);
let reader_param_type = format!("{}__{}__param", parent_type.name, client_field.name);

let link_field_imports = if link_fields {
"import type { Link } from '@isograph/react';\n".to_string()
} else {
"".to_string()
};

let loadable_field_imports = if !loadable_fields.is_empty() {
let param_imports =
param_type_imports_to_import_param_statement(&loadable_fields, file_extensions);
Expand All @@ -234,6 +242,7 @@ pub(crate) fn generate_eager_reader_param_type_artifact(
let indent = " ";
let param_type_content = format!(
"{param_type_import_statement}\
{link_field_imports}\
{loadable_field_imports}\
{parameters_import}\n\
export type {reader_param_type} = {{\n\
Expand Down
143 changes: 86 additions & 57 deletions crates/graphql_artifact_generation/src/generate_artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ use crate::{
generate_entrypoint_artifacts_with_client_field_traversal_result,
},
format_parameter_type::format_parameter_type,
import_statements::ParamTypeImports,
import_statements::{LinkImports, ParamTypeImports},
iso_overload_file::build_iso_overload_artifact,
refetch_reader_artifact::{
generate_refetch_output_type_artifact, generate_refetch_reader_artifact,
Expand Down Expand Up @@ -131,8 +131,9 @@ pub fn get_artifact_path_and_content(
}
FieldType::ClientField(encountered_client_field_id) => {
let encountered_client_field = schema.client_field(*encountered_client_field_id);
// Generate reader ASTs for all encountered client fields, which may be reader or refetch reader

match &encountered_client_field.variant {
ClientFieldVariant::Link => (),
ClientFieldVariant::UserWritten(info) => {
path_and_contents.extend(generate_eager_reader_artifacts(
schema,
Expand Down Expand Up @@ -248,6 +249,7 @@ pub fn get_artifact_path_and_content(

for user_written_client_field in schema.client_fields.iter().flat_map(|field| match field {
ClientType::ClientField(field) => match field.variant {
ClientFieldVariant::Link => None,
ClientFieldVariant::UserWritten(_) => Some(field),
ClientFieldVariant::ImperativelyLoadedField(_) => None,
},
Expand Down Expand Up @@ -283,20 +285,25 @@ pub fn get_artifact_path_and_content(

for output_type_id in encountered_output_types {
let client_field = schema.client_field(output_type_id);
let path_and_content = match client_field.variant {
ClientFieldVariant::UserWritten(info) => generate_eager_reader_output_type_artifact(
schema,
client_field,
project_root,
artifact_directory,
info,
file_extensions,
),
let artifact_path_and_content = match client_field.variant {
ClientFieldVariant::Link => None,
ClientFieldVariant::UserWritten(info) => {
Some(generate_eager_reader_output_type_artifact(
schema,
client_field,
project_root,
artifact_directory,
info,
file_extensions,
))
}
ClientFieldVariant::ImperativelyLoadedField(_) => {
generate_refetch_output_type_artifact(schema, client_field)
Some(generate_refetch_output_type_artifact(schema, client_field))
}
};
path_and_contents.push(path_and_content);
if let Some(path_and_content) = artifact_path_and_content {
path_and_contents.push(path_and_content);
};
}

path_and_contents.push(build_iso_overload_artifact(
Expand Down Expand Up @@ -403,6 +410,7 @@ pub(crate) fn get_serialized_field_arguments(
pub(crate) fn generate_output_type(client_field: &ValidatedClientField) -> ClientFieldOutputType {
let variant = &client_field.variant;
match variant {
ClientFieldVariant::Link => ClientFieldOutputType("Link".to_string()),
ClientFieldVariant::UserWritten(info) => match info.user_written_component_variant {
UserWrittenComponentVariant::Eager => {
ClientFieldOutputType("ReturnType<typeof resolver>".to_string())
Expand Down Expand Up @@ -438,6 +446,7 @@ pub(crate) fn generate_client_field_parameter_type(
nested_client_field_imports: &mut ParamTypeImports,
loadable_fields: &mut ParamTypeImports,
indentation_level: u8,
link_fields: &mut LinkImports,
) -> ClientFieldParameterType {
// TODO use unwraps
let mut client_field_parameter_type = "{\n".to_string();
Expand All @@ -450,13 +459,15 @@ pub(crate) fn generate_client_field_parameter_type(
nested_client_field_imports,
loadable_fields,
indentation_level + 1,
link_fields,
);
}
client_field_parameter_type.push_str(&format!("{}}}", " ".repeat(indentation_level as usize)));

ClientFieldParameterType(client_field_parameter_type)
}

#[allow(clippy::too_many_arguments)]
fn write_param_type_from_selection(
schema: &ValidatedSchema,
query_type_declaration: &mut String,
Expand All @@ -465,6 +476,7 @@ fn write_param_type_from_selection(
nested_client_field_imports: &mut ParamTypeImports,
loadable_fields: &mut ParamTypeImports,
indentation_level: u8,
link_fields: &mut LinkImports,
) {
match &selection.item {
ServerFieldSelection::ScalarField(scalar_field_selection) => {
Expand Down Expand Up @@ -516,55 +528,71 @@ fn write_param_type_from_selection(
query_type_declaration
.push_str(&" ".repeat(indentation_level as usize).to_string());

nested_client_field_imports.insert(client_field.type_and_field);
let inner_output_type = format!(
"{}__output_type",
client_field.type_and_field.underscore_separated()
);

let output_type = match scalar_field_selection.associated_data.selection_variant
{
ValidatedIsographSelectionVariant::Regular => inner_output_type,
ValidatedIsographSelectionVariant::Loadable(_) => {
loadable_fields.insert(client_field.type_and_field);
let provided_arguments = get_provided_arguments(
client_field.variable_definitions.iter().map(|x| &x.item),
&scalar_field_selection.arguments,
match client_field.variant {
ClientFieldVariant::Link => {
*link_fields = true;
let output_type = "Link";
query_type_declaration.push_str(
&(format!(
"readonly {}: {},\n",
scalar_field_selection.name_or_alias().item,
output_type
)),
);
}
ClientFieldVariant::UserWritten(_)
| ClientFieldVariant::ImperativelyLoadedField(_) => {
nested_client_field_imports.insert(client_field.type_and_field);
let inner_output_type = format!(
"{}__output_type",
client_field.type_and_field.underscore_separated()
);
let output_type = match scalar_field_selection
.associated_data
.selection_variant
{
ValidatedIsographSelectionVariant::Regular => inner_output_type,
ValidatedIsographSelectionVariant::Loadable(_) => {
loadable_fields.insert(client_field.type_and_field);
let provided_arguments = get_provided_arguments(
client_field.variable_definitions.iter().map(|x| &x.item),
&scalar_field_selection.arguments,
);

let indent = " ".repeat((indentation_level + 1) as usize);
let provided_args_type = if provided_arguments.is_empty() {
"".to_string()
} else {
format!(
",\n{indent}Omit<ExtractParameters<{}__param>, keyof {}>",
client_field.type_and_field.underscore_separated(),
get_loadable_field_type_from_arguments(
schema,
provided_arguments
let indent = " ".repeat((indentation_level + 1) as usize);
let provided_args_type = if provided_arguments.is_empty() {
"".to_string()
} else {
format!(
",\n{indent}Omit<ExtractParameters<{}__param>, keyof {}>",
client_field.type_and_field.underscore_separated(),
get_loadable_field_type_from_arguments(
schema,
provided_arguments
)
)
};

format!(
"LoadableField<\n\
{indent}{}__param,\n\
{indent}{inner_output_type}\
{provided_args_type}\n\
{}>",
client_field.type_and_field.underscore_separated(),
" ".repeat(indentation_level as usize),
)
)
}
};

format!(
"LoadableField<\n\
{indent}{}__param,\n\
{indent}{inner_output_type}\
{provided_args_type}\n\
{}>",
client_field.type_and_field.underscore_separated(),
" ".repeat(indentation_level as usize),
)
query_type_declaration.push_str(
&(format!(
"readonly {}: {},\n",
scalar_field_selection.name_or_alias().item,
output_type
)),
);
}
};

query_type_declaration.push_str(
&(format!(
"readonly {}: {},\n",
scalar_field_selection.name_or_alias().item,
output_type
)),
);
}
}
}
}
Expand Down Expand Up @@ -603,6 +631,7 @@ fn write_param_type_from_selection(
nested_client_field_imports,
loadable_fields,
indentation_level,
link_fields,
)
}),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ impl ImportedFileCategory {

pub(crate) type ReaderImports = BTreeSet<(ObjectTypeAndFieldName, ImportedFileCategory)>;
pub(crate) type ParamTypeImports = BTreeSet<ObjectTypeAndFieldName>;
pub(crate) type LinkImports = bool;

pub(crate) fn reader_imports_to_import_statement(
reader_imports: &ReaderImports,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ fn user_written_fields(
.iter()
.filter_map(|client_field| match client_field {
ClientType::ClientField(client_field) => match client_field.variant {
ClientFieldVariant::Link => None,
ClientFieldVariant::UserWritten(info) => {
Some((client_field, info.user_written_component_variant))
}
Expand Down
51 changes: 37 additions & 14 deletions crates/graphql_artifact_generation/src/reader_ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ use isograph_lang_types::{
LoadableDirectiveParameters, RefetchQueryIndex, SelectionType, ServerFieldSelection,
};
use isograph_schema::{
categorize_field_loadability, transform_arguments_with_child_context, FieldType, Loadability,
NameAndArguments, NormalizationKey, ObjectTypeAndFieldName, PathToRefetchField,
RefetchedPathsMap, SchemaServerFieldVariant, ValidatedClientField,
categorize_field_loadability, transform_arguments_with_child_context, ClientFieldVariant,
FieldType, Loadability, NameAndArguments, NormalizationKey, ObjectTypeAndFieldName,
PathToRefetchField, RefetchedPathsMap, SchemaServerFieldVariant, ValidatedClientField,
ValidatedIsographSelectionVariant, ValidatedLinkedFieldSelection,
ValidatedScalarFieldSelection, ValidatedSchema, ValidatedSelection, VariableContext,
};
Expand Down Expand Up @@ -204,20 +204,43 @@ fn scalar_client_defined_field_ast_node(
indentation_level,
scalar_field_selection,
),
None => user_written_variant_ast_node(
scalar_field_selection,
indentation_level,
client_field,
schema,
path,
root_refetched_paths,
reader_imports,
&client_field_variable_context,
parent_variable_context,
),
None => match client_field.variant {
ClientFieldVariant::Link => {
link_variant_ast_node(scalar_field_selection, indentation_level)
}
ClientFieldVariant::UserWritten(_) | ClientFieldVariant::ImperativelyLoadedField(_) => {
user_written_variant_ast_node(
scalar_field_selection,
indentation_level,
client_field,
schema,
path,
root_refetched_paths,
reader_imports,
&client_field_variable_context,
parent_variable_context,
)
}
},
}
}

fn link_variant_ast_node(
scalar_field_selection: &ValidatedScalarFieldSelection,
indentation_level: u8,
) -> String {
let alias = scalar_field_selection.name_or_alias().item;
let indent_1 = " ".repeat(indentation_level as usize);
let indent_2 = " ".repeat((indentation_level + 1) as usize);

format!(
"{indent_1}{{\n\
{indent_2}kind: \"Link\",\n\
{indent_2}alias: \"{alias}\",\n\
{indent_1}}},\n",
)
}

#[allow(clippy::too_many_arguments)]
fn user_written_variant_ast_node(
scalar_field_selection: &ValidatedScalarFieldSelection,
Expand Down
1 change: 1 addition & 0 deletions crates/isograph_compiler/src/source_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ impl SourceFiles {
process_iso_literals(schema, self.contains_iso)?;
process_exposed_fields(schema)?;
schema.add_fields_to_subtypes(&outcome.type_refinement_maps.supertype_to_subtype_map)?;
schema.add_link_fields()?;
schema
.add_pointers_to_supertypes(&outcome.type_refinement_maps.subtype_to_supertype_map)?;
add_refetch_fields_to_objects(schema)?;
Expand Down
Loading

0 comments on commit 3a405a0

Please sign in to comment.