From 1b5359e296c46a15452b786c0d96883b482ba725 Mon Sep 17 00:00:00 2001 From: Robert Balicki Date: Thu, 7 Nov 2024 10:51:29 -0500 Subject: [PATCH 01/10] more updates to incremental compilation design doc --- .../design-docs/incremental-compilation.md | 147 +++++++++++++----- 1 file changed, 111 insertions(+), 36 deletions(-) diff --git a/docs-website/docs/design-docs/incremental-compilation.md b/docs-website/docs/design-docs/incremental-compilation.md index fde6f29c..8f844315 100644 --- a/docs-website/docs/design-docs/incremental-compilation.md +++ b/docs-website/docs/design-docs/incremental-compilation.md @@ -42,14 +42,14 @@ When a source node is updated, it's value is compared to the old one. If differe A computed node is a deterministic function that always receives, as its first parameter, the database (as `&mut self`), and as its second parameter, receives a single struct of parameters. It must always call `db.calculate` immediately. This is not enforced, but could be via `#[derive]` macro. ```rs -fn syntax_highlighting(db_view: &mut DatabaseView, file_name: FileName) -> &SyntaxHighlighting { - db_view.calculate( +fn syntax_highlighting(parent_db_view: &mut DatabaseView, file_name: FileName) -> &SyntaxHighlighting { + parent_db_view.calculate( "syntax_highlighting", file_name: T, // inner function: - |nested_db_view, file_name: &FileName /* &T */| { - let asts = get_asts_in(nested_db_view, file_name); - calculate_syntax_highlighting_from_asts(asts) + |self_db_view, file_name: &FileName /* &T */| { + let asts = get_asts(self_db_view, file_name); // tracked + calculate_syntax_highlighting_from_asts(asts) // pure } ) } @@ -57,8 +57,8 @@ fn syntax_highlighting(db_view: &mut DatabaseView, file_name: FileName) -> &Synt // Or if we have a proc macro that does the db_view.calculate stuff: #[track] fn syntax_highlighting(db_view: &mut DatabaseView, file_name: &FileName) -> SyntaxHighlighting { - let asts = get_asts_in(nested_db_view, file_name); - calculate_syntax_highlighting_from_asts(asts) // this is a SyntaxHighlighting + let asts = get_asts(nested_db_view, file_name); + calculate_syntax_highlighting_from_asts(asts) // this is a SyntaxHighlighting } ``` @@ -146,39 +146,39 @@ hover info syntax highlighting go to definition - the `time_calculated` epoch. - Invariant: `time_calculated <= time_verified` - perhaps: `time_accessed` epoch (for garbage collection) -- list of dependencies from last call +- `Vec` ### Dependencies A sketch of how to store dependencies ```rs -struct DatabaseView<'parent> { - database: &'parent Database, - dependencies: Vec RevalidationResult>> + 'parent +struct DatabaseView<'parent, 'db: 'parent> { + database: &'db Database, + dependencies: Vec bool>> + 'parent parent_view: &'parent DatabaseView } -impl DatabaseView { +impl<'parent, 'db: 'parent> DatabaseView<'parent, 'db> { fn calculate( + &'parent mut self, static_key: &'static str, param: TParam, - inner_fn: impl Fn(&mut DatabaseView /* nested db view */, &TParam) -> TOutput; - ) -> &TOutput { - self.parent_view.dependencies.push(Box::new(|| { - let recalculate_inner = |database_view| { inner_fn(database_view, param) }; - - || self.database.revalidate( - static_key, - param, - recalculate_inner - ) - })) - - if let Some(mut result) = self.database.get_mut(static_key, param) { - let dependencies = result.dependencies; + inner_fn: impl Fn(&'db mut DatabaseView /* self db view */, &TParam) -> TOutput + 'parent; + ) -> &'db TOutput { + let parent_db_view = self; + // tell parent_db_view that "syntax_highlighting" was called + parent_db_view.dependencies.push(Box::new(DerivedNodeDependency { + static_key, + param_hash: hash(param), + recalculate_inner_no_params: Box::new(|database_view| { + inner_fn(database_view, param) + }), + })); + + if let Some(mut previously_derived_node) = self.database.get_mut(static_key, param) { let has_changed = false; - for revalidate_dependency in dependencies.iter_mut() { + for revalidate_dependency in previously_derived_node.dependencies.iter_mut() { has_changed = revalidate_dependency(); if has_changed { break; @@ -186,28 +186,102 @@ impl DatabaseView { } if has_changed { let nested_view = self.nested_view(); - let result = inner_fn(nested_view, ¶m); - // ? - result.dependencies = nested_view.dependencies; + let derived_node_value = Box::new(inner_fn(&mut nested_view, ¶m)); + + parent_db_view.max_time_calculated = std::cmp::max( + parent_db_view.max_time_calculated, + nested_view.max_time_calculated, + ); + + // database.store returns a reference to the derived_node_value + return self.database.store(static_key, param, DerivedNode { + derived_node_value, // Box, + time_verified: self.database.epoch, + time_calculated: nested_view.max_time_calculated, + dependencies: nested_view.dependencies, + }).downcast_ref::().expect("Expected to be the correct type") + } else { + return &previously_derived_node.derived_node_value; } } else { - let nested_view = self.nested_view(); - let result = inner_fn(nested_view, ¶m); - // ? - self.dependencies = nested_view.dependencies; - // calculate initially + panic!("same thing as above"); } + } fn nested_view(&mut self) -> DatabaseView { Self { database: self.database, dependencies: vec![], - parent_view: &self, + max_time_calculated: 0, } } } +impl Database { + fn revalidate<'db>( + &'db mut self, + parent_static_key: &'static str, + parent_param: HashString, + parent_recalculate_inner: impl Fn(&'db mut DatabaseView) -> Box + 'db + ) -> ChangeStatus { + match self.get_mut(static_key, param) { + Some(previously_derived_node) => { + // check each dependency + // if all dependencies are not invalidated, return HasNotChanged + // if any dependency is invalidated, recalculate_inner, compare, and + // if different, store and return HasChanged + // always set revalidated at to current epoch + + let dependency_change_status = ChangeStatus::Unchanged; + for dependency in previously_derived_node.dependencies.iter_mut() { + if let Some(dependency_pdn) = self.get_mut(dependency.static_key, dependency.param_hash) { + + if (dependency_pdn.time_validated === self.current_epoch) { + continue; + } else { + // potentially + dependency_change_status = ChangeStatus::Changed; + break; + } + + } else { + panic!("Expected nested dep to exist"); + } + } + match dependency_change_status { + ChangeStatus::Unchanged => { + return ChangeStatus::Unchanged + }, + ChangeStatus::Changed => { + let mut database_view = self.database_view(); + let new_value = parent_recalculate_inner(&mut database_view); + + previously_derived_node.dependencies = database_view.dependencies; + previously_derived_node.time_verified = self.current_epoch; + + if new_value != previously_derived_node.derived_node_value { + previously_derived_node.derived_node_value = new_value; + previously_derived_node.time_calculated = database_view.max_time_calculated; + return ChangeStatus::Changed + } + + return ChangeStatus::Unchanged + }, + } + } + None => { + panic!("Expected to exist if revalidating") + } + } + } +} + +enum ChangeStatus { + Changed, + Unchanged, +} + ``` @@ -223,6 +297,7 @@ impl DatabaseView { - references - hash maps +- tracked inputs # unneeded From 995311c8a239427e14b693701ac4fb50fce0db8a Mon Sep 17 00:00:00 2001 From: Robert Balicki Date: Thu, 7 Nov 2024 14:35:24 -0500 Subject: [PATCH 02/10] rename ci.yml steps --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c568153b..67d67376 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,7 +103,7 @@ jobs: steps: - uses: actions/checkout@v2 - uses: pnpm/action-setup@v2 - - name: Use Node.js 18 + - name: Use Node.js 22 uses: actions/setup-node@v3 with: node-version: 22 @@ -124,7 +124,7 @@ jobs: steps: - uses: actions/checkout@v2 - uses: pnpm/action-setup@v2 - - name: Use Node.js 18 + - name: Use Node.js 22 uses: actions/setup-node@v3 with: node-version: 22 @@ -150,7 +150,7 @@ jobs: steps: - uses: actions/checkout@v3 - uses: pnpm/action-setup@v2 - - name: Use Node.js 18 + - name: Use Node.js 22 uses: actions/setup-node@v3 with: node-version: 22 @@ -176,7 +176,7 @@ jobs: steps: - uses: actions/checkout@v3 - uses: pnpm/action-setup@v2 - - name: Use Node.js 18 + - name: Use Node.js 22 uses: actions/setup-node@v3 with: node-version: 22 @@ -249,7 +249,7 @@ jobs: steps: - uses: actions/checkout@v3 - uses: pnpm/action-setup@v2 - - name: Use Node.js 18 + - name: Use Node.js 22 uses: actions/setup-node@v3 with: node-version: 22 From bc0ecdf0b43eb53881a548eeac6d9d661942d0e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Wa=C5=82ach?= <35966385+PatrykWalach@users.noreply.github.com> Date: Thu, 7 Nov 2024 20:37:07 +0100 Subject: [PATCH 03/10] fix clippy suggestions (#256) --- crates/graphql_artifact_generation/src/entrypoint_artifact.rs | 1 + crates/isograph_schema/src/process_type_definition.rs | 2 +- crates/isograph_schema/src/refetch_strategy.rs | 2 +- crates/isograph_schema/src/validate_schema.rs | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/graphql_artifact_generation/src/entrypoint_artifact.rs b/crates/graphql_artifact_generation/src/entrypoint_artifact.rs index f2c6ca0a..ae3a2c44 100644 --- a/crates/graphql_artifact_generation/src/entrypoint_artifact.rs +++ b/crates/graphql_artifact_generation/src/entrypoint_artifact.rs @@ -68,6 +68,7 @@ pub(crate) fn generate_entrypoint_artifacts( ) } +#[allow(clippy::too_many_arguments)] pub(crate) fn generate_entrypoint_artifacts_with_client_field_traversal_result<'a>( schema: &ValidatedSchema, entrypoint: &ValidatedClientField, diff --git a/crates/isograph_schema/src/process_type_definition.rs b/crates/isograph_schema/src/process_type_definition.rs index 41de3504..8e0915c6 100644 --- a/crates/isograph_schema/src/process_type_definition.rs +++ b/crates/isograph_schema/src/process_type_definition.rs @@ -253,7 +253,7 @@ impl UnvalidatedSchema { let result = (*self .server_field_data .defined_types - .get(&unvalidated_type_name.into()) + .get(&unvalidated_type_name) .ok_or_else(|| { WithLocation::new( ProcessTypeDefinitionError::IsographObjectTypeNameNotDefined { diff --git a/crates/isograph_schema/src/refetch_strategy.rs b/crates/isograph_schema/src/refetch_strategy.rs index 18eadcf1..d886b764 100644 --- a/crates/isograph_schema/src/refetch_strategy.rs +++ b/crates/isograph_schema/src/refetch_strategy.rs @@ -54,7 +54,7 @@ impl< } } } - +#[allow(clippy::too_many_arguments)] pub fn generate_refetch_field_strategy< TClientFieldSelectionScalarFieldAssociatedData, TClientFieldSelectionLinkedFieldAssociatedData, diff --git a/crates/isograph_schema/src/validate_schema.rs b/crates/isograph_schema/src/validate_schema.rs index eb46da75..8a944417 100644 --- a/crates/isograph_schema/src/validate_schema.rs +++ b/crates/isograph_schema/src/validate_schema.rs @@ -281,7 +281,7 @@ pub(crate) fn get_all_errors_or_tuple_ok( ) -> Result<(T1, T2), Vec> { match (a, b) { (Ok(v1), Ok(v2)) => Ok((v1, v2)), - (Err(e1), Err(e2)) => Err(e1.into_iter().chain(e2.into_iter()).collect()), + (Err(e1), Err(e2)) => Err(e1.into_iter().chain(e2).collect()), (_, Err(e)) => Err(e.into_iter().collect()), (Err(e), _) => Err(e.into_iter().collect()), } From d60ff77a6eb5eba2a5db456d642cbc4e2f524831 Mon Sep 17 00:00:00 2001 From: Robert Balicki Date: Thu, 7 Nov 2024 23:25:05 -0500 Subject: [PATCH 04/10] fix issue where the compiler panics if it encounters an invalid token in an iso literal. --- .../src/parse_iso_literal.rs | 12 +---- .../src/peekable_lexer.rs | 53 ++++--------------- 2 files changed, 13 insertions(+), 52 deletions(-) diff --git a/crates/isograph_lang_parser/src/parse_iso_literal.rs b/crates/isograph_lang_parser/src/parse_iso_literal.rs index 4a513e35..0756b298 100644 --- a/crates/isograph_lang_parser/src/parse_iso_literal.rs +++ b/crates/isograph_lang_parser/src/parse_iso_literal.rs @@ -35,7 +35,7 @@ pub fn parse_iso_literal( const_export_name: Option<&str>, text_source: TextSource, ) -> Result> { - let mut tokens = PeekableLexer::new(iso_literal_text, text_source); + let mut tokens = PeekableLexer::new(iso_literal_text); let discriminator = tokens .parse_source_of_kind(IsographLangTokenKind::Identifier) .map_err(|with_span| with_span.map(IsographLiteralParseError::from)) @@ -724,21 +724,13 @@ fn from_control_flow(control_flow: impl FnOnce() -> ControlFlow) -> #[cfg(test)] mod test { - use common_lang_types::TextSource; - use intern::string_key::Intern; use crate::{IsographLangTokenKind, PeekableLexer}; #[test] fn parse_literal_tests() { let source = "\"Description\" Query.foo { bar, baz, }"; - let mut lexer = PeekableLexer::new( - source, - TextSource { - path: "path".intern().into(), - span: None, - }, - ); + let mut lexer = PeekableLexer::new(source); loop { let token = lexer.parse_token(); diff --git a/crates/isograph_lang_parser/src/peekable_lexer.rs b/crates/isograph_lang_parser/src/peekable_lexer.rs index 2efd6769..313e4de4 100644 --- a/crates/isograph_lang_parser/src/peekable_lexer.rs +++ b/crates/isograph_lang_parser/src/peekable_lexer.rs @@ -1,5 +1,5 @@ use crate::IsographLangTokenKind; -use common_lang_types::{EmbeddedLocation, Location, Span, TextSource, WithLocation, WithSpan}; +use common_lang_types::{Span, WithSpan}; use intern::string_key::{Intern, StringKey}; use logos::Logos; use thiserror::Error; @@ -11,11 +11,10 @@ pub(crate) struct PeekableLexer<'source> { /// the byte offset of the *end* of the previous token end_index_of_last_parsed_token: u32, offset: u32, - text_source: TextSource, } impl<'source> PeekableLexer<'source> { - pub fn new(source: &'source str, text_source: TextSource) -> Self { + pub fn new(source: &'source str) -> Self { // To enable fast lookahead the parser needs to store at least the 'kind' (IsographLangTokenKind) // of the next token: the simplest option is to store the full current token, but // the Parser requires an initial value. Rather than incur runtime/code overhead @@ -31,7 +30,6 @@ impl<'source> PeekableLexer<'source> { source, end_index_of_last_parsed_token: 0, offset: 0, - text_source, }; // Advance to the first real token before doing any work @@ -41,44 +39,15 @@ impl<'source> PeekableLexer<'source> { /// Get the next token (and advance) pub fn parse_token(&mut self) -> WithSpan { - // Skip over (and record) any invalid tokens until either a valid token or an EOF is encountered - // - // Remove this allow after logic changed. - #[allow(clippy::never_loop)] - loop { - let kind = self - .lexer - .next() - .unwrap_or(IsographLangTokenKind::EndOfFile); - match kind { - IsographLangTokenKind::Error => { - // HACK we print out the location here, but we should return - // the error. In particular, we can show multiple such errors - // if they occur in different iso literals. - let span = self.lexer_span(); - // TODO propagate? continue? - panic!( - "Encountered an error. \ - This can occur if you commented out an iso literal, \ - or if an iso literal contains \ - an invalid token. \n{}", - WithLocation::new( - "", - Location::Embedded(EmbeddedLocation { - text_source: self.text_source, - span - }) - ) - ) - } - _ => { - self.end_index_of_last_parsed_token = self.current.span.end; - let span = self.lexer_span(); - // TODO why does self.current = ... not work here? - return std::mem::replace(&mut self.current, WithSpan::new(kind, span)); - } - } - } + let kind = self + .lexer + .next() + .unwrap_or(IsographLangTokenKind::EndOfFile); + + self.end_index_of_last_parsed_token = self.current.span.end; + let span = self.lexer_span(); + // TODO why does self.current = ... not work here? + return std::mem::replace(&mut self.current, WithSpan::new(kind, span)); } pub fn peek(&self) -> WithSpan { From 9c01b96f6570a8a5599b65240cc1a18464c8110e Mon Sep 17 00:00:00 2001 From: Robert Balicki Date: Thu, 7 Nov 2024 23:26:37 -0500 Subject: [PATCH 05/10] fix issue where the compiler panics if it encounters an invalid token in the graphql schema --- .../src/peekable_lexer.rs | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/crates/graphql_schema_parser/src/peekable_lexer.rs b/crates/graphql_schema_parser/src/peekable_lexer.rs index 2113a683..bfc2439a 100644 --- a/crates/graphql_schema_parser/src/peekable_lexer.rs +++ b/crates/graphql_schema_parser/src/peekable_lexer.rs @@ -42,27 +42,11 @@ impl<'source> PeekableLexer<'source> { /// Get the next token (and advance) pub fn parse_token(&mut self) -> WithSpan { - // Skip over (and record) any invalid tokens until either a valid token or an EOF is encountered - // - // TODO: remove this allow after logic changed. - #[allow(clippy::never_loop)] - loop { - let kind = self.lexer.next().unwrap_or(TokenKind::EndOfFile); - match kind { - TokenKind::Error => { - // TODO propagate? continue? - panic!( - "Encountered an error; this probably means you have an invalid character." - ) - } - _ => { - self.end_index_of_last_parsed_token = self.current.span.end; - let span = self.lexer_span(); - // TODO why does self.current = ... not work here? - return std::mem::replace(&mut self.current, WithSpan::new(kind, span)); - } - } - } + let kind = self.lexer.next().unwrap_or(TokenKind::EndOfFile); + self.end_index_of_last_parsed_token = self.current.span.end; + let span = self.lexer_span(); + // TODO why does self.current = ... not work here? + return std::mem::replace(&mut self.current, WithSpan::new(kind, span)); } pub fn peek(&self) -> WithSpan { From 50b13a7fefe27bb31c69fe2e4da4bf44675854ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Wa=C5=82ach?= <35966385+PatrykWalach@users.noreply.github.com> Date: Sat, 9 Nov 2024 18:19:19 +0100 Subject: [PATCH 06/10] parallelize build-cli workflow (#257) - extract building the CLI on various platforms into its own step - remove linux from the CLI-building matrix - build linux CLI as its own step - have the build demos CLI step depend on building linux, not on building the CLI for each platform - saves about ~18s --- .github/workflows/build-cli.yml | 51 +++++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 50 ++++++++++++-------------------- 2 files changed, 70 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/build-cli.yml diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml new file mode 100644 index 00000000..c2af5979 --- /dev/null +++ b/.github/workflows/build-cli.yml @@ -0,0 +1,51 @@ +on: + workflow_call: + inputs: + target: + required: true + type: string + os: + required: true + type: string + build-name: + required: true + type: string + artifact-name: + required: true + type: string + longpaths: + required: false + type: boolean + cross: + required: false + type: boolean + +jobs: + build-cli: + name: Build compiler + timeout-minutes: 10 + runs-on: ${{ inputs.os }} + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: 1.75.0 + override: true + target: ${{ inputs.target }} + # more info here:- https://github.com/rust-lang/cargo/issues/13020 + - name: Enable longer pathnames for git + if: inputs.longpaths + run: git config --system core.longpaths true + - name: Install cross + if: inputs.cross + uses: actions-rs/cargo@v1 + with: + command: install + args: cross + - name: 'Build isograph_cli with cargo (${{inputs.target}})' + run: ${{ inputs.cross && 'cross' || 'cargo' }} build --target ${{ inputs.target }} --release + - uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact-name }} + path: target/${{ inputs.target }}/release/${{ inputs.build-name }} + if-no-files-found: error diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67d67376..8c77595b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,14 +10,9 @@ permissions: jobs: build-cli: name: Build compiler - timeout-minutes: 10 strategy: matrix: target: - - target: x86_64-unknown-linux-musl - os: ubuntu-latest - build-name: isograph_cli - artifact-name: isograph_cli-linux-x64 - target: aarch64-unknown-linux-musl os: ubuntu-latest build-name: isograph_cli @@ -36,36 +31,28 @@ jobs: longpaths: true build-name: isograph_cli.exe artifact-name: isograph_cli-bin-win-x64 - runs-on: ${{ matrix.target.os }} - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: 1.75.0 - override: true - target: ${{ matrix.target.target }} - # more info here:- https://github.com/rust-lang/cargo/issues/13020 - - name: Enable longer pathnames for git - if: matrix.target.longpaths - run: git config --system core.longpaths true - - name: Install cross - if: matrix.target.cross - uses: actions-rs/cargo@v1 - with: - command: install - args: cross - - name: 'Build isograph_cli with cargo (${{matrix.target.target}})' - run: ${{ matrix.target.cross && 'cross' || 'cargo' }} build --target ${{ matrix.target.target }} --release - - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.target.artifact-name }} - path: target/${{ matrix.target.target }}/release/${{ matrix.target.build-name }} - if-no-files-found: error + uses: ./.github/workflows/build-cli.yml + with: + target: ${{ matrix.target.target }} + os: ${{ matrix.target.os }} + build-name: ${{ matrix.target.build-name }} + artifact-name: ${{ matrix.target.artifact-name }} + longpaths: ${{ matrix.target.longpaths || false }} + cross: ${{ matrix.target.cross || false }} + + build-cli-linux: + name: Build compiler (x86_64-unknown-linux-musl) + uses: ./.github/workflows/build-cli.yml + with: + target: x86_64-unknown-linux-musl + os: ubuntu-latest + build-name: isograph_cli + artifact-name: isograph_cli-linux-x64 build-demos: name: Build demos runs-on: ubuntu-latest - needs: [build-cli] + needs: [build-cli-linux] strategy: matrix: target: @@ -227,6 +214,7 @@ jobs: [ build-js-packages, build-cli, + build-cli-linux, build-demos, build-website, prettier, From c57157a72e89877086747e6ba6cfb825af7c85f0 Mon Sep 17 00:00:00 2001 From: Robert Balicki Date: Sat, 9 Nov 2024 13:54:40 -0500 Subject: [PATCH 07/10] create a SelectionType enum, and use that instead of a custom enum for SelectableServerFieldId and SchemaType. Remove unused SchemaInputType and SchemaOutputType --- .../src/query_text.rs | 6 +-- crates/isograph_lang_types/src/id_types.rs | 8 +-- crates/isograph_schema/src/isograph_schema.rs | 52 ++++--------------- 3 files changed, 18 insertions(+), 48 deletions(-) diff --git a/crates/graphql_artifact_generation/src/query_text.rs b/crates/graphql_artifact_generation/src/query_text.rs index 1ffefc10..a727d103 100644 --- a/crates/graphql_artifact_generation/src/query_text.rs +++ b/crates/graphql_artifact_generation/src/query_text.rs @@ -1,8 +1,8 @@ -use common_lang_types::{HasName, QueryOperationName, UnvalidatedTypeName}; +use common_lang_types::{QueryOperationName, UnvalidatedTypeName}; use graphql_lang_types::GraphQLTypeAnnotation; use isograph_lang_types::{ArgumentKeyAndValue, NonConstantValue}; use isograph_schema::{ - MergedSelectionMap, MergedServerSelection, RootOperationName, ValidatedSchema, + get_name, MergedSelectionMap, MergedServerSelection, RootOperationName, ValidatedSchema, ValidatedVariableDefinition, }; @@ -49,7 +49,7 @@ fn write_variables_to_string<'a>( let schema_input_type = schema .server_field_data .lookup_unvalidated_type(input_type_id); - schema_input_type.name() + get_name(schema_input_type) }); // TODO this is dangerous, since variable.item.name is a WithLocation, which impl's Display. // We should find a way to make WithLocation not impl display, without making error's hard diff --git a/crates/isograph_lang_types/src/id_types.rs b/crates/isograph_lang_types/src/id_types.rs index 15d7d3b9..186530d4 100644 --- a/crates/isograph_lang_types/src/id_types.rs +++ b/crates/isograph_lang_types/src/id_types.rs @@ -13,10 +13,12 @@ u32_newtype!(ServerObjectId); u32_newtype!(ServerScalarId); +pub type SelectableServerFieldId = SelectionType; + #[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)] -pub enum SelectableServerFieldId { - Object(ServerObjectId), - Scalar(ServerScalarId), +pub enum SelectionType { + Object(TObject), + Scalar(TScalar), } impl TryFrom for ServerScalarId { diff --git a/crates/isograph_schema/src/isograph_schema.rs b/crates/isograph_schema/src/isograph_schema.rs index 71b4a209..5aa05975 100644 --- a/crates/isograph_schema/src/isograph_schema.rs +++ b/crates/isograph_schema/src/isograph_schema.rs @@ -4,9 +4,9 @@ use std::{ }; use common_lang_types::{ - ArtifactFileType, DescriptionValue, GraphQLInterfaceTypeName, GraphQLScalarTypeName, HasName, - InputTypeName, IsographObjectTypeName, JavascriptName, SelectableFieldName, - UnvalidatedTypeName, WithLocation, WithSpan, + ArtifactFileType, DescriptionValue, GraphQLInterfaceTypeName, GraphQLScalarTypeName, + IsographObjectTypeName, JavascriptName, SelectableFieldName, UnvalidatedTypeName, WithLocation, + WithSpan, }; use graphql_lang_types::{ GraphQLConstantValue, GraphQLDirective, GraphQLFieldDefinition, @@ -14,8 +14,8 @@ use graphql_lang_types::{ }; use intern::string_key::Intern; use isograph_lang_types::{ - ArgumentKeyAndValue, ClientFieldId, SelectableServerFieldId, Selection, ServerFieldId, - ServerObjectId, ServerScalarId, ServerStrongIdFieldId, TypeAnnotation, Unwrap, + ArgumentKeyAndValue, ClientFieldId, SelectableServerFieldId, Selection, SelectionType, + ServerFieldId, ServerObjectId, ServerScalarId, ServerStrongIdFieldId, TypeAnnotation, Unwrap, VariableDefinition, }; use lazy_static::lazy_static; @@ -255,44 +255,12 @@ impl ServerFieldData { } } -#[derive(Clone, Copy, Debug)] -pub enum SchemaType<'a> { - Object(&'a SchemaObject), - Scalar(&'a SchemaScalar), -} - -impl<'a> HasName for SchemaType<'a> { - type Name = UnvalidatedTypeName; - - fn name(&self) -> Self::Name { - match self { - SchemaType::Object(object) => object.name.into(), - SchemaType::Scalar(scalar) => scalar.name.item.into(), - } - } -} +pub type SchemaType<'a> = SelectionType<&'a SchemaObject, &'a SchemaScalar>; -#[derive(Clone, Copy, Debug)] -pub enum SchemaOutputType<'a> { - Object(&'a SchemaObject), - Scalar(&'a SchemaScalar), - // excludes input object -} - -#[derive(Clone, Copy, Debug)] -pub enum SchemaInputType<'a> { - Scalar(&'a SchemaScalar), - // input object - // enum -} - -impl<'a> HasName for SchemaInputType<'a> { - type Name = InputTypeName; - - fn name(&self) -> Self::Name { - match self { - SchemaInputType::Scalar(x) => x.name.item.into(), - } +pub fn get_name<'a>(schema_type: SchemaType<'a>) -> UnvalidatedTypeName { + match schema_type { + SelectionType::Object(object) => object.name.into(), + SelectionType::Scalar(scalar) => scalar.name.item.into(), } } From 3a36e9fb7fdc492990c44f9d05a9b1e3ea5690e8 Mon Sep 17 00:00:00 2001 From: Robert Balicki Date: Sat, 9 Nov 2024 13:55:53 -0500 Subject: [PATCH 08/10] remove unused HasName trait --- crates/common_lang_types/src/lib.rs | 2 -- crates/common_lang_types/src/traits.rs | 4 ---- .../src/client_field_declaration.rs | 15 ++------------- 3 files changed, 2 insertions(+), 19 deletions(-) delete mode 100644 crates/common_lang_types/src/traits.rs diff --git a/crates/common_lang_types/src/lib.rs b/crates/common_lang_types/src/lib.rs index 287e5327..4cc08e74 100644 --- a/crates/common_lang_types/src/lib.rs +++ b/crates/common_lang_types/src/lib.rs @@ -3,10 +3,8 @@ mod path_and_content; mod span; mod string_key_types; mod text_with_carats; -mod traits; pub use location::*; pub use path_and_content::*; pub use span::*; pub use string_key_types::*; -pub use traits::*; diff --git a/crates/common_lang_types/src/traits.rs b/crates/common_lang_types/src/traits.rs deleted file mode 100644 index cfc4f40a..00000000 --- a/crates/common_lang_types/src/traits.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub trait HasName { - type Name; - fn name(&self) -> Self::Name; -} diff --git a/crates/isograph_lang_types/src/client_field_declaration.rs b/crates/isograph_lang_types/src/client_field_declaration.rs index ff022a3e..06277265 100644 --- a/crates/isograph_lang_types/src/client_field_declaration.rs +++ b/crates/isograph_lang_types/src/client_field_declaration.rs @@ -2,9 +2,8 @@ use std::fmt::Debug; use common_lang_types::{ ConstExportName, DescriptionValue, EnumLiteralValue, FieldArgumentName, FieldNameOrAlias, - FilePath, HasName, LinkedFieldAlias, LinkedFieldName, ScalarFieldAlias, ScalarFieldName, - SelectableFieldName, StringLiteralValue, UnvalidatedTypeName, ValueKeyName, VariableName, - WithLocation, WithSpan, + FilePath, LinkedFieldAlias, LinkedFieldName, ScalarFieldAlias, ScalarFieldName, + StringLiteralValue, UnvalidatedTypeName, ValueKeyName, VariableName, WithLocation, WithSpan, }; use graphql_lang_types::{FloatValue, GraphQLTypeAnnotation, NameValuePair}; use serde::Deserialize; @@ -186,16 +185,6 @@ impl ServerFieldSelection HasName for ServerFieldSelection { - type Name = SelectableFieldName; - fn name(&self) -> Self::Name { - match self { - ServerFieldSelection::ScalarField(s) => s.name.item.into(), - ServerFieldSelection::LinkedField(l) => l.name.item.into(), - } - } -} - #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)] pub struct ScalarFieldSelection { pub name: WithLocation, From beecd3bfeb2488f9427816b83a901a2322884f62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Wa=C5=82ach?= <35966385+PatrykWalach@users.noreply.github.com> Date: Sat, 9 Nov 2024 22:06:49 +0100 Subject: [PATCH 09/10] customize store id (#167) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - This commit makes it so that IDs do not have to be globally unique. Instead, they can be unique within a given typename. - In order to implement this, we change how the store is structured. Instead of `{ [id]: Record }`, it is structured as `{ [TypeName]: { [id]: Record } }` - Linked fields already have a `concreteType` field that is non-null if the linked field is concrete and a `typename` selection if the linked field is not concrete. - This commit also adds a `concreteType` to entrypoints, so that we know the name of the root object (e.g. `Query`, `Mutation`, etc. This is customizable — it is `Root` in the Star Wars graphql API.) - Given this information, we can read and write to the store in the two-layer fashion. Missing field handlers: - In addition, we remove the default missing field handler for `node`. This raises the importance of having some way to associate a given field (like `user(id)`) with a type (`User`), and to: - write refetch queries using that field, and - encourage users to fetch through that field - Not using `node` is a best practice, anyway! - We consider an inefficient `node` missing field handler that looks through all the concrete types in the store, or (more speculatively) some way of keeping track of all the IDs to typenames in a map. Some other advantages: - we can garbage collect entire object classes at once, which may be more performant in some cases (speculative) - we'll have the ability to iterate all objects of a given type. (When we implement "subtypes", like a Friend is a subset of User, then this will make a lot more sense!) We may be able to expose top-level fields like `allUsers` or whatnot, or do some SQL-like stuff as a result. I'm not convinced this (as stated) is a best practice, but it would be nice if we were able to expose some SQL-like interface for filtering, etc. --- .../src/entrypoint_artifact.rs | 54 ++++++-- .../src/generate_artifacts.rs | 2 +- .../src/imperatively_loaded_fields.rs | 6 + .../src/create_merged_selection_set.rs | 2 + crates/isograph_schema/src/isograph_schema.rs | 10 +- .../__isograph/Query/HomePage/__refetch__0.ts | 1 + .../__isograph/Query/HomePage/entrypoint.ts | 1 + .../Query/PullRequest/entrypoint.ts | 1 + .../Query/RepositoryPage/entrypoint.ts | 1 + .../__isograph/Query/UserPage/entrypoint.ts | 1 + .../User/RepositoryConnection/entrypoint.ts | 1 + demos/pet-demo/pages/_app.tsx | 39 ++---- .../AdItem/AdItemDisplay/entrypoint.ts | 1 + .../BlogItem/BlogItemMoreDetail/entrypoint.ts | 1 + .../Image/ImageDisplay/entrypoint.ts | 1 + .../Mutation/SetTagline/entrypoint.ts | 1 + .../Pet/PetCheckinsCard/__refetch__0.ts | 1 + .../Pet/PetCheckinsCard/entrypoint.ts | 1 + .../Pet/PetCheckinsCardList/__refetch__0.ts | 1 + .../Pet/PetCheckinsCardList/entrypoint.ts | 1 + .../__isograph/Query/HomeRoute/entrypoint.ts | 1 + .../__isograph/Query/Newsfeed/entrypoint.ts | 1 + .../__isograph/Query/PetByName/entrypoint.ts | 1 + .../Query/PetCheckinListRoute/__refetch__0.ts | 1 + .../Query/PetCheckinListRoute/entrypoint.ts | 1 + .../PetDetailDeferredRoute/entrypoint.ts | 1 + .../Query/PetDetailRoute/__refetch__0.ts | 1 + .../Query/PetDetailRoute/__refetch__1.ts | 1 + .../Query/PetDetailRoute/__refetch__2.ts | 1 + .../Query/PetDetailRoute/__refetch__3.ts | 1 + .../Query/PetDetailRoute/__refetch__4.ts | 1 + .../Query/PetDetailRoute/entrypoint.ts | 1 + .../Query/PetFavoritePhrase/entrypoint.ts | 1 + .../NewsfeedPaginationComponent/entrypoint.ts | 1 + .../__isograph/Query/HomePage/entrypoint.ts | 1 + libs/isograph-react/schema.graphql | 1 + .../src/core/FragmentReference.ts | 2 +- .../src/core/IsographEnvironment.ts | 40 ++---- libs/isograph-react/src/core/cache.ts | 131 ++++++++++++++---- libs/isograph-react/src/core/check.ts | 15 +- libs/isograph-react/src/core/entrypoint.ts | 2 + .../src/core/garbageCollection.ts | 91 +++++++++--- libs/isograph-react/src/core/logging.ts | 4 +- .../src/core/makeNetworkRequest.ts | 9 +- libs/isograph-react/src/core/read.ts | 41 +++--- libs/isograph-react/src/index.ts | 1 - .../src/react/useImperativeReference.ts | 2 +- .../__isograph/Query/meName/entrypoint.ts | 1 + .../Query/meNameSuccessor/entrypoint.ts | 1 + .../__isograph/Query/nodeField/entrypoint.ts | 1 + .../__isograph/Query/subquery/entrypoint.ts | 67 +++++++++ .../__isograph/Query/subquery/output_type.ts | 3 + .../__isograph/Query/subquery/param_type.ts | 12 ++ .../Query/subquery/parameters_type.ts | 3 + .../Query/subquery/resolver_reader.ts | 47 +++++++ .../src/tests/__isograph/iso.ts | 10 ++ .../src/tests/garbageCollection.test.ts | 79 ++++++----- .../src/tests/meNameSuccessor.ts | 5 + libs/isograph-react/src/tests/nodeQuery.ts | 2 + .../src/tests/normalizeData.test.ts | 120 ++++++++++++++++ 60 files changed, 653 insertions(+), 179 deletions(-) create mode 100644 libs/isograph-react/src/tests/__isograph/Query/subquery/entrypoint.ts create mode 100644 libs/isograph-react/src/tests/__isograph/Query/subquery/output_type.ts create mode 100644 libs/isograph-react/src/tests/__isograph/Query/subquery/param_type.ts create mode 100644 libs/isograph-react/src/tests/__isograph/Query/subquery/parameters_type.ts create mode 100644 libs/isograph-react/src/tests/__isograph/Query/subquery/resolver_reader.ts create mode 100644 libs/isograph-react/src/tests/normalizeData.test.ts diff --git a/crates/graphql_artifact_generation/src/entrypoint_artifact.rs b/crates/graphql_artifact_generation/src/entrypoint_artifact.rs index ae3a2c44..00406a80 100644 --- a/crates/graphql_artifact_generation/src/entrypoint_artifact.rs +++ b/crates/graphql_artifact_generation/src/entrypoint_artifact.rs @@ -1,9 +1,11 @@ use std::collections::BTreeSet; -use common_lang_types::{ArtifactPathAndContent, QueryOperationName, VariableName}; +use common_lang_types::{ + ArtifactPathAndContent, IsographObjectTypeName, QueryOperationName, VariableName, +}; use intern::{string_key::Intern, Lookup}; use isograph_config::GenerateFileExtensionsOption; -use isograph_lang_types::{ClientFieldId, IsographSelectionVariant}; +use isograph_lang_types::{ClientFieldId, IsographSelectionVariant, ServerObjectId}; use isograph_schema::{ create_merged_selection_map_for_field_and_insert_into_global_map, current_target_merged_selections, get_imperatively_loaded_artifact_info, @@ -30,6 +32,7 @@ struct EntrypointArtifactInfo<'schema> { query_text: QueryText, normalization_ast_text: NormalizationAstText, refetch_query_artifact_import: RefetchQueryArtifactImport, + concrete_type: IsographObjectTypeName, } pub(crate) fn generate_entrypoint_artifacts( @@ -63,7 +66,7 @@ pub(crate) fn generate_entrypoint_artifacts( .variable_definitions .iter() .map(|variable_definition| &variable_definition.item), - &schema.mutation_root_operation_name(), + &schema.find_mutation(), file_extensions, ) } @@ -76,7 +79,7 @@ pub(crate) fn generate_entrypoint_artifacts_with_client_field_traversal_result<' traversal_state: &ScalarClientFieldTraversalState, encountered_client_field_map: &ClientFieldToCompletedMergeTraversalStateMap, variable_definitions: impl Iterator + 'a, - default_root_operation_name: &Option<&RootOperationName>, + default_root_operation: &Option<(&ServerObjectId, &RootOperationName)>, file_extensions: GenerateFileExtensionsOption, ) -> Vec { let query_name = entrypoint.name.into(); @@ -88,14 +91,16 @@ pub(crate) fn generate_entrypoint_artifacts_with_client_field_traversal_result<' .fetchable_types .get(&entrypoint.parent_object_id) .unwrap_or_else(|| { - default_root_operation_name.unwrap_or_else(|| { - schema - .fetchable_types - .iter() - .next() - .expect("Expected at least one fetchable type to exist") - .1 - }) + default_root_operation + .map(|(_, operation_name)| operation_name) + .unwrap_or_else(|| { + schema + .fetchable_types + .iter() + .next() + .expect("Expected at least one fetchable type to exist") + .1 + }) }); let parent_object = schema.server_field_data.object(entrypoint.parent_object_id); @@ -144,12 +149,33 @@ pub(crate) fn generate_entrypoint_artifacts_with_client_field_traversal_result<' let normalization_ast_text = generate_normalization_ast_text(schema, merged_selection_map.values(), 0); + let concrete_type = schema.server_field_data.object( + if schema + .fetchable_types + .contains_key(&entrypoint.parent_object_id) + { + entrypoint.parent_object_id + } else { + *default_root_operation + .map(|(operation_id, _)| operation_id) + .unwrap_or_else(|| { + schema + .fetchable_types + .iter() + .next() + .expect("Expected at least one fetchable type to exist") + .0 + }) + }, + ); + let mut paths_and_contents = vec![EntrypointArtifactInfo { query_text, query_name, parent_type: parent_object, normalization_ast_text, refetch_query_artifact_import, + concrete_type: concrete_type.name, } .path_and_content(file_extensions)]; @@ -250,6 +276,7 @@ impl<'schema> EntrypointArtifactInfo<'schema> { refetch_query_artifact_import, query_name, parent_type, + concrete_type, } = self; let ts_file_extension = file_extensions.ts(); let entrypoint_params_typename = format!("{}__{}__param", parent_type.name, query_name); @@ -278,6 +305,7 @@ impl<'schema> EntrypointArtifactInfo<'schema> { {} queryText,\n\ {} normalizationAst,\n\ {}}},\n\ + {}concreteType: \"{concrete_type}\",\n\ {}readerWithRefetchQueries: {{\n\ {} kind: \"ReaderWithRefetchQueries\",\n\ {} nestedRefetchQueries,\n\ @@ -285,7 +313,7 @@ impl<'schema> EntrypointArtifactInfo<'schema> { {}}},\n\ }};\n\n\ export default artifact;\n", - " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " " + " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", ) } } diff --git a/crates/graphql_artifact_generation/src/generate_artifacts.rs b/crates/graphql_artifact_generation/src/generate_artifacts.rs index f0bc11c5..80265a2d 100644 --- a/crates/graphql_artifact_generation/src/generate_artifacts.rs +++ b/crates/graphql_artifact_generation/src/generate_artifacts.rs @@ -222,7 +222,7 @@ pub fn get_artifact_path_and_content( &traversal_state, &encountered_client_field_map, variable_definitions_iter, - &schema.query_root_operation_name(), + &schema.find_query(), file_extensions, ), ); diff --git a/crates/graphql_artifact_generation/src/imperatively_loaded_fields.rs b/crates/graphql_artifact_generation/src/imperatively_loaded_fields.rs index 3fe223b4..6f2de199 100644 --- a/crates/graphql_artifact_generation/src/imperatively_loaded_fields.rs +++ b/crates/graphql_artifact_generation/src/imperatively_loaded_fields.rs @@ -16,6 +16,7 @@ pub(crate) struct ImperativelyLoadedEntrypointArtifactInfo { pub root_fetchable_field: SelectableFieldName, pub root_fetchable_field_parent_object: IsographObjectTypeName, pub refetch_query_index: RefetchQueryIndex, + pub concrete_type: IsographObjectTypeName, } impl ImperativelyLoadedEntrypointArtifactInfo { @@ -46,6 +47,7 @@ impl ImperativelyLoadedEntrypointArtifactInfo { let ImperativelyLoadedEntrypointArtifactInfo { normalization_ast_text: normalization_ast, query_text, + concrete_type, .. } = self; @@ -60,6 +62,7 @@ impl ImperativelyLoadedEntrypointArtifactInfo { {} queryText,\n\ {} normalizationAst,\n\ {}}},\n\ + {}concreteType: \"{concrete_type}\",\n\ }};\n\n\ export default artifact;\n", " ", @@ -68,6 +71,7 @@ impl ImperativelyLoadedEntrypointArtifactInfo { " ", " ", " ", + " ", ) } @@ -85,6 +89,7 @@ pub(crate) fn get_artifact_for_imperatively_loaded_field( variable_definitions, root_operation_name, query_name, + concrete_type, } = imperatively_loaded_field_artifact_info; let query_text = generate_query_text( @@ -106,6 +111,7 @@ pub(crate) fn get_artifact_for_imperatively_loaded_field( root_fetchable_field, root_fetchable_field_parent_object: root_parent_object, refetch_query_index, + concrete_type, } .path_and_content() } diff --git a/crates/isograph_schema/src/create_merged_selection_set.rs b/crates/isograph_schema/src/create_merged_selection_set.rs index 2bf7db3c..f8c21005 100644 --- a/crates/isograph_schema/src/create_merged_selection_set.rs +++ b/crates/isograph_schema/src/create_merged_selection_set.rs @@ -178,6 +178,7 @@ pub struct ImperativelyLoadedFieldArtifactInfo { pub root_operation_name: RootOperationName, pub query_name: QueryOperationName, + pub concrete_type: IsographObjectTypeName, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -592,6 +593,7 @@ fn process_imperatively_loaded_field( refetch_query_index: RefetchQueryIndex(index as u32), root_operation_name, query_name, + concrete_type: schema.server_field_data.object(root_object_id).name, } } diff --git a/crates/isograph_schema/src/isograph_schema.rs b/crates/isograph_schema/src/isograph_schema.rs index 5aa05975..ce7dedbc 100644 --- a/crates/isograph_schema/src/isograph_schema.rs +++ b/crates/isograph_schema/src/isograph_schema.rs @@ -116,18 +116,16 @@ impl Schema Option<&RootOperationName> { + pub fn find_mutation(&self) -> Option<(&ServerObjectId, &RootOperationName)> { self.fetchable_types .iter() - .find(|(_, root_operation_name)| root_operation_name.0 == "query") - .map(|(_, root_operation_name)| root_operation_name) + .find(|(_, root_operation_name)| root_operation_name.0 == "mutation") } - pub fn mutation_root_operation_name(&self) -> Option<&RootOperationName> { + pub fn find_query(&self) -> Option<(&ServerObjectId, &RootOperationName)> { self.fetchable_types .iter() - .find(|(_, root_operation_name)| root_operation_name.0 == "mutation") - .map(|(_, root_operation_name)| root_operation_name) + .find(|(_, root_operation_name)| root_operation_name.0 == "query") } } diff --git a/demos/github-demo/src/isograph-components/__isograph/Query/HomePage/__refetch__0.ts b/demos/github-demo/src/isograph-components/__isograph/Query/HomePage/__refetch__0.ts index 877075fd..6373b761 100644 --- a/demos/github-demo/src/isograph-components/__isograph/Query/HomePage/__refetch__0.ts +++ b/demos/github-demo/src/isograph-components/__isograph/Query/HomePage/__refetch__0.ts @@ -222,6 +222,7 @@ const artifact: RefetchQueryNormalizationArtifact = { queryText, normalizationAst, }, + concreteType: "Query", }; export default artifact; diff --git a/demos/github-demo/src/isograph-components/__isograph/Query/HomePage/entrypoint.ts b/demos/github-demo/src/isograph-components/__isograph/Query/HomePage/entrypoint.ts index 4b1df6c0..362f1222 100644 --- a/demos/github-demo/src/isograph-components/__isograph/Query/HomePage/entrypoint.ts +++ b/demos/github-demo/src/isograph-components/__isograph/Query/HomePage/entrypoint.ts @@ -214,6 +214,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Query", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/demos/github-demo/src/isograph-components/__isograph/Query/PullRequest/entrypoint.ts b/demos/github-demo/src/isograph-components/__isograph/Query/PullRequest/entrypoint.ts index 1e76b47d..75db112b 100644 --- a/demos/github-demo/src/isograph-components/__isograph/Query/PullRequest/entrypoint.ts +++ b/demos/github-demo/src/isograph-components/__isograph/Query/PullRequest/entrypoint.ts @@ -181,6 +181,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Query", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/demos/github-demo/src/isograph-components/__isograph/Query/RepositoryPage/entrypoint.ts b/demos/github-demo/src/isograph-components/__isograph/Query/RepositoryPage/entrypoint.ts index b7650f92..6be9f97c 100644 --- a/demos/github-demo/src/isograph-components/__isograph/Query/RepositoryPage/entrypoint.ts +++ b/demos/github-demo/src/isograph-components/__isograph/Query/RepositoryPage/entrypoint.ts @@ -316,6 +316,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Query", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/demos/github-demo/src/isograph-components/__isograph/Query/UserPage/entrypoint.ts b/demos/github-demo/src/isograph-components/__isograph/Query/UserPage/entrypoint.ts index 17cf6153..348b9cb3 100644 --- a/demos/github-demo/src/isograph-components/__isograph/Query/UserPage/entrypoint.ts +++ b/demos/github-demo/src/isograph-components/__isograph/Query/UserPage/entrypoint.ts @@ -232,6 +232,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Query", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/demos/github-demo/src/isograph-components/__isograph/User/RepositoryConnection/entrypoint.ts b/demos/github-demo/src/isograph-components/__isograph/User/RepositoryConnection/entrypoint.ts index c0cb4c84..1c2e894c 100644 --- a/demos/github-demo/src/isograph-components/__isograph/User/RepositoryConnection/entrypoint.ts +++ b/demos/github-demo/src/isograph-components/__isograph/User/RepositoryConnection/entrypoint.ts @@ -212,6 +212,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Query", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/demos/pet-demo/pages/_app.tsx b/demos/pet-demo/pages/_app.tsx index c891079b..d8ece8e4 100644 --- a/demos/pet-demo/pages/_app.tsx +++ b/demos/pet-demo/pages/_app.tsx @@ -1,14 +1,12 @@ import { - DataId, - Link, - StoreRecord, - defaultMissingFieldHandler, IsographEnvironmentProvider, + StoreRecord, createIsographEnvironment, createIsographStore, + type Link, } from '@isograph/react'; -import { useMemo } from 'react'; import type { AppProps } from 'next/app'; +import { useMemo } from 'react'; function makeNetworkRequest( queryText: string, @@ -45,27 +43,16 @@ const missingFieldHandler = ( arguments_: { [index: string]: any } | null, variables: { [index: string]: any } | null, ): Link | undefined => { - const val = defaultMissingFieldHandler( - storeRecord, - root, - fieldName, - arguments_, - variables, - ); - if (val == undefined) { - // This is the custom missing field handler - // - // N.B. this **not** correct. We need to pass the correct variables/args here. - // But it works for this demo. - if ( - fieldName === 'pet' && - variables?.id != null && - root.__link === '__ROOT' - ) { - return { __link: variables.id }; - } - } else { - return val; + // This is the custom missing field handler + // + // N.B. this **not** correct. We need to pass the correct variables/args here. + // But it works for this demo. + if ( + fieldName === 'pet' && + variables?.id != null && + root.__link === '__ROOT' + ) { + return { __link: variables.id, __typename: 'Pet' }; } }; diff --git a/demos/pet-demo/src/components/__isograph/AdItem/AdItemDisplay/entrypoint.ts b/demos/pet-demo/src/components/__isograph/AdItem/AdItemDisplay/entrypoint.ts index 7cc3ddeb..4e09394a 100644 --- a/demos/pet-demo/src/components/__isograph/AdItem/AdItemDisplay/entrypoint.ts +++ b/demos/pet-demo/src/components/__isograph/AdItem/AdItemDisplay/entrypoint.ts @@ -66,6 +66,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Query", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/demos/pet-demo/src/components/__isograph/BlogItem/BlogItemMoreDetail/entrypoint.ts b/demos/pet-demo/src/components/__isograph/BlogItem/BlogItemMoreDetail/entrypoint.ts index 977d8ff0..105ff064 100644 --- a/demos/pet-demo/src/components/__isograph/BlogItem/BlogItemMoreDetail/entrypoint.ts +++ b/demos/pet-demo/src/components/__isograph/BlogItem/BlogItemMoreDetail/entrypoint.ts @@ -60,6 +60,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Query", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/demos/pet-demo/src/components/__isograph/Image/ImageDisplay/entrypoint.ts b/demos/pet-demo/src/components/__isograph/Image/ImageDisplay/entrypoint.ts index 5ee41a26..369e9373 100644 --- a/demos/pet-demo/src/components/__isograph/Image/ImageDisplay/entrypoint.ts +++ b/demos/pet-demo/src/components/__isograph/Image/ImageDisplay/entrypoint.ts @@ -60,6 +60,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Query", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/demos/pet-demo/src/components/__isograph/Mutation/SetTagline/entrypoint.ts b/demos/pet-demo/src/components/__isograph/Mutation/SetTagline/entrypoint.ts index 870cb754..054f5b22 100644 --- a/demos/pet-demo/src/components/__isograph/Mutation/SetTagline/entrypoint.ts +++ b/demos/pet-demo/src/components/__isograph/Mutation/SetTagline/entrypoint.ts @@ -56,6 +56,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Mutation", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/demos/pet-demo/src/components/__isograph/Pet/PetCheckinsCard/__refetch__0.ts b/demos/pet-demo/src/components/__isograph/Pet/PetCheckinsCard/__refetch__0.ts index fc80cf21..d97fd72b 100644 --- a/demos/pet-demo/src/components/__isograph/Pet/PetCheckinsCard/__refetch__0.ts +++ b/demos/pet-demo/src/components/__isograph/Pet/PetCheckinsCard/__refetch__0.ts @@ -68,6 +68,7 @@ const artifact: RefetchQueryNormalizationArtifact = { queryText, normalizationAst, }, + concreteType: "Mutation", }; export default artifact; diff --git a/demos/pet-demo/src/components/__isograph/Pet/PetCheckinsCard/entrypoint.ts b/demos/pet-demo/src/components/__isograph/Pet/PetCheckinsCard/entrypoint.ts index b76a4cd2..dfd7fc50 100644 --- a/demos/pet-demo/src/components/__isograph/Pet/PetCheckinsCard/entrypoint.ts +++ b/demos/pet-demo/src/components/__isograph/Pet/PetCheckinsCard/entrypoint.ts @@ -95,6 +95,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Query", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/demos/pet-demo/src/components/__isograph/Pet/PetCheckinsCardList/__refetch__0.ts b/demos/pet-demo/src/components/__isograph/Pet/PetCheckinsCardList/__refetch__0.ts index fc80cf21..d97fd72b 100644 --- a/demos/pet-demo/src/components/__isograph/Pet/PetCheckinsCardList/__refetch__0.ts +++ b/demos/pet-demo/src/components/__isograph/Pet/PetCheckinsCardList/__refetch__0.ts @@ -68,6 +68,7 @@ const artifact: RefetchQueryNormalizationArtifact = { queryText, normalizationAst, }, + concreteType: "Mutation", }; export default artifact; diff --git a/demos/pet-demo/src/components/__isograph/Pet/PetCheckinsCardList/entrypoint.ts b/demos/pet-demo/src/components/__isograph/Pet/PetCheckinsCardList/entrypoint.ts index 22010bf3..4de5ac26 100644 --- a/demos/pet-demo/src/components/__isograph/Pet/PetCheckinsCardList/entrypoint.ts +++ b/demos/pet-demo/src/components/__isograph/Pet/PetCheckinsCardList/entrypoint.ts @@ -95,6 +95,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Query", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/demos/pet-demo/src/components/__isograph/Query/HomeRoute/entrypoint.ts b/demos/pet-demo/src/components/__isograph/Query/HomeRoute/entrypoint.ts index 2cf08c5e..dcfe8ae2 100644 --- a/demos/pet-demo/src/components/__isograph/Query/HomeRoute/entrypoint.ts +++ b/demos/pet-demo/src/components/__isograph/Query/HomeRoute/entrypoint.ts @@ -53,6 +53,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Query", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/demos/pet-demo/src/components/__isograph/Query/Newsfeed/entrypoint.ts b/demos/pet-demo/src/components/__isograph/Query/Newsfeed/entrypoint.ts index 5fd0d9fe..d2414e40 100644 --- a/demos/pet-demo/src/components/__isograph/Query/Newsfeed/entrypoint.ts +++ b/demos/pet-demo/src/components/__isograph/Query/Newsfeed/entrypoint.ts @@ -135,6 +135,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Query", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/demos/pet-demo/src/components/__isograph/Query/PetByName/entrypoint.ts b/demos/pet-demo/src/components/__isograph/Query/PetByName/entrypoint.ts index d78e9e38..b70ba544 100644 --- a/demos/pet-demo/src/components/__isograph/Query/PetByName/entrypoint.ts +++ b/demos/pet-demo/src/components/__isograph/Query/PetByName/entrypoint.ts @@ -46,6 +46,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Query", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/demos/pet-demo/src/components/__isograph/Query/PetCheckinListRoute/__refetch__0.ts b/demos/pet-demo/src/components/__isograph/Query/PetCheckinListRoute/__refetch__0.ts index ac905210..b73c63ca 100644 --- a/demos/pet-demo/src/components/__isograph/Query/PetCheckinListRoute/__refetch__0.ts +++ b/demos/pet-demo/src/components/__isograph/Query/PetCheckinListRoute/__refetch__0.ts @@ -62,6 +62,7 @@ const artifact: RefetchQueryNormalizationArtifact = { queryText, normalizationAst, }, + concreteType: "Mutation", }; export default artifact; diff --git a/demos/pet-demo/src/components/__isograph/Query/PetCheckinListRoute/entrypoint.ts b/demos/pet-demo/src/components/__isograph/Query/PetCheckinListRoute/entrypoint.ts index 7bcc689a..2bc40039 100644 --- a/demos/pet-demo/src/components/__isograph/Query/PetCheckinListRoute/entrypoint.ts +++ b/demos/pet-demo/src/components/__isograph/Query/PetCheckinListRoute/entrypoint.ts @@ -81,6 +81,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Query", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/demos/pet-demo/src/components/__isograph/Query/PetDetailDeferredRoute/entrypoint.ts b/demos/pet-demo/src/components/__isograph/Query/PetDetailDeferredRoute/entrypoint.ts index 7ced35b1..15d46f99 100644 --- a/demos/pet-demo/src/components/__isograph/Query/PetDetailDeferredRoute/entrypoint.ts +++ b/demos/pet-demo/src/components/__isograph/Query/PetDetailDeferredRoute/entrypoint.ts @@ -46,6 +46,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Query", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/__refetch__0.ts b/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/__refetch__0.ts index 3d054031..52a40343 100644 --- a/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/__refetch__0.ts +++ b/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/__refetch__0.ts @@ -226,6 +226,7 @@ const artifact: RefetchQueryNormalizationArtifact = { queryText, normalizationAst, }, + concreteType: "Query", }; export default artifact; diff --git a/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/__refetch__1.ts b/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/__refetch__1.ts index be4702b6..512986f9 100644 --- a/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/__refetch__1.ts +++ b/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/__refetch__1.ts @@ -227,6 +227,7 @@ const artifact: RefetchQueryNormalizationArtifact = { queryText, normalizationAst, }, + concreteType: "Mutation", }; export default artifact; diff --git a/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/__refetch__2.ts b/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/__refetch__2.ts index 2861d357..f76fd47f 100644 --- a/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/__refetch__2.ts +++ b/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/__refetch__2.ts @@ -222,6 +222,7 @@ const artifact: RefetchQueryNormalizationArtifact = { queryText, normalizationAst, }, + concreteType: "Mutation", }; export default artifact; diff --git a/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/__refetch__3.ts b/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/__refetch__3.ts index d750639a..0c8c993b 100644 --- a/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/__refetch__3.ts +++ b/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/__refetch__3.ts @@ -68,6 +68,7 @@ const artifact: RefetchQueryNormalizationArtifact = { queryText, normalizationAst, }, + concreteType: "Mutation", }; export default artifact; diff --git a/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/__refetch__4.ts b/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/__refetch__4.ts index 92863be0..0dd86e03 100644 --- a/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/__refetch__4.ts +++ b/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/__refetch__4.ts @@ -72,6 +72,7 @@ const artifact: RefetchQueryNormalizationArtifact = { queryText, normalizationAst, }, + concreteType: "Query", }; export default artifact; diff --git a/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/entrypoint.ts b/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/entrypoint.ts index 004aab3a..d61e7803 100644 --- a/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/entrypoint.ts +++ b/demos/pet-demo/src/components/__isograph/Query/PetDetailRoute/entrypoint.ts @@ -231,6 +231,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Query", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/demos/pet-demo/src/components/__isograph/Query/PetFavoritePhrase/entrypoint.ts b/demos/pet-demo/src/components/__isograph/Query/PetFavoritePhrase/entrypoint.ts index 7f2dbca5..4aee60bb 100644 --- a/demos/pet-demo/src/components/__isograph/Query/PetFavoritePhrase/entrypoint.ts +++ b/demos/pet-demo/src/components/__isograph/Query/PetFavoritePhrase/entrypoint.ts @@ -52,6 +52,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Query", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/demos/pet-demo/src/components/__isograph/Viewer/NewsfeedPaginationComponent/entrypoint.ts b/demos/pet-demo/src/components/__isograph/Viewer/NewsfeedPaginationComponent/entrypoint.ts index d8451da6..c5d2f1aa 100644 --- a/demos/pet-demo/src/components/__isograph/Viewer/NewsfeedPaginationComponent/entrypoint.ts +++ b/demos/pet-demo/src/components/__isograph/Viewer/NewsfeedPaginationComponent/entrypoint.ts @@ -154,6 +154,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Query", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/demos/vite-demo/src/components/__isograph/Query/HomePage/entrypoint.ts b/demos/vite-demo/src/components/__isograph/Query/HomePage/entrypoint.ts index b6724ac3..7667834a 100644 --- a/demos/vite-demo/src/components/__isograph/Query/HomePage/entrypoint.ts +++ b/demos/vite-demo/src/components/__isograph/Query/HomePage/entrypoint.ts @@ -75,6 +75,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Query", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/libs/isograph-react/schema.graphql b/libs/isograph-react/schema.graphql index 6dcfe04a..dea0c9f0 100644 --- a/libs/isograph-react/schema.graphql +++ b/libs/isograph-react/schema.graphql @@ -2,6 +2,7 @@ type Query { me: Economist! you: Economist! node(id: ID!): Node + query: Query! } type Economist implements Node { diff --git a/libs/isograph-react/src/core/FragmentReference.ts b/libs/isograph-react/src/core/FragmentReference.ts index 5254350f..11153553 100644 --- a/libs/isograph-react/src/core/FragmentReference.ts +++ b/libs/isograph-react/src/core/FragmentReference.ts @@ -35,7 +35,7 @@ export type FragmentReference< export function stableIdForFragmentReference( fragmentReference: FragmentReference, ): string { - return `${fragmentReference.root.__link}/TODO_FRAGMENT_NAME/${serializeVariables(fragmentReference.variables ?? {})}`; + return `${fragmentReference.root.__typename}/${fragmentReference.root.__link}/TODO_FRAGMENT_NAME/${serializeVariables(fragmentReference.variables ?? {})}`; } function serializeVariables(variables: Variables) { diff --git a/libs/isograph-react/src/core/IsographEnvironment.ts b/libs/isograph-react/src/core/IsographEnvironment.ts index 84ef715c..a8f266c2 100644 --- a/libs/isograph-react/src/core/IsographEnvironment.ts +++ b/libs/isograph-react/src/core/IsographEnvironment.ts @@ -82,6 +82,7 @@ export type IsographNetworkFunction = ( export type Link = { readonly __link: DataId; + readonly __typename: TypeName; }; export type DataTypeValue = @@ -111,8 +112,12 @@ export type DataId = string; export const ROOT_ID: DataId & '__ROOT' = '__ROOT'; export type IsographStore = { - [index: DataId]: StoreRecord | null; - readonly __ROOT: StoreRecord; + [index: TypeName]: { + [index: DataId]: StoreRecord | null; + } | null; + readonly Query: { + readonly __ROOT: StoreRecord; + }; }; const DEFAULT_GC_BUFFER_SIZE = 10; @@ -139,29 +144,12 @@ export function createIsographEnvironment( export function createIsographStore(): IsographStore { return { - [ROOT_ID]: {}, + Query: { + [ROOT_ID]: {}, + }, }; } -export function defaultMissingFieldHandler( - _storeRecord: StoreRecord, - _root: Link, - fieldName: string, - arguments_: { [index: string]: any } | null, - variables: Variables | null, -): Link | undefined { - if (fieldName === 'node' || fieldName === 'user') { - const variable = arguments_?.['id']; - const value = variables?.[variable]; - - // TODO can we handle explicit nulls here too? Probably, after wrapping in objects - if (value != null) { - // @ts-expect-error - return { __link: value }; - } - } -} - export function assertLink(link: DataTypeValue): Link | null | undefined { if (Array.isArray(link)) { throw new Error('Unexpected array'); @@ -179,10 +167,12 @@ export function getLink(maybeLink: DataTypeValue): Link | null { if ( maybeLink != null && typeof maybeLink === 'object' && - // @ts-expect-error this is safe - maybeLink.__link != null + '__link' in maybeLink && + maybeLink.__link != null && + '__typename' in maybeLink && + maybeLink.__typename != null ) { - return maybeLink as any; + return maybeLink; } return null; } diff --git a/libs/isograph-react/src/core/cache.ts b/libs/isograph-react/src/core/cache.ts index a6e3ec86..2e796c25 100644 --- a/libs/isograph-react/src/core/cache.ts +++ b/libs/isograph-react/src/core/cache.ts @@ -12,6 +12,7 @@ import { DataTypeValue, getLink, FragmentSubscription, + type TypeName, } from './IsographEnvironment'; import { IsographEntrypoint, @@ -35,7 +36,7 @@ import { wrapResolvedValue } from './PromiseWrapper'; import { logMessage } from './logging'; import { DEFAULT_SHOULD_FETCH_VALUE, FetchOptions } from './check'; -const TYPENAME_FIELD_NAME = '__typename'; +export const TYPENAME_FIELD_NAME = '__typename'; export function getOrCreateItemInSuspenseCache< TReadFromStore extends { parameters: object; data: object }, @@ -113,7 +114,7 @@ export function getOrCreateCacheForArtifact< nestedRefetchQueries: entrypoint.readerWithRefetchQueries.nestedRefetchQueries, }), - root: { __link: ROOT_ID }, + root: { __link: ROOT_ID, __typename: entrypoint.concreteType }, variables, networkRequest: networkRequest, }, @@ -137,6 +138,7 @@ export type NetworkResponseObject = { // undefined should not *actually* be present in the network response. [index: string]: undefined | NetworkResponseValue; id?: DataId; + __typename?: TypeName; }; export function normalizeData( @@ -145,8 +147,9 @@ export function normalizeData( networkResponse: NetworkResponseObject, variables: Variables, nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[], -): Set { - const encounteredIds = new Set(); + root: Link, +): EncounteredIds { + const encounteredIds: EncounteredIds = new Map(); logMessage(environment, { kind: 'AboutToNormalize', @@ -154,13 +157,17 @@ export function normalizeData( networkResponse, variables, }); + + const recordsById = (environment.store[root.__typename] ??= {}); + const newStoreRecord = (recordsById[root.__link] ??= {}); + normalizeDataIntoRecord( environment, normalizationAst, networkResponse, - environment.store.__ROOT, - { __link: ROOT_ID }, - variables as any, + newStoreRecord, + root, + variables, nestedRefetchQueries, encounteredIds, ); @@ -255,7 +262,7 @@ function withErrorHandling(f: (t: T) => void): (t: T) => void { function callSubscriptions( environment: IsographEnvironment, - recordsEncounteredWhenNormalizing: Set, + recordsEncounteredWhenNormalizing: EncounteredIds, ) { environment.subscriptions.forEach( withErrorHandling((subscription) => { @@ -318,9 +325,9 @@ function callSubscriptions( } case 'AnyChangesToRecord': { if ( - recordsEncounteredWhenNormalizing.has( - subscription.recordLink.__link, - ) + recordsEncounteredWhenNormalizing + .get(subscription.recordLink.__typename) + ?.has(subscription.recordLink.__link) ) { subscription.callback(); } @@ -337,7 +344,25 @@ function callSubscriptions( ); } -function hasOverlappingIds(set1: Set, set2: Set): boolean { +function hasOverlappingIds( + ids1: EncounteredIds, + ids2: EncounteredIds, +): boolean { + for (const [typeName, set1] of ids1.entries()) { + const set2 = ids2.get(typeName); + if (set2 === undefined) { + continue; + } + + if (isNotDisjointFrom(set1, set2)) { + return true; + } + } + return false; +} + +// TODO use a polyfill library +function isNotDisjointFrom(set1: Set, set2: Set): boolean { for (const id of set1) { if (set2.has(id)) { return true; @@ -346,6 +371,7 @@ function hasOverlappingIds(set1: Set, set2: Set): boolean { return false; } +export type EncounteredIds = Map>; /** * Mutate targetParentRecord according to the normalizationAst and networkResponseParentRecord. */ @@ -357,7 +383,7 @@ function normalizeDataIntoRecord( targetParentRecordLink: Link, variables: Variables, nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[], - mutableEncounteredIds: Set, + mutableEncounteredIds: EncounteredIds, ): RecordHasBeenUpdated { let recordHasBeenUpdated = false; for (const normalizationNode of normalizationAst) { @@ -412,11 +438,25 @@ function normalizeDataIntoRecord( } } if (recordHasBeenUpdated) { - mutableEncounteredIds.add(targetParentRecordLink.__link); + let encounteredRecordsIds = insertIfNotExists( + mutableEncounteredIds, + targetParentRecordLink.__typename, + ); + + encounteredRecordsIds.add(targetParentRecordLink.__link); } return recordHasBeenUpdated; } +export function insertIfNotExists(map: Map>, key: K) { + let result = map.get(key); + if (result === undefined) { + result = new Set(); + map.set(key, result); + } + return result; +} + type RecordHasBeenUpdated = boolean; function normalizeScalarField( astNode: NormalizationScalarField, @@ -451,7 +491,7 @@ function normalizeLinkedField( targetParentRecordLink: Link, variables: Variables, nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[], - mutableEncounteredIds: Set, + mutableEncounteredIds: EncounteredIds, ): RecordHasBeenUpdated { const networkResponseKey = getNetworkResponseKey(astNode); const networkResponseData = networkResponseParentRecord[networkResponseKey]; @@ -492,7 +532,18 @@ function normalizeLinkedField( mutableEncounteredIds, ); - dataIds.push({ __link: newStoreRecordId }); + const __typename = + astNode.concreteType ?? networkResponseObject[TYPENAME_FIELD_NAME]; + if (__typename == null) { + throw new Error( + 'Unexpected missing __typename in network response when normalizing a linked field. ' + + 'This is indicative of a bug in Isograph.', + ); + } + dataIds.push({ + __link: newStoreRecordId, + __typename, + }); } targetParentRecord[parentRecordKey] = dataIds; return !dataIdsAreTheSame(existingValue, dataIds); @@ -508,11 +559,23 @@ function normalizeLinkedField( mutableEncounteredIds, ); + let __typename = + astNode.concreteType ?? networkResponseData[TYPENAME_FIELD_NAME]; + + if (__typename == null) { + throw new Error( + 'Unexpected missing __typename in network response when normalizing a linked field. ' + + 'This is indicative of a bug in Isograph.', + ); + } + targetParentRecord[parentRecordKey] = { __link: newStoreRecordId, + __typename, }; + const link = getLink(existingValue); - return link?.__link !== newStoreRecordId; + return link?.__link !== newStoreRecordId || link.__typename !== __typename; } } @@ -527,7 +590,7 @@ function normalizeInlineFragment( targetParentRecordLink: Link, variables: Variables, nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[], - mutableEncounteredIds: Set, + mutableEncounteredIds: EncounteredIds, ): RecordHasBeenUpdated { const typeToRefineTo = astNode.type; if (networkResponseParentRecord[TYPENAME_FIELD_NAME] === typeToRefineTo) { @@ -556,7 +619,10 @@ function dataIdsAreTheSame( } for (let i = 0; i < newDataIds.length; i++) { const maybeLink = getLink(existingValue[i]); - if (newDataIds[i]?.__link !== maybeLink?.__link) { + if ( + newDataIds[i]?.__link !== maybeLink?.__link || + newDataIds[i]?.__typename !== maybeLink?.__typename + ) { return false; } } @@ -574,7 +640,7 @@ function normalizeNetworkResponseObject( variables: Variables, index: number | null, nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[], - mutableEncounteredIds: Set, + mutableEncounteredIds: EncounteredIds, ): DataId /* The id of the modified or newly created item */ { const newStoreRecordId = getDataIdOfNetworkResponse( targetParentRecordLink, @@ -583,16 +649,25 @@ function normalizeNetworkResponseObject( variables, index, ); + const __typename = + astNode.concreteType ?? networkResponseData[TYPENAME_FIELD_NAME]; - const newStoreRecord = environment.store[newStoreRecordId] ?? {}; - environment.store[newStoreRecordId] = newStoreRecord; + if (__typename == null) { + throw new Error( + 'Unexpected missing __typename in network response object. ' + + 'This is indicative of a bug in Isograph.', + ); + } + + const recordsById = (environment.store[__typename] ??= {}); + const newStoreRecord = (recordsById[newStoreRecordId] ??= {}); normalizeDataIntoRecord( environment, astNode.selections, networkResponseData, newStoreRecord, - { __link: newStoreRecordId }, + { __link: newStoreRecordId, __typename: __typename }, variables, nestedRefetchQueries, mutableEncounteredIds, @@ -726,10 +801,16 @@ export const SECOND_SPLIT_KEY = '___'; function getDataIdOfNetworkResponse( parentRecordLink: Link, dataToNormalize: NetworkResponseObject, - astNode: NormalizationLinkedField | NormalizationScalarField, + astNode: NormalizationLinkedField, variables: Variables, index: number | null, ): DataId { + // If we are dealing with nested Query, use __ROOT as id + // TODO do not hard code this value here + if (astNode.concreteType === 'Query') { + return ROOT_ID; + } + // Check whether the dataToNormalize has an id field. If so, that is the key. // If not, we construct an id from the parentRecordId and the field parameters. @@ -738,7 +819,7 @@ function getDataIdOfNetworkResponse( return dataId; } - let storeKey = `${parentRecordLink.__link}.${astNode.fieldName}`; + let storeKey = `${parentRecordLink.__typename}:${parentRecordLink.__link}.${astNode.fieldName}`; if (index != null) { storeKey += `.${index}`; } diff --git a/libs/isograph-react/src/core/check.ts b/libs/isograph-react/src/core/check.ts index e43c55ae..24d5234d 100644 --- a/libs/isograph-react/src/core/check.ts +++ b/libs/isograph-react/src/core/check.ts @@ -5,7 +5,6 @@ import { getLink, IsographEnvironment, Link, - ROOT_ID, StoreRecord, } from './IsographEnvironment'; import { logMessage } from './logging'; @@ -31,13 +30,17 @@ export function check( environment: IsographEnvironment, normalizationAst: NormalizationAst, variables: Variables, + root: Link, ): CheckResult { + const recordsById = (environment.store[root.__typename] ??= {}); + const newStoreRecord = (recordsById[root.__link] ??= {}); + const checkResult = checkFromRecord( environment, normalizationAst, variables, - environment.store[ROOT_ID], - { __link: ROOT_ID }, + newStoreRecord, + root, ); logMessage(environment, { kind: 'EnvironmentCheck', @@ -97,7 +100,8 @@ function checkFromRecord( ); } - const linkedRecord = environment.store[link.__link]; + const linkedRecord = + environment.store[link.__typename]?.[link.__link]; if (linkedRecord === undefined) { return { @@ -130,7 +134,8 @@ function checkFromRecord( ); } - const linkedRecord = environment.store[link.__link]; + const linkedRecord = + environment.store[link.__typename]?.[link.__link]; if (linkedRecord === undefined) { return { diff --git a/libs/isograph-react/src/core/entrypoint.ts b/libs/isograph-react/src/core/entrypoint.ts index 6f612674..694fc728 100644 --- a/libs/isograph-react/src/core/entrypoint.ts +++ b/libs/isograph-react/src/core/entrypoint.ts @@ -32,6 +32,7 @@ export type IsographEntrypoint< TReadFromStore, TClientFieldValue >; + readonly concreteType: TypeName; }; export type IsographEntrypointLoader< @@ -75,6 +76,7 @@ export type NormalizationInlineFragment = { export type RefetchQueryNormalizationArtifact = { readonly kind: 'RefetchQuery'; readonly networkRequestInfo: NetworkRequestInfo; + readonly concreteType: TypeName; }; // TODO rename diff --git a/libs/isograph-react/src/core/garbageCollection.ts b/libs/isograph-react/src/core/garbageCollection.ts index 3eb9951a..1442a179 100644 --- a/libs/isograph-react/src/core/garbageCollection.ts +++ b/libs/isograph-react/src/core/garbageCollection.ts @@ -3,9 +3,10 @@ import { DataId, IsographEnvironment, IsographStore, - ROOT_ID, StoreRecord, assertLink, + type Link, + type TypeName, } from './IsographEnvironment'; import { getParentRecordKey } from './cache'; import { NormalizationAst } from './entrypoint'; @@ -13,6 +14,7 @@ import { NormalizationAst } from './entrypoint'; export type RetainedQuery = { readonly normalizationAst: NormalizationAst; readonly variables: {}; + readonly root: Link; }; type DidUnretainSomeQuery = boolean; @@ -42,7 +44,7 @@ export function retainQuery( } export function garbageCollectEnvironment(environment: IsographEnvironment) { - const retainedIds = new Set([ROOT_ID]); + const retainedIds: RetainedIds = {}; for (const query of environment.retainedQueries) { recordReachableIds(environment.store, query, retainedIds); @@ -51,31 +53,61 @@ export function garbageCollectEnvironment(environment: IsographEnvironment) { recordReachableIds(environment.store, query, retainedIds); } - for (const dataId in environment.store) { - if (!retainedIds.has(dataId)) { - delete environment.store[dataId]; + for (const typeName in environment.store) { + const dataById = environment.store[typeName]; + if (dataById == null) continue; + const retainedTypeIds = retainedIds[typeName]; + + // delete all objects + if (retainedTypeIds == undefined || retainedTypeIds.size == 0) { + delete environment.store[typeName]; + continue; + } + + for (const dataId in dataById) { + if (!retainedTypeIds.has(dataId)) { + delete dataById[dataId]; + } + } + + if (Object.keys(dataById).length === 0) { + delete environment.store[typeName]; } } } +interface RetainedIds { + [typeName: TypeName]: Set; +} + function recordReachableIds( store: IsographStore, retainedQuery: RetainedQuery, - mutableRetainedIds: Set, + mutableRetainedIds: RetainedIds, ) { - recordReachableIdsFromRecord( - store, - store[ROOT_ID], - mutableRetainedIds, - retainedQuery.normalizationAst, - retainedQuery.variables, - ); + const record = + store[retainedQuery.root.__typename]?.[retainedQuery.root.__link]; + + const retainedRecordsIds = (mutableRetainedIds[ + retainedQuery.root.__typename + ] ??= new Set()); + retainedRecordsIds.add(retainedQuery.root.__link); + + if (record) { + recordReachableIdsFromRecord( + store, + record, + mutableRetainedIds, + retainedQuery.normalizationAst, + retainedQuery.variables, + ); + } } function recordReachableIdsFromRecord( store: IsographStore, currentRecord: StoreRecord, - mutableRetainedIds: Set, + mutableRetainedIds: RetainedIds, selections: NormalizationAst, variables: Variables | null, ) { @@ -85,25 +117,44 @@ function recordReachableIdsFromRecord( const linkKey = getParentRecordKey(selection, variables ?? {}); const linkedFieldOrFields = currentRecord[linkKey]; - const ids = []; + const links: Link[] = []; if (Array.isArray(linkedFieldOrFields)) { for (const maybeLink of linkedFieldOrFields) { const link = assertLink(maybeLink); if (link != null) { - ids.push(link.__link); + links.push(link); } } } else { const link = assertLink(linkedFieldOrFields); if (link != null) { - ids.push(link.__link); + links.push(link); } } - for (const nextRecordId of ids) { - const nextRecord = store[nextRecordId]; + let typeStore = + selection.concreteType !== null + ? store[selection.concreteType] + : null; + + if (typeStore == null && selection.concreteType !== null) { + continue; + } + + for (const nextRecordLink of links) { + let __typename = nextRecordLink.__typename; + + const resolvedTypeStore = typeStore ?? store[__typename]; + + if (resolvedTypeStore == null) { + continue; + } + + const nextRecord = resolvedTypeStore[nextRecordLink.__link]; if (nextRecord != null) { - mutableRetainedIds.add(nextRecordId); + const retainedRecordsIds = (mutableRetainedIds[__typename] ??= + new Set()); + retainedRecordsIds.add(nextRecordLink.__link); recordReachableIdsFromRecord( store, nextRecord, diff --git a/libs/isograph-react/src/core/logging.ts b/libs/isograph-react/src/core/logging.ts index 2f587544..379a2609 100644 --- a/libs/isograph-react/src/core/logging.ts +++ b/libs/isograph-react/src/core/logging.ts @@ -11,7 +11,7 @@ import { RefetchQueryNormalizationArtifact, } from './entrypoint'; import { FragmentReference, Variables } from './FragmentReference'; -import { NetworkResponseObject } from './cache'; +import { NetworkResponseObject, type EncounteredIds } from './cache'; import { Arguments } from './util'; import { ReadDataResult } from './read'; import { CheckResult } from './check'; @@ -32,7 +32,7 @@ export type LogMessage = | { kind: 'AfterNormalization'; store: IsographStore; - encounteredIds: Set; + encounteredIds: EncounteredIds; } | { kind: 'DeepEqualityCheck'; diff --git a/libs/isograph-react/src/core/makeNetworkRequest.ts b/libs/isograph-react/src/core/makeNetworkRequest.ts index a25dd05a..200ead4f 100644 --- a/libs/isograph-react/src/core/makeNetworkRequest.ts +++ b/libs/isograph-react/src/core/makeNetworkRequest.ts @@ -10,7 +10,7 @@ import { retainQuery, unretainQuery, } from './garbageCollection'; -import { IsographEnvironment } from './IsographEnvironment'; +import { IsographEnvironment, ROOT_ID } from './IsographEnvironment'; import { AnyError, PromiseWrapper, @@ -41,6 +41,10 @@ export function maybeMakeNetworkRequest( environment, artifact.networkRequestInfo.normalizationAst, variables, + { + __link: ROOT_ID, + __typename: artifact.concreteType, + }, ); if (result.kind === 'EnoughData') { return [wrapResolvedValue(undefined), () => {}]; @@ -88,6 +92,7 @@ export function makeNetworkRequest( } if (status.kind === 'UndisposedIncomplete') { + const root = { __link: ROOT_ID, __typename: artifact.concreteType }; normalizeData( environment, artifact.networkRequestInfo.normalizationAst, @@ -96,10 +101,12 @@ export function makeNetworkRequest( artifact.kind === 'Entrypoint' ? artifact.readerWithRefetchQueries.nestedRefetchQueries : [], + root, ); const retainedQuery = { normalizationAst: artifact.networkRequestInfo.normalizationAst, variables, + root, }; status = { kind: 'UndisposedComplete', diff --git a/libs/isograph-react/src/core/read.ts b/libs/isograph-react/src/core/read.ts index 2043fb81..b553db2a 100644 --- a/libs/isograph-react/src/core/read.ts +++ b/libs/isograph-react/src/core/read.ts @@ -1,4 +1,9 @@ -import { getParentRecordKey, onNextChangeToRecord } from './cache'; +import { + getParentRecordKey, + insertIfNotExists, + onNextChangeToRecord, + type EncounteredIds, +} from './cache'; import { getOrCreateCachedComponent } from './componentCache'; import { IsographEntrypoint, @@ -12,8 +17,6 @@ import { } from './FragmentReference'; import { assertLink, - DataId, - defaultMissingFieldHandler, getOrLoadIsographArtifact, IsographEnvironment, type Link, @@ -33,7 +36,7 @@ import { CleanupFn } from '@isograph/disposable-types'; import { DEFAULT_SHOULD_FETCH_VALUE, FetchOptions } from './check'; export type WithEncounteredRecords = { - readonly encounteredRecords: Set; + readonly encounteredRecords: EncounteredIds; readonly item: ExtractData; }; @@ -44,7 +47,7 @@ export function readButDoNotEvaluate< fragmentReference: FragmentReference, networkRequestOptions: NetworkRequestReaderOptions, ): WithEncounteredRecords { - const mutableEncounteredRecords = new Set(); + const mutableEncounteredRecords: EncounteredIds = new Map(); const readerWithRefetchQueries = readPromise( fragmentReference.readerWithRefetchQueries, @@ -99,7 +102,7 @@ export type ReadDataResult = | { readonly kind: 'Success'; readonly data: ExtractData; - readonly encounteredRecords: Set; + readonly encounteredRecords: EncounteredIds; } | { readonly kind: 'MissingData'; @@ -116,10 +119,14 @@ function readData( nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[], networkRequest: PromiseWrapper, networkRequestOptions: NetworkRequestReaderOptions, - mutableEncounteredRecords: Set, + mutableEncounteredRecords: EncounteredIds, ): ReadDataResult { - mutableEncounteredRecords.add(root.__link); - let storeRecord = environment.store[root.__link]; + const encounteredIds = insertIfNotExists( + mutableEncounteredRecords, + root.__typename, + ); + encounteredIds.add(root.__link); + let storeRecord = environment.store[root.__typename]?.[root.__link]; if (storeRecord === undefined) { return { kind: 'MissingData', @@ -179,6 +186,7 @@ function readData( results.push(null); continue; } + const result = readData( environment, field.selections, @@ -248,10 +256,9 @@ function readData( if (link === undefined) { // TODO make this configurable, and also generated and derived from the schema - const missingFieldHandler = - environment.missingFieldHandler ?? defaultMissingFieldHandler; + const missingFieldHandler = environment.missingFieldHandler; - const altLink = missingFieldHandler( + const altLink = missingFieldHandler?.( storeRecord, root, field.fieldName, @@ -464,7 +471,9 @@ function readData( return [ // Stable id - root.__link + + root.__typename + + ':' + + root.__link + '/' + field.name + '/' + @@ -496,7 +505,7 @@ function readData( } as const), // TODO localVariables is not guaranteed to have an id field - root: { __link: localVariables.id }, + root, variables: localVariables, networkRequest, }; @@ -569,7 +578,7 @@ function readData( ), // TODO localVariables is not guaranteed to have an id field - root: { __link: localVariables.id }, + root, variables: localVariables, networkRequest, }; @@ -693,7 +702,7 @@ export function getNetworkRequestOptionsWithDefaults( // TODO call stableStringifyArgs on the variable values, as well. // This doesn't matter for now, since we are just using primitive values // in the demo. -function stableStringifyArgs(args: Object) { +function stableStringifyArgs(args: object) { const keys = Object.keys(args); keys.sort(); let s = ''; diff --git a/libs/isograph-react/src/index.ts b/libs/isograph-react/src/index.ts index 461b07c8..8eb7f91c 100644 --- a/libs/isograph-react/src/index.ts +++ b/libs/isograph-react/src/index.ts @@ -24,7 +24,6 @@ export { type StoreRecord, createIsographEnvironment, createIsographStore, - defaultMissingFieldHandler, } from './core/IsographEnvironment'; export { type EagerReaderArtifact, diff --git a/libs/isograph-react/src/react/useImperativeReference.ts b/libs/isograph-react/src/react/useImperativeReference.ts index bf18600d..d12bee69 100644 --- a/libs/isograph-react/src/react/useImperativeReference.ts +++ b/libs/isograph-react/src/react/useImperativeReference.ts @@ -57,7 +57,7 @@ export function useImperativeReference< nestedRefetchQueries: entrypoint.readerWithRefetchQueries.nestedRefetchQueries, }), - root: { __link: ROOT_ID }, + root: { __link: ROOT_ID, __typename: entrypoint.concreteType }, variables, networkRequest, }, diff --git a/libs/isograph-react/src/tests/__isograph/Query/meName/entrypoint.ts b/libs/isograph-react/src/tests/__isograph/Query/meName/entrypoint.ts index 93a09010..81294fca 100644 --- a/libs/isograph-react/src/tests/__isograph/Query/meName/entrypoint.ts +++ b/libs/isograph-react/src/tests/__isograph/Query/meName/entrypoint.ts @@ -41,6 +41,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Query", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/libs/isograph-react/src/tests/__isograph/Query/meNameSuccessor/entrypoint.ts b/libs/isograph-react/src/tests/__isograph/Query/meNameSuccessor/entrypoint.ts index 9a264dfc..c1262b19 100644 --- a/libs/isograph-react/src/tests/__isograph/Query/meNameSuccessor/entrypoint.ts +++ b/libs/isograph-react/src/tests/__isograph/Query/meNameSuccessor/entrypoint.ts @@ -79,6 +79,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Query", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/libs/isograph-react/src/tests/__isograph/Query/nodeField/entrypoint.ts b/libs/isograph-react/src/tests/__isograph/Query/nodeField/entrypoint.ts index 1ec242d9..083b2d32 100644 --- a/libs/isograph-react/src/tests/__isograph/Query/nodeField/entrypoint.ts +++ b/libs/isograph-react/src/tests/__isograph/Query/nodeField/entrypoint.ts @@ -46,6 +46,7 @@ const artifact: IsographEntrypoint< queryText, normalizationAst, }, + concreteType: "Query", readerWithRefetchQueries: { kind: "ReaderWithRefetchQueries", nestedRefetchQueries, diff --git a/libs/isograph-react/src/tests/__isograph/Query/subquery/entrypoint.ts b/libs/isograph-react/src/tests/__isograph/Query/subquery/entrypoint.ts new file mode 100644 index 00000000..e22e746b --- /dev/null +++ b/libs/isograph-react/src/tests/__isograph/Query/subquery/entrypoint.ts @@ -0,0 +1,67 @@ +import type {IsographEntrypoint, NormalizationAst, RefetchQueryNormalizationArtifactWrapper} from '@isograph/react'; +import {Query__subquery__param} from './param_type'; +import {Query__subquery__output_type} from './output_type'; +import readerResolver from './resolver_reader'; +const nestedRefetchQueries: RefetchQueryNormalizationArtifactWrapper[] = []; + +const queryText = 'query subquery ($id: ID!) {\ + query {\ + node____id___v_id: node(id: $id) {\ + __typename,\ + id,\ + },\ + },\ +}'; + +const normalizationAst: NormalizationAst = [ + { + kind: "Linked", + fieldName: "query", + arguments: null, + concreteType: "Query", + selections: [ + { + kind: "Linked", + fieldName: "node", + arguments: [ + [ + "id", + { kind: "Variable", name: "id" }, + ], + ], + concreteType: null, + selections: [ + { + kind: "Scalar", + fieldName: "__typename", + arguments: null, + }, + { + kind: "Scalar", + fieldName: "id", + arguments: null, + }, + ], + }, + ], + }, +]; +const artifact: IsographEntrypoint< + Query__subquery__param, + Query__subquery__output_type +> = { + kind: "Entrypoint", + networkRequestInfo: { + kind: "NetworkRequestInfo", + queryText, + normalizationAst, + }, + concreteType: "Query", + readerWithRefetchQueries: { + kind: "ReaderWithRefetchQueries", + nestedRefetchQueries, + readerArtifact: readerResolver, + }, +}; + +export default artifact; diff --git a/libs/isograph-react/src/tests/__isograph/Query/subquery/output_type.ts b/libs/isograph-react/src/tests/__isograph/Query/subquery/output_type.ts new file mode 100644 index 00000000..caadfd26 --- /dev/null +++ b/libs/isograph-react/src/tests/__isograph/Query/subquery/output_type.ts @@ -0,0 +1,3 @@ +import type React from 'react'; +import { subquery as resolver } from '../../../normalizeData.test'; +export type Query__subquery__output_type = ReturnType; \ No newline at end of file diff --git a/libs/isograph-react/src/tests/__isograph/Query/subquery/param_type.ts b/libs/isograph-react/src/tests/__isograph/Query/subquery/param_type.ts new file mode 100644 index 00000000..c4eba80a --- /dev/null +++ b/libs/isograph-react/src/tests/__isograph/Query/subquery/param_type.ts @@ -0,0 +1,12 @@ +import type { Query__subquery__parameters } from './parameters_type'; + +export type Query__subquery__param = { + readonly data: { + readonly query: { + readonly node: ({ + readonly id: string, + } | null), + }, + }, + readonly parameters: Query__subquery__parameters, +}; diff --git a/libs/isograph-react/src/tests/__isograph/Query/subquery/parameters_type.ts b/libs/isograph-react/src/tests/__isograph/Query/subquery/parameters_type.ts new file mode 100644 index 00000000..4708ec4c --- /dev/null +++ b/libs/isograph-react/src/tests/__isograph/Query/subquery/parameters_type.ts @@ -0,0 +1,3 @@ +export type Query__subquery__parameters = { + readonly id: string, +}; diff --git a/libs/isograph-react/src/tests/__isograph/Query/subquery/resolver_reader.ts b/libs/isograph-react/src/tests/__isograph/Query/subquery/resolver_reader.ts new file mode 100644 index 00000000..b60771a5 --- /dev/null +++ b/libs/isograph-react/src/tests/__isograph/Query/subquery/resolver_reader.ts @@ -0,0 +1,47 @@ +import type { EagerReaderArtifact, ReaderAst } from '@isograph/react'; +import { Query__subquery__param } from './param_type'; +import { Query__subquery__output_type } from './output_type'; +import { subquery as resolver } from '../../../normalizeData.test'; + +const readerAst: ReaderAst = [ + { + kind: "Linked", + fieldName: "query", + alias: null, + arguments: null, + condition: null, + selections: [ + { + kind: "Linked", + fieldName: "node", + alias: null, + arguments: [ + [ + "id", + { kind: "Variable", name: "id" }, + ], + ], + condition: null, + selections: [ + { + kind: "Scalar", + fieldName: "id", + alias: null, + arguments: null, + }, + ], + }, + ], + }, +]; + +const artifact: EagerReaderArtifact< + Query__subquery__param, + Query__subquery__output_type +> = { + kind: "EagerReaderArtifact", + resolver, + readerAst, +}; + +export default artifact; diff --git a/libs/isograph-react/src/tests/__isograph/iso.ts b/libs/isograph-react/src/tests/__isograph/iso.ts index c605c031..62eafeb9 100644 --- a/libs/isograph-react/src/tests/__isograph/iso.ts +++ b/libs/isograph-react/src/tests/__isograph/iso.ts @@ -2,9 +2,11 @@ import type { IsographEntrypoint } from '@isograph/react'; import { type Query__meNameSuccessor__param } from './Query/meNameSuccessor/param_type'; import { type Query__meName__param } from './Query/meName/param_type'; import { type Query__nodeField__param } from './Query/nodeField/param_type'; +import { type Query__subquery__param } from './Query/subquery/param_type'; import entrypoint_Query__meNameSuccessor from '../__isograph/Query/meNameSuccessor/entrypoint'; import entrypoint_Query__meName from '../__isograph/Query/meName/entrypoint'; import entrypoint_Query__nodeField from '../__isograph/Query/nodeField/entrypoint'; +import entrypoint_Query__subquery from '../__isograph/Query/subquery/entrypoint'; // This is the type given to regular client fields. // This means that the type of the exported iso literal is exactly @@ -66,6 +68,10 @@ export function iso( param: T & MatchesWhitespaceAndString<'field Query.nodeField', T> ): IdentityWithParam; +export function iso( + param: T & MatchesWhitespaceAndString<'field Query.subquery', T> +): IdentityWithParam; + export function iso( param: T & MatchesWhitespaceAndString<'entrypoint Query.meNameSuccessor', T> ): typeof entrypoint_Query__meNameSuccessor; @@ -78,6 +84,10 @@ export function iso( param: T & MatchesWhitespaceAndString<'entrypoint Query.nodeField', T> ): typeof entrypoint_Query__nodeField; +export function iso( + param: T & MatchesWhitespaceAndString<'entrypoint Query.subquery', T> +): typeof entrypoint_Query__subquery; + export function iso(_isographLiteralText: string): | IdentityWithParam | IdentityWithParamComponent diff --git a/libs/isograph-react/src/tests/garbageCollection.test.ts b/libs/isograph-react/src/tests/garbageCollection.test.ts index 44cbb889..ba6e9cfd 100644 --- a/libs/isograph-react/src/tests/garbageCollection.test.ts +++ b/libs/isograph-react/src/tests/garbageCollection.test.ts @@ -2,6 +2,7 @@ import { describe, test, expect } from 'vitest'; import { ROOT_ID, createIsographEnvironment, + type IsographStore, } from '../core/IsographEnvironment'; import { garbageCollectEnvironment, @@ -10,32 +11,37 @@ import { import { iso } from './__isograph/iso'; import { nodeFieldRetainedQuery } from './nodeQuery'; -const getDefaultStore = () => ({ - [ROOT_ID]: { - me: { __link: '0' }, - you: { __link: '1' }, - node____id___0: { - __link: '0', +const getDefaultStore = (): IsographStore => ({ + Query: { + [ROOT_ID]: { + me: { __link: '0', __typename: 'Economist' }, + you: { __link: '1', __typename: 'Economist' }, + node____id___0: { + __link: '0', + __typename: 'Economist', + }, }, }, - 0: { - __typename: 'Economist', - id: '0', - name: 'Jeremy Bentham', - successor: { __link: '1' }, - }, - 1: { - __typename: 'Economist', - id: '1', - name: 'John Stuart Mill', - predecessor: { __link: '0' }, - successor: { __link: '2' }, - }, - 2: { - __typename: 'Economist', - id: '2', - name: 'Henry Sidgwick', - predecessor: { __link: '1' }, + Economist: { + 0: { + __typename: 'Economist', + id: '0', + name: 'Jeremy Bentham', + successor: { __link: '1', __typename: 'Economist' }, + }, + 1: { + __typename: 'Economist', + id: '1', + name: 'John Stuart Mill', + predecessor: { __link: '0', __typename: 'Economist' }, + successor: { __link: '2', __typename: 'Economist' }, + }, + 2: { + __typename: 'Economist', + id: '2', + name: 'Henry Sidgwick', + predecessor: { __link: '1', __typename: 'Economist' }, + }, }, }); @@ -51,6 +57,7 @@ const meNameEntrypoint = iso(`entrypoint Query.meName`); const meNameRetainedQuery = { normalizationAst: meNameEntrypoint.networkRequestInfo.normalizationAst, variables: {}, + root: { __link: ROOT_ID, __typename: 'Query' }, }; describe('garbage collection', () => { @@ -62,13 +69,13 @@ describe('garbage collection', () => { null as any, ); - expect(store[1]).not.toBe(undefined); + expect(store.Economist?.[1]).not.toBe(undefined); // TODO enable babel so we don't have to do this retainQuery(environment, meNameRetainedQuery); garbageCollectEnvironment(environment); - expect(store[1]).toBe(undefined); + expect(store.Economist?.[1]).toBe(undefined); }); test('Referenced records should not be garbage collected', () => { @@ -79,13 +86,13 @@ describe('garbage collection', () => { null as any, ); - expect(store[0]).not.toBe(undefined); + expect(store.Economist?.[0]).not.toBe(undefined); // TODO enable babel so we don't have to do this retainQuery(environment, meNameRetainedQuery); garbageCollectEnvironment(environment); - expect(store[0]).not.toBe(undefined); + expect(store.Economist?.[0]).not.toBe(undefined); }); test('Referenced records should not be garbage collected, and this should work with variables', () => { @@ -96,12 +103,12 @@ describe('garbage collection', () => { null as any, ); - expect(store[0]).not.toBe(undefined); + expect(store.Economist?.[0]).not.toBe(undefined); retainQuery(environment, nodeFieldRetainedQuery); garbageCollectEnvironment(environment); - expect(store[0]).not.toBe(undefined); + expect(store.Economist?.[0]).not.toBe(undefined); }); test('Referenced records should not be garbage collected, and this should work through multiple levels', () => { @@ -115,12 +122,12 @@ describe('garbage collection', () => { retainQuery(environment, meNameSuccessorRetainedQuery); garbageCollectEnvironment(environment); - expect(store[0]).not.toBe(undefined); - expect(store[1]).not.toBe(undefined); - expect(store[2]).not.toBe(undefined); + expect(store.Economist?.[0]).not.toBe(undefined); + expect(store.Economist?.[1]).not.toBe(undefined); + expect(store.Economist?.[2]).not.toBe(undefined); }); - test('ROOT_ID should not be garbage collected, even if there are no retained queries', () => { + test('ROOT_ID should be garbage collected, if there are no retained queries', () => { const store = getDefaultStore(); const environment = createIsographEnvironment( store, @@ -129,7 +136,7 @@ describe('garbage collection', () => { ); garbageCollectEnvironment(environment); - expect(store[ROOT_ID]).not.toBe(undefined); - expect(store[0]).toBe(undefined); + expect(store.Query?.[ROOT_ID]).toBe(undefined); + expect(store.Economist?.[0]).toBe(undefined); }); }); diff --git a/libs/isograph-react/src/tests/meNameSuccessor.ts b/libs/isograph-react/src/tests/meNameSuccessor.ts index c355714d..b3267816 100644 --- a/libs/isograph-react/src/tests/meNameSuccessor.ts +++ b/libs/isograph-react/src/tests/meNameSuccessor.ts @@ -1,3 +1,4 @@ +import { ROOT_ID } from '../core/IsographEnvironment'; import { iso } from './__isograph/iso'; export const meNameField = iso(` @@ -17,4 +18,8 @@ export const meNameSuccessorRetainedQuery = { normalizationAst: meNameSuccessorEntrypoint.networkRequestInfo.normalizationAst, variables: {}, + root: { + __link: ROOT_ID, + __typename: 'Query', + }, }; diff --git a/libs/isograph-react/src/tests/nodeQuery.ts b/libs/isograph-react/src/tests/nodeQuery.ts index 1843469a..0dd93b52 100644 --- a/libs/isograph-react/src/tests/nodeQuery.ts +++ b/libs/isograph-react/src/tests/nodeQuery.ts @@ -1,4 +1,5 @@ import { RetainedQuery } from '../core/garbageCollection'; +import { ROOT_ID } from '../core/IsographEnvironment'; import { iso } from './__isograph/iso'; // TODO investigate why this can't be in garbageCollection.test.ts without @@ -14,4 +15,5 @@ const nodeFieldEntrypoint = iso(`entrypoint Query.nodeField`); export const nodeFieldRetainedQuery: RetainedQuery = { normalizationAst: nodeFieldEntrypoint.networkRequestInfo.normalizationAst, variables: { id: 0 }, + root: { __link: ROOT_ID, __typename: 'Query' }, }; diff --git a/libs/isograph-react/src/tests/normalizeData.test.ts b/libs/isograph-react/src/tests/normalizeData.test.ts new file mode 100644 index 00000000..7e7a846d --- /dev/null +++ b/libs/isograph-react/src/tests/normalizeData.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, test, vi } from 'vitest'; +import { getOrCreateCacheForArtifact, normalizeData } from '../core/cache'; +import { + createIsographEnvironment, + createIsographStore, + ROOT_ID, + type IsographStore, +} from '../core/IsographEnvironment'; +import { + readButDoNotEvaluate, + type WithEncounteredRecords, +} from '../core/read'; +import { iso } from './__isograph/iso'; +import type { Query__subquery__param } from './__isograph/Query/subquery/param_type'; + +export const subquery = iso(` + field Query.subquery($id: ID!) { + query { + node(id: $id) { + id + } + } + } +`)(() => {}); + +const entrypoint = iso(`entrypoint Query.subquery`); + +describe('normalizeData', () => { + test('nested Query should be normalized', () => { + const store = createIsographStore(); + const networkFunction = vi + .fn() + .mockRejectedValue(new Error('Fetch failed')); + const environment = createIsographEnvironment(store, networkFunction); + + normalizeData( + environment, + entrypoint.networkRequestInfo.normalizationAst, + { + query: { node____id___v_id: { __typename: 'Economist', id: '1' } }, + }, + { id: '1' }, + entrypoint.readerWithRefetchQueries.nestedRefetchQueries, + { __link: ROOT_ID, __typename: entrypoint.concreteType }, + ); + + expect(store).toStrictEqual({ + Economist: { + '1': { + __typename: 'Economist', + id: '1', + }, + }, + Query: { + [ROOT_ID]: { + node____id___1: { + __typename: 'Economist', + __link: '1', + }, + query: { + __link: ROOT_ID, + __typename: 'Query', + }, + }, + }, + } satisfies IsographStore); + }); +}); + +describe('readData', () => { + test('nested Query should be read', () => { + const store: IsographStore = { + Economist: { + '1': { + __typename: 'Economist', + id: '1', + }, + }, + Query: { + [ROOT_ID]: { + node____id___1: { + __typename: 'Economist', + __link: '1', + }, + query: { + __link: ROOT_ID, + __typename: 'Query', + }, + }, + }, + }; + const networkFunction = vi + .fn() + .mockRejectedValue(new Error('Fetch failed')); + const environment = createIsographEnvironment(store, networkFunction); + const [_cacheItem, item, _disposeOfTemporaryRetain] = + getOrCreateCacheForArtifact(environment, entrypoint, { + id: '1', + }).getOrPopulateAndTemporaryRetain(); + + const data = readButDoNotEvaluate(environment, item, { + suspendIfInFlight: true, + throwOnNetworkError: false, + }); + + expect(data).toStrictEqual({ + encounteredRecords: new Map([ + ['Economist', new Set(['1'])], + ['Query', new Set([ROOT_ID])], + ]), + item: { + query: { + node: { + id: '1', + }, + }, + }, + } satisfies WithEncounteredRecords); + }); +}); From 58e38cf994fc6afb76e7ac229ad05ca8e7df2552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Wa=C5=82ach?= <35966385+PatrykWalach@users.noreply.github.com> Date: Sun, 10 Nov 2024 06:50:17 +0100 Subject: [PATCH 10/10] make `client_fields` an enum (#253) In anticipation of support for client pointers, make some changes to some types: - in places where we will have either a client field or client pointer, replace `ClientFieldId` with a `ClientType`. `ClientType` currently has one variant (`ClientField`) but will soon include a `ClientPointer` variant - Modify the unvalidated schema's `ServerFieldTypeAssociatedData` to be ``` ServerFieldTypeAssociatedData> // where ServerFieldTypeAssociatedData is pub struct ServerFieldTypeAssociatedData { pub type_name: TTypename, pub variant: SchemaServerFieldVariant, } ``` - Pre-validating, we only know the UnvalidatedTypeName (aka string value) of the inner type, but we have also created some inline fragments. So, the struct contains both of those. - Modify the validated schema's `ServerFieldTypeAssociatedData` to be ``` // (1) pub type ValidatedServerFieldTypeAssociatedData = SelectionType< ServerFieldTypeAssociatedData>, TypeAnnotation, > ``` - After validation, we know that an inline fragment/linked field can only exist if the server field is an object, so we reflect that in the enum structure - add `pub field_id: FieldType,` to `ValidatedLinkedFieldAssociatedData`, which allows us to - match on whether the field is a server field or client pointer (the `()` above). The client pointer code panics with a `todo!()` for now - if the type of the linked field is a server field, look up the server field, look at its associated data (1), and thus determine whether it is an object or scalar. If scalar, we panic (this can probably be fixed by having a distinction between scalar and object server fields, somehow?) - if an object, we can determine whether it is an inline fragment or regular linked field --- .../src/eager_reader_artifact.rs | 4 +- .../src/format_parameter_type.rs | 19 +- .../src/generate_artifacts.rs | 108 ++++---- .../src/iso_overload_file.rs | 15 +- .../src/reader_ast.rs | 48 ++-- .../isograph_compiler/src/refetch_fields.rs | 12 +- .../src/add_pointers_to_supertypes.rs | 23 +- crates/isograph_schema/src/argument_map.rs | 3 +- .../src/create_merged_selection_set.rs | 256 ++++++++++-------- .../src/expose_field_directive.rs | 20 +- crates/isograph_schema/src/isograph_schema.rs | 78 +++--- .../src/process_client_field_declaration.rs | 63 ++--- .../src/process_type_definition.rs | 16 +- .../isograph_schema/src/unvalidated_schema.rs | 34 ++- .../src/validate_client_field.rs | 110 ++++---- .../src/validate_entrypoint.rs | 6 +- crates/isograph_schema/src/validate_schema.rs | 16 +- .../src/validate_server_field.rs | 61 ++++- 18 files changed, 528 insertions(+), 364 deletions(-) diff --git a/crates/graphql_artifact_generation/src/eager_reader_artifact.rs b/crates/graphql_artifact_generation/src/eager_reader_artifact.rs index c658b09b..57dcd898 100644 --- a/crates/graphql_artifact_generation/src/eager_reader_artifact.rs +++ b/crates/graphql_artifact_generation/src/eager_reader_artifact.rs @@ -3,7 +3,7 @@ use intern::Lookup; use isograph_config::GenerateFileExtensionsOption; use isograph_schema::{ - RefetchedPathsMap, SchemaServerFieldInlineFragmentVariant, UserWrittenClientFieldInfo, + RefetchedPathsMap, ServerFieldTypeAssociatedDataInlineFragment, UserWrittenClientFieldInfo, UserWrittenComponentVariant, ValidatedClientField, ValidatedSchema, ValidatedSchemaServerField, }; use std::{ @@ -133,7 +133,7 @@ pub(crate) fn generate_eager_reader_artifacts( pub(crate) fn generate_eager_reader_condition_artifact( schema: &ValidatedSchema, encountered_server_field: &ValidatedSchemaServerField, - inline_fragment: &SchemaServerFieldInlineFragmentVariant, + inline_fragment: &ServerFieldTypeAssociatedDataInlineFragment, refetch_paths: &RefetchedPathsMap, file_extensions: GenerateFileExtensionsOption, ) -> ArtifactPathAndContent { diff --git a/crates/graphql_artifact_generation/src/format_parameter_type.rs b/crates/graphql_artifact_generation/src/format_parameter_type.rs index c04ce474..a6fefb04 100644 --- a/crates/graphql_artifact_generation/src/format_parameter_type.rs +++ b/crates/graphql_artifact_generation/src/format_parameter_type.rs @@ -2,9 +2,10 @@ use common_lang_types::SelectableFieldName; use graphql_lang_types::{GraphQLNonNullTypeAnnotation, GraphQLTypeAnnotation}; use isograph_lang_types::{ - ClientFieldId, SelectableServerFieldId, ServerFieldId, TypeAnnotation, UnionVariant, + ClientFieldId, SelectableServerFieldId, SelectionType, ServerFieldId, TypeAnnotation, + UnionVariant, }; -use isograph_schema::{FieldType, ValidatedSchema}; +use isograph_schema::{ClientType, FieldType, ValidatedSchema}; pub(crate) fn format_parameter_type( schema: &ValidatedSchema, @@ -74,17 +75,25 @@ fn format_server_field_type( fn format_field_definition( schema: &ValidatedSchema, name: &SelectableFieldName, - type_: &FieldType, + type_: &FieldType>, indentation_level: u8, ) -> String { match type_ { FieldType::ServerField(server_field_id) => { - let type_annotation = &schema.server_field(*server_field_id).associated_data; + let type_annotation = match &schema.server_field(*server_field_id).associated_data { + SelectionType::Object(associated_data) => associated_data + .type_name + .clone() + .map(&mut SelectionType::Object), + SelectionType::Scalar(type_name) => { + type_name.clone().map(&mut SelectionType::Scalar) + } + }; format!( "{}readonly {}: {},\n", " ".repeat(indentation_level as usize), name, - format_type_annotation(schema, type_annotation, indentation_level + 1), + format_type_annotation(schema, &type_annotation, indentation_level + 1), ) } FieldType::ClientField(_) => { diff --git a/crates/graphql_artifact_generation/src/generate_artifacts.rs b/crates/graphql_artifact_generation/src/generate_artifacts.rs index 80265a2d..25373bd9 100644 --- a/crates/graphql_artifact_generation/src/generate_artifacts.rs +++ b/crates/graphql_artifact_generation/src/generate_artifacts.rs @@ -10,12 +10,12 @@ use intern::{string_key::Intern, Lookup}; use isograph_config::{GenerateFileExtensionsOption, OptionalValidationLevel}; use isograph_lang_types::{ ArgumentKeyAndValue, ClientFieldId, NonConstantValue, SelectableServerFieldId, Selection, - ServerFieldSelection, TypeAnnotation, UnionVariant, VariableDefinition, + SelectionType, ServerFieldSelection, TypeAnnotation, UnionVariant, VariableDefinition, }; use isograph_schema::{ - get_provided_arguments, selection_map_wrapped, ClientFieldVariant, FieldTraversalResult, - FieldType, NameAndArguments, NormalizationKey, RequiresRefinement, SchemaObject, - SchemaServerFieldVariant, UserWrittenComponentVariant, ValidatedClientField, + get_provided_arguments, selection_map_wrapped, ClientFieldVariant, ClientType, + FieldTraversalResult, FieldType, NameAndArguments, NormalizationKey, RequiresRefinement, + SchemaObject, SchemaServerFieldVariant, UserWrittenComponentVariant, ValidatedClientField, ValidatedIsographSelectionVariant, ValidatedSchema, ValidatedSelection, ValidatedVariableDefinition, }; @@ -113,17 +113,20 @@ pub fn get_artifact_path_and_content( FieldType::ServerField(encountered_server_field_id) => { let encountered_server_field = schema.server_field(*encountered_server_field_id); - match &encountered_server_field.variant { - SchemaServerFieldVariant::LinkedField => {} - SchemaServerFieldVariant::InlineFragment(inline_fragment) => { - path_and_contents.push(generate_eager_reader_condition_artifact( - schema, - encountered_server_field, - inline_fragment, - &traversal_state.refetch_paths, - file_extensions, - )); - } + match &encountered_server_field.associated_data { + SelectionType::Scalar(_) => {} + SelectionType::Object(associated_data) => match &associated_data.variant { + SchemaServerFieldVariant::LinkedField => {} + SchemaServerFieldVariant::InlineFragment(inline_fragment) => { + path_and_contents.push(generate_eager_reader_condition_artifact( + schema, + encountered_server_field, + inline_fragment, + &traversal_state.refetch_paths, + file_extensions, + )); + } + }, }; } FieldType::ClientField(encountered_client_field_id) => { @@ -243,15 +246,12 @@ pub fn get_artifact_path_and_content( } } - for user_written_client_field in - schema - .client_fields - .iter() - .filter(|field| match field.variant { - ClientFieldVariant::UserWritten(_) => true, - ClientFieldVariant::ImperativelyLoadedField(_) => false, - }) - { + for user_written_client_field in schema.client_fields.iter().flat_map(|field| match field { + ClientType::ClientField(field) => match field.variant { + ClientFieldVariant::UserWritten(_) => Some(field), + ClientFieldVariant::ImperativelyLoadedField(_) => None, + }, + }) { // For each user-written client field, generate a param type artifact path_and_contents.push(generate_eager_reader_param_type_artifact( schema, @@ -487,19 +487,18 @@ fn write_param_type_from_selection( let name_or_alias = scalar_field_selection.name_or_alias().item; - // TODO there should be a clever way to print without cloning - let output_type = - field.associated_data.clone().map(&mut |output_type_id| { - // TODO not just scalars, enums as well. Both should have a javascript name - let scalar_id = if let SelectableServerFieldId::Scalar(scalar) = - output_type_id - { - scalar - } else { - panic!("output_type_id should be a scalar"); - }; - schema.server_field_data.scalar(scalar_id).javascript_name - }); + let output_type = match &field.associated_data { + // TODO there should be a clever way to print without cloning + SelectionType::Scalar(type_name) => { + type_name.clone().map(&mut |scalar_id| { + schema.server_field_data.scalar(scalar_id).javascript_name + }) + } + // TODO not just scalars, enums as well. Both should have a javascript name + SelectionType::Object(_) => { + panic!("output_type_id should be a scalar") + } + }; query_type_declaration.push_str(&format!( "{}readonly {}: {},\n", @@ -587,21 +586,28 @@ fn write_param_type_from_selection( .push_str(&" ".repeat(indentation_level as usize).to_string()); let name_or_alias = linked_field.name_or_alias().item; - let type_annotation = field.associated_data.clone().map(&mut |output_type_id| { - let object_id = output_type_id.try_into().expect( + let type_annotation = match &field.associated_data { + SelectionType::Scalar(_) => panic!( "output_type_id should be an object. \ - This is indicative of a bug in Isograph.", - ); - let object = schema.server_field_data.object(object_id); - generate_client_field_parameter_type( - schema, - &linked_field.selection_set, - object, - nested_client_field_imports, - loadable_fields, - indentation_level, - ) - }); + This is indicative of a bug in Isograph.", + ), + SelectionType::Object(associated_data) => associated_data + .type_name + .clone() + .map(&mut |output_type_id| { + let object_id = output_type_id; + let object = schema.server_field_data.object(object_id); + generate_client_field_parameter_type( + schema, + &linked_field.selection_set, + object, + nested_client_field_imports, + loadable_fields, + indentation_level, + ) + }), + }; + query_type_declaration.push_str(&format!( "readonly {}: {},\n", name_or_alias, diff --git a/crates/graphql_artifact_generation/src/iso_overload_file.rs b/crates/graphql_artifact_generation/src/iso_overload_file.rs index 260ee6b2..85d347c0 100644 --- a/crates/graphql_artifact_generation/src/iso_overload_file.rs +++ b/crates/graphql_artifact_generation/src/iso_overload_file.rs @@ -4,7 +4,8 @@ use std::{cmp::Ordering, path::PathBuf}; use common_lang_types::{ArtifactPathAndContent, SelectableFieldName}; use isograph_schema::{ - ClientFieldVariant, UserWrittenComponentVariant, ValidatedClientField, ValidatedSchema, + ClientFieldVariant, ClientType, UserWrittenComponentVariant, ValidatedClientField, + ValidatedSchema, }; use crate::generate_artifacts::ISO_TS; @@ -260,10 +261,12 @@ fn user_written_fields( schema .client_fields .iter() - .filter_map(|client_field| match client_field.variant { - ClientFieldVariant::UserWritten(info) => { - Some((client_field, info.user_written_component_variant)) - } - ClientFieldVariant::ImperativelyLoadedField(_) => None, + .filter_map(|client_field| match client_field { + ClientType::ClientField(client_field) => match client_field.variant { + ClientFieldVariant::UserWritten(info) => { + Some((client_field, info.user_written_component_variant)) + } + ClientFieldVariant::ImperativelyLoadedField(_) => None, + }, }) } diff --git a/crates/graphql_artifact_generation/src/reader_ast.rs b/crates/graphql_artifact_generation/src/reader_ast.rs index d2f86cf3..ba324e32 100644 --- a/crates/graphql_artifact_generation/src/reader_ast.rs +++ b/crates/graphql_artifact_generation/src/reader_ast.rs @@ -2,7 +2,7 @@ use std::collections::{BTreeSet, HashSet}; use common_lang_types::{SelectableFieldName, WithSpan}; use isograph_lang_types::{ - LoadableDirectiveParameters, RefetchQueryIndex, Selection, ServerFieldSelection, + LoadableDirectiveParameters, RefetchQueryIndex, Selection, SelectionType, ServerFieldSelection, }; use isograph_schema::{ categorize_field_loadability, transform_arguments_with_child_context, FieldType, Loadability, @@ -123,25 +123,35 @@ fn linked_field_ast_node( let indent_1 = " ".repeat(indentation_level as usize); let indent_2 = " ".repeat((indentation_level + 1) as usize); - let condition = match linked_field.associated_data.variant { - SchemaServerFieldVariant::InlineFragment(_) => { - let object = schema - .server_field_data - .object(linked_field.associated_data.parent_object_id); - - let type_and_field = ObjectTypeAndFieldName { - field_name: linked_field.name.item.into(), - type_name: object.name, - }; - - let reader_artifact_import_name = - format!("{}__resolver_reader", type_and_field.underscore_separated()); - - reader_imports.insert((type_and_field, ImportedFileCategory::ResolverReader)); - - reader_artifact_import_name + let condition = match linked_field.associated_data.field_id { + FieldType::ClientField(_) => todo!(), + FieldType::ServerField(server_field_id) => { + match &schema.server_field(server_field_id).associated_data { + SelectionType::Scalar(_) => panic!("Expected object"), + SelectionType::Object(associated_data) => match &associated_data.variant { + SchemaServerFieldVariant::InlineFragment(inline_fragment) => { + let parent_object_id = schema + .server_field(inline_fragment.server_field_id) + .parent_type_id; + let object = schema.server_field_data.object(parent_object_id); + + let type_and_field = ObjectTypeAndFieldName { + field_name: linked_field.name.item.into(), + type_name: object.name, + }; + + let reader_artifact_import_name = + format!("{}__resolver_reader", type_and_field.underscore_separated()); + + reader_imports + .insert((type_and_field, ImportedFileCategory::ResolverReader)); + + reader_artifact_import_name + } + SchemaServerFieldVariant::LinkedField => "null".to_string(), + }, + } } - SchemaServerFieldVariant::LinkedField => "null".to_string(), }; format!( diff --git a/crates/isograph_compiler/src/refetch_fields.rs b/crates/isograph_compiler/src/refetch_fields.rs index 5a056a94..a7b0388e 100644 --- a/crates/isograph_compiler/src/refetch_fields.rs +++ b/crates/isograph_compiler/src/refetch_fields.rs @@ -4,7 +4,7 @@ use intern::string_key::Intern; use isograph_lang_types::ServerObjectId; use isograph_schema::{ generate_refetch_field_strategy, id_arguments, id_selection, id_top_level_arguments, - ClientField, ClientFieldVariant, FieldType, ImperativelyLoadedFieldVariant, + ClientField, ClientFieldVariant, ClientType, FieldType, ImperativelyLoadedFieldVariant, ObjectTypeAndFieldName, RefetchStrategy, RequiresRefinement, SchemaObject, UnvalidatedClientField, UnvalidatedSchema, NODE_FIELD_NAME, REFETCH_FIELD_NAME, }; @@ -32,7 +32,7 @@ pub fn add_refetch_fields_to_objects( fn add_refetch_field_to_object( object: &mut SchemaObject, - client_fields: &mut Vec, + client_fields: &mut Vec>, query_id: ServerObjectId, ) -> Option> { match object @@ -43,9 +43,11 @@ fn add_refetch_field_to_object( Entry::Vacant(vacant_entry) => { let next_client_field_id = client_fields.len().into(); - vacant_entry.insert(FieldType::ClientField(next_client_field_id)); + vacant_entry.insert(FieldType::ClientField(ClientType::ClientField( + next_client_field_id, + ))); - client_fields.push(ClientField { + client_fields.push(ClientType::ClientField(ClientField { description: Some( format!("A refetch field for the {} type.", object.name) .intern() @@ -86,7 +88,7 @@ fn add_refetch_field_to_object( None, )) }), - }); + })); } } None diff --git a/crates/isograph_schema/src/add_pointers_to_supertypes.rs b/crates/isograph_schema/src/add_pointers_to_supertypes.rs index 4a6e3356..cc3e82cc 100644 --- a/crates/isograph_schema/src/add_pointers_to_supertypes.rs +++ b/crates/isograph_schema/src/add_pointers_to_supertypes.rs @@ -5,8 +5,9 @@ use isograph_lang_types::{ScalarFieldSelection, Selection, ServerFieldSelection} use crate::{ FieldType, ProcessTypeDefinitionError, ProcessTypeDefinitionResult, SchemaObject, - SchemaServerField, SchemaServerFieldInlineFragmentVariant, SchemaServerFieldVariant, - UnvalidatedSchema, ValidatedIsographSelectionVariant, ValidatedScalarFieldAssociatedData, + SchemaServerField, SchemaServerFieldVariant, ServerFieldTypeAssociatedData, + ServerFieldTypeAssociatedDataInlineFragment, UnvalidatedSchema, + ValidatedIsographSelectionVariant, ValidatedScalarFieldAssociatedData, ValidatedTypeRefinementMap, }; use common_lang_types::Location; @@ -70,15 +71,17 @@ impl UnvalidatedSchema { name: WithLocation::new(field_name, Location::generated()), parent_type_id: subtype.id, arguments: vec![], - associated_data, + associated_data: ServerFieldTypeAssociatedData { + type_name: associated_data, + variant: SchemaServerFieldVariant::InlineFragment( + ServerFieldTypeAssociatedDataInlineFragment { + server_field_id: next_server_field_id, + concrete_type, + condition_selection_set, + }, + ), + }, is_discriminator: false, - variant: SchemaServerFieldVariant::InlineFragment( - SchemaServerFieldInlineFragmentVariant { - server_field_id: next_server_field_id, - concrete_type, - condition_selection_set, - }, - ), }; self.server_fields.push(server_field); diff --git a/crates/isograph_schema/src/argument_map.rs b/crates/isograph_schema/src/argument_map.rs index 4a7aaddf..b34b8350 100644 --- a/crates/isograph_schema/src/argument_map.rs +++ b/crates/isograph_schema/src/argument_map.rs @@ -276,7 +276,8 @@ impl ModifiedArgument { match field { PotentiallyModifiedField::Unmodified(field_id) => { let field_object = schema.server_field(*field_id); - let field_object_type = field_object.associated_data.inner(); + let field_object_type = + field_object.associated_data.type_name.inner(); // N.B. this should be done via a validation pass. match schema diff --git a/crates/isograph_schema/src/create_merged_selection_set.rs b/crates/isograph_schema/src/create_merged_selection_set.rs index f8c21005..8deb1d00 100644 --- a/crates/isograph_schema/src/create_merged_selection_set.rs +++ b/crates/isograph_schema/src/create_merged_selection_set.rs @@ -10,8 +10,8 @@ use graphql_lang_types::{ use intern::{string_key::Intern, Lookup}; use isograph_lang_types::{ ArgumentKeyAndValue, ClientFieldId, IsographSelectionVariant, NonConstantValue, - RefetchQueryIndex, SelectableServerFieldId, Selection, SelectionFieldArgument, ServerFieldId, - ServerFieldSelection, ServerObjectId, VariableDefinition, + RefetchQueryIndex, SelectableServerFieldId, Selection, SelectionFieldArgument, SelectionType, + ServerFieldId, ServerFieldSelection, ServerObjectId, VariableDefinition, }; use lazy_static::lazy_static; @@ -733,65 +733,81 @@ fn merge_validated_selections_into_selection_map( let type_id = linked_field_selection.associated_data.parent_object_id; let linked_field_parent_type = schema.server_field_data.object(type_id); - match &linked_field_selection.associated_data.variant { - SchemaServerFieldVariant::InlineFragment(inline_fragment_variant) => { - let type_to_refine_to = linked_field_parent_type.name; - let normalization_key = - NormalizationKey::InlineFragment(type_to_refine_to); - - let inline_fragment = - parent_map.entry(normalization_key).or_insert_with(|| { - MergedServerSelection::InlineFragment( - MergedInlineFragmentSelection { - type_to_refine_to, - selection_map: BTreeMap::new(), - }, - ) - }); - - match inline_fragment { - MergedServerSelection::ScalarField(_) => { - panic!( + match linked_field_selection.associated_data.field_id { + FieldType::ClientField(_client_pointer_id) => todo!(), + FieldType::ServerField(server_field_id) => { + let server_field = schema.server_field(server_field_id); + + match &server_field.associated_data { + SelectionType::Scalar(_) => {} + SelectionType::Object(object) => { + match &object.variant { + SchemaServerFieldVariant::InlineFragment( + inline_fragment_variant, + ) => { + let type_to_refine_to = + linked_field_parent_type.name; + let normalization_key = + NormalizationKey::InlineFragment( + type_to_refine_to, + ); + + let inline_fragment = parent_map + .entry(normalization_key) + .or_insert_with(|| { + MergedServerSelection::InlineFragment( + MergedInlineFragmentSelection { + type_to_refine_to, + selection_map: BTreeMap::new(), + }, + ) + }); + + match inline_fragment { + MergedServerSelection::ScalarField(_) => { + panic!( "Expected inline fragment, but encountered scalar. \ This is indicative of a bug in Isograph." ) - } - MergedServerSelection::LinkedField(_) => { - panic!( + } + MergedServerSelection::LinkedField(_) => { + panic!( "Expected inline fragment, but encountered linked field. \ This is indicative of a bug in Isograph." ) - } - MergedServerSelection::InlineFragment( - existing_inline_fragment, - ) => { - let linked_field_parent_type = schema - .server_field_data - .object(linked_field_parent_type.id); - - merge_validated_selections_into_selection_map( - schema, - &mut existing_inline_fragment.selection_map, - linked_field_parent_type, - &inline_fragment_variant.condition_selection_set, - merge_traversal_state, - encountered_client_field_map, - variable_context, - ); - merge_validated_selections_into_selection_map( - schema, - &mut existing_inline_fragment.selection_map, - linked_field_parent_type, - &linked_field_selection.selection_set, - merge_traversal_state, - encountered_client_field_map, - variable_context, - ); + } + MergedServerSelection::InlineFragment( + existing_inline_fragment, + ) => { + let linked_field_parent_type = schema + .server_field_data + .object(linked_field_parent_type.id); + + merge_validated_selections_into_selection_map( + schema, + &mut existing_inline_fragment.selection_map, + linked_field_parent_type, + &inline_fragment_variant + .condition_selection_set, + merge_traversal_state, + encountered_client_field_map, + variable_context, + ); + merge_validated_selections_into_selection_map( + schema, + &mut existing_inline_fragment.selection_map, + linked_field_parent_type, + &linked_field_selection.selection_set, + merge_traversal_state, + encountered_client_field_map, + variable_context, + ); - let server_field = schema - .server_field(inline_fragment_variant.server_field_id); + let server_field = schema.server_field( + inline_fragment_variant.server_field_id, + ); - create_merged_selection_map_for_field_and_insert_into_global_map( + create_merged_selection_map_for_field_and_insert_into_global_map( schema, parent_type, &linked_field_selection.selection_set, @@ -799,69 +815,81 @@ fn merge_validated_selections_into_selection_map( FieldType::ServerField(inline_fragment_variant.server_field_id), &server_field.initial_variable_context() ); - } - } - } - SchemaServerFieldVariant::LinkedField => { - let normalization_key = create_transformed_name_and_arguments( - linked_field_selection.name.item.into(), - &linked_field_selection.arguments, - variable_context, - ) - .normalization_key(); - - merge_traversal_state - .traversal_path - .push(normalization_key.clone()); - - // We are creating the linked field, and inserting it into the parent object - // first, because otherwise, when we try to merge the results into the parent - // selection_map, we find that the linked field we are about to insert is - // missing, and panic. - // - // This might be indicative of poor modeling. - let linked_field = - parent_map.entry(normalization_key).or_insert_with(|| { - MergedServerSelection::LinkedField( - MergedLinkedFieldSelection { - concrete_type: linked_field_selection - .associated_data - .concrete_type, - name: linked_field_selection.name.item, - selection_map: BTreeMap::new(), - arguments: transform_arguments_with_child_context( - linked_field_selection - .arguments - .iter() - .map(|arg| arg.item.into_key_and_value()), - variable_context, - ), - }, - ) - }); - match linked_field { - MergedServerSelection::ScalarField(_) => { - panic!( + } + } + } + SchemaServerFieldVariant::LinkedField => { + let normalization_key = + create_transformed_name_and_arguments( + linked_field_selection.name.item.into(), + &linked_field_selection.arguments, + variable_context, + ) + .normalization_key(); + + merge_traversal_state + .traversal_path + .push(normalization_key.clone()); + + // We are creating the linked field, and inserting it into the parent object + // first, because otherwise, when we try to merge the results into the parent + // selection_map, we find that the linked field we are about to insert is + // missing, and panic. + // + // This might be indicative of poor modeling. + let linked_field = parent_map + .entry(normalization_key) + .or_insert_with(|| { + MergedServerSelection::LinkedField( + MergedLinkedFieldSelection { + concrete_type: linked_field_selection + .associated_data + .concrete_type, + name: linked_field_selection.name.item, + selection_map: BTreeMap::new(), + arguments: + transform_arguments_with_child_context( + linked_field_selection + .arguments + .iter() + .map(|arg| { + arg.item + .into_key_and_value() + }), + variable_context, + ), + }, + ) + }); + match linked_field { + MergedServerSelection::ScalarField(_) => { + panic!( "Expected linked field, but encountered scalar. \ - This is indicative of a bug in Isograph." - ) - } - MergedServerSelection::LinkedField(existing_linked_field) => { - merge_validated_selections_into_selection_map( - schema, - &mut existing_linked_field.selection_map, - linked_field_parent_type, - &linked_field_selection.selection_set, - merge_traversal_state, - encountered_client_field_map, - variable_context, - ); - } - MergedServerSelection::InlineFragment(_) => { - panic!( - "Expected linked field, but encountered inline fragment. \ - This is indicative of a bug in Isograph." + This is indicative of a bug in Isograph." ) + } + MergedServerSelection::LinkedField( + existing_linked_field, + ) => { + merge_validated_selections_into_selection_map( + schema, + &mut existing_linked_field.selection_map, + linked_field_parent_type, + &linked_field_selection.selection_set, + merge_traversal_state, + encountered_client_field_map, + variable_context, + ); + } + MergedServerSelection::InlineFragment(_) => { + panic!( + "Expected linked field, but encountered inline fragment. \ + This is indicative of a bug in Isograph." + ) + } + } + } + } } } } diff --git a/crates/isograph_schema/src/expose_field_directive.rs b/crates/isograph_schema/src/expose_field_directive.rs index 8ab5eb18..54d6e45f 100644 --- a/crates/isograph_schema/src/expose_field_directive.rs +++ b/crates/isograph_schema/src/expose_field_directive.rs @@ -14,10 +14,10 @@ use isograph_lang_types::{ use serde::Deserialize; use crate::{ - generate_refetch_field_strategy, ArgumentMap, ClientField, ClientFieldVariant, FieldMapItem, - FieldType, ImperativelyLoadedFieldVariant, ObjectTypeAndFieldName, PrimaryFieldInfo, - ProcessTypeDefinitionError, ProcessTypeDefinitionResult, ProcessedFieldMapItem, - UnvalidatedSchema, UnvalidatedVariableDefinition, + generate_refetch_field_strategy, ArgumentMap, ClientField, ClientFieldVariant, ClientType, + FieldMapItem, FieldType, ImperativelyLoadedFieldVariant, ObjectTypeAndFieldName, + PrimaryFieldInfo, ProcessTypeDefinitionError, ProcessTypeDefinitionResult, + ProcessedFieldMapItem, UnvalidatedSchema, UnvalidatedVariableDefinition, }; use lazy_static::lazy_static; @@ -121,7 +121,7 @@ impl UnvalidatedSchema { // TODO do not use mutation naming here let mutation_field = self.server_field(mutation_subfield_id); - let mutation_field_payload_type_name = *mutation_field.associated_data.inner(); + let mutation_field_payload_type_name = *mutation_field.associated_data.type_name.inner(); let client_field_scalar_selection_name = expose_as.unwrap_or(mutation_field.name.item); // TODO what is going on here. Should mutation_field have a checked way of converting to LinkedField? let top_level_schema_field_name = mutation_field.name.item.lookup().intern().into(); @@ -163,7 +163,7 @@ impl UnvalidatedSchema { let server_field = self.server_field(*server_field_id); // This is the parent type name (Pet) - let inner = server_field.associated_data.inner(); + let inner = server_field.associated_data.type_name.inner(); // TODO validate that the payload object has no plural fields in between @@ -289,7 +289,8 @@ impl UnvalidatedSchema { ), )), }; - self.client_fields.push(mutation_client_field); + self.client_fields + .push(ClientType::ClientField(mutation_client_field)); self.insert_client_field_on_object( client_field_scalar_selection_name, @@ -314,7 +315,10 @@ impl UnvalidatedSchema { .object_mut(client_field_parent_object_id); if client_field_parent .encountered_fields - .insert(mutation_field_name, FieldType::ClientField(client_field_id)) + .insert( + mutation_field_name, + FieldType::ClientField(ClientType::ClientField(client_field_id)), + ) .is_some() { return Err(WithLocation::new( diff --git a/crates/isograph_schema/src/isograph_schema.rs b/crates/isograph_schema/src/isograph_schema.rs index ce7dedbc..acf22a21 100644 --- a/crates/isograph_schema/src/isograph_schema.rs +++ b/crates/isograph_schema/src/isograph_schema.rs @@ -21,7 +21,8 @@ use isograph_lang_types::{ use lazy_static::lazy_static; use crate::{ - refetch_strategy::RefetchStrategy, ClientFieldVariant, NormalizationKey, ValidatedSelection, + refetch_strategy::RefetchStrategy, ClientFieldVariant, NormalizationKey, + ServerFieldTypeAssociatedData, }; lazy_static! { @@ -79,12 +80,10 @@ pub struct Schema { TSchemaValidationState::VariableDefinitionInnerType, >, >, - pub client_fields: Vec< - ClientField< - TSchemaValidationState::ClientFieldSelectionScalarFieldAssociatedData, - TSchemaValidationState::ClientFieldSelectionLinkedFieldAssociatedData, - TSchemaValidationState::VariableDefinitionInnerType, - >, + pub client_fields: ClientFields< + TSchemaValidationState::ClientFieldSelectionScalarFieldAssociatedData, + TSchemaValidationState::ClientFieldSelectionLinkedFieldAssociatedData, + TSchemaValidationState::VariableDefinitionInnerType, >, // TODO consider whether this belongs here. It could just be a free variable. pub entrypoints: TSchemaValidationState::Entrypoint, @@ -104,6 +103,20 @@ pub struct Schema { pub fetchable_types: BTreeMap, } +type ClientFields< + TClientFieldSelectionScalarFieldAssociatedData, + TClientFieldSelectionLinkedFieldAssociatedData, + TClientFieldVariableDefinitionAssociatedData, +> = Vec< + ClientType< + ClientField< + TClientFieldSelectionScalarFieldAssociatedData, + TClientFieldSelectionLinkedFieldAssociatedData, + TClientFieldVariableDefinitionAssociatedData, + >, + >, +>; + impl Schema { /// This is a smell, and we should refactor away from it, or all schema's /// should have a root type. @@ -145,6 +158,12 @@ pub enum FieldType { ClientField(TClient), } +#[derive(Debug, Clone, Copy, Ord, PartialOrd, PartialEq, Eq, Hash)] +pub enum ClientType { + ClientField(TField), + // ClientPointer(TPointer) +} + impl FieldType { pub fn as_server_field(&self) -> Option<&TFieldAssociatedData> { match self { @@ -189,20 +208,28 @@ impl Schema { - &self.client_fields[client_field_id.as_usize()] + match &self.client_fields[client_field_id.as_usize()] { + ClientType::ClientField(client_field) => client_field, + } } } impl< - TFieldAssociatedData: Clone + Ord + Copy + Debug, - TSchemaValidationState: SchemaValidationState>, + TObjectFieldAssociatedData: Clone + Ord + Copy + Debug, + TScalarFieldAssociatedData: Clone + Ord + Copy + Debug, + TSchemaValidationState: SchemaValidationState< + ServerFieldTypeAssociatedData = SelectionType< + ServerFieldTypeAssociatedData>, + TypeAnnotation, + >, + >, > Schema { // This should not be this complicated! /// Get a reference to a given id field by its id. pub fn id_field< TError: Debug, - TIdFieldAssociatedData: TryFrom + Copy + Debug, + TIdFieldAssociatedData: TryFrom + Copy + Debug, >( &self, id_field_id: ServerStrongIdFieldId, @@ -211,7 +238,12 @@ impl< let field = self .server_field(field_id) - .and_then(|e| e.inner_non_null().try_into()) + .and_then(|e| match e { + SelectionType::Object(_) => panic!( + "We had an id field, it should be scalar. This indicates a bug in Isograph.", + ), + SelectionType::Scalar(e) => e.inner_non_null().try_into(), + }) .expect( // N.B. this expect should never be triggered. This is only because server_field // does not have a .map method. TODO implement .map @@ -329,7 +361,8 @@ pub struct SchemaObject { /// TODO remove id_field from fields, and change the type of Option /// to something else. pub id_field: Option, - pub encountered_fields: BTreeMap>, + pub encountered_fields: + BTreeMap>>, /// Some if the object is concrete; None otherwise. pub concrete_type: Option, } @@ -348,21 +381,6 @@ pub struct SchemaServerField>>, // TODO remove this. This is indicative of poor modeling. pub is_discriminator: bool, - - pub variant: SchemaServerFieldVariant, -} - -#[derive(Debug, Clone)] -pub enum SchemaServerFieldVariant { - InlineFragment(SchemaServerFieldInlineFragmentVariant), - LinkedField, -} - -#[derive(Debug, Clone)] -pub struct SchemaServerFieldInlineFragmentVariant { - pub server_field_id: ServerFieldId, - pub concrete_type: IsographObjectTypeName, - pub condition_selection_set: Vec>, } impl @@ -380,7 +398,6 @@ impl parent_type_id: self.parent_type_id, arguments: self.arguments.clone(), is_discriminator: self.is_discriminator, - variant: self.variant.clone(), }) } @@ -396,7 +413,6 @@ impl parent_type_id: self.parent_type_id, arguments: self.arguments.clone(), is_discriminator: self.is_discriminator, - variant: self.variant.clone(), } } } @@ -592,7 +608,6 @@ impl parent_type_id, arguments, is_discriminator, - variant: is_inline, } = self; ( SchemaServerField { @@ -603,7 +618,6 @@ impl parent_type_id, arguments, is_discriminator, - variant: is_inline, }, associated_data, ) diff --git a/crates/isograph_schema/src/process_client_field_declaration.rs b/crates/isograph_schema/src/process_client_field_declaration.rs index aad3be90..ae7a9550 100644 --- a/crates/isograph_schema/src/process_client_field_declaration.rs +++ b/crates/isograph_schema/src/process_client_field_declaration.rs @@ -13,7 +13,7 @@ use thiserror::Error; use crate::{ refetch_strategy::{generate_refetch_field_strategy, id_selection, RefetchStrategy}, - ClientField, FieldMapItem, FieldType, ObjectTypeAndFieldName, RequiresRefinement, + ClientField, ClientType, FieldMapItem, FieldType, ObjectTypeAndFieldName, RequiresRefinement, UnvalidatedSchema, UnvalidatedVariableDefinition, NODE_FIELD_NAME, }; @@ -70,7 +70,7 @@ impl UnvalidatedSchema { .encountered_fields .insert( client_field_name.into(), - FieldType::ClientField(next_client_field_id), + FieldType::ClientField(ClientType::ClientField(next_client_field_id)), ) .is_some() { @@ -87,35 +87,36 @@ impl UnvalidatedSchema { let name = client_field_declaration.item.client_field_name.item.into(); let variant = get_client_variant(&client_field_declaration.item); - self.client_fields.push(ClientField { - description: client_field_declaration.item.description.map(|x| x.item), - name, - id: next_client_field_id, - reader_selection_set: Some(client_field_declaration.item.selection_set), - unwraps: client_field_declaration.item.unwraps, - variant, - variable_definitions: client_field_declaration.item.variable_definitions, - type_and_field: ObjectTypeAndFieldName { - type_name: object.name, - field_name: name, - }, - - parent_object_id, - refetch_strategy: object.id_field.map(|_| { - // Assume that if we have an id field, this implements Node - RefetchStrategy::UseRefetchField(generate_refetch_field_strategy( - vec![id_selection()], - query_id, - format!("refetch__{}", object.name).intern().into(), - *NODE_FIELD_NAME, - id_top_level_arguments(), - None, - RequiresRefinement::Yes(object.name), - None, - None, - )) - }), - }); + self.client_fields + .push(ClientType::ClientField(ClientField { + description: client_field_declaration.item.description.map(|x| x.item), + name, + id: next_client_field_id, + reader_selection_set: Some(client_field_declaration.item.selection_set), + unwraps: client_field_declaration.item.unwraps, + variant, + variable_definitions: client_field_declaration.item.variable_definitions, + type_and_field: ObjectTypeAndFieldName { + type_name: object.name, + field_name: name, + }, + + parent_object_id, + refetch_strategy: object.id_field.map(|_| { + // Assume that if we have an id field, this implements Node + RefetchStrategy::UseRefetchField(generate_refetch_field_strategy( + vec![id_selection()], + query_id, + format!("refetch__{}", object.name).intern().into(), + *NODE_FIELD_NAME, + id_top_level_arguments(), + None, + RequiresRefinement::Yes(object.name), + None, + None, + )) + }), + })); Ok(()) } } diff --git a/crates/isograph_schema/src/process_type_definition.rs b/crates/isograph_schema/src/process_type_definition.rs index 8e0915c6..20640e79 100644 --- a/crates/isograph_schema/src/process_type_definition.rs +++ b/crates/isograph_schema/src/process_type_definition.rs @@ -3,8 +3,8 @@ use std::collections::{hash_map::Entry, BTreeMap, HashMap}; use crate::{ EncounteredRootTypes, FieldType, IsographObjectTypeDefinition, ProcessedRootTypes, RootOperationName, RootTypes, Schema, SchemaObject, SchemaScalar, SchemaServerField, - SchemaServerFieldVariant, UnvalidatedObjectFieldInfo, UnvalidatedSchema, - UnvalidatedSchemaSchemaField, ID_GRAPHQL_TYPE, STRING_JAVASCRIPT_TYPE, + SchemaServerFieldVariant, ServerFieldTypeAssociatedData, UnvalidatedObjectFieldInfo, + UnvalidatedSchema, UnvalidatedSchemaSchemaField, ID_GRAPHQL_TYPE, STRING_JAVASCRIPT_TYPE, }; use common_lang_types::{ GraphQLObjectTypeName, GraphQLScalarTypeName, IsographObjectTypeName, Location, @@ -617,7 +617,10 @@ fn get_field_objects_ids_and_names( description: field.item.description.map(|d| d.item), name: field.item.name, id: next_server_field_id, - associated_data: field.item.type_, + associated_data: ServerFieldTypeAssociatedData { + type_name: field.item.type_, + variant: SchemaServerFieldVariant::LinkedField, + }, parent_type_id, arguments: field .item @@ -626,7 +629,6 @@ fn get_field_objects_ids_and_names( .map(graphql_input_value_definition_to_variable_definition) .collect::, _>>()?, is_discriminator: false, - variant: SchemaServerFieldVariant::LinkedField, }); server_field_ids.push(next_server_field_id); } @@ -655,11 +657,13 @@ fn get_field_objects_ids_and_names( description: None, name: typename_name, id: typename_field_id, - associated_data: typename_type.clone(), + associated_data: ServerFieldTypeAssociatedData { + type_name: typename_type.clone(), + variant: SchemaServerFieldVariant::LinkedField, + }, parent_type_id, arguments: vec![], is_discriminator: true, - variant: SchemaServerFieldVariant::LinkedField, }); if encountered_fields diff --git a/crates/isograph_schema/src/unvalidated_schema.rs b/crates/isograph_schema/src/unvalidated_schema.rs index 8820539a..a21213ee 100644 --- a/crates/isograph_schema/src/unvalidated_schema.rs +++ b/crates/isograph_schema/src/unvalidated_schema.rs @@ -1,7 +1,8 @@ use std::collections::{BTreeMap, HashMap}; use common_lang_types::{ - JavascriptName, Location, TextSource, UnvalidatedTypeName, WithLocation, WithSpan, + IsographObjectTypeName, JavascriptName, Location, TextSource, UnvalidatedTypeName, + WithLocation, WithSpan, }; use graphql_lang_types::GraphQLTypeAnnotation; use intern::string_key::Intern; @@ -11,8 +12,8 @@ use isograph_lang_types::{ }; use crate::{ - ClientField, FieldType, Schema, SchemaScalar, SchemaServerField, SchemaValidationState, - ServerFieldData, UseRefetchFieldRefetchStrategy, + ClientField, ClientType, FieldType, Schema, SchemaScalar, SchemaServerField, + SchemaValidationState, ServerFieldData, UseRefetchFieldRefetchStrategy, ValidatedSelection, }; use lazy_static::lazy_static; @@ -23,20 +24,43 @@ lazy_static! { #[derive(Debug)] pub struct UnvalidatedSchemaState {} +type UnvalidatedServerFieldTypeAssociatedData = + ServerFieldTypeAssociatedData>; + impl SchemaValidationState for UnvalidatedSchemaState { - type ServerFieldTypeAssociatedData = GraphQLTypeAnnotation; + type ServerFieldTypeAssociatedData = UnvalidatedServerFieldTypeAssociatedData; type ClientFieldSelectionScalarFieldAssociatedData = IsographSelectionVariant; type ClientFieldSelectionLinkedFieldAssociatedData = IsographSelectionVariant; type VariableDefinitionInnerType = UnvalidatedTypeName; type Entrypoint = Vec<(TextSource, WithSpan)>; } +#[derive(Debug, Clone)] + +pub struct ServerFieldTypeAssociatedData { + pub type_name: TTypename, + pub variant: SchemaServerFieldVariant, +} + +#[derive(Debug, Clone)] +pub enum SchemaServerFieldVariant { + LinkedField, + InlineFragment(ServerFieldTypeAssociatedDataInlineFragment), +} + +#[derive(Debug, Clone)] +pub struct ServerFieldTypeAssociatedDataInlineFragment { + pub server_field_id: ServerFieldId, + pub concrete_type: IsographObjectTypeName, + pub condition_selection_set: Vec>, +} + pub type UnvalidatedSchema = Schema; /// On unvalidated schema objects, the encountered types are either a type annotation /// for server fields with an unvalidated inner type, or a ScalarFieldName (the name of the /// client field.) -pub type UnvalidatedObjectFieldInfo = FieldType; +pub type UnvalidatedObjectFieldInfo = FieldType>; pub(crate) type UnvalidatedSchemaSchemaField = SchemaServerField< ::ServerFieldTypeAssociatedData, diff --git a/crates/isograph_schema/src/validate_client_field.rs b/crates/isograph_schema/src/validate_client_field.rs index 70566de9..b13922cf 100644 --- a/crates/isograph_schema/src/validate_client_field.rs +++ b/crates/isograph_schema/src/validate_client_field.rs @@ -7,35 +7,36 @@ use common_lang_types::{ use intern::{string_key::Intern, Lookup}; use isograph_lang_types::{ reachable_variables, ClientFieldId, IsographSelectionVariant, LinkedFieldSelection, - ScalarFieldSelection, SelectableServerFieldId, SelectionFieldArgument, - UnvalidatedScalarFieldSelection, UnvalidatedSelection, VariableDefinition, + ScalarFieldSelection, SelectionFieldArgument, SelectionType, UnvalidatedScalarFieldSelection, + UnvalidatedSelection, VariableDefinition, }; use lazy_static::lazy_static; use crate::{ get_all_errors_or_all_ok, get_all_errors_or_all_ok_as_hashmap, get_all_errors_or_all_ok_iter, - get_all_errors_or_tuple_ok, ClientField, FieldType, ObjectTypeAndFieldName, RefetchStrategy, - SchemaObject, ServerFieldData, UnvalidatedClientField, UnvalidatedLinkedFieldSelection, - UnvalidatedRefetchFieldStrategy, UnvalidatedVariableDefinition, ValidateSchemaError, - ValidateSchemaResult, ValidatedClientField, ValidatedIsographSelectionVariant, - ValidatedLinkedFieldAssociatedData, ValidatedLinkedFieldSelection, - ValidatedRefetchFieldStrategy, ValidatedScalarFieldAssociatedData, - ValidatedScalarFieldSelection, ValidatedSchemaServerField, ValidatedSelection, - ValidatedVariableDefinition, + get_all_errors_or_tuple_ok, ClientField, ClientType, FieldType, ObjectTypeAndFieldName, + RefetchStrategy, SchemaObject, ServerFieldData, UnvalidatedClientField, + UnvalidatedLinkedFieldSelection, UnvalidatedRefetchFieldStrategy, + UnvalidatedVariableDefinition, ValidateSchemaError, ValidateSchemaResult, ValidatedClientField, + ValidatedIsographSelectionVariant, ValidatedLinkedFieldAssociatedData, + ValidatedLinkedFieldSelection, ValidatedRefetchFieldStrategy, + ValidatedScalarFieldAssociatedData, ValidatedScalarFieldSelection, ValidatedSchemaServerField, + ValidatedSelection, ValidatedVariableDefinition, }; type UsedVariables = BTreeSet; -type ClientFieldArgsMap = HashMap>>; +type ClientFieldArgsMap = + HashMap, Vec>>; lazy_static! { static ref ID: FieldArgumentName = "id".intern().into(); } pub(crate) fn validate_and_transform_client_fields( - client_fields: Vec, + client_fields: Vec>, schema_data: &ServerFieldData, server_fields: &[ValidatedSchemaServerField], -) -> Result, Vec>> { +) -> Result>, Vec>> { // TODO this smells. We probably should do this in two passes instead of doing it this // way. We are validating client fields, which includes validating their selections. When // validating a selection of a client field, we need to ensure that we pass the correct @@ -43,24 +44,33 @@ pub(crate) fn validate_and_transform_client_fields( // // For now, we'll make a new datastructure containing all of the client field's arguments, // cloned. - let client_field_args = get_all_errors_or_all_ok_as_hashmap(client_fields.iter().map( - |unvalidated_client_field| { - let validated_variable_definitions = validate_variable_definitions( - schema_data, - unvalidated_client_field.variable_definitions.clone(), - )?; - Ok((unvalidated_client_field.id, validated_variable_definitions)) - }, - ))?; + let client_field_args = + get_all_errors_or_all_ok_as_hashmap(client_fields.iter().map(|unvalidated_client| { + match unvalidated_client { + ClientType::ClientField(unvalidated_client_field) => { + let validated_variable_definitions = validate_variable_definitions( + schema_data, + unvalidated_client_field.variable_definitions.clone(), + )?; + Ok(( + ClientType::ClientField(unvalidated_client_field.id), + validated_variable_definitions, + )) + } + } + }))?; get_all_errors_or_all_ok_iter(client_fields.into_iter().map(|client_field| { - validate_client_field_selection_set( - schema_data, - client_field, - server_fields, - &client_field_args, - ) - .map_err(|err| err.into_iter()) + match client_field { + ClientType::ClientField(client_field) => validate_client_field_selection_set( + schema_data, + client_field, + server_fields, + &client_field_args, + ) + .map(ClientType::ClientField) + .map_err(|err| err.into_iter()), + } })) } @@ -123,7 +133,7 @@ fn validate_client_field_selection_set( }; let variable_definitions = client_field_args - .get(&top_level_client_field.id) + .get(&ClientType::ClientField(top_level_client_field.id)) .expect( "Expected variable definitions to exist. \ This is indicative of a bug in Isograph", @@ -328,8 +338,8 @@ fn validate_field_type_exists_and_is_scalar( variable_definitions, )?; - match server_field.associated_data.inner_non_null() { - SelectableServerFieldId::Scalar(_) => Ok(ScalarFieldSelection { + match &server_field.associated_data { + SelectionType::Scalar(_) => Ok(ScalarFieldSelection { name: scalar_field_selection.name, associated_data: ValidatedScalarFieldAssociatedData { location: FieldType::ServerField(*server_field_id), @@ -358,14 +368,14 @@ fn validate_field_type_exists_and_is_scalar( arguments: scalar_field_selection.arguments, directives: scalar_field_selection.directives, }), - SelectableServerFieldId::Object(object_id) => Err(WithLocation::new( + SelectionType::Object(object_id) => Err(WithLocation::new( ValidateSchemaError::ClientFieldSelectionFieldIsNotScalar { field_parent_type_name: scalar_field_selection_parent_object.name, field_name: scalar_field_name, field_type: "an object", target_type_name: top_level_client_field_info .schema_data - .object(object_id) + .object(object_id.type_name.inner_non_null()) .name .into(), client_field_parent_type_name: top_level_client_field_info @@ -379,13 +389,15 @@ fn validate_field_type_exists_and_is_scalar( )), } } - FieldType::ClientField(client_field_id) => validate_client_field( - client_field_id, - scalar_field_selection, - used_variables, - variable_definitions, - top_level_client_field_info, - ), + FieldType::ClientField(ClientType::ClientField(client_field_id)) => { + validate_client_field( + client_field_id, + scalar_field_selection, + used_variables, + variable_definitions, + top_level_client_field_info, + ) + } }, None => Err(WithLocation::new( ValidateSchemaError::ClientFieldSelectionFieldDoesNotExist { @@ -412,7 +424,7 @@ fn validate_client_field( ) -> ValidateSchemaResult { let argument_definitions = top_level_client_field_info .client_field_args - .get(client_field_id) + .get(&ClientType::ClientField(*client_field_id)) .expect( "Expected client field to exist in map. \ This is indicative of a bug in Isograph.", @@ -467,15 +479,15 @@ fn validate_field_type_exists_and_is_linked( FieldType::ServerField(server_field_id) => { let server_field = &top_level_client_field_info.server_fields[server_field_id.as_usize()]; - match server_field.associated_data.inner_non_null() { - SelectableServerFieldId::Scalar(scalar_id) => Err(WithLocation::new( + match &server_field.associated_data { + SelectionType::Scalar(scalar_id) => Err(WithLocation::new( ValidateSchemaError::ClientFieldSelectionFieldIsScalar { field_parent_type_name: field_parent_object.name, field_name: linked_field_name, field_type: "a scalar", target_type_name: top_level_client_field_info .schema_data - .scalar(scalar_id) + .scalar(scalar_id.inner_non_null()) .name .item .into(), @@ -488,11 +500,11 @@ fn validate_field_type_exists_and_is_linked( }, linked_field_selection.name.location, )), - SelectableServerFieldId::Object(object_id) => { + SelectionType::Object(object_id) => { let linked_field_target_object = top_level_client_field_info .schema_data .server_objects - .get(object_id.as_usize()) + .get(object_id.type_name.inner_non_null().as_usize()) .unwrap(); let missing_arguments = get_missing_arguments_and_validate_argument_types( @@ -524,7 +536,8 @@ fn validate_field_type_exists_and_is_linked( unwraps: linked_field_selection.unwraps, associated_data: ValidatedLinkedFieldAssociatedData { concrete_type: linked_field_target_object.concrete_type, - parent_object_id: object_id, + parent_object_id: object_id.type_name.inner_non_null(), + field_id: FieldType::ServerField(server_field.id), selection_variant: match linked_field_selection.associated_data { IsographSelectionVariant::Regular => { assert_no_missing_arguments(missing_arguments, linked_field_selection.name.location)?; @@ -535,7 +548,6 @@ fn validate_field_type_exists_and_is_linked( ValidatedIsographSelectionVariant::Loadable((l, missing_arguments)) }, }, - variant: server_field.variant.clone() }, arguments: linked_field_selection.arguments, directives: linked_field_selection.directives, diff --git a/crates/isograph_schema/src/validate_entrypoint.rs b/crates/isograph_schema/src/validate_entrypoint.rs index 30b158e4..a1f7819d 100644 --- a/crates/isograph_schema/src/validate_entrypoint.rs +++ b/crates/isograph_schema/src/validate_entrypoint.rs @@ -7,7 +7,7 @@ use isograph_lang_types::{ }; use thiserror::Error; -use crate::{FieldType, UnvalidatedSchema}; +use crate::{ClientType, FieldType, UnvalidatedSchema}; impl UnvalidatedSchema { pub fn validate_entrypoint_type_and_field( @@ -96,7 +96,9 @@ impl UnvalidatedSchema { }, Location::new(text_source, field_name.span), )), - FieldType::ClientField(client_field_id) => Ok(*client_field_id), + FieldType::ClientField(ClientType::ClientField(client_field_id)) => { + Ok(*client_field_id) + } }, None => Err(WithLocation::new( ValidateEntrypointDeclarationError::ClientFieldMustExist { diff --git a/crates/isograph_schema/src/validate_schema.rs b/crates/isograph_schema/src/validate_schema.rs index 8a944417..909f56c4 100644 --- a/crates/isograph_schema/src/validate_schema.rs +++ b/crates/isograph_schema/src/validate_schema.rs @@ -7,8 +7,8 @@ use common_lang_types::{ use intern::Lookup; use isograph_lang_types::{ ClientFieldId, LinkedFieldSelection, LoadableDirectiveParameters, ScalarFieldSelection, - SelectableServerFieldId, Selection, SelectionFieldArgument, ServerFieldId, ServerObjectId, - ServerScalarId, TypeAnnotation, VariableDefinition, + SelectableServerFieldId, Selection, SelectionFieldArgument, SelectionType, ServerFieldId, + ServerObjectId, ServerScalarId, TypeAnnotation, VariableDefinition, }; use thiserror::Error; @@ -16,7 +16,7 @@ use crate::{ validate_client_field::validate_and_transform_client_fields, validate_server_field::validate_and_transform_server_fields, ClientField, ClientFieldVariant, FieldType, ImperativelyLoadedFieldVariant, Schema, SchemaIdField, SchemaObject, - SchemaServerField, SchemaServerFieldVariant, SchemaValidationState, ServerFieldData, + SchemaServerField, SchemaValidationState, ServerFieldData, ServerFieldTypeAssociatedData, UnvalidatedSchema, UnvalidatedVariableDefinition, UseRefetchFieldRefetchStrategy, ValidateEntrypointDeclarationError, }; @@ -59,12 +59,11 @@ pub type ValidatedSchemaIdField = SchemaIdField; #[derive(Debug, Clone)] pub struct ValidatedLinkedFieldAssociatedData { pub parent_object_id: ServerObjectId, + pub field_id: FieldType, // N.B. we don't actually support loadable linked fields pub selection_variant: ValidatedIsographSelectionVariant, /// Some if the object is concrete; None otherwise. pub concrete_type: Option, - - pub variant: SchemaServerFieldVariant, } #[derive(Debug, Clone)] @@ -87,10 +86,15 @@ pub enum ValidatedIsographSelectionVariant { pub type MissingArguments = Vec; +pub type ValidatedServerFieldTypeAssociatedData = SelectionType< + ServerFieldTypeAssociatedData>, + TypeAnnotation, +>; + #[derive(Debug)] pub struct ValidatedSchemaState {} impl SchemaValidationState for ValidatedSchemaState { - type ServerFieldTypeAssociatedData = TypeAnnotation; + type ServerFieldTypeAssociatedData = ValidatedServerFieldTypeAssociatedData; type ClientFieldSelectionScalarFieldAssociatedData = ValidatedScalarFieldAssociatedData; type ClientFieldSelectionLinkedFieldAssociatedData = ValidatedLinkedFieldAssociatedData; type VariableDefinitionInnerType = SelectableServerFieldId; diff --git a/crates/isograph_schema/src/validate_server_field.rs b/crates/isograph_schema/src/validate_server_field.rs index 855bbc7b..f9d70d72 100644 --- a/crates/isograph_schema/src/validate_server_field.rs +++ b/crates/isograph_schema/src/validate_server_field.rs @@ -1,14 +1,16 @@ use common_lang_types::{SelectableFieldName, UnvalidatedTypeName, WithLocation}; use graphql_lang_types::GraphQLTypeAnnotation; use isograph_lang_types::{ - SelectableServerFieldId, ServerObjectId, TypeAnnotation, VariableDefinition, + SelectionType, ServerObjectId, ServerScalarId, TypeAnnotation, VariableDefinition, }; use crate::{ get_all_errors_or_all_ok, get_all_errors_or_all_ok_iter, SchemaServerField, - SchemaValidationState, ServerFieldData, UnvalidatedSchemaSchemaField, UnvalidatedSchemaState, - UnvalidatedVariableDefinition, ValidateSchemaError, ValidateSchemaResult, - ValidatedSchemaServerField, ValidatedVariableDefinition, + SchemaServerFieldVariant, SchemaValidationState, ServerFieldData, + ServerFieldTypeAssociatedData, ServerFieldTypeAssociatedDataInlineFragment, + UnvalidatedSchemaSchemaField, UnvalidatedSchemaState, UnvalidatedVariableDefinition, + ValidateSchemaError, ValidateSchemaResult, ValidatedSchemaServerField, + ValidatedVariableDefinition, }; pub(crate) fn validate_and_transform_server_fields( @@ -27,12 +29,13 @@ fn validate_and_transform_server_field( schema_data: &ServerFieldData, ) -> Result>> { // TODO rewrite as field.map(...).transpose() - let (empty_field, server_field_type) = field.split(); + let (empty_field, server_field) = field.split(); let mut errors = vec![]; let field_type = - match validate_server_field_type_exists(schema_data, &server_field_type, &empty_field) { + match validate_server_field_type_exists(schema_data, &server_field.type_name, &empty_field) + { Ok(type_annotation) => Some(type_annotation), Err(e) => { errors.push(e); @@ -57,16 +60,39 @@ fn validate_and_transform_server_field( }; if let Some(field_type) = field_type { + let variant = match server_field.variant { + SchemaServerFieldVariant::LinkedField => SchemaServerFieldVariant::LinkedField, + SchemaServerFieldVariant::InlineFragment(associated_data) => match field_type { + SelectionType::Scalar(_) => { + panic!("Expected inline fragment server field type to be an object") + } + SelectionType::Object(_) => SchemaServerFieldVariant::InlineFragment( + ServerFieldTypeAssociatedDataInlineFragment { + concrete_type: associated_data.concrete_type, + condition_selection_set: associated_data.condition_selection_set, + server_field_id: associated_data.server_field_id, + }, + ), + }, + }; + if let Some(valid_arguments) = valid_arguments { return Ok(SchemaServerField { description: empty_field.description, name: empty_field.name, id: empty_field.id, - associated_data: field_type, + associated_data: match field_type { + SelectionType::Scalar(scalar_id) => SelectionType::Scalar(scalar_id), + SelectionType::Object(object_id) => { + SelectionType::Object(ServerFieldTypeAssociatedData { + type_name: object_id, + variant, + }) + } + }, parent_type_id: empty_field.parent_type_id, arguments: valid_arguments, is_discriminator: empty_field.is_discriminator, - variant: empty_field.variant, }); } } @@ -81,13 +107,24 @@ fn validate_server_field_type_exists( (), ::VariableDefinitionInnerType, >, -) -> ValidateSchemaResult> { +) -> ValidateSchemaResult< + SelectionType, TypeAnnotation>, +> { // look up the item in defined_types. If it's not there, error. match schema_data.defined_types.get(server_field_type.inner()) { // Why do we need to clone here? Can we avoid this? - Some(type_id) => Ok(TypeAnnotation::from_graphql_type_annotation( - server_field_type.clone().map(|_| *type_id), - )), + Some(type_id) => Ok(match type_id { + SelectionType::Scalar(scalar_id) => { + SelectionType::Scalar(TypeAnnotation::from_graphql_type_annotation( + server_field_type.clone().map(|_| *scalar_id), + )) + } + SelectionType::Object(object_id) => { + SelectionType::Object(TypeAnnotation::from_graphql_type_annotation( + server_field_type.clone().map(|_| *object_id), + )) + } + }), None => Err(WithLocation::new( ValidateSchemaError::FieldTypenameDoesNotExist { parent_type_name: schema_data.object(field.parent_type_id).name,