From 13ad82baefcb33b01e3643fb24dc1c5b7409c355 Mon Sep 17 00:00:00 2001 From: Shane Myrick Date: Mon, 31 Jul 2023 11:17:08 -0700 Subject: [PATCH 01/95] Create README.md for federation-internals (#2704) * Create README.md for federation-internals * Update internals-js/README.md Co-authored-by: Trevor Scheer --------- Co-authored-by: Trevor Scheer --- internals-js/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 internals-js/README.md diff --git a/internals-js/README.md b/internals-js/README.md new file mode 100644 index 000000000..3e47ca123 --- /dev/null +++ b/internals-js/README.md @@ -0,0 +1,10 @@ +# @apollo/federation-internals + +This is an **internal** package for core Federation components. This package may ship breaking changes at any time and does not make the same efforts to avoid breaking changes as our other Federation packages. + +If you are looking to create a subgraph in JavaScript, use [Apollo Server with @apollo/subgraph](https://www.apollographql.com/docs/apollo-server/using-federation/apollo-subgraph-setup). + +If you are looking to run a supergraph, use [Apollo Router or Apollo Gateway](https://www.apollographql.com/docs/federation/building-supergraphs/router). + +If you want to run composition locally, use the [Rover command `supergraph compose`](https://www.apollographql.com/docs/rover/commands/supergraphs#composing-a-supergraph-schema) + From 695db84cb4c68fccde778349f0c4a305cec2bd77 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 31 Jul 2023 11:47:38 -0700 Subject: [PATCH 02/95] block mfh@12 updates --- renovate.json5 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/renovate.json5 b/renovate.json5 index d14e3cd1b..7ee3b9d96 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -145,6 +145,11 @@ { "matchPackageNames": ["@typescript-eslint/eslint-plugin"], "allowedVersions": "5.x", + }, + // make-fetch-happen@12 drops support for node 14 + { + "matchPackageNames": ["make-fetch-happen"], + "allowedVersions": "11.x", } ] } From 0fad9d8837ec08dc6093a2e34efdd7401dd5c446 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 1 Aug 2023 16:14:40 -0600 Subject: [PATCH 03/95] Rename nav item and point to linter rules page --- docs/source/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/config.json b/docs/source/config.json index a51612e1b..adb151207 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -54,7 +54,7 @@ }, "Debugging & Metrics": { "Error codes": "/errors", - "Composition hints": "/hints", + "Composition linting": "https://www.apollographql.com/docs/graphos/delivery/linter-rules", "Federated trace data": "/metrics", "OpenTelemetry": "/opentelemetry" }, From 934ee83785e1290036e9659c315bdba01525127c Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 1 Aug 2023 16:16:02 -0600 Subject: [PATCH 04/95] Add redirect --- docs/source/_redirects | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/_redirects b/docs/source/_redirects index cd5a4504c..f23899a18 100644 --- a/docs/source/_redirects +++ b/docs/source/_redirects @@ -22,3 +22,5 @@ /managed-federation/monitoring/ /docs/federation/performance/monitoring /api/apollo-federation/ /docs/federation/api/apollo-subgraph/ + +/hints /docs/graphos/delivery/linter-rules \ No newline at end of file From 91c04bb3b3bc6e0b7072ff78451b768831e41166 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 1 Aug 2023 16:25:15 -0600 Subject: [PATCH 05/95] Add callout in composition doc --- docs/source/federated-types/composition.mdx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/source/federated-types/composition.mdx b/docs/source/federated-types/composition.mdx index 8fe500a86..3172529f4 100644 --- a/docs/source/federated-types/composition.mdx +++ b/docs/source/federated-types/composition.mdx @@ -47,6 +47,8 @@ graph LR; To learn how to perform composition with managed federation, see the [quickstart](../quickstart/studio-composition/). +> GraphOS also provides a [schema linter](/graphos/delivery/schema-linter) with [composition specific rules](/graphos/delivery/linter-rules#composition-rules) to help you follow best practices. You can set up schema checks for your graph in GraphOS Studio or perform one-off linting with the Rover CLI. Check out the [schema linting](/graphos/delivery/schema-linter) docs to learn more. + ### Manually with the Rover CLI The [Rover CLI](https://www.apollographql.com/docs/rover/) supports a `supergraph compose` command that you can use to compose a supergraph schema from a collection of subgraph schemas: @@ -216,7 +218,7 @@ This is a useful solution for shared types that encapsulate simple scalar data. > You can use the `@inaccessible` directive to incrementally add a value type field to multiple subgraphs _without_ breaking composition. [Learn more.](./sharing-types/#adding-new-shared-fields) -#### Make the shared type an entity. +#### Make the shared type an entity

@@ -363,7 +365,6 @@ As you can see, the supergraph schema includes only the input fields and argumen > > When defining input types and field arguments in multiple subgraphs, make sure that every non-nullable field and argument is consistent in _every subgraph_. For examples, see [Arguments](sharing-types#arguments). - ### Enums If an enum definition differs between subgraphs, the [composition strategy](#merging-types-from-multiple-subgraphs) depends on how the enum is used: From 091b00272ddbfc405755e282b18a9ad9398d5dc5 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 1 Aug 2023 16:26:18 -0600 Subject: [PATCH 06/95] Remove hints page --- docs/source/hints.md | 72 -------------------------------------------- 1 file changed, 72 deletions(-) delete mode 100644 docs/source/hints.md diff --git a/docs/source/hints.md b/docs/source/hints.md deleted file mode 100644 index 6d255e300..000000000 --- a/docs/source/hints.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: Composition hints ---- - -When you successfully [compose](./federated-types/composition) the schemas provided by your [subgraphs](./building-supergraphs/subgraphs-overview/) into a supergraph schema, the composition process might output **hints** that provide additional information about the result. Hints are primarily informative and _do not_ necessarily indicate that a problem needs to be fixed. - -Hints are categorized under the following levels: - -* `WARN`: Indicates a situation that might be expected but is usually temporary and should be double-checked. Typically, composition might have needed to ignore some elements from some subgraph when creating the supergraph. -* `INFO`: Suggests a potentially helpful improvement or highlights a noteworthy resolution made by composition. Can otherwise be ignored. -* `DEBUG`: Lower-level information that provides insight into the composition. These hints are of lesser importance/impact. - -Note that hints are first and foremost informative and don't necessarily correspond to a problem to be fixed. - -This document lists the hints that can be generated for each level, with a description of why each is generated. - - -The following hints might be generated during composition: - -## `WARN` - -
- -| Code | Description | Level | -|---|---|---| -| `INCONSISTENT_DEFAULT_VALUE_PRESENCE` | Indicates that an argument definition (of a field/input field/directive definition) has a default value in only some of the subgraphs that define the argument. | `WARN` | -| `INCONSISTENT_INPUT_OBJECT_FIELD` | Indicates that a field of an input object type definition is only defined in a subset of the subgraphs that declare the input object. | `WARN` | -| `INCONSISTENT_ENUM_VALUE_FOR_INPUT_ENUM` | Indicates that a value of an enum type definition (that is only used as an Input type) has not been merged into the supergraph because it is defined in only a subset of the subgraphs that declare the enum | `WARN` | -| `INCONSISTENT_EXECUTABLE_DIRECTIVE_PRESENCE` | Indicates that an executable directive definition is declared in only some of the subgraphs. | `WARN` | -| `NO_EXECUTABLE_DIRECTIVE_INTERSECTION` | Indicates that, for an executable directive definition, no location for it appears in all subgraphs. | `WARN` | -| `INCONSISTENT_EXECUTABLE_DIRECTIVE_REPEATABLE` | Indicates that an executable directive definition is marked repeatable in only a subset of the subgraphs (and will not be repeatable in the supergraph). | `WARN` | -| `INCONSISTENT_EXECUTABLE_DIRECTIVE_LOCATIONS` | Indicates that an executiable directive definition is declared with inconsistent locations across subgraphs (and will use the intersection of all locations in the supergraph). | `WARN` | -| `INCONSISTENT_DESCRIPTION` | Indicates that an element has a description in more than one subgraph, and the descriptions are not equal. | `WARN` | -| `INCONSISTENT_ARGUMENT_PRESENCE` | Indicates that an optional argument (of a field or directive definition) is not present in all subgraphs and will not be part of the supergraph. | `WARN` | -| `FROM_SUBGRAPH_DOES_NOT_EXIST` | Source subgraph specified by @override directive does not exist | `WARN` | -| `INCONSISTENT_NON_REPEATABLE_DIRECTIVE_ARGUMENTS` | A non-repeatable directive is applied to a schema element in different subgraphs but with arguments that are different. | `WARN` | -| `DIRECTIVE_COMPOSITION_WARN` | Indicates that an issue was detected when composing custom directives. | `WARN` | -| `INCONSISTENT_RUNTIME_TYPES_FOR_SHAREABLE_RETURN` | Indicates that a @shareable field returns different sets of runtime types in the different subgraphs in which it is defined. | `WARN` | - -
- -## `INFO` - -
- -| Code | Description | Level | -|---|---|---| -| `INCONSISTENT_BUT_COMPATIBLE_FIELD_TYPE` | Indicates that a field does not have the exact same types in all subgraphs, but that the types are "compatible" (2 types are compatible if one is a non-nullable version of the other, a list version, a subtype, or a combination of the former). | `INFO` | -| `INCONSISTENT_BUT_COMPATIBLE_ARGUMENT_TYPE` | Indicates that an argument type (of a field/input field/directive definition) does not have the exact same type in all subgraphs, but that the types are "compatible" (two types are compatible if one is a non-nullable version of the other, a list version, a subtype, or a combination of the former). | `INFO` | -| `INCONSISTENT_ENTITY` | Indicates that an object is declared as an entity (has a `@key`) in only some of the subgraphs in which the object is defined. | `INFO` | -| `OVERRIDDEN_FIELD_CAN_BE_REMOVED` | Field has been overridden by another subgraph. Consider removing. | `INFO` | -| `OVERRIDE_DIRECTIVE_CAN_BE_REMOVED` | Field with @override directive no longer exists in source subgraph, the directive can be safely removed | `INFO` | -| `MERGED_NON_REPEATABLE_DIRECTIVE_ARGUMENTS` | A non-repeatable directive has been applied to a schema element in different subgraphs with different arguments and the arguments values were merged using the directive configured strategies. | `INFO` | -| `DIRECTIVE_COMPOSITION_INFO` | Indicates that an issue was detected when composing custom directives. | `INFO` | - -
- -## `DEBUG` - -
- -| Code | Description | Level | -|---|---|---| -| `INCONSISTENT_OBJECT_VALUE_TYPE_FIELD` | Indicates that a field of an object "value type" (has no `@key` in any subgraph) is not defined in all the subgraphs that declare the type. | `DEBUG` | -| `INCONSISTENT_INTERFACE_VALUE_TYPE_FIELD` | Indicates that a field of an interface "value type" (has no `@key` in any subgraph) is not defined in all the subgraphs that declare the type. | `DEBUG` | -| `INCONSISTENT_UNION_MEMBER` | Indicates that a member of a union type definition is only defined in a subset of the subgraphs that declare the union. | `DEBUG` | -| `INCONSISTENT_ENUM_VALUE_FOR_OUTPUT_ENUM` | Indicates that a value of an enum type definition (that is only used as an Output type, or is unused) has been merged in the supergraph but is defined in only a subset of the subgraphs that declare the enum | `DEBUG` | -| `INCONSISTENT_TYPE_SYSTEM_DIRECTIVE_REPEATABLE` | Indicates that a type system directive definition is marked repeatable in only a subset of the subgraphs that declare the directive (and will be repeatable in the supergraph). | `DEBUG` | -| `INCONSISTENT_TYPE_SYSTEM_DIRECTIVE_LOCATIONS` | Indicates that a type system directive definition is declared with inconsistent locations across subgraphs (and will use the union of all locations in the supergraph). | `DEBUG` | -| `UNUSED_ENUM_TYPE` | Indicates that an enum type is defined in some subgraphs but is unused (no field/argument references it). All the values from subgraphs defining that enum will be included in the supergraph. | `DEBUG` | - -
From aa5bd5978904dc84f1562692e44c5cae89a938cf Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Wed, 2 Aug 2023 12:37:45 -0700 Subject: [PATCH 07/95] Fix `fallbackPollIntervalInMs` behavior (#2709) The `fallbackPollIntervalInMs` serves 2 purposes: * it allows users to provide an Uplink poll interval if Uplink doesn't provide one * it allows users to use a longer poll interval that what's prescribed by Uplink The second bullet is how the configuration option is documented, but not how it was previously implemented. This change corrects the behavior to respect this configuration if it's provided AND is longer than the Uplink interval. --- .changeset/grumpy-hairs-sleep.md | 12 ++++ .cspell/cspell-dict.txt | 1 + .../__tests__/UplinkSupergraphManager.test.ts | 69 ++++++++++++++++++- .../UplinkSupergraphManager/index.ts | 13 ++++ 4 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 .changeset/grumpy-hairs-sleep.md diff --git a/.changeset/grumpy-hairs-sleep.md b/.changeset/grumpy-hairs-sleep.md new file mode 100644 index 000000000..adac4c35b --- /dev/null +++ b/.changeset/grumpy-hairs-sleep.md @@ -0,0 +1,12 @@ +--- +"@apollo/gateway": patch +--- + +Fix `fallbackPollIntervalInMs` behavior. + +The `fallbackPollIntervalInMs` serves 2 purposes: +* it allows users to provide an Uplink poll interval if Uplink doesn't provide one +* it allows users to use a longer poll interval that what's prescribed by Uplink + +The second bullet is how the configuration option is documented, but not how it was previously implemented. This change corrects the behavior to respect this configuration if it's provided AND is longer than the Uplink interval. + \ No newline at end of file diff --git a/.cspell/cspell-dict.txt b/.cspell/cspell-dict.txt index 517d06da6..215e120a6 100644 --- a/.cspell/cspell-dict.txt +++ b/.cspell/cspell-dict.txt @@ -241,6 +241,7 @@ unaliased uncacheable uncesssary uncomposable +unconfigured undici unecesasry unecessarally diff --git a/gateway-js/src/supergraphManagers/UplinkSupergraphManager/__tests__/UplinkSupergraphManager.test.ts b/gateway-js/src/supergraphManagers/UplinkSupergraphManager/__tests__/UplinkSupergraphManager.test.ts index 19e46b6f2..9411055de 100644 --- a/gateway-js/src/supergraphManagers/UplinkSupergraphManager/__tests__/UplinkSupergraphManager.test.ts +++ b/gateway-js/src/supergraphManagers/UplinkSupergraphManager/__tests__/UplinkSupergraphManager.test.ts @@ -1,6 +1,12 @@ import mockedEnv from 'mocked-env'; import { UplinkSupergraphManager } from '@apollo/gateway'; +import nock from 'nock'; +import { SUPERGRAPH_SDL_QUERY } from '../loadSupergraphSdlFromStorage'; +import { + nockBeforeEach, + nockAfterEach, +} from '../../../__tests__/nockAssertions'; let cleanUp: (() => void) | undefined; @@ -29,7 +35,9 @@ describe('UplinkSupergraphManager', () => { it('uses default uplink URLs', async () => { const manager = new UplinkSupergraphManager({ apiKey, graphRef, logger }); - expect(manager.uplinkEndpoints).toEqual(UplinkSupergraphManager.DEFAULT_UPLINK_ENDPOINTS); + expect(manager.uplinkEndpoints).toEqual( + UplinkSupergraphManager.DEFAULT_UPLINK_ENDPOINTS, + ); }); it('can set uplink URLs via config', async () => { @@ -62,4 +70,63 @@ describe('UplinkSupergraphManager', () => { expect(manager.uplinkEndpoints).toEqual([uplinkUrl]); }); }); + + describe('fallbackPollIntervalInMs', () => { + beforeEach(nockBeforeEach); + afterEach(nockAfterEach); + + it('uses the provided fallback interval when Uplink provides a shorter interval', async () => { + // These are the two interesting values for the test. We just want to make + // sure that when the UplinkSupergraphManager receives 5s from Uplink that + // it opts for the fallbackInterval since it's longer. + const fallbackPollIntervalInMs = 15_000; + // This is used in the mock response from Uplink below + const minDelaySeconds = 5; + + const manager = new UplinkSupergraphManager({ + apiKey, + graphRef, + fallbackPollIntervalInMs, + }); + + mockUplinkResponse(minDelaySeconds); + + const { supergraphSdl, cleanup } = await manager.initialize({ + // These aren't really used for anything, just keeping TS happy + update: jest.fn(), + healthCheck: jest.fn(), + getDataSource: jest.fn(), + }); + cleanUp = cleanup; + + // validates we got a response from "Uplink" that we're happy with + expect(supergraphSdl).toEqual('supergraph sdl'); + + // validates that the fallback interval was used instead of the one from Uplink + expect(manager['pollIntervalMs']).toEqual(fallbackPollIntervalInMs); + }); + }); }); + +function mockUplinkResponse(minDelaySeconds: number) { + nock('https://uplink.api.apollographql.com/') + .post('/', { + query: SUPERGRAPH_SDL_QUERY, + variables: { + ref: graphRef, + apiKey, + ifAfterId: null, + }, + }) + .reply(200, { + data: { + __typename: 'Query', + routerConfig: { + __typename: 'RouterConfigResult', + id: '123', + supergraphSdl: 'supergraph sdl', + minDelaySeconds, + }, + }, + }); +} diff --git a/gateway-js/src/supergraphManagers/UplinkSupergraphManager/index.ts b/gateway-js/src/supergraphManagers/UplinkSupergraphManager/index.ts index 83d8409c1..4edb97cce 100644 --- a/gateway-js/src/supergraphManagers/UplinkSupergraphManager/index.ts +++ b/gateway-js/src/supergraphManagers/UplinkSupergraphManager/index.ts @@ -64,6 +64,7 @@ export class UplinkSupergraphManager implements SupergraphManager { UplinkSupergraphManager.DEFAULT_REQUEST_TIMEOUT_MS; private initialMaxRetries: number; private pollIntervalMs: number = UplinkSupergraphManager.MIN_POLL_INTERVAL_MS; + private fallbackPollIntervalInMs?: number; private logger: Logger; private update?: SupergraphSdlUpdateFunction; private shouldRunSubgraphHealthcheck: boolean = false; @@ -117,6 +118,7 @@ export class UplinkSupergraphManager implements SupergraphManager { this.initialMaxRetries = initialMaxRetries ?? this.maxRetries; this.pollIntervalMs = fallbackPollIntervalInMs ?? this.pollIntervalMs; + this.fallbackPollIntervalInMs = fallbackPollIntervalInMs; if (this.pollIntervalMs < UplinkSupergraphManager.MIN_POLL_INTERVAL_MS) { this.logger.warn( 'Polling Apollo services at a frequency of less than once per 10 seconds (10000) is disallowed. Instead, the minimum allowed pollInterval of 10000 will be used. Please reconfigure your `fallbackPollIntervalInMs` accordingly. If this is problematic for your team, please contact support.', @@ -229,6 +231,17 @@ export class UplinkSupergraphManager implements SupergraphManager { supergraphSdl = result.supergraphSdl; if (result?.minDelaySeconds) { this.pollIntervalMs = result.minDelaySeconds * 1000; + + // We only want to take the max of the two _if_ a fallback interval is + // configured. If we take the max above unconditionally, then a gateway + // with an unconfigured fallback interval will only ever lengthen its + // poll interval rather than adapt to changes coming from Uplink. + if (this.fallbackPollIntervalInMs) { + this.pollIntervalMs = Math.max( + this.pollIntervalMs, + this.fallbackPollIntervalInMs, + ); + } } } catch (e) { this.logger.debug( From 9c0815f8145524ac0b10cb7df2d28ce191ec4103 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Wed, 2 Aug 2023 19:39:24 -0600 Subject: [PATCH 08/95] Add info about mismatched keys --- docs/source/entities-advanced.mdx | 28 ++++++++++++++++++++++++++++ docs/source/entities.mdx | 4 +--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/docs/source/entities-advanced.mdx b/docs/source/entities-advanced.mdx index ebe329f7f..0f0c03060 100644 --- a/docs/source/entities-advanced.mdx +++ b/docs/source/entities-advanced.mdx @@ -57,6 +57,34 @@ type Organization { } ``` +### Mismatched `@key`s + +Conventionally, `@key` fields for an entity are the same between subgraphs, but this isn't required. + +For example, you could define a `Product` entity shared between products and inventory subgraphs, one with the `id` field as the `@key` field, and one with `sku` as the `@key` field: + + + +```graphql title="Products subgraph" +type Product @key(fields: "id") { + id: ID! + name: String! + price: Int +} +``` + +```graphql title="Inventory subgraph" +type Product @key(fields: "sku") { + sku: ID! + inStock: Boolean! +} +``` + + + +As long as `id` is a unique identifier within the products subgraph, and `sku` is a unique identifier for the same product in the inventory subgraph, the subgraphs can resolve the `Product` type. +In other words, **each key declaration is specific to the subgraph it belongs to** and you don't need to match `@key` fields between graphs. + ## Migrating entities and fields As your supergraph grows, you might want to move parts of an entity to a different subgraph. This section describes how to perform these migrations safely. diff --git a/docs/source/entities.mdx b/docs/source/entities.mdx index 46f743318..ea1f82c9a 100644 --- a/docs/source/entities.mdx +++ b/docs/source/entities.mdx @@ -55,14 +55,12 @@ type Product @key(fields: "id") { The `@key` directive defines the entity's **primary key**, which consists of one or more of the type's `fields`. In the example above, the `Product` entity's primary key is its `id` field. -**Every instance of an entity must be uniquely identifiable by its `@key` fields.** This is what enables your router to associate field data from _different_ subgraphs with the _same_ entity instance. +**Every instance of an entity must be uniquely identifiable by its `@key` fields within its own subgraph.** [See advanced options for `@key`s](./entities-advanced/#advanced-keys) for more information. > **An entity's `@key` _cannot_ include:** > > * Fields that return a union or interface > * Fields that take arguments -> -> [See advanced options for `@key`s.](./entities-advanced/#advanced-keys) ### 2. Define a reference resolver From 35179f086ce973e9ae7bb455f7ea7d73cdc10f69 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Thu, 3 Aug 2023 15:17:47 -0700 Subject: [PATCH 09/95] Prevent over-eager merging of fields which have differing directive applications (#2713) Fix over-eager merging of fields with different directive applications Previously, the following query would incorrectly combine the selection set of `hello`, with both fields ending up under the @skip condition: ```graphql query Test($skipField: Boolean!) { hello @skip(if: $skipField) { world } hello { goodbye } } ``` This change identifies those two selections on `hello` as unique while constructing our operation representation so they aren't merged at all, leaving it to the subgraph to handle the operation as-is. --- .changeset/curvy-rockets-bow.md | 21 ++ internals-js/src/__tests__/operations.test.ts | 25 +++ internals-js/src/operations.ts | 2 +- .../buildPlan.directiveMerging.test.ts | 192 ++++++++++++++++++ 4 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 .changeset/curvy-rockets-bow.md create mode 100644 query-planner-js/src/__tests__/buildPlan.directiveMerging.test.ts diff --git a/.changeset/curvy-rockets-bow.md b/.changeset/curvy-rockets-bow.md new file mode 100644 index 000000000..990f26943 --- /dev/null +++ b/.changeset/curvy-rockets-bow.md @@ -0,0 +1,21 @@ +--- +"@apollo/query-planner": patch +"@apollo/federation-internals": patch +--- + +Fix over-eager merging of fields with different directive applications + +Previously, the following query would incorrectly combine the selection set of `hello`, with both fields ending up under the @skip condition: +```graphql +query Test($skipField: Boolean!) { + hello @skip(if: $skipField) { + world + } + hello { + goodbye + } +} +``` + +This change identifies those two selections on `hello` as unique while constructing our operation representation so they aren't merged at all, leaving it to the subgraph to handle the operation as-is. + \ No newline at end of file diff --git a/internals-js/src/__tests__/operations.test.ts b/internals-js/src/__tests__/operations.test.ts index c9c92404f..512860de8 100644 --- a/internals-js/src/__tests__/operations.test.ts +++ b/internals-js/src/__tests__/operations.test.ts @@ -9,6 +9,7 @@ import { FragmentRestrictionAtType, MutableSelectionSet, NamedFragmentDefinition import './matchers'; import { DocumentNode, FieldNode, GraphQLError, Kind, OperationDefinitionNode, OperationTypeNode, parse, SelectionNode, SelectionSetNode, validate } from 'graphql'; import { assert } from '../utils'; +import gql from 'graphql-tag'; function parseSchema(schema: string): Schema { try { @@ -2519,6 +2520,30 @@ describe('basic operations', () => { ['T', 'v2'], ]); }) + + test('fields are keyed on both name and directive applications', () => { + const operation = operationFromDocument(schema, gql` + query Test($skipIf: Boolean!) { + t { + v1 + } + t @skip(if: $skipIf) { + v2 + } + } + `); + + expect(operation.toString()).toMatchString(` + query Test($skipIf: Boolean!) { + t { + v1 + } + t @skip(if: $skipIf) { + v2 + } + } + `); + }) }); describe('MutableSelectionSet', () => { diff --git a/internals-js/src/operations.ts b/internals-js/src/operations.ts index 771c4725c..0103382d9 100644 --- a/internals-js/src/operations.ts +++ b/internals-js/src/operations.ts @@ -146,7 +146,7 @@ export class Field ex } key(): string { - return this.responseName(); + return this.responseName() + this.appliedDirectivesToString(); } asPathElement(): string { diff --git a/query-planner-js/src/__tests__/buildPlan.directiveMerging.test.ts b/query-planner-js/src/__tests__/buildPlan.directiveMerging.test.ts new file mode 100644 index 000000000..11509a9c0 --- /dev/null +++ b/query-planner-js/src/__tests__/buildPlan.directiveMerging.test.ts @@ -0,0 +1,192 @@ +import { operationFromDocument } from '@apollo/federation-internals'; +import gql from 'graphql-tag'; +import { composeAndCreatePlanner } from './testHelper'; + +describe('merging @skip / @include directives', () => { + const subgraph1 = { + name: 'S1', + typeDefs: gql` + type Query { + hello: Hello! + extraFieldToPreventSkipIncludeNodes: String! + } + + type Hello { + world: String! + goodbye: String! + } + `, + }; + + const [api, queryPlanner] = composeAndCreatePlanner(subgraph1); + + it('with fragment', () => { + const operation = operationFromDocument( + api, + gql` + query Test($skipField: Boolean!) { + ...ConditionalSkipFragment + hello { + world + } + extraFieldToPreventSkipIncludeNodes + } + + fragment ConditionalSkipFragment on Query { + hello @skip(if: $skipField) { + goodbye + } + } + `, + ); + + const plan = queryPlanner.buildQueryPlan(operation); + expect(plan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "S1") { + { + hello @skip(if: $skipField) { + goodbye + } + hello { + world + } + extraFieldToPreventSkipIncludeNodes + } + }, + } + `); + }); + + it('without fragment', () => { + const operation = operationFromDocument( + api, + gql` + query Test($skipField: Boolean!) { + hello @skip(if: $skipField) { + world + } + hello { + goodbye + } + extraFieldToPreventSkipIncludeNodes + } + `, + ); + + const plan = queryPlanner.buildQueryPlan(operation); + expect(plan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "S1") { + { + hello @skip(if: $skipField) { + world + } + hello { + goodbye + } + extraFieldToPreventSkipIncludeNodes + } + }, + } + `); + }); + + it('multiple applications identical', () => { + const operation = operationFromDocument( + api, + gql` + query Test($skipField: Boolean!, $includeField: Boolean!) { + hello @skip(if: $skipField) @include(if: $includeField) { + world + } + hello @skip(if: $skipField) @include(if: $includeField) { + goodbye + } + extraFieldToPreventSkipIncludeNodes + } + `, + ); + + const plan = queryPlanner.buildQueryPlan(operation); + expect(plan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "S1") { + { + hello @skip(if: $skipField) @include(if: $includeField) { + world + goodbye + } + extraFieldToPreventSkipIncludeNodes + } + }, + } + `); + }); + + it('multiple applications differing order', () => { + const operation = operationFromDocument( + api, + gql` + query Test($skipField: Boolean!, $includeField: Boolean!) { + hello @skip(if: $skipField) @include(if: $includeField) { + world + } + hello @include(if: $includeField) @skip(if: $skipField) { + goodbye + } + extraFieldToPreventSkipIncludeNodes + } + `, + ); + + const plan = queryPlanner.buildQueryPlan(operation); + expect(plan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "S1") { + { + hello @include(if: $includeField) @skip(if: $skipField) { + world + goodbye + } + extraFieldToPreventSkipIncludeNodes + } + }, + } + `); + }); + + it('multiple applications differing quantity', () => { + const operation = operationFromDocument( + api, + gql` + query Test($skipField: Boolean!, $includeField: Boolean!) { + hello @skip(if: $skipField) @include(if: $includeField) { + world + } + hello @include(if: $includeField) { + goodbye + } + extraFieldToPreventSkipIncludeNodes + } + `, + ); + + const plan = queryPlanner.buildQueryPlan(operation); + expect(plan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "S1") { + { + hello @skip(if: $skipField) @include(if: $includeField) { + world + } + hello @include(if: $includeField) { + goodbye + } + extraFieldToPreventSkipIncludeNodes + } + }, + } + `); + }); +}); From 1b07580b04402ffde1afe113aed38872d722e173 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 4 Aug 2023 09:23:39 -0600 Subject: [PATCH 10/95] Copy edit --- docs/source/entities-advanced.mdx | 10 ++++------ docs/source/entities.mdx | 4 +++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/source/entities-advanced.mdx b/docs/source/entities-advanced.mdx index 0f0c03060..6d5d326ff 100644 --- a/docs/source/entities-advanced.mdx +++ b/docs/source/entities-advanced.mdx @@ -57,11 +57,9 @@ type Organization { } ``` -### Mismatched `@key`s +### Differing `@key`s across subgraphs -Conventionally, `@key` fields for an entity are the same between subgraphs, but this isn't required. - -For example, you could define a `Product` entity shared between products and inventory subgraphs, one with the `id` field as the `@key` field, and one with `sku` as the `@key` field: +An entity often has the same @key fields across subgraphs, but this isn't required. For example, you can define a `Product` entity shared between products and inventory subgraphs, one with `id` as the `@key` field and one with `sku` as the `@key` field: @@ -82,8 +80,8 @@ type Product @key(fields: "sku") { -As long as `id` is a unique identifier within the products subgraph, and `sku` is a unique identifier for the same product in the inventory subgraph, the subgraphs can resolve the `Product` type. -In other words, **each key declaration is specific to the subgraph it belongs to** and you don't need to match `@key` fields between graphs. +As long as `id` is a unique identifier within the products subgraph and `sku` is a unique identifier for the same product in the inventory subgraph, the subgraphs can each resolve the `Product` type. +In other words, **each key declaration is specific to the subgraph it belongs to**, and you don't need to match `@key` fields between graphs. ## Migrating entities and fields diff --git a/docs/source/entities.mdx b/docs/source/entities.mdx index ea1f82c9a..0b72e35dc 100644 --- a/docs/source/entities.mdx +++ b/docs/source/entities.mdx @@ -55,13 +55,15 @@ type Product @key(fields: "id") { The `@key` directive defines the entity's **primary key**, which consists of one or more of the type's `fields`. In the example above, the `Product` entity's primary key is its `id` field. -**Every instance of an entity must be uniquely identifiable by its `@key` fields within its own subgraph.** [See advanced options for `@key`s](./entities-advanced/#advanced-keys) for more information. +**Every instance of an entity must be uniquely identifiable by its `@key` fields within its own subgraph.** > **An entity's `@key` _cannot_ include:** > > * Fields that return a union or interface > * Fields that take arguments +[See advanced options for `@key`s](./entities-advanced/#advanced-keys) for more information. + ### 2. Define a reference resolver The `@key` directive effectively tells the router, "This subgraph can resolve an instance of this entity if you provide its primary key." In order for this to be true, the subgraph needs to define a **reference resolver** for the entity. From 5b706b281ebc13c6ddad341d27ad195a3d991f08 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 4 Aug 2023 10:28:42 -0700 Subject: [PATCH 11/95] Spare gh user from getting tagged in our release notes --- .changeset/curvy-rockets-bow.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/curvy-rockets-bow.md b/.changeset/curvy-rockets-bow.md index 990f26943..b600fb2ec 100644 --- a/.changeset/curvy-rockets-bow.md +++ b/.changeset/curvy-rockets-bow.md @@ -5,7 +5,7 @@ Fix over-eager merging of fields with different directive applications -Previously, the following query would incorrectly combine the selection set of `hello`, with both fields ending up under the @skip condition: +Previously, the following query would incorrectly combine the selection set of `hello`, with both fields ending up under the `@skip` condition: ```graphql query Test($skipField: Boolean!) { hello @skip(if: $skipField) { From d5241e8044e2064c35641df8d403fae7b666c8a7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 4 Aug 2023 10:34:00 -0700 Subject: [PATCH 12/95] Version Packages (#2699) Co-authored-by: github-actions[bot] --- .changeset/curvy-rockets-bow.md | 21 ------------ .changeset/dull-gorillas-drive.md | 10 ------ .changeset/grumpy-hairs-sleep.md | 12 ------- composition-js/CHANGELOG.md | 7 ++++ composition-js/package.json | 6 ++-- .../CHANGELOG.md | 2 ++ .../package.json | 2 +- gateway-js/CHANGELOG.md | 22 +++++++++++++ gateway-js/package.json | 8 ++--- internals-js/CHANGELOG.md | 20 ++++++++++++ internals-js/package.json | 2 +- package-lock.json | 32 +++++++++---------- query-graphs-js/CHANGELOG.md | 6 ++++ query-graphs-js/package.json | 4 +-- query-planner-js/CHANGELOG.md | 23 +++++++++++++ query-planner-js/package.json | 6 ++-- subgraph-js/CHANGELOG.md | 6 ++++ subgraph-js/package.json | 4 +-- 18 files changed, 118 insertions(+), 75 deletions(-) delete mode 100644 .changeset/curvy-rockets-bow.md delete mode 100644 .changeset/dull-gorillas-drive.md delete mode 100644 .changeset/grumpy-hairs-sleep.md diff --git a/.changeset/curvy-rockets-bow.md b/.changeset/curvy-rockets-bow.md deleted file mode 100644 index b600fb2ec..000000000 --- a/.changeset/curvy-rockets-bow.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -"@apollo/query-planner": patch -"@apollo/federation-internals": patch ---- - -Fix over-eager merging of fields with different directive applications - -Previously, the following query would incorrectly combine the selection set of `hello`, with both fields ending up under the `@skip` condition: -```graphql -query Test($skipField: Boolean!) { - hello @skip(if: $skipField) { - world - } - hello { - goodbye - } -} -``` - -This change identifies those two selections on `hello` as unique while constructing our operation representation so they aren't merged at all, leaving it to the subgraph to handle the operation as-is. - \ No newline at end of file diff --git a/.changeset/dull-gorillas-drive.md b/.changeset/dull-gorillas-drive.md deleted file mode 100644 index e5b84d9ed..000000000 --- a/.changeset/dull-gorillas-drive.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@apollo/gateway": patch ---- - -Remove extraneous call to `span.setStatus()` on a span which has already ended. - -In cases where a subgraph responded with an error, we would sometimes try to set -the status of a span which had already ended. This resulted in a warning log to -the console (but no effect otherwise). This warning should no longer happen. - \ No newline at end of file diff --git a/.changeset/grumpy-hairs-sleep.md b/.changeset/grumpy-hairs-sleep.md deleted file mode 100644 index adac4c35b..000000000 --- a/.changeset/grumpy-hairs-sleep.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@apollo/gateway": patch ---- - -Fix `fallbackPollIntervalInMs` behavior. - -The `fallbackPollIntervalInMs` serves 2 purposes: -* it allows users to provide an Uplink poll interval if Uplink doesn't provide one -* it allows users to use a longer poll interval that what's prescribed by Uplink - -The second bullet is how the configuration option is documented, but not how it was previously implemented. This change corrects the behavior to respect this configuration if it's provided AND is longer than the Uplink interval. - \ No newline at end of file diff --git a/composition-js/CHANGELOG.md b/composition-js/CHANGELOG.md index ad425316d..d04faf3d2 100644 --- a/composition-js/CHANGELOG.md +++ b/composition-js/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG for `@apollo/composition` +## 2.5.2 +### Patch Changes + +- Updated dependencies [[`35179f08`](https://github.com/apollographql/federation/commit/35179f086ce973e9ae7bb455f7ea7d73cdc10f69)]: + - @apollo/federation-internals@2.5.2 + - @apollo/query-graphs@2.5.2 + ## 2.5.1 ### Patch Changes diff --git a/composition-js/package.json b/composition-js/package.json index 9f4d478a7..e00335e53 100644 --- a/composition-js/package.json +++ b/composition-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/composition", - "version": "2.5.1", + "version": "2.5.2", "description": "Apollo Federation composition utilities", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -27,8 +27,8 @@ "access": "public" }, "dependencies": { - "@apollo/federation-internals": "2.5.1", - "@apollo/query-graphs": "2.5.1" + "@apollo/federation-internals": "2.5.2", + "@apollo/query-graphs": "2.5.2" }, "peerDependencies": { "graphql": "^16.5.0" diff --git a/federation-integration-testsuite-js/CHANGELOG.md b/federation-integration-testsuite-js/CHANGELOG.md index 90922c22a..766b53a89 100644 --- a/federation-integration-testsuite-js/CHANGELOG.md +++ b/federation-integration-testsuite-js/CHANGELOG.md @@ -1,5 +1,7 @@ # CHANGELOG for `federation-integration-testsuite-js` +## 2.5.2 + ## 2.5.1 ## 2.5.0 diff --git a/federation-integration-testsuite-js/package.json b/federation-integration-testsuite-js/package.json index edadde5db..3c348f6cc 100644 --- a/federation-integration-testsuite-js/package.json +++ b/federation-integration-testsuite-js/package.json @@ -1,7 +1,7 @@ { "name": "apollo-federation-integration-testsuite", "private": true, - "version": "2.5.1", + "version": "2.5.2", "description": "Apollo Federation Integrations / Test Fixtures", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/gateway-js/CHANGELOG.md b/gateway-js/CHANGELOG.md index 00c89822f..afa090c84 100644 --- a/gateway-js/CHANGELOG.md +++ b/gateway-js/CHANGELOG.md @@ -1,5 +1,27 @@ # CHANGELOG for `@apollo/gateway` +## 2.5.2 +### Patch Changes + + +- Remove extraneous call to `span.setStatus()` on a span which has already ended. ([#2697](https://github.com/apollographql/federation/pull/2697)) + + In cases where a subgraph responded with an error, we would sometimes try to set + the status of a span which had already ended. This resulted in a warning log to + the console (but no effect otherwise). This warning should no longer happen. + +- Fix `fallbackPollIntervalInMs` behavior. ([#2709](https://github.com/apollographql/federation/pull/2709)) + + The `fallbackPollIntervalInMs` serves 2 purposes: + * it allows users to provide an Uplink poll interval if Uplink doesn't provide one + * it allows users to use a longer poll interval that what's prescribed by Uplink + + The second bullet is how the configuration option is documented, but not how it was previously implemented. This change corrects the behavior to respect this configuration if it's provided AND is longer than the Uplink interval. +- Updated dependencies [[`35179f08`](https://github.com/apollographql/federation/commit/35179f086ce973e9ae7bb455f7ea7d73cdc10f69)]: + - @apollo/query-planner@2.5.2 + - @apollo/federation-internals@2.5.2 + - @apollo/composition@2.5.2 + ## 2.5.1 ### Patch Changes diff --git a/gateway-js/package.json b/gateway-js/package.json index acde0257e..0f4c77e5a 100644 --- a/gateway-js/package.json +++ b/gateway-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/gateway", - "version": "2.5.1", + "version": "2.5.2", "description": "Apollo Gateway", "author": "Apollo ", "main": "dist/index.js", @@ -25,9 +25,9 @@ "access": "public" }, "dependencies": { - "@apollo/composition": "2.5.1", - "@apollo/federation-internals": "2.5.1", - "@apollo/query-planner": "2.5.1", + "@apollo/composition": "2.5.2", + "@apollo/federation-internals": "2.5.2", + "@apollo/query-planner": "2.5.2", "@apollo/server-gateway-interface": "^1.1.0", "@apollo/usage-reporting-protobuf": "^4.1.0", "@apollo/utils.createhash": "^2.0.0", diff --git a/internals-js/CHANGELOG.md b/internals-js/CHANGELOG.md index 8a3e60b58..c7443c452 100644 --- a/internals-js/CHANGELOG.md +++ b/internals-js/CHANGELOG.md @@ -1,5 +1,25 @@ # CHANGELOG for `@apollo/federation-internals` +## 2.5.2 +### Patch Changes + + +- Fix over-eager merging of fields with different directive applications ([#2713](https://github.com/apollographql/federation/pull/2713)) + + Previously, the following query would incorrectly combine the selection set of `hello`, with both fields ending up under the `@skip` condition: + ```graphql + query Test($skipField: Boolean!) { + hello @skip(if: $skipField) { + world + } + hello { + goodbye + } + } + ``` + + This change identifies those two selections on `hello` as unique while constructing our operation representation so they aren't merged at all, leaving it to the subgraph to handle the operation as-is. + ## 2.5.1 ### Patch Changes diff --git a/internals-js/package.json b/internals-js/package.json index 824609c71..ec3377c1c 100644 --- a/internals-js/package.json +++ b/internals-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/federation-internals", - "version": "2.5.1", + "version": "2.5.2", "description": "Apollo Federation internal utilities", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/package-lock.json b/package-lock.json index 91eecb43d..29d86e3b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,11 +69,11 @@ }, "composition-js": { "name": "@apollo/composition", - "version": "2.5.1", + "version": "2.5.2", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { - "@apollo/federation-internals": "2.5.1", - "@apollo/query-graphs": "2.5.1" + "@apollo/federation-internals": "2.5.2", + "@apollo/query-graphs": "2.5.2" }, "engines": { "node": ">=14.15.0" @@ -84,7 +84,7 @@ }, "federation-integration-testsuite-js": { "name": "apollo-federation-integration-testsuite", - "version": "2.5.1", + "version": "2.5.2", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { "graphql-tag": "^2.12.6", @@ -93,12 +93,12 @@ }, "gateway-js": { "name": "@apollo/gateway", - "version": "2.5.1", + "version": "2.5.2", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { - "@apollo/composition": "2.5.1", - "@apollo/federation-internals": "2.5.1", - "@apollo/query-planner": "2.5.1", + "@apollo/composition": "2.5.2", + "@apollo/federation-internals": "2.5.2", + "@apollo/query-planner": "2.5.2", "@apollo/server-gateway-interface": "^1.1.0", "@apollo/usage-reporting-protobuf": "^4.1.0", "@apollo/utils.createhash": "^2.0.0", @@ -124,7 +124,7 @@ }, "internals-js": { "name": "@apollo/federation-internals", - "version": "2.5.1", + "version": "2.5.2", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { "@types/uuid": "^9.0.0", @@ -17503,10 +17503,10 @@ }, "query-graphs-js": { "name": "@apollo/query-graphs", - "version": "2.5.1", + "version": "2.5.2", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { - "@apollo/federation-internals": "2.5.1", + "@apollo/federation-internals": "2.5.2", "deep-equal": "^2.0.5", "ts-graphviz": "^1.5.4", "uuid": "^9.0.0" @@ -17520,11 +17520,11 @@ }, "query-planner-js": { "name": "@apollo/query-planner", - "version": "2.5.1", + "version": "2.5.2", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { - "@apollo/federation-internals": "2.5.1", - "@apollo/query-graphs": "2.5.1", + "@apollo/federation-internals": "2.5.2", + "@apollo/query-graphs": "2.5.2", "@apollo/utils.keyvaluecache": "^2.1.0", "chalk": "^4.1.0", "deep-equal": "^2.0.5", @@ -17553,11 +17553,11 @@ }, "subgraph-js": { "name": "@apollo/subgraph", - "version": "2.5.1", + "version": "2.5.2", "license": "MIT", "dependencies": { "@apollo/cache-control-types": "^1.0.2", - "@apollo/federation-internals": "2.5.1" + "@apollo/federation-internals": "2.5.2" }, "engines": { "node": ">=14.15.0" diff --git a/query-graphs-js/CHANGELOG.md b/query-graphs-js/CHANGELOG.md index 6c6d94708..fa1f8204d 100644 --- a/query-graphs-js/CHANGELOG.md +++ b/query-graphs-js/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG for `@apollo/query-graphs` +## 2.5.2 +### Patch Changes + +- Updated dependencies [[`35179f08`](https://github.com/apollographql/federation/commit/35179f086ce973e9ae7bb455f7ea7d73cdc10f69)]: + - @apollo/federation-internals@2.5.2 + ## 2.5.1 ### Patch Changes diff --git a/query-graphs-js/package.json b/query-graphs-js/package.json index f0c2f9cab..86f5a94a9 100644 --- a/query-graphs-js/package.json +++ b/query-graphs-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/query-graphs", - "version": "2.5.1", + "version": "2.5.2", "description": "Apollo Federation library to work with 'query graphs'", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -23,7 +23,7 @@ "node": ">=14.15.0" }, "dependencies": { - "@apollo/federation-internals": "2.5.1", + "@apollo/federation-internals": "2.5.2", "deep-equal": "^2.0.5", "ts-graphviz": "^1.5.4", "uuid": "^9.0.0" diff --git a/query-planner-js/CHANGELOG.md b/query-planner-js/CHANGELOG.md index 964919e9f..c35292770 100644 --- a/query-planner-js/CHANGELOG.md +++ b/query-planner-js/CHANGELOG.md @@ -1,5 +1,28 @@ # CHANGELOG for `@apollo/query-planner` +## 2.5.2 +### Patch Changes + + +- Fix over-eager merging of fields with different directive applications ([#2713](https://github.com/apollographql/federation/pull/2713)) + + Previously, the following query would incorrectly combine the selection set of `hello`, with both fields ending up under the `@skip` condition: + ```graphql + query Test($skipField: Boolean!) { + hello @skip(if: $skipField) { + world + } + hello { + goodbye + } + } + ``` + + This change identifies those two selections on `hello` as unique while constructing our operation representation so they aren't merged at all, leaving it to the subgraph to handle the operation as-is. +- Updated dependencies [[`35179f08`](https://github.com/apollographql/federation/commit/35179f086ce973e9ae7bb455f7ea7d73cdc10f69)]: + - @apollo/federation-internals@2.5.2 + - @apollo/query-graphs@2.5.2 + ## 2.5.1 ### Patch Changes diff --git a/query-planner-js/package.json b/query-planner-js/package.json index 9b336273a..906704b7a 100644 --- a/query-planner-js/package.json +++ b/query-planner-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/query-planner", - "version": "2.5.1", + "version": "2.5.2", "description": "Apollo Query Planner", "author": "Apollo ", "main": "dist/index.js", @@ -25,8 +25,8 @@ "access": "public" }, "dependencies": { - "@apollo/federation-internals": "2.5.1", - "@apollo/query-graphs": "2.5.1", + "@apollo/federation-internals": "2.5.2", + "@apollo/query-graphs": "2.5.2", "@apollo/utils.keyvaluecache": "^2.1.0", "chalk": "^4.1.0", "deep-equal": "^2.0.5", diff --git a/subgraph-js/CHANGELOG.md b/subgraph-js/CHANGELOG.md index 326f7c99c..f97e39bae 100644 --- a/subgraph-js/CHANGELOG.md +++ b/subgraph-js/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG for `@apollo/subgraph` +## 2.5.2 +### Patch Changes + +- Updated dependencies [[`35179f08`](https://github.com/apollographql/federation/commit/35179f086ce973e9ae7bb455f7ea7d73cdc10f69)]: + - @apollo/federation-internals@2.5.2 + ## 2.5.1 ### Patch Changes diff --git a/subgraph-js/package.json b/subgraph-js/package.json index 46150c19d..1debf0e1d 100644 --- a/subgraph-js/package.json +++ b/subgraph-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/subgraph", - "version": "2.5.1", + "version": "2.5.2", "description": "Apollo Subgraph Utilities", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -25,7 +25,7 @@ }, "dependencies": { "@apollo/cache-control-types": "^1.0.2", - "@apollo/federation-internals": "2.5.1" + "@apollo/federation-internals": "2.5.2" }, "peerDependencies": { "graphql": "^16.5.0" From e4a09b66ce976cf323604224cafb3de9bc759455 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 4 Aug 2023 15:17:01 -0600 Subject: [PATCH 13/95] Add back key info --- docs/source/entities.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/entities.mdx b/docs/source/entities.mdx index 0b72e35dc..3cc6c6768 100644 --- a/docs/source/entities.mdx +++ b/docs/source/entities.mdx @@ -55,7 +55,7 @@ type Product @key(fields: "id") { The `@key` directive defines the entity's **primary key**, which consists of one or more of the type's `fields`. In the example above, the `Product` entity's primary key is its `id` field. -**Every instance of an entity must be uniquely identifiable by its `@key` fields within its own subgraph.** +**Every instance of an entity must be uniquely identifiable by its `@key` fields within its own subgraph.** This is what enables your router to associate field data from _different_ subgraphs with the _same_ entity instance. > **An entity's `@key` _cannot_ include:** > From 7756ab8f24f03d41cf4f8c238519eb09a735b79f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 5 Aug 2023 01:54:44 +0000 Subject: [PATCH 14/95] chore(deps): update all non-major dependencies (#2718) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 28 ++++++++++++++-------------- package.json | 4 ++-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 29d86e3b0..f5c665ac1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "devDependencies": { "@apollo/cache-control-types": "1.0.3", "@apollo/client": "3.7.17", - "@apollo/server": "4.9.0", + "@apollo/server": "4.9.1", "@apollo/utils.fetcher": "2.0.1", "@changesets/changelog-github": "0.4.8", "@changesets/cli": "2.26.2", @@ -53,7 +53,7 @@ "mocked-env": "1.3.5", "nock": "13.3.2", "node-fetch": "2.6.12", - "prettier": "3.0.0", + "prettier": "3.0.1", "semver": "7.5.4", "strip-indent": "3.0.0", "ts-jest": "29.1.1", @@ -293,9 +293,9 @@ "link": true }, "node_modules/@apollo/server": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@apollo/server/-/server-4.9.0.tgz", - "integrity": "sha512-ca1tO5no7PC402ArVP2qDhjpwnZm0ThpEJTfrKoY62mzPaKH0tLjy7Mt909QGyLziwf6c15Q+V+hQYVZKdcUrw==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@apollo/server/-/server-4.9.1.tgz", + "integrity": "sha512-uUzkHt7DU/NEdwMvkb4GZq8ho2EYJAJXTiBq0HUhhjOuxMVfZ7fbKgOIcSF33Ur7c67fLdWwulXMAvv89Cyv0w==", "dev": true, "dependencies": { "@apollo/cache-control-types": "^1.0.3", @@ -4465,9 +4465,9 @@ } }, "node_modules/@whatwg-node/fetch/node_modules/@types/node": { - "version": "18.17.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.1.tgz", - "integrity": "sha512-xlR1jahfizdplZYRU59JlUx9uzF1ARa8jbhM11ccpCJya8kvos5jwdm2ZAgxSCwOl0fq21svP18EVwPBXMQudw==", + "version": "18.17.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.2.tgz", + "integrity": "sha512-wBo3KqP/PBqje5TI9UTiuL3yWfP6sdPtjtygSOqcYZWT232dfDeDOnkDps5wqZBP9NgGgYrNejinl0faAuE+HQ==", "dev": true, "peer": true }, @@ -7239,9 +7239,9 @@ } }, "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.1.tgz", - "integrity": "sha512-CvefSOsDdaYYvxChovdrPo/ZGt8d5lrJWleAc1diXRKhHGiTYEI26cvo8Kle/wGnsizoCJjK73FMg1/IkIwiNA==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "peer": true, "dependencies": { @@ -14259,9 +14259,9 @@ } }, "node_modules/prettier": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", - "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.1.tgz", + "integrity": "sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" diff --git a/package.json b/package.json index fc5ceda61..6c34bd440 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "devDependencies": { "@apollo/cache-control-types": "1.0.3", "@apollo/client": "3.7.17", - "@apollo/server": "4.9.0", + "@apollo/server": "4.9.1", "@apollo/utils.fetcher": "2.0.1", "@changesets/changelog-github": "0.4.8", "@changesets/cli": "2.26.2", @@ -79,7 +79,7 @@ "mocked-env": "1.3.5", "nock": "13.3.2", "node-fetch": "2.6.12", - "prettier": "3.0.0", + "prettier": "3.0.1", "semver": "7.5.4", "strip-indent": "3.0.0", "ts-jest": "29.1.1", From e2ad4846903e349e79bddcf2f3007ffecd8b7a70 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 20:18:54 +0000 Subject: [PATCH 15/95] chore(deps): update dependency @apollo/client to v3.8.0 (#2723) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 57 +++++++++++++++++++++-------------------------- package.json | 2 +- 2 files changed, 26 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index f5c665ac1..cb8721ff1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ ], "devDependencies": { "@apollo/cache-control-types": "1.0.3", - "@apollo/client": "3.7.17", + "@apollo/client": "3.8.0", "@apollo/server": "4.9.1", "@apollo/utils.fetcher": "2.0.1", "@changesets/changelog-github": "0.4.8", @@ -196,18 +196,18 @@ } }, "node_modules/@apollo/client": { - "version": "3.7.17", - "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.7.17.tgz", - "integrity": "sha512-0EErSHEtKPNl5wgWikHJbKFAzJ/k11O0WO2QyqZSHpdxdAnw7UWHY4YiLbHCFG7lhrD+NTQ3Z/H9Jn4rcikoJA==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.8.0.tgz", + "integrity": "sha512-y/NadA2AKVtV18rO+Hi++6a6f4IqhUNvRPny7wAWx39ldzy8I4f6pWOYRSiJmL6hNFciVdT/YZqaoxnF2NRIuQ==", "dev": true, "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", - "@wry/context": "^0.7.0", - "@wry/equality": "^0.5.0", - "@wry/trie": "^0.4.0", + "@wry/context": "^0.7.3", + "@wry/equality": "^0.5.6", + "@wry/trie": "^0.4.3", "graphql-tag": "^2.12.6", "hoist-non-react-statics": "^3.3.2", - "optimism": "^0.16.2", + "optimism": "^0.17.5", "prop-types": "^15.7.2", "response-iterator": "^0.2.6", "symbol-observable": "^4.0.0", @@ -237,18 +237,6 @@ } } }, - "node_modules/@apollo/client/node_modules/@wry/trie": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.4.3.tgz", - "integrity": "sha512-I6bHwH0fSf6RqQcnnXLJKhkSXG45MFral3GxPaY4uAl0LYDZM+YDVDAiU9bYwjTuysy1S0IeecWtmq1SZA3M1w==", - "dev": true, - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@apollo/composition": { "resolved": "composition-js", "link": true @@ -4465,9 +4453,9 @@ } }, "node_modules/@whatwg-node/fetch/node_modules/@types/node": { - "version": "18.17.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.2.tgz", - "integrity": "sha512-wBo3KqP/PBqje5TI9UTiuL3yWfP6sdPtjtygSOqcYZWT232dfDeDOnkDps5wqZBP9NgGgYrNejinl0faAuE+HQ==", + "version": "18.17.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.3.tgz", + "integrity": "sha512-2x8HWtFk0S99zqVQABU9wTpr8wPoaDHZUcAkoTKH+nL7kPv3WUI9cRi/Kk5Mz4xdqXSqTkKP7IWNoQQYCnDsTA==", "dev": true, "peer": true }, @@ -4487,9 +4475,10 @@ } }, "node_modules/@wry/context": { - "version": "0.7.0", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.3.tgz", + "integrity": "sha512-Nl8WTesHp89RF803Se9X3IiHjdmLBrIvPMaJkl+rKVJAYyPsz1TEUbu89943HpvujtSJgDUx9W4vZw3K1Mr3sA==", "dev": true, - "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, @@ -4498,9 +4487,10 @@ } }, "node_modules/@wry/equality": { - "version": "0.5.3", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.6.tgz", + "integrity": "sha512-D46sfMTngaYlrH+OspKf8mIJETntFnf6Hsjb0V41jAXJ7Bx2kB8Rv8RCUujuVWYttFtHkUNp7g+FwxNQAr6mXA==", "dev": true, - "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, @@ -4509,9 +4499,10 @@ } }, "node_modules/@wry/trie": { - "version": "0.3.2", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.4.3.tgz", + "integrity": "sha512-I6bHwH0fSf6RqQcnnXLJKhkSXG45MFral3GxPaY4uAl0LYDZM+YDVDAiU9bYwjTuysy1S0IeecWtmq1SZA3M1w==", "dev": true, - "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, @@ -13817,12 +13808,14 @@ } }, "node_modules/optimism": { - "version": "0.16.2", + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.17.5.tgz", + "integrity": "sha512-TEcp8ZwK1RczmvMnvktxHSF2tKgMWjJ71xEFGX5ApLh67VsMSTy1ZUlipJw8W+KaqgOmQ+4pqwkeivY89j+4Vw==", "dev": true, - "license": "MIT", "dependencies": { "@wry/context": "^0.7.0", - "@wry/trie": "^0.3.0" + "@wry/trie": "^0.4.3", + "tslib": "^2.3.0" } }, "node_modules/optionator": { diff --git a/package.json b/package.json index 6c34bd440..f69b923c3 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ ], "devDependencies": { "@apollo/cache-control-types": "1.0.3", - "@apollo/client": "3.7.17", + "@apollo/client": "3.8.0", "@apollo/server": "4.9.1", "@apollo/utils.fetcher": "2.0.1", "@changesets/changelog-github": "0.4.8", From 2d8103636b1609663194aae70c1c5044c77d3035 Mon Sep 17 00:00:00 2001 From: Dariusz Kuc <9501705+dariuszkuc@users.noreply.github.com> Date: Mon, 7 Aug 2023 15:58:01 -0500 Subject: [PATCH 16/95] docs: subgraph compatibility updates (#2722) latest subgraph compatibility result updates --- .../supported-subgraphs.md | 105 ++++++++++-------- 1 file changed, 60 insertions(+), 45 deletions(-) diff --git a/docs/source/building-supergraphs/supported-subgraphs.md b/docs/source/building-supergraphs/supported-subgraphs.md index 4c3abc957..577df6449 100644 --- a/docs/source/building-supergraphs/supported-subgraphs.md +++ b/docs/source/building-supergraphs/supported-subgraphs.md @@ -15,6 +15,21 @@ The following open-source GraphQL server libraries and hosted solutions support | ❌ | Critical functionality is NOT supported | | 🔲 | Additional federation functionality is NOT supported | +## Ballerina + + + + + + + + + +
LibraryFederation 1 SupportFederation 2 Support
Ballerina GraphQL Module
A spec-compliant, production-ready, Standard Library module for building and interacting with GraphQL APIs using Ballerina.

Github: ballerina-platform/module-ballerina-graphql
+Type: Code first
+Stars: 51 ⭐
+Last Release: 2023-06-30

_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🔲
@provides🔲
federated tracing🔲
@link🟢
@shareable🔲
@tag🔲
@override🔲
@inaccessible🔲
@composeDirective🔲
@interfaceObject🔲
+ ## C# / .NET @@ -25,13 +40,13 @@ The following open-source GraphQL server libraries and hosted solutions support +Stars: 5.6k ⭐
+Last Release: 2023-06-17

+Stars: 4.5k ⭐
+Last Release: 2023-07-28

GraphQL for .NET
GraphQL for .NET

Github: graphql-dotnet/graphql-dotnet
Type: Code first | SDL first
-Stars: 5.5k ⭐
-Last Release: 2023-02-27

_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🔲
@requires🟢
@provides🟢
federated tracing🔲
@link
@shareable🔲
@tag🔲
@override🔲
@inaccessible🔲
@composeDirective🔲
@interfaceObject🔲
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🔲
@requires🟢
@provides🟢
federated tracing🔲
@link
@shareable🔲
@tag🔲
@override🔲
@inaccessible🔲
@composeDirective🔲
@interfaceObject🔲
Hot Chocolate
Open-source GraphQL server for the Microsoft .NET platform that takes the complexity away and lets you focus on delivering the next big thing.

Github: ChilliCream/graphql-platform
Type: Code first | SDL first
-Stars: 4.3k ⭐
-Last Release: 2023-03-22

_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🔲
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link
@shareable🔲
@tag🔲
@override🔲
@inaccessible🔲
@composeDirective🔲
@interfaceObject🔲
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🔲
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link
@shareable🔲
@tag🔲
@override🔲
@inaccessible🔲
@composeDirective🔲
@interfaceObject🔲
@@ -46,7 +61,7 @@ Last Release: 2023-03-22

+Last Release: 2021-09-28

Federation Library: DivvyPayHQ/absinthe_federation
_service<
The GraphQL toolkit for Elixir

Github: absinthe-graphql/absinthe
Type: Code first
Stars: 4.1k ⭐
-Last Release: 2021-09-28

Federation Library: Absinthe.Federation
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
@@ -60,12 +75,12 @@ Last Release: 2021-09-28

Federation Library: gqlgen go generate based graphql server library

Github: 99designs/gqlgen
Type: SDL first
-Stars: 8.7k ⭐
-Last Release: 2023-03-20

_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🔲
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
+Stars: 9.1k ⭐
+Last Release: 2023-07-27

_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🔲
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
GraphQL Go (fork) This is a fork of graphql-go/graphql that adds Federation support

Github: dariuszkuc/graphql
Type: Code first
-Stars: 1 ⭐
+Stars: 2 ⭐
Last Release: 2022-11-11

_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
@@ -80,23 +95,23 @@ Last Release: 2022-11-11

+Stars: 2.7k ⭐
+Last Release: 2023-07-27

Core Library: GraphQL Java
Federation Library: apollographql/federation-jvm
+Last Release: 2022-12-05

Core Library: GraphQL Java
Federation Library: apollographql/federation-jvm
+Last Release: 2023-06-19

Core Library: GraphQL Java
+Type: SDL first
+Stars: 1.4k ⭐
+Last Release: 2023-07-18

Core Library: GraphQL Java
Federation Library: apollographql/federation-jvm
_service<
dgs-framework
GraphQL for Java with Spring Boot made easy.

Github: netflix/dgs-framework
Type: SDL first
-Stars: 2.6k ⭐
-Last Release: 2023-02-17

Core Library: GraphQL Java
Federation Library: Federation JVM
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
GraphQL Java Kickstart (Spring Boot)
GraphQL and GraphiQL Spring Framework Boot Starters - Forked from oembedler/graphql-spring-boot due to inactivity.

Github: graphql-java-kickstart/graphql-spring-boot
-Type: Schema first
+Type: SDL first
Stars: 1.5k ⭐
-Last Release: 2022-12-05

Core Library: GraphQL Java
Federation Library: Federation JVM
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
GraphQL Kotlin
Libraries for running GraphQL in Kotlin

Github: ExpediaGroup/graphql-kotlin
Type: Code first
Stars: 1.6k ⭐
-Last Release: 2023-03-15

Core Library: GraphQL Java
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
Spring GraphQL
Spring Integration for GraphQL

Github: spring-projects/spring-graphql
-Type: Schema first
-Stars: 1.3k ⭐
-Last Release: 2023-03-21

Core Library: GraphQL Java
Federation Library: Federation JVM
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
@@ -110,8 +125,8 @@ Last Release: 2023-03-21

Core Library: Apollo Server 🌍  Spec-compliant and production ready JavaScript GraphQL server that lets you develop in a schema-first way. Built for Express, Connect, Hapi, Koa, and more.

Github: apollographql/apollo-server
Type: SDL first
-Stars: 13.3k ⭐
-Last Release: 2023-03-15

Core Library: GraphQL.js
Federation Library: Apollo Subgraph
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
+Stars: 13.4k ⭐
+Last Release: 2023-07-27

Core Library: GraphQL.js
Federation Library: Apollo Subgraph
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
express-graphql Create a GraphQL HTTP server with Express.

Github: graphql/express-graphql
Type: SDL first
@@ -120,33 +135,33 @@ Last Release: 2020-11-19

Core Library: GraphQL Yoga The fully-featured GraphQL server with focus on easy setup, performance and great developer experience.

Github: dotansimha/graphql-yoga
Type: SDL first
-Stars: 7.5k ⭐
-Last Release: 2023-03-14

Core Library: GraphQL.js
Federation Library: Apollo Subgraph
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
+Stars: 7.7k ⭐
+Last Release: 2023-07-24

Core Library: GraphQL.js
Federation Library: Apollo Subgraph
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
GraphQL Helix A highly evolved and framework-agnostic GraphQL HTTP server.

Github: contra/graphql-helix
Type: SDL first
-Stars: 810 ⭐
+Stars: 823 ⭐
Last Release: 2022-07-09

Core Library: GraphQL.js
Federation Library: Apollo Subgraph
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
Mercurius Implement GraphQL servers and gateways with Fastify

Github: mercurius-js/mercurius
Type: SDL first
-Stars: 2.1k ⭐
-Last Release: 2023-02-22

Core Library: GraphQL.js
Federation Library: Apollo Subgraph
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
+Stars: 2.2k ⭐
+Last Release: 2023-07-12

Core Library: GraphQL.js
Federation Library: Apollo Subgraph
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
NestJS (code first) A progressive Node.js framework for building efficient, reliable and scalable server-side applications.

Github: nestjs/graphql
Type: Code first
Stars: 1.3k ⭐
-Last Release: 2023-03-20

Core Library: GraphQL.js
Federation Library: Apollo Subgraph
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🟢
+Last Release: 2023-06-16

Core Library: GraphQL.js
Federation Library: Apollo Subgraph
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🔲
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🟢
NestJS (SDL First) A progressive Node.js framework for building efficient, reliable and scalable server-side applications.

Github: nestjs/graphql
Type: SDL first
Stars: 1.3k ⭐
-Last Release: 2023-03-20

Core Library: GraphQL.js
Federation Library: Apollo Subgraph
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
+Last Release: 2023-06-16

Core Library: GraphQL.js
Federation Library: Apollo Subgraph
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
Pothos GraphQL Plugin based GraphQL schema builder that makes building graphql schemas with TypeScript easy, fast and enjoyable.

Github: hayes/pothos
Type: Code first
-Stars: 1.8k ⭐
-Last Release: 2023-03-24

Core Library: GraphQL.js
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
+Stars: 2.0k ⭐
+Last Release: 2023-07-25

Core Library: GraphQL.js
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
@@ -160,13 +175,13 @@ Last Release: 2023-03-24

Core Library: Lighthouse (Laravel) A framework for serving GraphQL from Laravel

Github: nuwave/lighthouse
Type: SDL first
-Stars: 3.1k ⭐
-Last Release: 2023-03-24

Core Library: graphql-php
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link
@shareable🔲
@tag🔲
@override🔲
@inaccessible🔲
@composeDirective🔲
@interfaceObject🔲
+Stars: 3.2k ⭐
+Last Release: 2023-07-20

Core Library: webonyx/graphql-php
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link
@shareable🔲
@tag🔲
@override🔲
@inaccessible🔲
@composeDirective🔲
@interfaceObject🔲
GraphQL PHP PHP implementation of the GraphQL specification based on the reference implementation in JavaScript

Github: webonyx/graphql-php
Type: Code first
Stars: 4.5k ⭐
-Last Release: 2023-03-20

Federation Library: Apollo Federation PHP
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link
@shareable🔲
@tag🔲
@override🔲
@inaccessible🔲
@composeDirective🔲
@interfaceObject🔲
+Last Release: 2023-07-24

Federation Library: Skillshare/apollo-federation-php
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link
@shareable🔲
@tag🔲
@override🔲
@inaccessible🔲
@composeDirective🔲
@interfaceObject🔲
@@ -180,18 +195,18 @@ Last Release: 2023-03-20

Federation Library: Ariadne Python library for implementing GraphQL servers using schema-first approach.

Github: mirumee/ariadne
Type: SDL first
-Stars: 2.0k ⭐
-Last Release: 2023-02-22

Core Library: GraphQL-core 3
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🟢
+Stars: 2.1k ⭐
+Last Release: 2023-06-27

Core Library: GraphQL-core 3
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🟢
Graphene GraphQL framework for Python

Github: graphql-python/graphene
Type: Code first
-Stars: 7.6k ⭐
-Last Release: 2023-03-13

Core Library: GraphQL-core 3
Federation Library: graphene-federation
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
+Stars: 7.7k ⭐
+Last Release: 2023-07-26

Core Library: GraphQL-core 3
Federation Library: graphql-python/graphene-federation
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
Strawberry A GraphQL library for Python that leverages type annotations 🍓

Github: strawberry-graphql/strawberry
Type: Code first
-Stars: 3.0k ⭐
-Last Release: 2023-03-25

Core Library: GraphQL-core 3
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
+Stars: 3.3k ⭐
+Last Release: 2023-07-28

Core Library: GraphQL-core 3
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
@@ -205,7 +220,7 @@ Last Release: 2023-03-25

Core Library: GraphQL Ruby Ruby implementation of GraphQL

Github: rmosolgo/graphql-ruby
Type: Code first
-Stars: 5.2k ⭐
+Stars: 5.3k ⭐
Last Release: 2021-02-12

Federation Library: Gusto/apollo-federation-ruby
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🟢
@@ -220,7 +235,7 @@ Last Release: 2021-02-12

Federation Library: async-graphql A GraphQL server library implemented in Rust

Github: async-graphql/async-graphql
Type: Code first
-Stars: 2.8k ⭐
+Stars: 2.9k ⭐
Last Release: 2022-11-28

_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
@@ -235,13 +250,13 @@ Last Release: 2022-11-28

+Stars: 878 ⭐
+Last Release: 2023-05-13

+Last Release: 2023-06-20

Federation Library: sangria-graphql/sangria-federated
_service<
Caliban
Functional GraphQL library for Scala

Github: ghostdogpr/caliban
Type: Code first
-Stars: 861 ⭐
-Last Release: 2022-12-19

_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🔲
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
Sangria
Scala GraphQL implementation

Github: sangria-graphql/sangria
Type: Code first
Stars: 1.9k ⭐
-Last Release: 2023-03-09

Federation Library: Sangria Federated
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
@@ -255,8 +270,8 @@ Last Release: 2023-03-09

Federation Library: Graphiti The Swift GraphQL Schema framework for macOS and Linux

Github: GraphQLSwift/Graphiti
Type: SDL first
-Stars: 493 ⭐
-Last Release: 2023-03-19

_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
+Stars: 507 ⭐
+Last Release: 2023-06-12

_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
@@ -274,8 +289,8 @@ Last Release: 2023-03-19

+Stars: 2.9k ⭐
+Last Release: 2023-07-28

From 5184523090651e82ea5e45522172b4b994337cfa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 11 Aug 2023 00:16:43 +0000 Subject: [PATCH 17/95] chore(deps): update all non-major dependencies (#2729) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 245 +++++++++++++++++++++++++--------------------- package.json | 6 +- 2 files changed, 137 insertions(+), 114 deletions(-) diff --git a/package-lock.json b/package-lock.json index cb8721ff1..041a7fba4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ ], "devDependencies": { "@apollo/cache-control-types": "1.0.3", - "@apollo/client": "3.8.0", + "@apollo/client": "3.8.1", "@apollo/server": "4.9.1", "@apollo/utils.fetcher": "2.0.1", "@changesets/changelog-github": "0.4.8", @@ -41,7 +41,7 @@ "@types/uuid": "9.0.2", "@typescript-eslint/eslint-plugin": "5.62.0", "bunyan": "1.8.15", - "cspell": "6.31.2", + "cspell": "6.31.3", "graphql": "16.7.1", "graphql-http": "1.21.0", "graphql-tag": "2.12.6", @@ -196,9 +196,9 @@ } }, "node_modules/@apollo/client": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.8.0.tgz", - "integrity": "sha512-y/NadA2AKVtV18rO+Hi++6a6f4IqhUNvRPny7wAWx39ldzy8I4f6pWOYRSiJmL6hNFciVdT/YZqaoxnF2NRIuQ==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.8.1.tgz", + "integrity": "sha512-JGGj/9bdoLEqzatRikDeN8etseY5qeFAY0vSAx/Pd0ePNsaflKzHx6V2NZ0NsGkInq+9IXXX3RLVDf0EotizMA==", "dev": true, "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", @@ -1800,9 +1800,9 @@ } }, "node_modules/@cspell/cspell-bundled-dicts": { - "version": "6.31.2", - "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-6.31.2.tgz", - "integrity": "sha512-rQ5y/U1Ah5AaduIh3NU2z371hRrOr1cmNdhhP8oiuz2E4VqmcoVHflXIct9DgY8uIJpwsSCdR6ypOQWZYXYnwA==", + "version": "6.31.3", + "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-6.31.3.tgz", + "integrity": "sha512-KXy3qKWYzXOGYwqOGMCXHem3fV39iEmoKLiNhoWWry/SFdvAafmeY+LIDcQTXAcOQLkMDCwP2/rY/NadcWnrjg==", "dev": true, "dependencies": { "@cspell/dict-ada": "^4.0.1", @@ -1856,34 +1856,49 @@ "node": ">=14" } }, + "node_modules/@cspell/cspell-json-reporter": { + "version": "6.31.3", + "resolved": "https://registry.npmjs.org/@cspell/cspell-json-reporter/-/cspell-json-reporter-6.31.3.tgz", + "integrity": "sha512-ZJwj2vT4lxncYxduXcxy0dCvjjMvXIfphbLSCN5CXvufrtupB4KlcjZUnOofCi4pfpp8qocCSn1lf2DU9xgUXA==", + "dev": true, + "dependencies": { + "@cspell/cspell-types": "6.31.3" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@cspell/cspell-pipe": { - "version": "6.31.1", + "version": "6.31.3", + "resolved": "https://registry.npmjs.org/@cspell/cspell-pipe/-/cspell-pipe-6.31.3.tgz", + "integrity": "sha512-Lv/y4Ya/TJyU1pf66yl1te7LneFZd3lZg1bN5oe1cPrKSmfWdiX48v7plTRecWd/OWyLGd0yN807v79A+/0W7A==", "dev": true, - "license": "MIT", "engines": { "node": ">=14" } }, "node_modules/@cspell/cspell-service-bus": { - "version": "6.31.1", + "version": "6.31.3", + "resolved": "https://registry.npmjs.org/@cspell/cspell-service-bus/-/cspell-service-bus-6.31.3.tgz", + "integrity": "sha512-x5j8j3n39KN8EXOAlv75CpircdpF5WEMCC5pcO916o6GBmJBy8SrdzdsBGJhVcYGGilqy6pf8R9RCZ3yAmG8gQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=14" } }, "node_modules/@cspell/cspell-types": { - "version": "6.31.1", + "version": "6.31.3", + "resolved": "https://registry.npmjs.org/@cspell/cspell-types/-/cspell-types-6.31.3.tgz", + "integrity": "sha512-wZ+t+lUsQJB65M31btZM4fH3K1CkRgE8pSeTiCwxYcnCL19pi4TMcEEMKdO8yFZMdocW4B7VRwzxNoQMw2ewBg==", "dev": true, - "license": "MIT", "engines": { "node": ">=14" } }, "node_modules/@cspell/dict-ada": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-ada/-/dict-ada-4.0.1.tgz", - "integrity": "sha512-/E9o3nHrXOhYmQE43deKbxZcR3MIJAsa+66IzP9TXGHheKEx8b9dVMVVqydDDH8oom1H0U20NRPtu6KRVbT9xw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@cspell/dict-ada/-/dict-ada-4.0.2.tgz", + "integrity": "sha512-0kENOWQeHjUlfyId/aCM/mKXtkEgV0Zu2RhUXCBr4hHo9F9vph+Uu8Ww2b0i5a4ZixoIkudGA+eJvyxrG1jUpA==", "dev": true }, "node_modules/@cspell/dict-aws": { @@ -1899,15 +1914,15 @@ "dev": true }, "node_modules/@cspell/dict-companies": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/@cspell/dict-companies/-/dict-companies-3.0.17.tgz", - "integrity": "sha512-vo1jbozgZWSzz2evIL26kLd35tVb+5kW/UTvTzAwaWutSWRloRyKx38nj2CaLJ2IFxBdiATteCFGTzKCvJJl6A==", + "version": "3.0.19", + "resolved": "https://registry.npmjs.org/@cspell/dict-companies/-/dict-companies-3.0.19.tgz", + "integrity": "sha512-hO7rS4DhFA333qyvf89wIVoclCtXe/2sftY6aS0oMIH1bMZLjLx2B2sQJj6dCiu6gG/By1S9YZ0fXabiPk2Tkg==", "dev": true }, "node_modules/@cspell/dict-cpp": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@cspell/dict-cpp/-/dict-cpp-5.0.3.tgz", - "integrity": "sha512-7sx/RFsf0hB3q8chx8OHYl9Kd+g0pqA1laphwaAQ+/jPwoAreYT3kNQWbJ3bIt/rMoORetFSQxckSbaJXwwqpw==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@cspell/dict-cpp/-/dict-cpp-5.0.4.tgz", + "integrity": "sha512-Vmz/CCb2d91ES5juaO8+CFWeTa2AFsbpR8bkCPJq+P8cRP16+37tY0zNXEBSK/1ur4MakaRf76jeQBijpZxw0Q==", "dev": true }, "node_modules/@cspell/dict-cryptocurrencies": { @@ -1929,15 +1944,15 @@ "dev": true }, "node_modules/@cspell/dict-dart": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@cspell/dict-dart/-/dict-dart-2.0.2.tgz", - "integrity": "sha512-jigcODm7Z4IFZ4vParwwP3IT0fIgRq/9VoxkXfrxBMsLBGGM2QltHBj7pl+joX+c4cOHxfyZktGJK1B1wFtR4Q==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@cspell/dict-dart/-/dict-dart-2.0.3.tgz", + "integrity": "sha512-cLkwo1KT5CJY5N5RJVHks2genFkNCl/WLfj+0fFjqNR+tk3tBI1LY7ldr9piCtSFSm4x9pO1x6IV3kRUY1lLiw==", "dev": true }, "node_modules/@cspell/dict-data-science": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@cspell/dict-data-science/-/dict-data-science-1.0.7.tgz", - "integrity": "sha512-Q9VUFaarUpqM6CAmR8peP4o9alk0XQ4rgVoE2R2XalpC2cqPI8Hmg6QwMU2UPioSUcWMJCqLc/KzJti0gBMuxA==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@cspell/dict-data-science/-/dict-data-science-1.0.10.tgz", + "integrity": "sha512-7ZsRCnW0f4Bdo6Cqq8V4gHr8K58h+MP8majcDeMNhpMFUPiiSnvKsDuG9V5jciI/0t+lptPrZwGGIVEDF4Kqtg==", "dev": true }, "node_modules/@cspell/dict-django": { @@ -1947,9 +1962,9 @@ "dev": true }, "node_modules/@cspell/dict-docker": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@cspell/dict-docker/-/dict-docker-1.1.6.tgz", - "integrity": "sha512-zCCiRTZ6EOQpBnSOm0/3rnKW1kCcAUDUA7SxJG3SuH6iZvKi3I8FEg8+O83WQUeXg0SyPNerD9F40JLnnJjJig==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@cspell/dict-docker/-/dict-docker-1.1.7.tgz", + "integrity": "sha512-XlXHAr822euV36GGsl2J1CkBIVg3fZ6879ZOg5dxTIssuhUOCiV2BuzKZmt6aIFmcdPmR14+9i9Xq+3zuxeX0A==", "dev": true }, "node_modules/@cspell/dict-dotnet": { @@ -1965,9 +1980,9 @@ "dev": true }, "node_modules/@cspell/dict-en_us": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@cspell/dict-en_us/-/dict-en_us-4.3.4.tgz", - "integrity": "sha512-mR2yqWmFip1zTKja2SqyVMbzuqEThqkEJk9M32bMDziPJpEyOIPvLA0UPmj3cyRKJkRuVF0bhDCE33O+at38hw==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@cspell/dict-en_us/-/dict-en_us-4.3.6.tgz", + "integrity": "sha512-odhgsjNZI9BtEOJdvqfAuv/3yz5aB1ngfBNaph7WSnYVt//9e3fhrElZ6/pIIkoyuGgeQPwz1fXt+tMgcnLSEQ==", "dev": true }, "node_modules/@cspell/dict-en-common-misspellings": { @@ -2073,9 +2088,9 @@ "dev": true }, "node_modules/@cspell/dict-npm": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@cspell/dict-npm/-/dict-npm-5.0.7.tgz", - "integrity": "sha512-6SegF0HsVaBTl6PlHjeErG8Av+tRYkUG1yaXUQIGWXU0A8oxhI0o4PuL65UWH5lkCKhJyGai69Cd0iytL0oVFg==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@cspell/dict-npm/-/dict-npm-5.0.8.tgz", + "integrity": "sha512-KuqH8tEsFD6DPKqKwIfWr9E+admE3yghaC0AKXG8jPaf77N0lkctKaS3dm0oxWUXkYKA/eXj6LCtz3VcTyxFPg==", "dev": true }, "node_modules/@cspell/dict-php": { @@ -2091,15 +2106,15 @@ "dev": true }, "node_modules/@cspell/dict-public-licenses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@cspell/dict-public-licenses/-/dict-public-licenses-2.0.2.tgz", - "integrity": "sha512-baKkbs/WGEV2lCWZoL0KBPh3uiPcul5GSDwmXEBAsR5McEW52LF94/b7xWM0EmSAc/y8ODc5LnPYC7RDRLi6LQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@cspell/dict-public-licenses/-/dict-public-licenses-2.0.3.tgz", + "integrity": "sha512-JSLEdpEYufQ1H+93UHi+axlqQm1fhgK6kpdLHp6uPHu//CsvETcqNVawjB+qOdI/g38JTMw5fBqSd0aGNxa6Dw==", "dev": true }, "node_modules/@cspell/dict-python": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@cspell/dict-python/-/dict-python-4.1.2.tgz", - "integrity": "sha512-Whcn4K8R0Ux/hcx/P9Fbx6i29GwTaXgT3LTt95AuCnV5RRLrzsqoyZkz851hcg5z4kjUQVMduDl3HECGgW/FNw==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@cspell/dict-python/-/dict-python-4.1.5.tgz", + "integrity": "sha512-wWUWyHdyJtx5iG6Fz9rBQ17BtdpEsB17vmutao+gixQD28Jzb6XoLgDQ6606M0RnFjBSFhs5iT4CJBzlD2Kq6g==", "dev": true, "dependencies": { "@cspell/dict-data-science": "^1.0.0" @@ -2130,15 +2145,15 @@ "dev": true }, "node_modules/@cspell/dict-software-terms": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@cspell/dict-software-terms/-/dict-software-terms-3.2.0.tgz", - "integrity": "sha512-RI6sv4Bc4i42YH/ofVelv8lXpJRhCyS9IhI2BtejUoMXKhKA9gC01ATXOylx+oaQmj3t5ark4R50xKFRvC7ENA==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@cspell/dict-software-terms/-/dict-software-terms-3.2.1.tgz", + "integrity": "sha512-+QXmyoONVc/3aNgKW+0F0u3XUCRTfNRkWKLZQA78i+9fOfde8ZT4JmROmZgRveH/MxD4n6pNFceIRcYI6C8WuQ==", "dev": true }, "node_modules/@cspell/dict-sql": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@cspell/dict-sql/-/dict-sql-2.1.0.tgz", - "integrity": "sha512-Bb+TNWUrTNNABO0bmfcYXiTlSt0RD6sB2MIY+rNlaMyIwug43jUjeYmkLz2tPkn3+2uvySeFEOMVYhMVfcuDKg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@cspell/dict-sql/-/dict-sql-2.1.1.tgz", + "integrity": "sha512-v1mswi9NF40+UDUMuI148YQPEQvWjac72P6ZsjlRdLjEiQEEMEsTQ+zlkIdnzC9QCNyJaqD5Liq9Mn78/8Zxtw==", "dev": true }, "node_modules/@cspell/dict-svelte": { @@ -2166,9 +2181,10 @@ "dev": true }, "node_modules/@cspell/dynamic-import": { - "version": "6.31.1", + "version": "6.31.3", + "resolved": "https://registry.npmjs.org/@cspell/dynamic-import/-/dynamic-import-6.31.3.tgz", + "integrity": "sha512-A6sT00+6UNGFksQ5SxW2ohNl6vUutai8F4jwJMHTjZL/9vivQpU7y5V4PpsfoPZtx3WZcbrzuTvJ+tLfdbWc4A==", "dev": true, - "license": "MIT", "dependencies": { "import-meta-resolve": "^2.2.2" }, @@ -2177,9 +2193,10 @@ } }, "node_modules/@cspell/strong-weak-map": { - "version": "6.31.1", + "version": "6.31.3", + "resolved": "https://registry.npmjs.org/@cspell/strong-weak-map/-/strong-weak-map-6.31.3.tgz", + "integrity": "sha512-znwc9IlgGUPioHGshP/zyM8HsuYg1OY5S7HSiVXARh5H8RqcyBsnyn8abc0PPhqPrfDy9Fh5xHsAEPZ55dl1vQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=14.6" } @@ -3362,13 +3379,13 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", + "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@manypkg/find-root": { @@ -4453,9 +4470,9 @@ } }, "node_modules/@whatwg-node/fetch/node_modules/@types/node": { - "version": "18.17.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.3.tgz", - "integrity": "sha512-2x8HWtFk0S99zqVQABU9wTpr8wPoaDHZUcAkoTKH+nL7kPv3WUI9cRi/Kk5Mz4xdqXSqTkKP7IWNoQQYCnDsTA==", + "version": "18.17.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.4.tgz", + "integrity": "sha512-ATL4WLgr7/W40+Sp1WnNTSKbgVn6Pvhc/2RHAdt8fl6NsQyp4oPCi2eKcGOvA494bwf1K/W6nGgZ9TwDqvpjdw==", "dev": true, "peer": true }, @@ -6065,20 +6082,21 @@ } }, "node_modules/cspell": { - "version": "6.31.2", - "resolved": "https://registry.npmjs.org/cspell/-/cspell-6.31.2.tgz", - "integrity": "sha512-HJcQ8jqL/1N3Mj5dufFnIZCX3ACuRoFTSVY6h3Bo5wBqd2iiJTyeQ1SY9Zymlxtb2KyJ6jQRiFmkWeFx2HVs7w==", + "version": "6.31.3", + "resolved": "https://registry.npmjs.org/cspell/-/cspell-6.31.3.tgz", + "integrity": "sha512-VeeShDLWVM6YPiU/imeGy0lmg6ki63tbLEa6hz20BExhzzpmINOP5nSTYtpY0H9zX9TrF/dLbI38TuuYnyG3Uw==", "dev": true, "dependencies": { - "@cspell/cspell-pipe": "6.31.1", - "@cspell/cspell-types": "6.31.1", - "@cspell/dynamic-import": "6.31.1", + "@cspell/cspell-json-reporter": "6.31.3", + "@cspell/cspell-pipe": "6.31.3", + "@cspell/cspell-types": "6.31.3", + "@cspell/dynamic-import": "6.31.3", "chalk": "^4.1.2", "commander": "^10.0.0", - "cspell-gitignore": "6.31.2", - "cspell-glob": "6.31.2", - "cspell-io": "6.31.2", - "cspell-lib": "6.31.2", + "cspell-gitignore": "6.31.3", + "cspell-glob": "6.31.3", + "cspell-io": "6.31.3", + "cspell-lib": "6.31.3", "fast-glob": "^3.2.12", "fast-json-stable-stringify": "^2.1.0", "file-entry-cache": "^6.0.1", @@ -6100,13 +6118,14 @@ } }, "node_modules/cspell-dictionary": { - "version": "6.31.1", + "version": "6.31.3", + "resolved": "https://registry.npmjs.org/cspell-dictionary/-/cspell-dictionary-6.31.3.tgz", + "integrity": "sha512-3w5P3Md/tbHLVGPKVL0ePl1ObmNwhdDiEuZ2TXfm2oAIwg4aqeIrw42A2qmhaKLcuAIywpqGZsrGg8TviNNhig==", "dev": true, - "license": "MIT", "dependencies": { - "@cspell/cspell-pipe": "6.31.1", - "@cspell/cspell-types": "6.31.1", - "cspell-trie-lib": "6.31.1", + "@cspell/cspell-pipe": "6.31.3", + "@cspell/cspell-types": "6.31.3", + "cspell-trie-lib": "6.31.3", "fast-equals": "^4.0.3", "gensequence": "^5.0.2" }, @@ -6115,12 +6134,12 @@ } }, "node_modules/cspell-gitignore": { - "version": "6.31.2", - "resolved": "https://registry.npmjs.org/cspell-gitignore/-/cspell-gitignore-6.31.2.tgz", - "integrity": "sha512-B1i8aiXCIbb/08u0K3xnDyXtg0qD+lb5B2itOOXi7KXlPkKvIuN4hWyXxhVDweWyYWEzyXD5wBpPrqICVrStHQ==", + "version": "6.31.3", + "resolved": "https://registry.npmjs.org/cspell-gitignore/-/cspell-gitignore-6.31.3.tgz", + "integrity": "sha512-vCfVG4ZrdwJnsZHl/cdp8AY+YNPL3Ga+0KR9XJsaz69EkQpgI6porEqehuwle7hiXw5e3L7xFwNEbpCBlxgLRA==", "dev": true, "dependencies": { - "cspell-glob": "6.31.2", + "cspell-glob": "6.31.3", "find-up": "^5.0.0" }, "bin": { @@ -6188,9 +6207,9 @@ } }, "node_modules/cspell-glob": { - "version": "6.31.2", - "resolved": "https://registry.npmjs.org/cspell-glob/-/cspell-glob-6.31.2.tgz", - "integrity": "sha512-ceTjHM4HaBgvG5S3oiB+PTPYq58EQYG6MmYpycDHzpR5I2H1NurK9lxWHfANmLbi0DsHn58tIZNDMUnnQj19Jw==", + "version": "6.31.3", + "resolved": "https://registry.npmjs.org/cspell-glob/-/cspell-glob-6.31.3.tgz", + "integrity": "sha512-+koUJPSCOittQwhR0T1mj4xXT3N+ZnY2qQ53W6Gz9HY3hVfEEy0NpbwE/Uy7sIvFMbc426fK0tGXjXyIj72uhQ==", "dev": true, "dependencies": { "micromatch": "^4.0.5" @@ -6200,12 +6219,13 @@ } }, "node_modules/cspell-grammar": { - "version": "6.31.1", + "version": "6.31.3", + "resolved": "https://registry.npmjs.org/cspell-grammar/-/cspell-grammar-6.31.3.tgz", + "integrity": "sha512-TZYaOLIGAumyHlm4w7HYKKKcR1ZgEMKt7WNjCFqq7yGVW7U+qyjQqR8jqnLiUTZl7c2Tque4mca7n0CFsjVv5A==", "dev": true, - "license": "MIT", "dependencies": { - "@cspell/cspell-pipe": "6.31.1", - "@cspell/cspell-types": "6.31.1" + "@cspell/cspell-pipe": "6.31.3", + "@cspell/cspell-types": "6.31.3" }, "bin": { "cspell-grammar": "bin.mjs" @@ -6215,12 +6235,12 @@ } }, "node_modules/cspell-io": { - "version": "6.31.2", - "resolved": "https://registry.npmjs.org/cspell-io/-/cspell-io-6.31.2.tgz", - "integrity": "sha512-Lp7LsF/f35LaOneROb/9mWiprShz2ONxjYFAt3bYP7gIxq41lWi8QhO+SN6spoqPp/wQXjSqJ7MuTZsemxPRnA==", + "version": "6.31.3", + "resolved": "https://registry.npmjs.org/cspell-io/-/cspell-io-6.31.3.tgz", + "integrity": "sha512-yCnnQ5bTbngUuIAaT5yNSdI1P0Kc38uvC8aynNi7tfrCYOQbDu1F9/DcTpbdhrsCv+xUn2TB1YjuCmm0STfJlA==", "dev": true, "dependencies": { - "@cspell/cspell-service-bus": "6.31.1", + "@cspell/cspell-service-bus": "6.31.3", "node-fetch": "^2.6.9" }, "engines": { @@ -6228,24 +6248,24 @@ } }, "node_modules/cspell-lib": { - "version": "6.31.2", - "resolved": "https://registry.npmjs.org/cspell-lib/-/cspell-lib-6.31.2.tgz", - "integrity": "sha512-LqaB2ZfVfQHKL5aZzYoKU6/UxxAtWeXAYwpC9l+satXmajYyXtAh4kWmuW+y7kKRH2jA79rJQS3QE6ToeSqgQQ==", + "version": "6.31.3", + "resolved": "https://registry.npmjs.org/cspell-lib/-/cspell-lib-6.31.3.tgz", + "integrity": "sha512-Dv55aecaMvT/5VbNryKo0Zos8dtHon7e1K0z8DR4/kGZdQVT0bOFWeotSLhuaIqoNFdEt8ypfKbrIHIdbgt1Hg==", "dev": true, "dependencies": { - "@cspell/cspell-bundled-dicts": "6.31.2", - "@cspell/cspell-pipe": "6.31.1", - "@cspell/cspell-types": "6.31.1", - "@cspell/strong-weak-map": "6.31.1", + "@cspell/cspell-bundled-dicts": "6.31.3", + "@cspell/cspell-pipe": "6.31.3", + "@cspell/cspell-types": "6.31.3", + "@cspell/strong-weak-map": "6.31.3", "clear-module": "^4.1.2", "comment-json": "^4.2.3", "configstore": "^5.0.1", "cosmiconfig": "8.0.0", - "cspell-dictionary": "6.31.1", - "cspell-glob": "6.31.2", - "cspell-grammar": "6.31.1", - "cspell-io": "6.31.2", - "cspell-trie-lib": "6.31.1", + "cspell-dictionary": "6.31.3", + "cspell-glob": "6.31.3", + "cspell-grammar": "6.31.3", + "cspell-io": "6.31.3", + "cspell-trie-lib": "6.31.3", "fast-equals": "^4.0.3", "find-up": "^5.0.0", "gensequence": "^5.0.2", @@ -6347,12 +6367,13 @@ } }, "node_modules/cspell-trie-lib": { - "version": "6.31.1", + "version": "6.31.3", + "resolved": "https://registry.npmjs.org/cspell-trie-lib/-/cspell-trie-lib-6.31.3.tgz", + "integrity": "sha512-HNUcLWOZAvtM3E34U+7/mSSpO0F6nLd/kFlRIcvSvPb9taqKe8bnSa0Yyb3dsdMq9rMxUmuDQtF+J6arZK343g==", "dev": true, - "license": "MIT", "dependencies": { - "@cspell/cspell-pipe": "6.31.1", - "@cspell/cspell-types": "6.31.1", + "@cspell/cspell-pipe": "6.31.3", + "@cspell/cspell-types": "6.31.3", "gensequence": "^5.0.2" }, "engines": { @@ -7781,8 +7802,9 @@ }, "node_modules/fast-equals": { "version": "4.0.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", + "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==", + "dev": true }, "node_modules/fast-glob": { "version": "3.2.12", @@ -8122,8 +8144,9 @@ }, "node_modules/gensequence": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/gensequence/-/gensequence-5.0.2.tgz", + "integrity": "sha512-JlKEZnFc6neaeSVlkzBGGgkIoIaSxMgvdamRoPN8r3ozm2r9dusqxeKqYQ7lhzmj2UhFQP8nkyfCaiLQxiLrDA==", "dev": true, - "license": "MIT", "engines": { "node": ">=14" } diff --git a/package.json b/package.json index f69b923c3..7f8af64bb 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ ], "devDependencies": { "@apollo/cache-control-types": "1.0.3", - "@apollo/client": "3.8.0", + "@apollo/client": "3.8.1", "@apollo/server": "4.9.1", "@apollo/utils.fetcher": "2.0.1", "@changesets/changelog-github": "0.4.8", @@ -67,7 +67,7 @@ "@types/uuid": "9.0.2", "@typescript-eslint/eslint-plugin": "5.62.0", "bunyan": "1.8.15", - "cspell": "6.31.2", + "cspell": "6.31.3", "graphql": "16.7.1", "graphql-http": "1.21.0", "graphql-tag": "2.12.6", @@ -99,7 +99,7 @@ ] }, "volta": { - "node": "18.17.0", + "node": "18.17.1", "npm": "9.8.1" } } From e963de112d2096aa936e2b2f28f7e37b0c4e004c Mon Sep 17 00:00:00 2001 From: Ilya Denisov Date: Fri, 11 Aug 2023 11:32:53 +0300 Subject: [PATCH 18/95] Fix "TypeError: Cannot read properties of null" (#2716) Fix "TypeError: Cannot read properties of null" The "data rewrites" logic that sometimes applies on data pre or post subgraph fetches was not handling `null` correctly, leading to potential execution errors. This fixes the issue. --------- Co-authored-by: Sylvain Lebresne --- .changeset/five-experts-give.md | 8 ++ .../src/__tests__/executeQueryPlan.test.ts | 93 +++++++++++++++++++ gateway-js/src/dataRewrites.ts | 2 +- 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 .changeset/five-experts-give.md diff --git a/.changeset/five-experts-give.md b/.changeset/five-experts-give.md new file mode 100644 index 000000000..c94947fe6 --- /dev/null +++ b/.changeset/five-experts-give.md @@ -0,0 +1,8 @@ +--- +"@apollo/gateway": major +--- + +Fix execution error in some cases where aliases are used and some values are `null`. + +The error would manifest itself as an `INTERNAL_SERVER_ERROR` with a message of the form `Cannot read properties of null`. + \ No newline at end of file diff --git a/gateway-js/src/__tests__/executeQueryPlan.test.ts b/gateway-js/src/__tests__/executeQueryPlan.test.ts index 99642a458..1980fecd5 100644 --- a/gateway-js/src/__tests__/executeQueryPlan.test.ts +++ b/gateway-js/src/__tests__/executeQueryPlan.test.ts @@ -6140,4 +6140,97 @@ describe('executeQueryPlan', () => { } `); }); + + it('handles post-query rewrites correctly even when null values are involved', async () => { + // Tests reproducing the issue of #2715 + const s1 = { + name: 'S1', + typeDefs: gql` + type Query { + tests: [Test!]! + } + + type Test { + node: Node + } + + interface Node { + id: ID! + } + + type Foo implements Node { + id: ID! + foo: String + } + + type Bar implements Node { + id: ID! + bar: String + } + `, + resolvers: { + Query: { + tests() { + return [{ node: null }]; + }, + }, + Node: { + __resolveType(obj: any) { + if (obj['foo']) { + return 'Foo' + } + if (obj['bar']) { + return 'Bar' + } + return undefined; + } + } + } + } + + const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1 ]); + const operation = parseOp(` + query { + tests { + node { + ... on Foo { test: foo } + ... on Bar { test: bar } + } + } + } + `, schema); + + const queryPlan = buildPlan(operation, queryPlanner); + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "S1") { + { + tests { + node { + __typename + ... on Foo { + test: foo + } + ... on Bar { + test__alias_0: bar + } + } + } + } + }, + } + `); + const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + expect(response.errors).toBeUndefined(); + expect(response.extensions).toBeUndefined(); + expect(response.data).toMatchInlineSnapshot(` + Object { + "tests": Array [ + Object { + "node": null, + }, + ], + } + `); + }) }); diff --git a/gateway-js/src/dataRewrites.ts b/gateway-js/src/dataRewrites.ts index f1d713f94..2e1dc08a1 100644 --- a/gateway-js/src/dataRewrites.ts +++ b/gateway-js/src/dataRewrites.ts @@ -63,7 +63,7 @@ function applyAtPath(schema: GraphQLSchema, path: string[], value: any, fct: (ob return; } - if (typeof value !== 'object') { + if (typeof value !== 'object' || value === null) { return; } From 93988db7d9d99565ec7fb7839a3388c7c7ce7617 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 14 Aug 2023 11:52:24 -0600 Subject: [PATCH 19/95] Add link to federated architecture article (#2732) This adds a link to the[ new federated arch article](https://graphql.com/learn/federated-architecture/). --- docs/source/config.json | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/config.json b/docs/source/config.json index a51612e1b..c2192b246 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -15,6 +15,7 @@ "Changelog": "/federation-versions", "Interactive tutorial": "https://www.apollographql.com/tutorials/voyage-part1", "Demo app": "https://github.com/apollographql/supergraph-demo-fed2", + "Federated architecture": "https://graphql.com/learn/federated-architecture/", "Quickstart": { "1️⃣ Project setup": "/quickstart/setup", "2️⃣ Composition in Apollo Studio": "/quickstart/studio-composition", From 1add932c5cd1297853fb5af9a3a6aaa71243f63a Mon Sep 17 00:00:00 2001 From: Sylvain Lebresne Date: Tue, 15 Aug 2023 16:21:51 +0200 Subject: [PATCH 20/95] Expands over-eager merging of field fix to handle `@defer` consistently (#2720) The previously committed [#2713](https://github.com/apollographql/federation/pull/2713) fixed an issue introduced by [#2387](https://github.com/apollographql/federation/pull/2387), ensuring that querying the same field with different directives applications was not merged, similar to what was/is done for fragments. But the exact behaviour slightly differs between fields and fragments when it comes to `@defer` in that for fragments, we never merge 2 similar fragments where both have `@defer`, which we do merge for fields. Or to put it more concretely, in the following query: ```graphq query Test($skipField: Boolean!) { x { ... on X @defer { a } ... on X @defer { b } } } ``` the 2 `... on X @defer` are not merged, resulting in 2 deferred sections that can run in parallel. But following [#2713](https://github.com/apollographql/federation/pull/2713), query: ```graphq query Test($skipField: Boolean!) { x @defer { a } x @defer { b } } ``` _will_ merge both `x @defer`, resulting in a single deferred section. This fix changes that later behaviour so that the 2 `x @defer` are not merged and result in 2 deferred sections, consistently with both 1) the case of fragments and 2) the behaviour prior to [#2387](https://github.com/apollographql/federation/pull/2387). --- .changeset/sixty-walls-chew.md | 41 +++ internals-js/src/__tests__/operations.test.ts | 318 +++++++++++++++++- internals-js/src/operations.ts | 11 +- 3 files changed, 348 insertions(+), 22 deletions(-) create mode 100644 .changeset/sixty-walls-chew.md diff --git a/.changeset/sixty-walls-chew.md b/.changeset/sixty-walls-chew.md new file mode 100644 index 000000000..d60b505e5 --- /dev/null +++ b/.changeset/sixty-walls-chew.md @@ -0,0 +1,41 @@ +--- +"@apollo/federation-internals": patch +--- + +Expands over-eager merging of field fix to handle `@defer` consistently + +The previously committed [#2713](https://github.com/apollographql/federation/pull/2713) fixed an issue introduced by +[#2387](https://github.com/apollographql/federation/pull/2387), ensuring that querying the same field with different +directives applications was not merged, similar to what was/is done for fragments. But the exact behaviour slightly +differs between fields and fragments when it comes to `@defer` in that for fragments, we never merge 2 similar fragments +where both have `@defer`, which we do merge for fields. Or to put it more concretely, in the following query: +```graphq +query Test($skipField: Boolean!) { + x { + ... on X @defer { + a + } + ... on X @defer { + b + } + } +} +``` +the 2 `... on X @defer` are not merged, resulting in 2 deferred sections that can run in parallel. But following +[#2713](https://github.com/apollographql/federation/pull/2713), query: +```graphq +query Test($skipField: Boolean!) { + x @defer { + a + } + x @defer { + b + } +} +``` +_will_ merge both `x @defer`, resulting in a single deferred section. + +This fix changes that later behaviour so that the 2 `x @defer` are not merged and result in 2 deferred sections, +consistently with both 1) the case of fragments and 2) the behaviour prior to +[#2387](https://github.com/apollographql/federation/pull/2387). + \ No newline at end of file diff --git a/internals-js/src/__tests__/operations.test.ts b/internals-js/src/__tests__/operations.test.ts index 512860de8..a6093dd77 100644 --- a/internals-js/src/__tests__/operations.test.ts +++ b/internals-js/src/__tests__/operations.test.ts @@ -2476,6 +2476,8 @@ describe('basic operations', () => { b1: Int b2: T } + + directive @customSkip(if: Boolean!, label: String!) on FIELD | INLINE_FRAGMENT `); const operation = parseOperation(schema, ` @@ -2521,29 +2523,309 @@ describe('basic operations', () => { ]); }) - test('fields are keyed on both name and directive applications', () => { - const operation = operationFromDocument(schema, gql` - query Test($skipIf: Boolean!) { - t { - v1 + describe('same field merging', () => { + test('do merge when same field and no directive', () => { + const operation = operationFromDocument(schema, gql` + query Test { + t { + v1 + } + t { + v2 + } } - t @skip(if: $skipIf) { - v2 + `); + + expect(operation.toString()).toMatchString(` + query Test { + t { + v1 + v2 + } } - } - `); + `); + }); - expect(operation.toString()).toMatchString(` - query Test($skipIf: Boolean!) { - t { - v1 + test('do merge when both have the _same_ directive', () => { + const operation = operationFromDocument(schema, gql` + query Test($skipIf: Boolean!) { + t @skip(if: $skipIf) { + v1 + } + t @skip(if: $skipIf) { + v2 + } } - t @skip(if: $skipIf) { - v2 + `); + + expect(operation.toString()).toMatchString(` + query Test($skipIf: Boolean!) { + t @skip(if: $skipIf) { + v1 + v2 + } } - } - `); - }) + `); + }); + + test('do merge when both have the _same_ directive, even if argument order differs', () => { + const operation = operationFromDocument(schema, gql` + query Test($skipIf: Boolean!) { + t @customSkip(if: $skipIf, label: "foo") { + v1 + } + t @customSkip(label: "foo", if: $skipIf) { + v2 + } + } + `); + + expect(operation.toString()).toMatchString(` + query Test($skipIf: Boolean!) { + t @customSkip(if: $skipIf, label: "foo") { + v1 + v2 + } + } + `); + }); + + test('do not merge when one has a directive and the other do not', () => { + const operation = operationFromDocument(schema, gql` + query Test($skipIf: Boolean!) { + t { + v1 + } + t @skip(if: $skipIf) { + v2 + } + } + `); + + expect(operation.toString()).toMatchString(` + query Test($skipIf: Boolean!) { + t { + v1 + } + t @skip(if: $skipIf) { + v2 + } + } + `); + }); + + test('do not merge when both have _differing_ directives', () => { + const operation = operationFromDocument(schema, gql` + query Test($skip1: Boolean!, $skip2: Boolean!) { + t @skip(if: $skip1) { + v1 + } + t @skip(if: $skip2) { + v2 + } + } + `); + + expect(operation.toString()).toMatchString(` + query Test($skip1: Boolean!, $skip2: Boolean!) { + t @skip(if: $skip1) { + v1 + } + t @skip(if: $skip2) { + v2 + } + } + `); + }); + + test('do not merge @defer directive, even if applied the same way', () => { + const operation = operationFromDocument(schema, gql` + query Test { + t @defer { + v1 + } + t @defer { + v2 + } + } + `); + + expect(operation.toString()).toMatchString(` + query Test { + t @defer { + v1 + } + t @defer { + v2 + } + } + `); + }); + }); + + describe('same fragment merging', () => { + test('do merge when same fragment and no directive', () => { + const operation = operationFromDocument(schema, gql` + query Test { + t { + ... on T { + v1 + } + ... on T { + v2 + } + } + } + `); + + expect(operation.toString()).toMatchString(` + query Test { + t { + ... on T { + v1 + v2 + } + } + } + `); + }); + + test('do merge when both have the _same_ directive', () => { + const operation = operationFromDocument(schema, gql` + query Test($skipIf: Boolean!) { + t { + ... on T @skip(if: $skipIf) { + v1 + } + ... on T @skip(if: $skipIf) { + v2 + } + } + } + `); + + expect(operation.toString()).toMatchString(` + query Test($skipIf: Boolean!) { + t { + ... on T @skip(if: $skipIf) { + v1 + v2 + } + } + } + `); + }); + + test('do merge when both have the _same_ directive, even if argument order differs', () => { + const operation = operationFromDocument(schema, gql` + query Test($skipIf: Boolean!) { + t { + ... on T @customSkip(if: $skipIf, label: "foo") { + v1 + } + ... on T @customSkip(label: "foo", if: $skipIf) { + v2 + } + } + } + `); + + expect(operation.toString()).toMatchString(` + query Test($skipIf: Boolean!) { + t { + ... on T @customSkip(if: $skipIf, label: "foo") { + v1 + v2 + } + } + } + `); + }); + + test('do not merge when one has a directive and the other do not', () => { + const operation = operationFromDocument(schema, gql` + query Test($skipIf: Boolean!) { + t { + ... on T { + v1 + } + ... on T @skip(if: $skipIf) { + v2 + } + } + } + `); + + expect(operation.toString()).toMatchString(` + query Test($skipIf: Boolean!) { + t { + ... on T { + v1 + } + ... on T @skip(if: $skipIf) { + v2 + } + } + } + `); + }); + + test('do not merge when both have _differing_ directives', () => { + const operation = operationFromDocument(schema, gql` + query Test($skip1: Boolean!, $skip2: Boolean!) { + t { + ... on T @skip(if: $skip1) { + v1 + } + ... on T @skip(if: $skip2) { + v2 + } + } + } + `); + + expect(operation.toString()).toMatchString(` + query Test($skip1: Boolean!, $skip2: Boolean!) { + t { + ... on T @skip(if: $skip1) { + v1 + } + ... on T @skip(if: $skip2) { + v2 + } + } + } + `); + }); + + test('do not merge @defer directive, even if applied the same way', () => { + const operation = operationFromDocument(schema, gql` + query Test { + t { + ... on T @defer { + v1 + } + ... on T @defer { + v2 + } + } + } + `); + + expect(operation.toString()).toMatchString(` + query Test { + t { + ... on T @defer { + v1 + } + ... on T @defer { + v2 + } + } + } + `); + }); + }); }); describe('MutableSelectionSet', () => { diff --git a/internals-js/src/operations.ts b/internals-js/src/operations.ts index 0103382d9..80c0ef420 100644 --- a/internals-js/src/operations.ts +++ b/internals-js/src/operations.ts @@ -113,6 +113,10 @@ abstract class AbstractOperationElement> e } } } + + protected keyForDirectives(): string { + return this.appliedDirectives.map((d) => keyForDirective(d)).join(' '); + } } export class Field extends AbstractOperationElement> { @@ -146,7 +150,7 @@ export class Field ex } key(): string { - return this.responseName() + this.appliedDirectivesToString(); + return this.responseName() + this.keyForDirectives(); } asPathElement(): string { @@ -394,7 +398,7 @@ export class Field ex * 2. we sort the argument (by their name) before converting them to string, since argument order does not matter in graphQL. */ function keyForDirective( - directive: Directive, + directive: Directive>, directivesNeverEqualToThemselves: string[] = [ 'defer' ], ): string { if (directivesNeverEqualToThemselves.includes(directive.name)) { @@ -436,8 +440,7 @@ export class FragmentElement extends AbstractOperationElement { if (!this.computedKey) { // The key is such that 2 fragments with the same key within a selection set gets merged together. So the type-condition // is include, but so are the directives. - const keyForDirectives = this.appliedDirectives.map((d) => keyForDirective(d)).join(' '); - this.computedKey = '...' + (this.typeCondition ? ' on ' + this.typeCondition.name : '') + keyForDirectives; + this.computedKey = '...' + (this.typeCondition ? ' on ' + this.typeCondition.name : '') + this.keyForDirectives(); } return this.computedKey; } From f01cdacc9d62fc08ce7758183b0176b335579fd1 Mon Sep 17 00:00:00 2001 From: Dariusz Kuc <9501705+dariuszkuc@users.noreply.github.com> Date: Tue, 15 Aug 2023 10:01:54 -0500 Subject: [PATCH 21/95] docs: federation v2.5 documentation updates (#2688) Co-authored-by: Maria Elisabeth Schreiber --- .../federated-types/federated-directives.mdx | 68 ++++++++++ docs/source/federation-versions.mdx | 120 ++++++++++++++++++ docs/source/subgraph-spec.mdx | 7 + 3 files changed, 195 insertions(+) diff --git a/docs/source/federated-types/federated-directives.mdx b/docs/source/federated-types/federated-directives.mdx index a80877359..7a8dd10ee 100644 --- a/docs/source/federated-types/federated-directives.mdx +++ b/docs/source/federated-types/federated-directives.mdx @@ -349,6 +349,74 @@ For more information, see [Migrating entities and fields](../entities-advanced/#
_service<
GraphQL Mesh
Executable GraphQL schema from multiple data sources, query anything, run anywhere.

Github: Urigo/graphql-mesh
Type: undefined
-Stars: 2.8k ⭐
-Last Release: 2023-03-23

_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
StepZen, an IBM Company
Build GraphQL APIs for all your data in a declarative way. Federate across any data source, including GraphQL.

_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🔲
repeatable @key🔲
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
+## Controlling access + +### `@authenticated` + +> ⚠️ **This directive is available in Apollo Federation 2.5 and later.** +> **It is an [Enterprise feature](/router/enterprise-features) of the Apollo Router** and requires an organization with a [GraphOS Enterprise plan](https://www.apollographql.com/pricing/).
+> +> If your organization _doesn't_ currently have an Enterprise plan, you can test out this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). + +```graphql +directive @authenticated on + FIELD_DEFINITION + | OBJECT + | INTERFACE + | SCALAR + | ENUM +``` + +Indicates to composition that the target element is accessible only to the authenticated supergraph users. For more granular access control, see the [`@requiresScope`](#requiresScope) directive below. Refer to the [Apollo Router article](/router/configuration/authorization#authenticated) for additional details. + +### `@requiresScope` + +> ⚠️ **This directive is available in Apollo Federation 2.5 and later.** +> **Is is an [Enterprise feature](/router/enterprise-features) of the Apollo Router** and requires an organization with a [GraphOS Enterprise plan](https://www.apollographql.com/pricing/).
+> +> If your organization _doesn't_ currently have an Enterprise plan, you can test out this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). + +```graphql +directive @requiresScopes(scopes: [federation__Scope!]!) on + FIELD_DEFINITION + | OBJECT + | INTERFACE + | SCALAR + | ENUM +``` + +Indicates to composition that the target element is accessible only to the authenticated supergraph users with the appropriate JWT scopes. Refer to the [Apollo Router article](/router/configuration/authorization#requiresscopes) for additional details. + +#### Arguments + + + + + + + + + + + + + + + + + +
Name /
Type
Description
+ +##### `scopes` + +`[federation__Scope!]!` + + + +**Required.** List of JWT scopes that must be granted to the user in order to access the underlying element data. + +
+ ## Referencing external fields ### `@external` diff --git a/docs/source/federation-versions.mdx b/docs/source/federation-versions.mdx index b62f0e047..19b2e314a 100644 --- a/docs/source/federation-versions.mdx +++ b/docs/source/federation-versions.mdx @@ -21,6 +21,126 @@ This article describes notable changes and additions introduced in each minor ve - **If you maintain a [subgraph-compatible library](./building-supergraphs/supported-subgraphs/),** consult this article to stay current with recently added directives. All of these directive definitions are also listed in the [subgraph specification](./subgraph-spec/#subgraph-schema-additions). +## v2.5 + +
+ + + +
+ +First release + +**July 2023** + +
+ +
+ +Available in GraphOS? + +**No** + +
+ +
+ +Minimum router version + +**`TBD`** + +
+ +
+ +
+ +#### Directive changes + + + + + + + + + + + + + + + + + + + + + +
TopicDescription
+ +##### `@authenticated` + + + + +Introduced. [Learn more.](/router/configuration/authorization) + +```graphql +directive @authenticated on + FIELD_DEFINITION + | OBJECT + | INTERFACE + | SCALAR + | ENUM +``` + +
+ +##### `@requiresScope` + + + + +Introduced. [Learn more.](/router/configuration/authorization) + +```graphql +directive @requiresScopes(scopes: [Scope!]!) on + FIELD_DEFINITION + | OBJECT + | INTERFACE + | SCALAR + | ENUM +``` + +
+ +#### Subgraph changes + + + + + + + + + + + + + + + +
TopicDescription
+ +Scope + + + +- Custom scalar representing a JWT scope. Used by new `@requiresScope` directive. + +
+ ## v2.4
diff --git a/docs/source/subgraph-spec.mdx b/docs/source/subgraph-spec.mdx index b25ae223b..a2e80ab96 100644 --- a/docs/source/subgraph-spec.mdx +++ b/docs/source/subgraph-spec.mdx @@ -30,6 +30,7 @@ union _Entity scalar _Any scalar FieldSet scalar link__Import +scalar federation__Scope enum link__Purpose { """ @@ -63,6 +64,8 @@ directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJEC directive @override(from: String!) on FIELD_DEFINITION directive @composeDirective(name: String!) repeatable on SCHEMA directive @interfaceObject on OBJECT +directive @authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM +directive @requiresScopes(scopes: [federation__Scope!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM # This definition is required only for libraries that don't support # GraphQL's built-in `extend` keyword @@ -445,6 +448,10 @@ This string-serialized scalar represents a set of fields that's passed to a fede Grammatically, a `FieldSet` is a [selection set](http://spec.graphql.org/draft/#sec-Selection-Sets) minus the outermost curly braces. It can represent a single field (`"upc"`), multiple fields (`"id countryCode"`), and even nested selection sets (`"id organization { id }"`). +#### `scalar Scope` + +This string-serialized scalar represents a JWT scope. + ### Directives See [Federation-specific GraphQL directives](./federated-types/federated-directives/). From 6f1fddb25d49262b2ebf6db953371a559dd62e9c Mon Sep 17 00:00:00 2001 From: Sylvain Lebresne Date: Wed, 16 Aug 2023 11:23:33 +0200 Subject: [PATCH 22/95] Generalizes heurisitic to cut indirect paths to the root of paths (#2669) The planner has a number of heuristics whose goal is to cut the number of concrete plans it has to fully generate and evaluate, which is important to its performance. One of them cut paths that take a "detour" in a subgraph when a more direct path exists, that is, if we have some path: ``` T(S1) --[x]--> U(S1) --[y]--> V(S1) ``` then we ignore a path that, from `T` in S1 goes to some S2 subgraph to get `x` to come back to S1, that is: ``` T(S1) --[key(id)] --> T(S2) --[x]--> U(S2) --[key(id)] --> U(S1) --[y]--> V(S1) ``` because the more direct option is always going to be better. However, the code for this optimisation was not working for essentially the same case but at the root of the path. That is, if we have path: ``` Query(S1) --[x]--> U(S1) --[y]--> V(S1) ``` then we were not ignoring: ``` Query(S2) --[x]--> U(S2) --[key(id)] --> U(S1) --[y]--> V(S1) ``` even though it similarly never going to be better (even if we must get other fields after `x` in S2, it's still better to get 2 top-level parallel fetch to both S1 and S2, rather than only getting to S2 to get to S1 next, as it's the same number of fetches but there is more serialization). Long story short, this commit slightly generalise the existing heuristic so it cut this root-level case too. Note that when a lot of queried field follow that pattern, this can cut the number of plans considered drastically, so this optimisation can also result in better generated plans due to the planner not falling into its "panic" mode where it semi-arbitrarily cut options to avoid exponential explosion, yielding potentially non-optimal plans in doing so. Fixes #2661. --- .changeset/tall-impalas-act.md | 11 +++ query-graphs-js/src/graphPath.ts | 57 +++++++++++---- .../src/__tests__/buildPlan.test.ts | 70 ++++++++++++++++++- query-planner-js/src/buildPlan.ts | 1 - 4 files changed, 123 insertions(+), 16 deletions(-) create mode 100644 .changeset/tall-impalas-act.md diff --git a/.changeset/tall-impalas-act.md b/.changeset/tall-impalas-act.md new file mode 100644 index 000000000..b0bec7533 --- /dev/null +++ b/.changeset/tall-impalas-act.md @@ -0,0 +1,11 @@ +--- +"@apollo/query-planner": patch +"@apollo/query-graphs": patch +--- + +More aggressive ignoring of indirect paths from root types when a more direct alternative exists. This optimisation +slightly generalize an existing heuristic of the query planner, allowing it to ignore some known inefficient options +earlier in its process. When this optimisation can be used, this yield faster query plan computation, but by reducing +the number of plans to be consider, this can sometimes prevent the planner to degrade it's output when it consider +there is too many plans to consider, which can result in more optimal query plans too. + diff --git a/query-graphs-js/src/graphPath.ts b/query-graphs-js/src/graphPath.ts index ba8ef2ea1..6fb38ba55 100644 --- a/query-graphs-js/src/graphPath.ts +++ b/query-graphs-js/src/graphPath.ts @@ -419,7 +419,9 @@ export class GraphPath `Shouldn't have conditions paths (got ${conditionsResolution.pathTree}) for edge without conditions (edge: ${edge})`); - let subgraphEnteringEdge = this.subgraphEnteringEdge; + // We clear `subgraphEnteringEdge` as we enter a @defer: that is because `subgraphEnteringEdge` is used to eliminate some + // non-optimal paths, but we don't want those optimizations to bypass a defer. + let subgraphEnteringEdge = defer ? undefined : this.subgraphEnteringEdge; if (edge) { if (edge.transition.kind === 'DownCast' && this.props.edgeToTail) { @@ -471,20 +473,16 @@ export class GraphPath Edge | null | undefined + triggerToEdge: (graph: QueryGraph, vertex: Vertex, t: TTrigger) => Edge | null | undefined, + prevSubgraphStartingVertex?: Vertex, ): Vertex | undefined { const enteringEdge = this.subgraphEnteringEdge; if (!enteringEdge) { return undefined; } - let prevSubgraphVertex = enteringEdge.edge.head; + + // Usually, the starting subgraph in which we want to look for a direct path is the head of + // `subgraphEnteringEdge`, that is, where we were just before coming to the current subgraph. + // But for subgraph entering edges, we're not coming from a subgraph, so instead we pass the + // "root" vertex of the subgraph of interest in `prevSubgraphStartingVertex`. And if that + // is undefined (for a subgraph entering edge), then that means the subgraph does not have + // the root type in question (say, no mutation type), and so there can be no direct path in + // that subgraph. + if (enteringEdge.edge.transition.kind === 'SubgraphEnteringTransition' && !prevSubgraphStartingVertex) { + return undefined; + } + + let prevSubgraphVertex = prevSubgraphStartingVertex ?? enteringEdge.edge.head; for (let i = enteringEdge.index + 1; i < this.size; i++) { const triggerToMatch = this.props.edgeTriggers[i]; const prevSubgraphMatchingEdge = triggerToEdge(this.graph, prevSubgraphVertex, triggerToMatch); @@ -1394,9 +1405,20 @@ function advancePathWithNonCollectingAndTypePreservingTransitions `${toAdvance} should be a root path if it starts with subgraph entering edge ${subgraphEnteringEdge.edge}`); + prevSubgraphEnteringVertex = rootVertexForSubgraph(toAdvance.graph, edge.tail.source, toAdvance.root.rootKind); + // If the entering edge is the root entering of subgraphs, then the "prev subgraph" is really `edge.tail.source` and + // so `edge` always get us back to that (but `subgraphEnteringEdge.edge.head.source` would be `FEDERATED_GRAPH_ROOT_SOURCE`, + // so the test we do in the `else` branch would not work here). + backToPreviousSubgraph = true; + } else { + backToPreviousSubgraph = subgraphEnteringEdge.edge.head.source === edge.tail.source; + } + const prevSubgraphVertex = toAdvance.checkDirectPathFromPreviousSubgraphTo(edge.tail.type.name, triggerToEdge, prevSubgraphEnteringVertex); const maxCost = toAdvance.subgraphEnteringEdge.cost + (backToPreviousSubgraph ? 0 : conditionResolution.cost); if (prevSubgraphVertex && ( @@ -1468,6 +1490,13 @@ function advancePathWithNonCollectingAndTypePreservingTransitions `Should not have ask for ${rootKind} as the graph does not have one`); + const subgraphRootEdge = graph.outEdges(root).find((e) => e.tail.source === subgraphName); + return subgraphRootEdge?.tail; +} + function conditionHasOverriddenFieldsInSource(schema: Schema, condition: SelectionSet): boolean { const externalDirective = federationMetadata(schema)!.externalDirective(); return allFieldDefinitionsInSelectionSet(condition).some((field) => { diff --git a/query-planner-js/src/__tests__/buildPlan.test.ts b/query-planner-js/src/__tests__/buildPlan.test.ts index 681de1fe9..72e9c0e68 100644 --- a/query-planner-js/src/__tests__/buildPlan.test.ts +++ b/query-planner-js/src/__tests__/buildPlan.test.ts @@ -6722,7 +6722,6 @@ test('does not error on some complex fetch group dependencies', () => { `); }); - test('does not evaluate plans relying on a key field to fetch that same field', () => { const subgraph1 = { name: 'Subgraph1', @@ -6808,3 +6807,72 @@ test('does not evaluate plans relying on a key field to fetch that same field', // cases could impact query planning performance quite a bit). expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe(1); }); + +test('avoid considering indirect paths from the root when a more direct one exists', () => { + const subgraph1 = { + name: 'Subgraph1', + typeDefs: gql` + type Query { + t: T @shareable + } + + type T @key(fields: "id") { + id: ID! + v0: Int @shareable + } + ` + } + + const subgraph2 = { + name: 'Subgraph2', + typeDefs: gql` + type Query { + t: T @shareable + } + + type T @key(fields: "id") { + id: ID! + v0: Int @shareable + v1: Int + } + ` + } + + // Each of id/v0 can have 2 options each, so that's 4 combinations. If we were to consider 2 options for each + // v1 value however, that would multiple it by 2 each times, so it would 32 possibilities. We limit the number of + // evaluated plans just above our expected number of 4 so that if we exceed it, the generated plan will be sub-optimal. + const [api, queryPlanner] = composeAndCreatePlannerWithOptions([subgraph1, subgraph2], { debug: { maxEvaluatedPlans: 6 } }); + const operation = operationFromDocument(api, gql` + { + t { + id + v0 + a0: v1 + a1: v1 + a2: v1 + } + } + `); + + const plan = queryPlanner.buildQueryPlan(operation); + expect(plan).toMatchInlineSnapshot(` + QueryPlan { + Fetch(service: "Subgraph2") { + { + t { + a0: v1 + a1: v1 + a2: v1 + id + v0 + } + } + }, + } + `); + + // As said above, we legit have 2 options for `id` and `v0`, and we cannot know which are best before we evaluate the + // plans completely. But for the multiple `v1`, we should recognize that going through the 1st subgraph (and taking a + // key) is never exactly a good idea. + expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe(4); +}); diff --git a/query-planner-js/src/buildPlan.ts b/query-planner-js/src/buildPlan.ts index bcf9c42f5..57f61aea2 100644 --- a/query-planner-js/src/buildPlan.ts +++ b/query-planner-js/src/buildPlan.ts @@ -1677,7 +1677,6 @@ function withFieldAliased(selectionSet: SelectionSet, aliases: FieldToAlias[]): } class DeferredInfo { - private constructor( readonly label: string, readonly path: GroupPath, From c6e0e76dbc62662c2aa6ff7f657e374047b11255 Mon Sep 17 00:00:00 2001 From: Sylvain Lebresne Date: Wed, 16 Aug 2023 11:27:28 +0200 Subject: [PATCH 23/95] Fix potential normalization assertion error for fragment on abstract types (#2725) An abstract type (interface or union) can have different runtime types in different subgraphs. For instance, a type `T` may implement some interface `I` in subgraph A, but not in subgraph B _even_ if `I` is otherwise defined in subgraph B (admittedly something not to be abused, but it can be convenient as a temporary state when evolving schema). For named fragment, this can create a case where a fragment on an abstract type can be "rebased" on a subgraph, but where some of its application (spread) in other fragments are invalid due to being use in the context of a type that intersect the abstrat type in the supergraph but not in the particular subgraph. When that's the case, the invalid spread (for the subgraph) needs to be expanded, and that expansion properly "rebased", but the code was not handling that case at all. Instead it kept the invalid spread, and this led to some later assertion errors. Fixes #2721. --- .changeset/nice-seahorses-cheer.md | 10 + internals-js/src/__tests__/operations.test.ts | 4 +- internals-js/src/operations.ts | 37 +- .../src/__tests__/buildPlan.test.ts | 501 ++++++++++++++++++ 4 files changed, 546 insertions(+), 6 deletions(-) create mode 100644 .changeset/nice-seahorses-cheer.md diff --git a/.changeset/nice-seahorses-cheer.md b/.changeset/nice-seahorses-cheer.md new file mode 100644 index 000000000..7c339c730 --- /dev/null +++ b/.changeset/nice-seahorses-cheer.md @@ -0,0 +1,10 @@ +--- +"@apollo/query-planner": patch +"@apollo/federation-internals": patch +--- + +Fix potential assertion error for named fragment on abstract types when the abstract type does not have the same +possible runtime types in all subgraphs. + +The error manifested itself during query planning with an error message of the form `Cannot normalize X at Y ...`. + \ No newline at end of file diff --git a/internals-js/src/__tests__/operations.test.ts b/internals-js/src/__tests__/operations.test.ts index a6093dd77..b7a411c02 100644 --- a/internals-js/src/__tests__/operations.test.ts +++ b/internals-js/src/__tests__/operations.test.ts @@ -3542,9 +3542,7 @@ describe('named fragment rebasing on subgraphs', () => { t { x y - ... on T { - z - } + z } } `); diff --git a/internals-js/src/operations.ts b/internals-js/src/operations.ts index 80c0ef420..2b7e6b92d 100644 --- a/internals-js/src/operations.ts +++ b/internals-js/src/operations.ts @@ -1406,7 +1406,10 @@ export class NamedFragments { return undefined; } - const rebasedSelection = fragment.selectionSet.rebaseOn({ parentType: rebasedType, fragments: newFragments, errorIfCannotRebase: false }); + let rebasedSelection = fragment.selectionSet.rebaseOn({ parentType: rebasedType, fragments: newFragments, errorIfCannotRebase: false }); + // Rebasing can leave some inefficiencies in some case (particularly when a spread has to be "expanded", see `FragmentSpreadSelection.rebaseOn`), + // so we do a top-level normalization to keep things clean. + rebasedSelection = rebasedSelection.normalize({ parentType: rebasedType }); return this.selectionSetIsWorthUsing(rebasedSelection) ? new NamedFragmentDefinition(schema, fragment.name, rebasedType).setSelectionSet(rebasedSelection) : undefined; @@ -1421,9 +1424,11 @@ export class NamedFragments { // dependency order, we know that `newFragments` will have every fragments that should be // kept/not expanded. const updatedSelectionSet = fragment.selectionSet.expandFragments(newFragments); + // Note that if we did expanded some fragments (the updated selection is not the original one), then the + // results may not be fully normalized, so we do it to be sure. return updatedSelectionSet === fragment.selectionSet ? fragment - : fragment.withUpdatedSelectionSet(updatedSelectionSet); + : fragment.withUpdatedSelectionSet(updatedSelectionSet.normalize({ parentType: updatedSelectionSet.parentType})); } else { return undefined; } @@ -3558,7 +3563,8 @@ class FragmentSpreadSelection extends FragmentSelection { // If we're rebasing on a _different_ schema, then we *must* have fragments, since reusing // `this.fragments` would be incorrect. If we're on the same schema though, we're happy to default // to `this.fragments`. - assert(fragments || this.parentType.schema() === parentType.schema(), `Must provide fragments is rebasing on other schema`); + const rebaseOnSameSchema = this.parentType.schema() === parentType.schema(); + assert(fragments || rebaseOnSameSchema, `Must provide fragments is rebasing on other schema`); const newFragments = fragments ?? this.fragments; const namedFragment = newFragments.get(this.namedFragment.name); // If we're rebasing on another schema (think a subgraph), then named fragments will have been rebased on that, and some @@ -3569,6 +3575,31 @@ class FragmentSpreadSelection extends FragmentSelection { validate(!errorIfCannotRebase, () => `Cannot rebase ${this.toString(false)} if it isn't part of the provided fragments`); return undefined; } + + // Lastly, if we rebase on a different schema, it's possible the fragment type does not intersect the + // parent type. For instance, the parent type could be some object type T while the fragment is an + // interface I, and T may implement I in the supergraph, but not in a particular subgraph (of course, + // if I don't exist at all in the subgraph, then we'll have exited above, but I may exist in the + // subgraph, just not be implemented by T for some reason). In that case, we can't reuse the fragment + // as its spread is essentially invalid in that position, so we have to replace it by the expansion + // of that fragment, which we rebase on the parentType (which in turn, will remove anythings within + // the fragment selection that needs removing, potentially everything). + if (!rebaseOnSameSchema && !runtimeTypesIntersects(parentType, namedFragment.typeCondition)) { + // Note that we've used the rebased `namedFragment` to check the type intersection because we needed to + // compare runtime types "for the schema we're rebasing into". But now that we're deciding to not reuse + // this rebased fragment, what we rebase is the selection set of the non-rebased fragment. And that's + // important because the very logic we're hitting here may need to happen inside the rebase do the + // fragment selection, but that logic would not be triggered if we used the rebased `namedFragment` since + // `rebaseOnSameSchema` would then be 'true'. + const expanded = this.namedFragment.selectionSet.rebaseOn({ parentType, fragments, errorIfCannotRebase }); + // In theory, we could return the selection set directly, but making `Selection.rebaseOn` sometimes + // return a `SelectionSet` complicate things quite a bit. So instead, we encapsulate the selection set + // in an "empty" inline fragment. This make for non-really-optimal selection sets in the (relatively + // rare) case where this is triggered, but in practice this "inefficiency" is removed by future calls + // to `normalize`. + return expanded.isEmpty() ? undefined : new InlineFragmentSelection(new FragmentElement(parentType), expanded); + } + return new FragmentSpreadSelection( parentType, newFragments, diff --git a/query-planner-js/src/__tests__/buildPlan.test.ts b/query-planner-js/src/__tests__/buildPlan.test.ts index 72e9c0e68..53411553c 100644 --- a/query-planner-js/src/__tests__/buildPlan.test.ts +++ b/query-planner-js/src/__tests__/buildPlan.test.ts @@ -4239,6 +4239,507 @@ describe('Named fragments preservation', () => { } `); }); + + it('handles fragment rebasing in a subgraph where some subtyping relation differs', () => { + // This test is designed such that type `Outer` implements the interface `I` in `Subgraph1` + // but not in `Subgraph2`, yet `I` exists in `Subgraph2` (but only `Inner` implements it + // there). Further, the operations we test have a fragment on I (`IFrag` below) that is + // used "in the context of `Outer`" (at the top-level of fragment `OuterFrag`). + // + // What this all means is that `IFrag` can be rebased in `Subgraph2` "as is" because `I` + // exists there with all its fields, but as we rebase `OuterFrag` on `Subgraph2`, we + // cannot use `...IFrag` inside it (at the top-level), because `I` and `Outer` do + // no intersect in `Subgraph2` and this would be an invalid selection. + // + // Previous versions of the code were not handling this case and were error out by + // creating the invalid selection (#2721), and this test ensures this is fixed. + const subgraph1 = { + name: 'Subgraph1', + typeDefs: gql` + type V @shareable { + x: Int + } + + interface I { + v: V + } + + type Outer implements I @key(fields: "id") { + id: ID! + v: V + } + ` + } + + const subgraph2 = { + name: 'Subgraph2', + typeDefs: gql` + type Query { + outer1: Outer + outer2: Outer + } + + type V @shareable { + x: Int + } + + interface I { + v: V + w: Int + } + + type Inner implements I { + v: V + w: Int + } + + type Outer @key(fields: "id") { + id: ID! + inner: Inner + w: Int + } + ` + } + + const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); + let operation = operationFromDocument(api, gql` + query { + outer1 { ...OuterFrag } + outer2 { ...OuterFrag } + } + + fragment OuterFrag on Outer { + ...IFrag + inner { ...IFrag } + } + + fragment IFrag on I { + v { x } + } + `); + + const expectedPlan = ` + QueryPlan { + Sequence { + Fetch(service: "Subgraph2") { + { + outer1 { + __typename + ...OuterFrag + id + } + outer2 { + __typename + ...OuterFrag + id + } + } + + fragment OuterFrag on Outer { + inner { + v { + x + } + } + } + }, + Parallel { + Flatten(path: "outer1") { + Fetch(service: "Subgraph1") { + { + ... on Outer { + __typename + id + } + } => + { + ... on Outer { + v { + x + } + } + } + }, + }, + Flatten(path: "outer2") { + Fetch(service: "Subgraph1") { + { + ... on Outer { + __typename + id + } + } => + { + ... on Outer { + v { + x + } + } + } + }, + }, + }, + }, + } + `; + expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot(expectedPlan); + + // We very slighly modify the operation to add an artificial indirection within `IFrag`. + // This does not really change the query, and should result in the same plan, but + // ensure the code handle correctly such indirection. + operation = operationFromDocument(api, gql` + query { + outer1 { ...OuterFrag } + outer2 { ...OuterFrag } + } + + fragment OuterFrag on Outer { + ...IFrag + inner { ...IFrag } + } + + fragment IFrag on I { + ...IFragDelegate + } + + fragment IFragDelegate on I { + v { x } + } + `); + + expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot(expectedPlan); + + // The previous cases tests the cases where nothing in the `...IFrag` spread at the + // top-level of `OuterFrag` applied at all: it all gets eliminated in the plan. But + // in the schema of `Subgraph2`, while `Outer` does not implement `I` (and does not + // have `v` in particular), it does contains field `w` that `I` also have, so we + // add that field to `IFrag` and make sure we still correctly query that field. + operation = operationFromDocument(api, gql` + query { + outer1 { ...OuterFrag } + outer2 { ...OuterFrag } + } + + fragment OuterFrag on Outer { + ...IFrag + inner { ...IFrag } + } + + fragment IFrag on I { + v { x } + w + } + `); + + expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "Subgraph2") { + { + outer1 { + __typename + ...OuterFrag + id + } + outer2 { + __typename + ...OuterFrag + id + } + } + + fragment OuterFrag on Outer { + w + inner { + v { + x + } + w + } + } + }, + Parallel { + Flatten(path: "outer1") { + Fetch(service: "Subgraph1") { + { + ... on Outer { + __typename + id + } + } => + { + ... on Outer { + v { + x + } + } + } + }, + }, + Flatten(path: "outer2") { + Fetch(service: "Subgraph1") { + { + ... on Outer { + __typename + id + } + } => + { + ... on Outer { + v { + x + } + } + } + }, + }, + }, + }, + } + `); + }); + + it('handles fragment rebasing in a subgraph where some union membership relation differs', () => { + // This test is similar to the subtyping case (it tests the same problems), but test the case + // of unions instead of interfaces. + const subgraph1 = { + name: 'Subgraph1', + typeDefs: gql` + type V @shareable { + x: Int + } + + union U = Outer + + type Outer @key(fields: "id") { + id: ID! + v: Int + } + ` + } + + const subgraph2 = { + name: 'Subgraph2', + typeDefs: gql` + type Query { + outer1: Outer + outer2: Outer + } + + union U = Inner + + type Inner { + v: Int + w: Int + } + + type Outer @key(fields: "id") { + id: ID! + inner: Inner + w: Int + } + ` + } + + const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); + let operation = operationFromDocument(api, gql` + query { + outer1 { ...OuterFrag } + outer2 { ...OuterFrag } + } + + fragment OuterFrag on Outer { + ...UFrag + inner { ...UFrag } + } + + fragment UFrag on U { + ... on Outer { + v + } + ... on Inner { + v + } + } + `); + + const expectedPlan = ` + QueryPlan { + Sequence { + Fetch(service: "Subgraph2") { + { + outer1 { + __typename + ...OuterFrag + id + } + outer2 { + __typename + ...OuterFrag + id + } + } + + fragment OuterFrag on Outer { + inner { + v + } + } + }, + Parallel { + Flatten(path: "outer1") { + Fetch(service: "Subgraph1") { + { + ... on Outer { + __typename + id + } + } => + { + ... on Outer { + v + } + } + }, + }, + Flatten(path: "outer2") { + Fetch(service: "Subgraph1") { + { + ... on Outer { + __typename + id + } + } => + { + ... on Outer { + v + } + } + }, + }, + }, + }, + } + `; + expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot(expectedPlan); + + // We very slighly modify the operation to add an artificial indirection within `IFrag`. + // This does not really change the query, and should result in the same plan, but + // ensure the code handle correctly such indirection. + operation = operationFromDocument(api, gql` + query { + outer1 { ...OuterFrag } + outer2 { ...OuterFrag } + } + + fragment OuterFrag on Outer { + ...UFrag + inner { ...UFrag } + } + + fragment UFrag on U { + ...UFragDelegate + } + + fragment UFragDelegate on U { + ... on Outer { + v + } + ... on Inner { + v + } + } + `); + + expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot(expectedPlan); + + // The previous cases tests the cases where nothing in the `...IFrag` spread at the + // top-level of `OuterFrag` applied at all: it all gets eliminated in the plan. But + // in the schema of `Subgraph2`, while `Outer` does not implement `I` (and does not + // have `v` in particular), it does contains field `w` that `I` also have, so we + // add that field to `IFrag` and make sure we still correctly query that field. + operation = operationFromDocument(api, gql` + query { + outer1 { ...OuterFrag } + outer2 { ...OuterFrag } + } + + fragment OuterFrag on Outer { + ...UFrag + inner { ...UFrag } + } + + fragment UFrag on U { + ... on Outer { + v + w + } + ... on Inner { + v + } + } + `); + + expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "Subgraph2") { + { + outer1 { + __typename + ...OuterFrag + id + } + outer2 { + __typename + ...OuterFrag + id + } + } + + fragment OuterFrag on Outer { + w + inner { + v + } + } + }, + Parallel { + Flatten(path: "outer1") { + Fetch(service: "Subgraph1") { + { + ... on Outer { + __typename + id + } + } => + { + ... on Outer { + v + } + } + }, + }, + Flatten(path: "outer2") { + Fetch(service: "Subgraph1") { + { + ... on Outer { + __typename + id + } + } => + { + ... on Outer { + v + } + } + }, + }, + }, + }, + } + `); + }); }); test('works with key chains', () => { From 4b9a512b62e02544d7854fa198942aac33b93feb Mon Sep 17 00:00:00 2001 From: Sylvain Lebresne Date: Wed, 16 Aug 2023 18:20:09 +0200 Subject: [PATCH 24/95] Modify type of argument of `@requiresScope` (#2738) This modifies the type for the argument of the `@requiresScopes` from `[federation__Scope!]!` to `[[federation__Scope!]!]!` in order to support "OR" semantic within subgraphs. Note that this PR does not bump any spec versions, even if technically `@requiresScopes` has already been part of 2.5.0+ because: - the overarching feature it exists for has not be released yet so the directive is not really effectively usable yet. - bumping the federation spec to 2.6, which this would require if we wanted to bump versions, would be disruption and confusing, so this feel unecessary given the previous. --- .changeset/great-lions-work.md | 13 +++++++++++++ composition-js/src/__tests__/compose.test.ts | 6 +++--- internals-js/src/requiresScopesSpec.ts | 2 +- 3 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 .changeset/great-lions-work.md diff --git a/.changeset/great-lions-work.md b/.changeset/great-lions-work.md new file mode 100644 index 000000000..0d0c8eb66 --- /dev/null +++ b/.changeset/great-lions-work.md @@ -0,0 +1,13 @@ +--- +"@apollo/composition": patch +"@apollo/federation-internals": patch +--- + +Modifies the type for the argument of the `@requiresScopes` from `[federation__Scope!]!` to `[[federation__Scope!]!]!`. + +The `@requiresScopes` directives has been pre-emptively introduced in 2.5.0 to support an upcoming Apollo Router +feature around scoped accesses. The argument for `@requiresScopes` in that upcoming feature is changed to accommodate a +new semantic. Note that this technically a breaking change to the `@requiresScopes` directive definition, but as the +full feature using that directive has been released yet, this directive cannot effectively be used and this should have +no concrete impact. + diff --git a/composition-js/src/__tests__/compose.test.ts b/composition-js/src/__tests__/compose.test.ts index a92320874..71c09d67e 100644 --- a/composition-js/src/__tests__/compose.test.ts +++ b/composition-js/src/__tests__/compose.test.ts @@ -4493,7 +4493,7 @@ describe('composition', () => { "https://specs.apollo.dev/requiresScopes/v0.1" ); expect(printDirectiveDefinition(result.schema.directive('requiresScopes')!)).toMatchString(` - directive @requiresScopes(scopes: [requiresScopes__Scope!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM + directive @requiresScopes(scopes: [[requiresScopes__Scope!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM `); }); @@ -4527,7 +4527,7 @@ describe('composition', () => { const invalidDefinition = { typeDefs: gql` scalar federation__Scope - directive @requiresScopes(scopes: [federation__Scope!]!) on ENUM_VALUE + directive @requiresScopes(scopes: [[federation__Scope!]!]!) on ENUM_VALUE type Query { a: Int @@ -4565,7 +4565,7 @@ describe('composition', () => { const result = composeAsFed2Subgraphs([invalidDefinition]); expect(errors(result)[0]).toEqual([ "DIRECTIVE_DEFINITION_INVALID", - "[invalidDefinition] Invalid definition for directive \"@requiresScopes\": argument \"scopes\" should have type \"[federation__Scope!]!\" but found type \"[federation__Scope]!\"", + "[invalidDefinition] Invalid definition for directive \"@requiresScopes\": argument \"scopes\" should have type \"[[federation__Scope!]!]!\" but found type \"[federation__Scope]!\"", ]); }); diff --git a/internals-js/src/requiresScopesSpec.ts b/internals-js/src/requiresScopesSpec.ts index f0bdb918a..2d0744eb3 100644 --- a/internals-js/src/requiresScopesSpec.ts +++ b/internals-js/src/requiresScopesSpec.ts @@ -41,7 +41,7 @@ export class RequiresScopesSpecDefinition extends FeatureDefinition { const scopeName = feature.typeNameInSchema(RequiresScopesTypeName.SCOPE); const scopeType = schema.type(scopeName); assert(scopeType, () => `Expected "${scopeName}" to be defined`); - return new NonNullType(new ListType(new NonNullType(scopeType))); + return new NonNullType(new ListType(new NonNullType(new ListType(new NonNullType(scopeType))))); }, compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.UNION, }], From db0ca7fd42252f5ee7237a59f88f8aa973ffeabf Mon Sep 17 00:00:00 2001 From: Dariusz Kuc <9501705+dariuszkuc@users.noreply.github.com> Date: Wed, 16 Aug 2023 15:03:00 -0500 Subject: [PATCH 25/95] docs: update requiresScope definition (#2739) see: #2738 --- docs/source/federated-types/federated-directives.mdx | 2 +- docs/source/federation-versions.mdx | 2 +- docs/source/subgraph-spec.mdx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/federated-types/federated-directives.mdx b/docs/source/federated-types/federated-directives.mdx index 7a8dd10ee..930d8fea3 100644 --- a/docs/source/federated-types/federated-directives.mdx +++ b/docs/source/federated-types/federated-directives.mdx @@ -377,7 +377,7 @@ Indicates to composition that the target element is accessible only to the authe > If your organization _doesn't_ currently have an Enterprise plan, you can test out this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). ```graphql -directive @requiresScopes(scopes: [federation__Scope!]!) on +directive @requiresScopes(scopes: [[federation__Scope!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE diff --git a/docs/source/federation-versions.mdx b/docs/source/federation-versions.mdx index 19b2e314a..cd27fec28 100644 --- a/docs/source/federation-versions.mdx +++ b/docs/source/federation-versions.mdx @@ -101,7 +101,7 @@ directive @authenticated on Introduced. [Learn more.](/router/configuration/authorization) ```graphql -directive @requiresScopes(scopes: [Scope!]!) on +directive @requiresScopes(scopes: [[Scope!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE diff --git a/docs/source/subgraph-spec.mdx b/docs/source/subgraph-spec.mdx index a2e80ab96..aadf554f9 100644 --- a/docs/source/subgraph-spec.mdx +++ b/docs/source/subgraph-spec.mdx @@ -65,7 +65,7 @@ directive @override(from: String!) on FIELD_DEFINITION directive @composeDirective(name: String!) repeatable on SCHEMA directive @interfaceObject on OBJECT directive @authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM -directive @requiresScopes(scopes: [federation__Scope!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM +directive @requiresScopes(scopes: [[federation__Scope!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM # This definition is required only for libraries that don't support # GraphQL's built-in `extend` keyword From b2fb37e4decc8fef213d843cbb945a938b7cda91 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 18 Aug 2023 12:22:24 -0600 Subject: [PATCH 26/95] Clarify differing @keys --- docs/source/entities-advanced.mdx | 77 +++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 5 deletions(-) diff --git a/docs/source/entities-advanced.mdx b/docs/source/entities-advanced.mdx index 6d5d326ff..72b267db0 100644 --- a/docs/source/entities-advanced.mdx +++ b/docs/source/entities-advanced.mdx @@ -59,29 +59,96 @@ type Organization { ### Differing `@key`s across subgraphs -An entity often has the same @key fields across subgraphs, but this isn't required. For example, you can define a `Product` entity shared between products and inventory subgraphs, one with `id` as the `@key` field and one with `sku` as the `@key` field: +An entity often has the exact same `@key` field(s) across subgraphs, but this isn't required. For example, you can define a `Product` entity shared between subgraphs, one with `sku` and `upc` as the `@key`s and one with only `upc` as the `@key` field: ```graphql title="Products subgraph" -type Product @key(fields: "id") { - id: ID! +type Product @key(fields: "sku") @key(fields: "upc") { + sku: ID! + upc: String! name: String! price: Int } ``` ```graphql title="Inventory subgraph" +type Product @key(fields: "upc") { + upc: String! + inStock: Boolean! +} +``` + + + +To merge entities between subgraphs, the entity must have at least one shared field between subgraphs. For example, you can't merge the `Product` entity defined in the following subgraphs because they don't share any fields: + +

+ + + +```graphql title="Products subgraph" type Product @key(fields: "sku") { sku: ID! + name: String! + price: Int +} +``` + +```graphql title="Inventory subgraph" +type Product @key(fields: "upc") { + upc: String! inStock: Boolean! } ``` -As long as `id` is a unique identifier within the products subgraph and `sku` is a unique identifier for the same product in the inventory subgraph, the subgraphs can each resolve the `Product` type. -In other words, **each key declaration is specific to the subgraph it belongs to**, and you don't need to match `@key` fields between graphs. +The following would allow you to merge the `Product` entity since both subgraph schemas include the `upc` field, even if it's only the `@key` for one of them: + + + +```graphql title="Products subgraph" +type Product @key(fields: "sku") { + sku: ID! + upc: String! + name: String! + price: Int +} +``` + +```graphql title="Inventory subgraph" +type Product @key(fields: "upc") { + upc: String! + inStock: Boolean! +} +``` + + + +If subgraphs share any fields—for example `upc` in the preceding example—it's best to use these shared field(s) as `@keys` for optimal [query planner](./entities#the-query-plan) performance. + +

+ + + +```graphql title="Products subgraph" +type Product @key(fields: "upc") { + sku: ID! + upc: String! + name: String! + price: Int +} +``` + +```graphql title="Inventory subgraph" +type Product @key(fields: "upc") { + upc: String! + inStock: Boolean! +} +``` + + ## Migrating entities and fields From d3ac48ab2f0a349567331e8e0092718370819c56 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 18 Aug 2023 13:33:56 -0600 Subject: [PATCH 27/95] Update docs/source/entities-advanced.mdx Co-authored-by: Dariusz Kuc <9501705+dariuszkuc@users.noreply.github.com> --- docs/source/entities-advanced.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/entities-advanced.mdx b/docs/source/entities-advanced.mdx index 72b267db0..e7a2075f1 100644 --- a/docs/source/entities-advanced.mdx +++ b/docs/source/entities-advanced.mdx @@ -81,7 +81,7 @@ type Product @key(fields: "upc") { -To merge entities between subgraphs, the entity must have at least one shared field between subgraphs. For example, you can't merge the `Product` entity defined in the following subgraphs because they don't share any fields: +To merge entities between subgraphs, the entity must have at least one shared field between subgraphs. For example, you can't merge the `Product` entity defined in the following subgraphs because they don't share any fields specified in `@key` selection set:

From 654715922feeb9f5cb5e2b52bd48c328c57e78a8 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 18 Aug 2023 13:49:41 -0600 Subject: [PATCH 28/95] Remove extraneous example --- docs/source/entities-advanced.mdx | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/docs/source/entities-advanced.mdx b/docs/source/entities-advanced.mdx index e7a2075f1..ad0b13de1 100644 --- a/docs/source/entities-advanced.mdx +++ b/docs/source/entities-advanced.mdx @@ -104,29 +104,7 @@ type Product @key(fields: "upc") { -The following would allow you to merge the `Product` entity since both subgraph schemas include the `upc` field, even if it's only the `@key` for one of them: - - - -```graphql title="Products subgraph" -type Product @key(fields: "sku") { - sku: ID! - upc: String! - name: String! - price: Int -} -``` - -```graphql title="Inventory subgraph" -type Product @key(fields: "upc") { - upc: String! - inStock: Boolean! -} -``` - - - -If subgraphs share any fields—for example `upc` in the preceding example—it's best to use these shared field(s) as `@keys` for optimal [query planner](./entities#the-query-plan) performance. +If subgraphs share any fields that act as unique identifiers, you can use these shared field(s) as `@keys` for optimal [query planner](./entities#the-query-plan) performance.

From 89131eb367307ffcc671f54d0535c63b6f797a65 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 18 Aug 2023 14:30:16 -0600 Subject: [PATCH 29/95] Remove another extraneous example --- docs/source/entities-advanced.mdx | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/docs/source/entities-advanced.mdx b/docs/source/entities-advanced.mdx index ad0b13de1..8d82b2a43 100644 --- a/docs/source/entities-advanced.mdx +++ b/docs/source/entities-advanced.mdx @@ -104,30 +104,6 @@ type Product @key(fields: "upc") { -If subgraphs share any fields that act as unique identifiers, you can use these shared field(s) as `@keys` for optimal [query planner](./entities#the-query-plan) performance. - -

- - - -```graphql title="Products subgraph" -type Product @key(fields: "upc") { - sku: ID! - upc: String! - name: String! - price: Int -} -``` - -```graphql title="Inventory subgraph" -type Product @key(fields: "upc") { - upc: String! - inStock: Boolean! -} -``` - - - ## Migrating entities and fields As your supergraph grows, you might want to move parts of an entity to a different subgraph. This section describes how to perform these migrations safely. From 08eabf514131aec5907bff11adbabf26490d5e5b Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 18 Aug 2023 15:52:34 -0600 Subject: [PATCH 30/95] Copy edit --- docs/source/entities-advanced.mdx | 2 +- docs/source/entities.mdx | 25 ++++++++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/source/entities-advanced.mdx b/docs/source/entities-advanced.mdx index 8d82b2a43..3488ec89a 100644 --- a/docs/source/entities-advanced.mdx +++ b/docs/source/entities-advanced.mdx @@ -81,7 +81,7 @@ type Product @key(fields: "upc") { -To merge entities between subgraphs, the entity must have at least one shared field between subgraphs. For example, you can't merge the `Product` entity defined in the following subgraphs because they don't share any fields specified in `@key` selection set: +To merge entities between subgraphs, the entity must have at least one shared field between subgraphs. For example, queries can't merge the `Product` entity defined in the following subgraphs because they don't share any fields specified in `@key` selection set:

diff --git a/docs/source/entities.mdx b/docs/source/entities.mdx index 3cc6c6768..9c9d134e5 100644 --- a/docs/source/entities.mdx +++ b/docs/source/entities.mdx @@ -53,16 +53,35 @@ type Product @key(fields: "id") { } ``` -The `@key` directive defines the entity's **primary key**, which consists of one or more of the type's `fields`. In the example above, the `Product` entity's primary key is its `id` field. +The `@key` directive defines the entity's **primary key**, which consists of one or more of the type's `fields`. In the example above, the `Product` entity's primary key is its `id` field. **Every instance of an entity must be uniquely identifiable by its `@key` fields.** This is what enables your router to associate field data from _different_ subgraphs with the _same_ entity instance. -**Every instance of an entity must be uniquely identifiable by its `@key` fields within its own subgraph.** This is what enables your router to associate field data from _different_ subgraphs with the _same_ entity instance. +In most cases, the `@key` field(s) for the same entity will be the same across subgraphs. For example, if one subgraph uses `id` as the `@key` field for the `Product` entity, other subgraphs should do the same. However, this [isn't strictly required](./entities-advanced#differing-keys-across-subgraphs). + + + +```graphql title="Products subgraph" +type Product @key(fields: "id") { + id: ID! + name: String! + price: Int +} +``` + +```graphql title="Inventory subgraph" +type Product @key(fields: "id") { + id: ID! + inStock: Boolean! +} +``` + + > **An entity's `@key` _cannot_ include:** > > * Fields that return a union or interface > * Fields that take arguments -[See advanced options for `@key`s](./entities-advanced/#advanced-keys) for more information. +For more information on advanced key options, like how to define [multiple keys](./entities-advanced#multiple-keys) or [compound keys](/entities-advanced#compound-keys), see [Advanced topics for federation entities](./entities-advanced). ### 2. Define a reference resolver From 6757b5d875ee85edd1e1a1a69874aef54f8f1602 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Aug 2023 15:53:47 -0700 Subject: [PATCH 31/95] Bump protobufjs from 6.11.3 to 6.11.4 (#2735) Bumps [protobufjs](https://github.com/protobufjs/protobuf.js) from 6.11.3 to 6.11.4. - [Release notes](https://github.com/protobufjs/protobuf.js/releases) - [Changelog](https://github.com/protobufjs/protobuf.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/protobufjs/protobuf.js/commits) --- updated-dependencies: - dependency-name: protobufjs dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Trevor Scheer --- package-lock.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 041a7fba4..b83800b40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14377,10 +14377,11 @@ } }, "node_modules/protobufjs": { - "version": "6.11.3", + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", "dev": true, "hasInstallScript": true, - "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", From ef07ec8d915d6dffa724a20d1bc223eee150ccba Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 19 Aug 2023 12:51:11 +0000 Subject: [PATCH 32/95] chore(deps): update all non-major dependencies (#2742) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 64 ++++++++++++++++++++++++----------------------- package.json | 8 +++--- 2 files changed, 37 insertions(+), 35 deletions(-) diff --git a/package-lock.json b/package-lock.json index b83800b40..7ebdbe019 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "@typescript-eslint/eslint-plugin": "5.62.0", "bunyan": "1.8.15", "cspell": "6.31.3", - "graphql": "16.7.1", + "graphql": "16.8.0", "graphql-http": "1.21.0", "graphql-tag": "2.12.6", "jest": "29.6.2", @@ -51,9 +51,9 @@ "jest-junit": "16.0.0", "log4js": "6.9.1", "mocked-env": "1.3.5", - "nock": "13.3.2", - "node-fetch": "2.6.12", - "prettier": "3.0.1", + "nock": "13.3.3", + "node-fetch": "2.6.13", + "prettier": "3.0.2", "semver": "7.5.4", "strip-indent": "3.0.0", "ts-jest": "29.1.1", @@ -2283,9 +2283,10 @@ "peer": true }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.20.0", + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "type-fest": "^0.20.2" @@ -4470,9 +4471,9 @@ } }, "node_modules/@whatwg-node/fetch/node_modules/@types/node": { - "version": "18.17.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.4.tgz", - "integrity": "sha512-ATL4WLgr7/W40+Sp1WnNTSKbgVn6Pvhc/2RHAdt8fl6NsQyp4oPCi2eKcGOvA494bwf1K/W6nGgZ9TwDqvpjdw==", + "version": "18.17.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.6.tgz", + "integrity": "sha512-fGmT/P7z7ecA6bv/ia5DlaWCH4YeZvAQMNpUhrJjtAhOhZfoxS1VLUgU2pdk63efSjQaOJWdXMuAJsws+8I6dg==", "dev": true, "peer": true }, @@ -7293,9 +7294,10 @@ } }, "node_modules/eslint/node_modules/globals": { - "version": "13.20.0", + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "type-fest": "^0.20.2" @@ -8080,22 +8082,22 @@ } }, "node_modules/fs-minipass": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.2.tgz", - "integrity": "sha512-2GAfyfoaCDRrM6jaOS3UsBts8yJ55VioXdWcOL7dK9zdAuKT71+WBA4ifnNYqVjYv+4SsPxjK0JT4yIIn4cA/g==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", "dependencies": { - "minipass": "^5.0.0" + "minipass": "^7.0.3" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/fs-minipass/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", + "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==", "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/fs.realpath": { @@ -8355,9 +8357,9 @@ "dev": true }, "node_modules/graphql": { - "version": "16.7.1", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.7.1.tgz", - "integrity": "sha512-DRYR9tf+UGU0KOsMcKAlXeFfX89UiiIZ0dRU3mR0yJfu6OjZqUcp68NnFLnqQU5RexygFoDy1EW+ccOYcPfmHg==", + "version": "16.8.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.0.tgz", + "integrity": "sha512-0oKGaR+y3qcS5mCu1vb7KG+a89vjn06C7Ihq/dDl3jA+A8B3TKomvi3CiEcVLJQGalbu8F52LxkOym7U5sSfbg==", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -13504,9 +13506,9 @@ } }, "node_modules/nock": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.2.tgz", - "integrity": "sha512-CwbljitiWJhF1gL83NbanhoKs1l23TDlRioNraPTZrzZIEooPemrHRj5m0FZCPkB1ecdYCSWWGcHysJgX/ngnQ==", + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.3.tgz", + "integrity": "sha512-z+KUlILy9SK/RjpeXDiDUEAq4T94ADPHE3qaRkf66mpEhzc/ytOMm3Bwdrbq6k1tMWkbdujiKim3G2tfQARuJw==", "dev": true, "dependencies": { "debug": "^4.1.0", @@ -13528,9 +13530,9 @@ "license": "MIT" }, "node_modules/node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -14275,9 +14277,9 @@ } }, "node_modules/prettier": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.1.tgz", - "integrity": "sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.2.tgz", + "integrity": "sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" diff --git a/package.json b/package.json index 7f8af64bb..a4b65d16e 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "@typescript-eslint/eslint-plugin": "5.62.0", "bunyan": "1.8.15", "cspell": "6.31.3", - "graphql": "16.7.1", + "graphql": "16.8.0", "graphql-http": "1.21.0", "graphql-tag": "2.12.6", "jest": "29.6.2", @@ -77,9 +77,9 @@ "jest-junit": "16.0.0", "log4js": "6.9.1", "mocked-env": "1.3.5", - "nock": "13.3.2", - "node-fetch": "2.6.12", - "prettier": "3.0.1", + "nock": "13.3.3", + "node-fetch": "2.6.13", + "prettier": "3.0.2", "semver": "7.5.4", "strip-indent": "3.0.0", "ts-jest": "29.1.1", From c44f0cad9637aebc0968baf6ceda20ee9dbd5c2d Mon Sep 17 00:00:00 2001 From: Sylvain Lebresne Date: Mon, 21 Aug 2023 15:21:13 +0200 Subject: [PATCH 33/95] Fix case where reusing fragments can still lead to invalid selection with conflicting fields (#2740) Trying to reuse fragments, if done without specific case, can in some context result in invalid selections due to fields conflicting. It is to avoid that problem that #2619 introduced the `FieldsConflictValidator` mechanism (later improved by #2635). Unfortunately, in some fairly specific setups with nested fragments, the code was missing some data in the validation mentioned above, which led to still having case where a subgraph fetch may be invalid due to some fields (within reusing fragments) conflicting at some point of the query. This commit fix that issue by ensuring we take everything we should into account when doing the aforementioned validation. --- internals-js/src/__tests__/operations.test.ts | 165 +++++++++++++++++- internals-js/src/operations.ts | 23 +-- .../src/__tests__/buildPlan.test.ts | 1 + 3 files changed, 169 insertions(+), 20 deletions(-) diff --git a/internals-js/src/__tests__/operations.test.ts b/internals-js/src/__tests__/operations.test.ts index b7a411c02..7022225ca 100644 --- a/internals-js/src/__tests__/operations.test.ts +++ b/internals-js/src/__tests__/operations.test.ts @@ -1988,6 +1988,142 @@ describe('fragments optimization', () => { } `); }); + + test('due to the trimmed selection of nested fragments', () => { + const schema = parseSchema(` + type Query { + u1: U + u2: U + u3: U + } + + union U = S | T + + type T { + id: ID! + vt: Int + } + + interface I { + vs: Int + } + + type S implements I { + vs: Int! + } + `); + const gqlSchema = schema.toGraphQLJSSchema(); + + const operation = parseOperation(schema, ` + { + u1 { + ...F1 + } + u2 { + ...F3 + } + u3 { + ...F3 + } + } + + fragment F1 on U { + ... on S { + __typename + vs + } + ... on T { + __typename + vt + } + } + + fragment F2 on T { + __typename + vt + } + + fragment F3 on U { + ... on I { + vs + } + ...F2 + } + `); + expect(validate(gqlSchema, parse(operation.toString()))).toStrictEqual([]); + + const withoutFragments = operation.expandAllFragments(); + expect(withoutFragments.toString()).toMatchString(` + { + u1 { + ... on S { + __typename + vs + } + ... on T { + __typename + vt + } + } + u2 { + ... on I { + vs + } + ... on T { + __typename + vt + } + } + u3 { + ... on I { + vs + } + ... on T { + __typename + vt + } + } + } + `); + + // We use `mapToExpandedSelectionSets` with a no-op mapper because this will still expand the selections + // and re-optimize them, which 1) happens to match what happens in the query planner and 2) is necessary + // for reproducing a bug that this test was initially added to cover. + const newFragments = operation.fragments!.mapToExpandedSelectionSets((s) => s); + const optimized = withoutFragments.optimize(newFragments, 2); + expect(validate(gqlSchema, parse(optimized.toString()))).toStrictEqual([]); + + expect(optimized.toString()).toMatchString(` + fragment F3 on U { + ... on I { + vs + } + ... on T { + __typename + vt + } + } + + { + u1 { + ... on S { + __typename + vs + } + ... on T { + __typename + vt + } + } + u2 { + ...F3 + } + u3 { + ...F3 + } + } + `); + }); }); test('does not leave unused fragments', () => { @@ -3094,12 +3230,25 @@ describe('named fragment selection set restrictions at type', () => { const frag = operation.fragments?.get('FonU1')!; - // Note that with unions, the fragments inside the unions can be "lifted" and so they everyting normalize to just the + // Note that with unions, the fragments inside the unions can be "lifted" and so that everything normalizes to just the // possible runtimes. let { selectionSet, validator } = expandAtType(frag, schema, 'U1'); expect(selectionSet.toString()).toBe('{ ... on T1 { x y } ... on T2 { z w } }'); - expect(validator?.toString()).toBeUndefined(); + // Similar remarks than on interfaces (the validator is strictly speaking not necessary, but + // this happens due to the "lifting" of selection mentioned above, is a bit hard to avoid, + // and is essentially harmess (it may result in a bit more cpu cycles in some cases but + // that is likely negligible). + expect(validator?.toString()).toMatchString(` + { + y: [ + T1.y + ] + w: [ + T2.w + ] + } + `); ({ selectionSet, validator } = expandAtType(frag, schema, 'U2')); expect(selectionSet.toString()).toBe('{ ... on T1 { x y } }'); @@ -3108,6 +3257,9 @@ describe('named fragment selection set restrictions at type', () => { z: [ T2.z ] + y: [ + T1.y + ] w: [ T2.w ] @@ -3124,6 +3276,9 @@ describe('named fragment selection set restrictions at type', () => { y: [ T1.y ] + w: [ + T2.w + ] } `); @@ -3135,12 +3290,12 @@ describe('named fragment selection set restrictions at type', () => { x: [ T1.x ] - y: [ - T1.y - ] z: [ T2.z ] + y: [ + T1.y + ] w: [ T2.w ] diff --git a/internals-js/src/operations.ts b/internals-js/src/operations.ts index 2b7e6b92d..b00f2664d 100644 --- a/internals-js/src/operations.ts +++ b/internals-js/src/operations.ts @@ -1073,13 +1073,6 @@ export class NamedFragmentDefinition extends DirectiveTargetElement { { t1 { other { + __typename id } } From 3b8c4dbd97f1aead46761ee18ee4473c462ba263 Mon Sep 17 00:00:00 2001 From: "Sachin D. Shinde" Date: Wed, 23 Aug 2023 13:56:45 -0700 Subject: [PATCH 34/95] Fix patch level in changesets (#2746) Fix changeset level for #2716 from major to patch. --- .changeset/five-experts-give.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/five-experts-give.md b/.changeset/five-experts-give.md index c94947fe6..5b5f2eb76 100644 --- a/.changeset/five-experts-give.md +++ b/.changeset/five-experts-give.md @@ -1,5 +1,5 @@ --- -"@apollo/gateway": major +"@apollo/gateway": patch --- Fix execution error in some cases where aliases are used and some values are `null`. From 7634e38774585d780f4875e9aaed2e2cefcf4a1d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Aug 2023 14:02:23 -0700 Subject: [PATCH 35/95] Version Packages (#2730) Co-authored-by: github-actions[bot] --- .changeset/five-experts-give.md | 8 --- .changeset/great-lions-work.md | 13 ----- .changeset/nice-seahorses-cheer.md | 10 ---- .changeset/sixty-walls-chew.md | 41 -------------- .changeset/tall-impalas-act.md | 11 ---- composition-js/CHANGELOG.md | 15 ++++++ composition-js/package.json | 6 +-- .../CHANGELOG.md | 2 + .../package.json | 2 +- gateway-js/CHANGELOG.md | 12 +++++ gateway-js/package.json | 8 +-- internals-js/CHANGELOG.md | 54 +++++++++++++++++++ internals-js/package.json | 2 +- package-lock.json | 32 +++++------ query-graphs-js/CHANGELOG.md | 12 +++++ query-graphs-js/package.json | 4 +- query-planner-js/CHANGELOG.md | 18 +++++++ query-planner-js/package.json | 6 +-- subgraph-js/CHANGELOG.md | 6 +++ subgraph-js/package.json | 4 +- 20 files changed, 151 insertions(+), 115 deletions(-) delete mode 100644 .changeset/five-experts-give.md delete mode 100644 .changeset/great-lions-work.md delete mode 100644 .changeset/nice-seahorses-cheer.md delete mode 100644 .changeset/sixty-walls-chew.md delete mode 100644 .changeset/tall-impalas-act.md diff --git a/.changeset/five-experts-give.md b/.changeset/five-experts-give.md deleted file mode 100644 index 5b5f2eb76..000000000 --- a/.changeset/five-experts-give.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@apollo/gateway": patch ---- - -Fix execution error in some cases where aliases are used and some values are `null`. - -The error would manifest itself as an `INTERNAL_SERVER_ERROR` with a message of the form `Cannot read properties of null`. - \ No newline at end of file diff --git a/.changeset/great-lions-work.md b/.changeset/great-lions-work.md deleted file mode 100644 index 0d0c8eb66..000000000 --- a/.changeset/great-lions-work.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@apollo/composition": patch -"@apollo/federation-internals": patch ---- - -Modifies the type for the argument of the `@requiresScopes` from `[federation__Scope!]!` to `[[federation__Scope!]!]!`. - -The `@requiresScopes` directives has been pre-emptively introduced in 2.5.0 to support an upcoming Apollo Router -feature around scoped accesses. The argument for `@requiresScopes` in that upcoming feature is changed to accommodate a -new semantic. Note that this technically a breaking change to the `@requiresScopes` directive definition, but as the -full feature using that directive has been released yet, this directive cannot effectively be used and this should have -no concrete impact. - diff --git a/.changeset/nice-seahorses-cheer.md b/.changeset/nice-seahorses-cheer.md deleted file mode 100644 index 7c339c730..000000000 --- a/.changeset/nice-seahorses-cheer.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@apollo/query-planner": patch -"@apollo/federation-internals": patch ---- - -Fix potential assertion error for named fragment on abstract types when the abstract type does not have the same -possible runtime types in all subgraphs. - -The error manifested itself during query planning with an error message of the form `Cannot normalize X at Y ...`. - \ No newline at end of file diff --git a/.changeset/sixty-walls-chew.md b/.changeset/sixty-walls-chew.md deleted file mode 100644 index d60b505e5..000000000 --- a/.changeset/sixty-walls-chew.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -"@apollo/federation-internals": patch ---- - -Expands over-eager merging of field fix to handle `@defer` consistently - -The previously committed [#2713](https://github.com/apollographql/federation/pull/2713) fixed an issue introduced by -[#2387](https://github.com/apollographql/federation/pull/2387), ensuring that querying the same field with different -directives applications was not merged, similar to what was/is done for fragments. But the exact behaviour slightly -differs between fields and fragments when it comes to `@defer` in that for fragments, we never merge 2 similar fragments -where both have `@defer`, which we do merge for fields. Or to put it more concretely, in the following query: -```graphq -query Test($skipField: Boolean!) { - x { - ... on X @defer { - a - } - ... on X @defer { - b - } - } -} -``` -the 2 `... on X @defer` are not merged, resulting in 2 deferred sections that can run in parallel. But following -[#2713](https://github.com/apollographql/federation/pull/2713), query: -```graphq -query Test($skipField: Boolean!) { - x @defer { - a - } - x @defer { - b - } -} -``` -_will_ merge both `x @defer`, resulting in a single deferred section. - -This fix changes that later behaviour so that the 2 `x @defer` are not merged and result in 2 deferred sections, -consistently with both 1) the case of fragments and 2) the behaviour prior to -[#2387](https://github.com/apollographql/federation/pull/2387). - \ No newline at end of file diff --git a/.changeset/tall-impalas-act.md b/.changeset/tall-impalas-act.md deleted file mode 100644 index b0bec7533..000000000 --- a/.changeset/tall-impalas-act.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@apollo/query-planner": patch -"@apollo/query-graphs": patch ---- - -More aggressive ignoring of indirect paths from root types when a more direct alternative exists. This optimisation -slightly generalize an existing heuristic of the query planner, allowing it to ignore some known inefficient options -earlier in its process. When this optimisation can be used, this yield faster query plan computation, but by reducing -the number of plans to be consider, this can sometimes prevent the planner to degrade it's output when it consider -there is too many plans to consider, which can result in more optimal query plans too. - diff --git a/composition-js/CHANGELOG.md b/composition-js/CHANGELOG.md index d04faf3d2..e5397b342 100644 --- a/composition-js/CHANGELOG.md +++ b/composition-js/CHANGELOG.md @@ -1,5 +1,20 @@ # CHANGELOG for `@apollo/composition` +## 2.5.3 +### Patch Changes + + +- Modifies the type for the argument of the `@requiresScopes` from `[federation__Scope!]!` to `[[federation__Scope!]!]!`. ([#2738](https://github.com/apollographql/federation/pull/2738)) + + The `@requiresScopes` directives has been pre-emptively introduced in 2.5.0 to support an upcoming Apollo Router + feature around scoped accesses. The argument for `@requiresScopes` in that upcoming feature is changed to accommodate a + new semantic. Note that this technically a breaking change to the `@requiresScopes` directive definition, but as the + full feature using that directive has been released yet, this directive cannot effectively be used and this should have + no concrete impact. +- Updated dependencies [[`4b9a512b`](https://github.com/apollographql/federation/commit/4b9a512b62e02544d7854fa198942aac33b93feb), [`c6e0e76d`](https://github.com/apollographql/federation/commit/c6e0e76dbc62662c2aa6ff7f657e374047b11255), [`1add932c`](https://github.com/apollographql/federation/commit/1add932c5cd1297853fb5af9a3a6aaa71243f63a), [`6f1fddb2`](https://github.com/apollographql/federation/commit/6f1fddb25d49262b2ebf6db953371a559dd62e9c)]: + - @apollo/federation-internals@2.5.3 + - @apollo/query-graphs@2.5.3 + ## 2.5.2 ### Patch Changes diff --git a/composition-js/package.json b/composition-js/package.json index e00335e53..49491ff40 100644 --- a/composition-js/package.json +++ b/composition-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/composition", - "version": "2.5.2", + "version": "2.5.3", "description": "Apollo Federation composition utilities", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -27,8 +27,8 @@ "access": "public" }, "dependencies": { - "@apollo/federation-internals": "2.5.2", - "@apollo/query-graphs": "2.5.2" + "@apollo/federation-internals": "2.5.3", + "@apollo/query-graphs": "2.5.3" }, "peerDependencies": { "graphql": "^16.5.0" diff --git a/federation-integration-testsuite-js/CHANGELOG.md b/federation-integration-testsuite-js/CHANGELOG.md index 766b53a89..ab8ff1d03 100644 --- a/federation-integration-testsuite-js/CHANGELOG.md +++ b/federation-integration-testsuite-js/CHANGELOG.md @@ -1,5 +1,7 @@ # CHANGELOG for `federation-integration-testsuite-js` +## 2.5.3 + ## 2.5.2 ## 2.5.1 diff --git a/federation-integration-testsuite-js/package.json b/federation-integration-testsuite-js/package.json index 3c348f6cc..04a44a175 100644 --- a/federation-integration-testsuite-js/package.json +++ b/federation-integration-testsuite-js/package.json @@ -1,7 +1,7 @@ { "name": "apollo-federation-integration-testsuite", "private": true, - "version": "2.5.2", + "version": "2.5.3", "description": "Apollo Federation Integrations / Test Fixtures", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/gateway-js/CHANGELOG.md b/gateway-js/CHANGELOG.md index afa090c84..4ad7f3c8a 100644 --- a/gateway-js/CHANGELOG.md +++ b/gateway-js/CHANGELOG.md @@ -1,5 +1,17 @@ # CHANGELOG for `@apollo/gateway` +## 2.5.3 +### Patch Changes + + +- Fix execution error in some cases where aliases are used and some values are `null`. ([#2716](https://github.com/apollographql/federation/pull/2716)) + + The error would manifest itself as an `INTERNAL_SERVER_ERROR` with a message of the form `Cannot read properties of null`. +- Updated dependencies [[`4b9a512b`](https://github.com/apollographql/federation/commit/4b9a512b62e02544d7854fa198942aac33b93feb), [`c6e0e76d`](https://github.com/apollographql/federation/commit/c6e0e76dbc62662c2aa6ff7f657e374047b11255), [`1add932c`](https://github.com/apollographql/federation/commit/1add932c5cd1297853fb5af9a3a6aaa71243f63a), [`6f1fddb2`](https://github.com/apollographql/federation/commit/6f1fddb25d49262b2ebf6db953371a559dd62e9c)]: + - @apollo/composition@2.5.3 + - @apollo/federation-internals@2.5.3 + - @apollo/query-planner@2.5.3 + ## 2.5.2 ### Patch Changes diff --git a/gateway-js/package.json b/gateway-js/package.json index 0f4c77e5a..b4390af3a 100644 --- a/gateway-js/package.json +++ b/gateway-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/gateway", - "version": "2.5.2", + "version": "2.5.3", "description": "Apollo Gateway", "author": "Apollo ", "main": "dist/index.js", @@ -25,9 +25,9 @@ "access": "public" }, "dependencies": { - "@apollo/composition": "2.5.2", - "@apollo/federation-internals": "2.5.2", - "@apollo/query-planner": "2.5.2", + "@apollo/composition": "2.5.3", + "@apollo/federation-internals": "2.5.3", + "@apollo/query-planner": "2.5.3", "@apollo/server-gateway-interface": "^1.1.0", "@apollo/usage-reporting-protobuf": "^4.1.0", "@apollo/utils.createhash": "^2.0.0", diff --git a/internals-js/CHANGELOG.md b/internals-js/CHANGELOG.md index c7443c452..a00cf6e1a 100644 --- a/internals-js/CHANGELOG.md +++ b/internals-js/CHANGELOG.md @@ -1,5 +1,59 @@ # CHANGELOG for `@apollo/federation-internals` +## 2.5.3 +### Patch Changes + + +- Modifies the type for the argument of the `@requiresScopes` from `[federation__Scope!]!` to `[[federation__Scope!]!]!`. ([#2738](https://github.com/apollographql/federation/pull/2738)) + + The `@requiresScopes` directives has been pre-emptively introduced in 2.5.0 to support an upcoming Apollo Router + feature around scoped accesses. The argument for `@requiresScopes` in that upcoming feature is changed to accommodate a + new semantic. Note that this technically a breaking change to the `@requiresScopes` directive definition, but as the + full feature using that directive has been released yet, this directive cannot effectively be used and this should have + no concrete impact. + +- Fix potential assertion error for named fragment on abstract types when the abstract type does not have the same ([#2725](https://github.com/apollographql/federation/pull/2725)) + possible runtime types in all subgraphs. + + The error manifested itself during query planning with an error message of the form `Cannot normalize X at Y ...`. + +- Expands over-eager merging of field fix to handle `@defer` consistently ([#2720](https://github.com/apollographql/federation/pull/2720)) + + The previously committed [#2713](https://github.com/apollographql/federation/pull/2713) fixed an issue introduced by + [#2387](https://github.com/apollographql/federation/pull/2387), ensuring that querying the same field with different + directives applications was not merged, similar to what was/is done for fragments. But the exact behaviour slightly + differs between fields and fragments when it comes to `@defer` in that for fragments, we never merge 2 similar fragments + where both have `@defer`, which we do merge for fields. Or to put it more concretely, in the following query: + ```graphq + query Test($skipField: Boolean!) { + x { + ... on X @defer { + a + } + ... on X @defer { + b + } + } + } + ``` + the 2 `... on X @defer` are not merged, resulting in 2 deferred sections that can run in parallel. But following + [#2713](https://github.com/apollographql/federation/pull/2713), query: + ```graphq + query Test($skipField: Boolean!) { + x @defer { + a + } + x @defer { + b + } + } + ``` + _will_ merge both `x @defer`, resulting in a single deferred section. + + This fix changes that later behaviour so that the 2 `x @defer` are not merged and result in 2 deferred sections, + consistently with both 1) the case of fragments and 2) the behaviour prior to + [#2387](https://github.com/apollographql/federation/pull/2387). + ## 2.5.2 ### Patch Changes diff --git a/internals-js/package.json b/internals-js/package.json index ec3377c1c..9f18e7d94 100644 --- a/internals-js/package.json +++ b/internals-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/federation-internals", - "version": "2.5.2", + "version": "2.5.3", "description": "Apollo Federation internal utilities", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/package-lock.json b/package-lock.json index 7ebdbe019..4268f8012 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,11 +69,11 @@ }, "composition-js": { "name": "@apollo/composition", - "version": "2.5.2", + "version": "2.5.3", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { - "@apollo/federation-internals": "2.5.2", - "@apollo/query-graphs": "2.5.2" + "@apollo/federation-internals": "2.5.3", + "@apollo/query-graphs": "2.5.3" }, "engines": { "node": ">=14.15.0" @@ -84,7 +84,7 @@ }, "federation-integration-testsuite-js": { "name": "apollo-federation-integration-testsuite", - "version": "2.5.2", + "version": "2.5.3", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { "graphql-tag": "^2.12.6", @@ -93,12 +93,12 @@ }, "gateway-js": { "name": "@apollo/gateway", - "version": "2.5.2", + "version": "2.5.3", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { - "@apollo/composition": "2.5.2", - "@apollo/federation-internals": "2.5.2", - "@apollo/query-planner": "2.5.2", + "@apollo/composition": "2.5.3", + "@apollo/federation-internals": "2.5.3", + "@apollo/query-planner": "2.5.3", "@apollo/server-gateway-interface": "^1.1.0", "@apollo/usage-reporting-protobuf": "^4.1.0", "@apollo/utils.createhash": "^2.0.0", @@ -124,7 +124,7 @@ }, "internals-js": { "name": "@apollo/federation-internals", - "version": "2.5.2", + "version": "2.5.3", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { "@types/uuid": "^9.0.0", @@ -17522,10 +17522,10 @@ }, "query-graphs-js": { "name": "@apollo/query-graphs", - "version": "2.5.2", + "version": "2.5.3", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { - "@apollo/federation-internals": "2.5.2", + "@apollo/federation-internals": "2.5.3", "deep-equal": "^2.0.5", "ts-graphviz": "^1.5.4", "uuid": "^9.0.0" @@ -17539,11 +17539,11 @@ }, "query-planner-js": { "name": "@apollo/query-planner", - "version": "2.5.2", + "version": "2.5.3", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { - "@apollo/federation-internals": "2.5.2", - "@apollo/query-graphs": "2.5.2", + "@apollo/federation-internals": "2.5.3", + "@apollo/query-graphs": "2.5.3", "@apollo/utils.keyvaluecache": "^2.1.0", "chalk": "^4.1.0", "deep-equal": "^2.0.5", @@ -17572,11 +17572,11 @@ }, "subgraph-js": { "name": "@apollo/subgraph", - "version": "2.5.2", + "version": "2.5.3", "license": "MIT", "dependencies": { "@apollo/cache-control-types": "^1.0.2", - "@apollo/federation-internals": "2.5.2" + "@apollo/federation-internals": "2.5.3" }, "engines": { "node": ">=14.15.0" diff --git a/query-graphs-js/CHANGELOG.md b/query-graphs-js/CHANGELOG.md index fa1f8204d..7aa1f6fd0 100644 --- a/query-graphs-js/CHANGELOG.md +++ b/query-graphs-js/CHANGELOG.md @@ -1,5 +1,17 @@ # CHANGELOG for `@apollo/query-graphs` +## 2.5.3 +### Patch Changes + + +- More aggressive ignoring of indirect paths from root types when a more direct alternative exists. This optimisation ([#2669](https://github.com/apollographql/federation/pull/2669)) + slightly generalize an existing heuristic of the query planner, allowing it to ignore some known inefficient options + earlier in its process. When this optimisation can be used, this yield faster query plan computation, but by reducing + the number of plans to be consider, this can sometimes prevent the planner to degrade it's output when it consider + there is too many plans to consider, which can result in more optimal query plans too. +- Updated dependencies [[`4b9a512b`](https://github.com/apollographql/federation/commit/4b9a512b62e02544d7854fa198942aac33b93feb), [`c6e0e76d`](https://github.com/apollographql/federation/commit/c6e0e76dbc62662c2aa6ff7f657e374047b11255), [`1add932c`](https://github.com/apollographql/federation/commit/1add932c5cd1297853fb5af9a3a6aaa71243f63a)]: + - @apollo/federation-internals@2.5.3 + ## 2.5.2 ### Patch Changes diff --git a/query-graphs-js/package.json b/query-graphs-js/package.json index 86f5a94a9..c39d7f425 100644 --- a/query-graphs-js/package.json +++ b/query-graphs-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/query-graphs", - "version": "2.5.2", + "version": "2.5.3", "description": "Apollo Federation library to work with 'query graphs'", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -23,7 +23,7 @@ "node": ">=14.15.0" }, "dependencies": { - "@apollo/federation-internals": "2.5.2", + "@apollo/federation-internals": "2.5.3", "deep-equal": "^2.0.5", "ts-graphviz": "^1.5.4", "uuid": "^9.0.0" diff --git a/query-planner-js/CHANGELOG.md b/query-planner-js/CHANGELOG.md index c35292770..171bb0e4a 100644 --- a/query-planner-js/CHANGELOG.md +++ b/query-planner-js/CHANGELOG.md @@ -1,5 +1,23 @@ # CHANGELOG for `@apollo/query-planner` +## 2.5.3 +### Patch Changes + + +- Fix potential assertion error for named fragment on abstract types when the abstract type does not have the same ([#2725](https://github.com/apollographql/federation/pull/2725)) + possible runtime types in all subgraphs. + + The error manifested itself during query planning with an error message of the form `Cannot normalize X at Y ...`. + +- More aggressive ignoring of indirect paths from root types when a more direct alternative exists. This optimisation ([#2669](https://github.com/apollographql/federation/pull/2669)) + slightly generalize an existing heuristic of the query planner, allowing it to ignore some known inefficient options + earlier in its process. When this optimisation can be used, this yield faster query plan computation, but by reducing + the number of plans to be consider, this can sometimes prevent the planner to degrade it's output when it consider + there is too many plans to consider, which can result in more optimal query plans too. +- Updated dependencies [[`4b9a512b`](https://github.com/apollographql/federation/commit/4b9a512b62e02544d7854fa198942aac33b93feb), [`c6e0e76d`](https://github.com/apollographql/federation/commit/c6e0e76dbc62662c2aa6ff7f657e374047b11255), [`1add932c`](https://github.com/apollographql/federation/commit/1add932c5cd1297853fb5af9a3a6aaa71243f63a), [`6f1fddb2`](https://github.com/apollographql/federation/commit/6f1fddb25d49262b2ebf6db953371a559dd62e9c)]: + - @apollo/federation-internals@2.5.3 + - @apollo/query-graphs@2.5.3 + ## 2.5.2 ### Patch Changes diff --git a/query-planner-js/package.json b/query-planner-js/package.json index 906704b7a..4ca7b8179 100644 --- a/query-planner-js/package.json +++ b/query-planner-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/query-planner", - "version": "2.5.2", + "version": "2.5.3", "description": "Apollo Query Planner", "author": "Apollo ", "main": "dist/index.js", @@ -25,8 +25,8 @@ "access": "public" }, "dependencies": { - "@apollo/federation-internals": "2.5.2", - "@apollo/query-graphs": "2.5.2", + "@apollo/federation-internals": "2.5.3", + "@apollo/query-graphs": "2.5.3", "@apollo/utils.keyvaluecache": "^2.1.0", "chalk": "^4.1.0", "deep-equal": "^2.0.5", diff --git a/subgraph-js/CHANGELOG.md b/subgraph-js/CHANGELOG.md index f97e39bae..8b027e2a0 100644 --- a/subgraph-js/CHANGELOG.md +++ b/subgraph-js/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG for `@apollo/subgraph` +## 2.5.3 +### Patch Changes + +- Updated dependencies [[`4b9a512b`](https://github.com/apollographql/federation/commit/4b9a512b62e02544d7854fa198942aac33b93feb), [`c6e0e76d`](https://github.com/apollographql/federation/commit/c6e0e76dbc62662c2aa6ff7f657e374047b11255), [`1add932c`](https://github.com/apollographql/federation/commit/1add932c5cd1297853fb5af9a3a6aaa71243f63a)]: + - @apollo/federation-internals@2.5.3 + ## 2.5.2 ### Patch Changes diff --git a/subgraph-js/package.json b/subgraph-js/package.json index 1debf0e1d..b65d236aa 100644 --- a/subgraph-js/package.json +++ b/subgraph-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/subgraph", - "version": "2.5.2", + "version": "2.5.3", "description": "Apollo Subgraph Utilities", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -25,7 +25,7 @@ }, "dependencies": { "@apollo/cache-control-types": "^1.0.2", - "@apollo/federation-internals": "2.5.2" + "@apollo/federation-internals": "2.5.3" }, "peerDependencies": { "graphql": "^16.5.0" From 8cc6bcea4125b05ad6bfd703dd10c74c6db9550c Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Thu, 24 Aug 2023 13:52:50 -0600 Subject: [PATCH 36/95] Reinstate hints page --- docs/source/_redirects | 4 +- docs/source/config.json | 2 +- docs/source/linter-rules.mdx | 72 ++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 docs/source/linter-rules.mdx diff --git a/docs/source/_redirects b/docs/source/_redirects index f23899a18..6512e2e69 100644 --- a/docs/source/_redirects +++ b/docs/source/_redirects @@ -21,6 +21,4 @@ /quickstart-pt-4/ /docs/federation/v2/quickstart/local-subgraphs/ /managed-federation/monitoring/ /docs/federation/performance/monitoring -/api/apollo-federation/ /docs/federation/api/apollo-subgraph/ - -/hints /docs/graphos/delivery/linter-rules \ No newline at end of file +/api/apollo-federation/ /docs/federation/api/apollo-subgraph/ \ No newline at end of file diff --git a/docs/source/config.json b/docs/source/config.json index 425b6f101..d59524c01 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -55,7 +55,7 @@ }, "Debugging & Metrics": { "Error codes": "/errors", - "Composition linting": "https://www.apollographql.com/docs/graphos/delivery/linter-rules", + "Composition linting": "/hints", "Federated trace data": "/metrics", "OpenTelemetry": "/opentelemetry" }, diff --git a/docs/source/linter-rules.mdx b/docs/source/linter-rules.mdx new file mode 100644 index 000000000..6d255e300 --- /dev/null +++ b/docs/source/linter-rules.mdx @@ -0,0 +1,72 @@ +--- +title: Composition hints +--- + +When you successfully [compose](./federated-types/composition) the schemas provided by your [subgraphs](./building-supergraphs/subgraphs-overview/) into a supergraph schema, the composition process might output **hints** that provide additional information about the result. Hints are primarily informative and _do not_ necessarily indicate that a problem needs to be fixed. + +Hints are categorized under the following levels: + +* `WARN`: Indicates a situation that might be expected but is usually temporary and should be double-checked. Typically, composition might have needed to ignore some elements from some subgraph when creating the supergraph. +* `INFO`: Suggests a potentially helpful improvement or highlights a noteworthy resolution made by composition. Can otherwise be ignored. +* `DEBUG`: Lower-level information that provides insight into the composition. These hints are of lesser importance/impact. + +Note that hints are first and foremost informative and don't necessarily correspond to a problem to be fixed. + +This document lists the hints that can be generated for each level, with a description of why each is generated. + + +The following hints might be generated during composition: + +## `WARN` + +
+ +| Code | Description | Level | +|---|---|---| +| `INCONSISTENT_DEFAULT_VALUE_PRESENCE` | Indicates that an argument definition (of a field/input field/directive definition) has a default value in only some of the subgraphs that define the argument. | `WARN` | +| `INCONSISTENT_INPUT_OBJECT_FIELD` | Indicates that a field of an input object type definition is only defined in a subset of the subgraphs that declare the input object. | `WARN` | +| `INCONSISTENT_ENUM_VALUE_FOR_INPUT_ENUM` | Indicates that a value of an enum type definition (that is only used as an Input type) has not been merged into the supergraph because it is defined in only a subset of the subgraphs that declare the enum | `WARN` | +| `INCONSISTENT_EXECUTABLE_DIRECTIVE_PRESENCE` | Indicates that an executable directive definition is declared in only some of the subgraphs. | `WARN` | +| `NO_EXECUTABLE_DIRECTIVE_INTERSECTION` | Indicates that, for an executable directive definition, no location for it appears in all subgraphs. | `WARN` | +| `INCONSISTENT_EXECUTABLE_DIRECTIVE_REPEATABLE` | Indicates that an executable directive definition is marked repeatable in only a subset of the subgraphs (and will not be repeatable in the supergraph). | `WARN` | +| `INCONSISTENT_EXECUTABLE_DIRECTIVE_LOCATIONS` | Indicates that an executiable directive definition is declared with inconsistent locations across subgraphs (and will use the intersection of all locations in the supergraph). | `WARN` | +| `INCONSISTENT_DESCRIPTION` | Indicates that an element has a description in more than one subgraph, and the descriptions are not equal. | `WARN` | +| `INCONSISTENT_ARGUMENT_PRESENCE` | Indicates that an optional argument (of a field or directive definition) is not present in all subgraphs and will not be part of the supergraph. | `WARN` | +| `FROM_SUBGRAPH_DOES_NOT_EXIST` | Source subgraph specified by @override directive does not exist | `WARN` | +| `INCONSISTENT_NON_REPEATABLE_DIRECTIVE_ARGUMENTS` | A non-repeatable directive is applied to a schema element in different subgraphs but with arguments that are different. | `WARN` | +| `DIRECTIVE_COMPOSITION_WARN` | Indicates that an issue was detected when composing custom directives. | `WARN` | +| `INCONSISTENT_RUNTIME_TYPES_FOR_SHAREABLE_RETURN` | Indicates that a @shareable field returns different sets of runtime types in the different subgraphs in which it is defined. | `WARN` | + +
+ +## `INFO` + +
+ +| Code | Description | Level | +|---|---|---| +| `INCONSISTENT_BUT_COMPATIBLE_FIELD_TYPE` | Indicates that a field does not have the exact same types in all subgraphs, but that the types are "compatible" (2 types are compatible if one is a non-nullable version of the other, a list version, a subtype, or a combination of the former). | `INFO` | +| `INCONSISTENT_BUT_COMPATIBLE_ARGUMENT_TYPE` | Indicates that an argument type (of a field/input field/directive definition) does not have the exact same type in all subgraphs, but that the types are "compatible" (two types are compatible if one is a non-nullable version of the other, a list version, a subtype, or a combination of the former). | `INFO` | +| `INCONSISTENT_ENTITY` | Indicates that an object is declared as an entity (has a `@key`) in only some of the subgraphs in which the object is defined. | `INFO` | +| `OVERRIDDEN_FIELD_CAN_BE_REMOVED` | Field has been overridden by another subgraph. Consider removing. | `INFO` | +| `OVERRIDE_DIRECTIVE_CAN_BE_REMOVED` | Field with @override directive no longer exists in source subgraph, the directive can be safely removed | `INFO` | +| `MERGED_NON_REPEATABLE_DIRECTIVE_ARGUMENTS` | A non-repeatable directive has been applied to a schema element in different subgraphs with different arguments and the arguments values were merged using the directive configured strategies. | `INFO` | +| `DIRECTIVE_COMPOSITION_INFO` | Indicates that an issue was detected when composing custom directives. | `INFO` | + +
+ +## `DEBUG` + +
+ +| Code | Description | Level | +|---|---|---| +| `INCONSISTENT_OBJECT_VALUE_TYPE_FIELD` | Indicates that a field of an object "value type" (has no `@key` in any subgraph) is not defined in all the subgraphs that declare the type. | `DEBUG` | +| `INCONSISTENT_INTERFACE_VALUE_TYPE_FIELD` | Indicates that a field of an interface "value type" (has no `@key` in any subgraph) is not defined in all the subgraphs that declare the type. | `DEBUG` | +| `INCONSISTENT_UNION_MEMBER` | Indicates that a member of a union type definition is only defined in a subset of the subgraphs that declare the union. | `DEBUG` | +| `INCONSISTENT_ENUM_VALUE_FOR_OUTPUT_ENUM` | Indicates that a value of an enum type definition (that is only used as an Output type, or is unused) has been merged in the supergraph but is defined in only a subset of the subgraphs that declare the enum | `DEBUG` | +| `INCONSISTENT_TYPE_SYSTEM_DIRECTIVE_REPEATABLE` | Indicates that a type system directive definition is marked repeatable in only a subset of the subgraphs that declare the directive (and will be repeatable in the supergraph). | `DEBUG` | +| `INCONSISTENT_TYPE_SYSTEM_DIRECTIVE_LOCATIONS` | Indicates that a type system directive definition is declared with inconsistent locations across subgraphs (and will use the union of all locations in the supergraph). | `DEBUG` | +| `UNUSED_ENUM_TYPE` | Indicates that an enum type is defined in some subgraphs but is unused (no field/argument references it). All the values from subgraphs defining that enum will be included in the supergraph. | `DEBUG` | + +
From 6a78284064d8312d4e295ff2c6a450d088f0e851 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Thu, 24 Aug 2023 14:40:25 -0600 Subject: [PATCH 37/95] Rewrite intro --- docs/source/config.json | 2 +- docs/source/hints.mdx | 19 ++++++++++ docs/source/linter-rules.mdx | 72 ------------------------------------ 3 files changed, 20 insertions(+), 73 deletions(-) create mode 100644 docs/source/hints.mdx delete mode 100644 docs/source/linter-rules.mdx diff --git a/docs/source/config.json b/docs/source/config.json index d59524c01..c2192b246 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -55,7 +55,7 @@ }, "Debugging & Metrics": { "Error codes": "/errors", - "Composition linting": "/hints", + "Composition hints": "/hints", "Federated trace data": "/metrics", "OpenTelemetry": "/opentelemetry" }, diff --git a/docs/source/hints.mdx b/docs/source/hints.mdx new file mode 100644 index 000000000..4502e186b --- /dev/null +++ b/docs/source/hints.mdx @@ -0,0 +1,19 @@ +--- +title: Composition hints +--- + +When you successfully [compose](./federated-types/composition) the schemas provided by your [subgraphs](./building-supergraphs/subgraphs-overview/) into a supergraph schema, the composition process can flag potential improvements. Specifically, composition can output violations of the [GraphOS schema linter's](/graphos/delivery/schema-linter) [composition rules](/graphos/delivery/linter-rules#composition-rules) in the GraphOS Studio or via the [Rover CLI](/rover/). Both [`rover supergraph compose`](/rover/commands/supergraphs#supergraph-compose) and [`rover subgraph check`](/rover/commands/subgraphs#subgraph-check) commands output rule violations with the [severity levels](/graphos/delivery/schema-linter/#setting-severity-levels) you've configured for those rules. + +See below for a list of composition rules with the code that GraphOS returns for each rule violation. Rules are categorized by rule type. Refer to the [rules reference page](./linter-rules) for a comprehensive list of linter rules. + +### Inconsistent elements + + + +### Overridden and unused elements + + + +### Directives + + \ No newline at end of file diff --git a/docs/source/linter-rules.mdx b/docs/source/linter-rules.mdx deleted file mode 100644 index 6d255e300..000000000 --- a/docs/source/linter-rules.mdx +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: Composition hints ---- - -When you successfully [compose](./federated-types/composition) the schemas provided by your [subgraphs](./building-supergraphs/subgraphs-overview/) into a supergraph schema, the composition process might output **hints** that provide additional information about the result. Hints are primarily informative and _do not_ necessarily indicate that a problem needs to be fixed. - -Hints are categorized under the following levels: - -* `WARN`: Indicates a situation that might be expected but is usually temporary and should be double-checked. Typically, composition might have needed to ignore some elements from some subgraph when creating the supergraph. -* `INFO`: Suggests a potentially helpful improvement or highlights a noteworthy resolution made by composition. Can otherwise be ignored. -* `DEBUG`: Lower-level information that provides insight into the composition. These hints are of lesser importance/impact. - -Note that hints are first and foremost informative and don't necessarily correspond to a problem to be fixed. - -This document lists the hints that can be generated for each level, with a description of why each is generated. - - -The following hints might be generated during composition: - -## `WARN` - -
- -| Code | Description | Level | -|---|---|---| -| `INCONSISTENT_DEFAULT_VALUE_PRESENCE` | Indicates that an argument definition (of a field/input field/directive definition) has a default value in only some of the subgraphs that define the argument. | `WARN` | -| `INCONSISTENT_INPUT_OBJECT_FIELD` | Indicates that a field of an input object type definition is only defined in a subset of the subgraphs that declare the input object. | `WARN` | -| `INCONSISTENT_ENUM_VALUE_FOR_INPUT_ENUM` | Indicates that a value of an enum type definition (that is only used as an Input type) has not been merged into the supergraph because it is defined in only a subset of the subgraphs that declare the enum | `WARN` | -| `INCONSISTENT_EXECUTABLE_DIRECTIVE_PRESENCE` | Indicates that an executable directive definition is declared in only some of the subgraphs. | `WARN` | -| `NO_EXECUTABLE_DIRECTIVE_INTERSECTION` | Indicates that, for an executable directive definition, no location for it appears in all subgraphs. | `WARN` | -| `INCONSISTENT_EXECUTABLE_DIRECTIVE_REPEATABLE` | Indicates that an executable directive definition is marked repeatable in only a subset of the subgraphs (and will not be repeatable in the supergraph). | `WARN` | -| `INCONSISTENT_EXECUTABLE_DIRECTIVE_LOCATIONS` | Indicates that an executiable directive definition is declared with inconsistent locations across subgraphs (and will use the intersection of all locations in the supergraph). | `WARN` | -| `INCONSISTENT_DESCRIPTION` | Indicates that an element has a description in more than one subgraph, and the descriptions are not equal. | `WARN` | -| `INCONSISTENT_ARGUMENT_PRESENCE` | Indicates that an optional argument (of a field or directive definition) is not present in all subgraphs and will not be part of the supergraph. | `WARN` | -| `FROM_SUBGRAPH_DOES_NOT_EXIST` | Source subgraph specified by @override directive does not exist | `WARN` | -| `INCONSISTENT_NON_REPEATABLE_DIRECTIVE_ARGUMENTS` | A non-repeatable directive is applied to a schema element in different subgraphs but with arguments that are different. | `WARN` | -| `DIRECTIVE_COMPOSITION_WARN` | Indicates that an issue was detected when composing custom directives. | `WARN` | -| `INCONSISTENT_RUNTIME_TYPES_FOR_SHAREABLE_RETURN` | Indicates that a @shareable field returns different sets of runtime types in the different subgraphs in which it is defined. | `WARN` | - -
- -## `INFO` - -
- -| Code | Description | Level | -|---|---|---| -| `INCONSISTENT_BUT_COMPATIBLE_FIELD_TYPE` | Indicates that a field does not have the exact same types in all subgraphs, but that the types are "compatible" (2 types are compatible if one is a non-nullable version of the other, a list version, a subtype, or a combination of the former). | `INFO` | -| `INCONSISTENT_BUT_COMPATIBLE_ARGUMENT_TYPE` | Indicates that an argument type (of a field/input field/directive definition) does not have the exact same type in all subgraphs, but that the types are "compatible" (two types are compatible if one is a non-nullable version of the other, a list version, a subtype, or a combination of the former). | `INFO` | -| `INCONSISTENT_ENTITY` | Indicates that an object is declared as an entity (has a `@key`) in only some of the subgraphs in which the object is defined. | `INFO` | -| `OVERRIDDEN_FIELD_CAN_BE_REMOVED` | Field has been overridden by another subgraph. Consider removing. | `INFO` | -| `OVERRIDE_DIRECTIVE_CAN_BE_REMOVED` | Field with @override directive no longer exists in source subgraph, the directive can be safely removed | `INFO` | -| `MERGED_NON_REPEATABLE_DIRECTIVE_ARGUMENTS` | A non-repeatable directive has been applied to a schema element in different subgraphs with different arguments and the arguments values were merged using the directive configured strategies. | `INFO` | -| `DIRECTIVE_COMPOSITION_INFO` | Indicates that an issue was detected when composing custom directives. | `INFO` | - -
- -## `DEBUG` - -
- -| Code | Description | Level | -|---|---|---| -| `INCONSISTENT_OBJECT_VALUE_TYPE_FIELD` | Indicates that a field of an object "value type" (has no `@key` in any subgraph) is not defined in all the subgraphs that declare the type. | `DEBUG` | -| `INCONSISTENT_INTERFACE_VALUE_TYPE_FIELD` | Indicates that a field of an interface "value type" (has no `@key` in any subgraph) is not defined in all the subgraphs that declare the type. | `DEBUG` | -| `INCONSISTENT_UNION_MEMBER` | Indicates that a member of a union type definition is only defined in a subset of the subgraphs that declare the union. | `DEBUG` | -| `INCONSISTENT_ENUM_VALUE_FOR_OUTPUT_ENUM` | Indicates that a value of an enum type definition (that is only used as an Output type, or is unused) has been merged in the supergraph but is defined in only a subset of the subgraphs that declare the enum | `DEBUG` | -| `INCONSISTENT_TYPE_SYSTEM_DIRECTIVE_REPEATABLE` | Indicates that a type system directive definition is marked repeatable in only a subset of the subgraphs that declare the directive (and will be repeatable in the supergraph). | `DEBUG` | -| `INCONSISTENT_TYPE_SYSTEM_DIRECTIVE_LOCATIONS` | Indicates that a type system directive definition is declared with inconsistent locations across subgraphs (and will use the union of all locations in the supergraph). | `DEBUG` | -| `UNUSED_ENUM_TYPE` | Indicates that an enum type is defined in some subgraphs but is unused (no field/argument references it). All the values from subgraphs defining that enum will be included in the supergraph. | `DEBUG` | - -
From 6bed1b7e30b2b474ce20abbc654cc332a80720a1 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Thu, 24 Aug 2023 16:03:11 -0600 Subject: [PATCH 38/95] Update docs/source/hints.mdx Co-authored-by: Avery Harnish --- docs/source/hints.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/hints.mdx b/docs/source/hints.mdx index 4502e186b..751020ad8 100644 --- a/docs/source/hints.mdx +++ b/docs/source/hints.mdx @@ -2,7 +2,7 @@ title: Composition hints --- -When you successfully [compose](./federated-types/composition) the schemas provided by your [subgraphs](./building-supergraphs/subgraphs-overview/) into a supergraph schema, the composition process can flag potential improvements. Specifically, composition can output violations of the [GraphOS schema linter's](/graphos/delivery/schema-linter) [composition rules](/graphos/delivery/linter-rules#composition-rules) in the GraphOS Studio or via the [Rover CLI](/rover/). Both [`rover supergraph compose`](/rover/commands/supergraphs#supergraph-compose) and [`rover subgraph check`](/rover/commands/subgraphs#subgraph-check) commands output rule violations with the [severity levels](/graphos/delivery/schema-linter/#setting-severity-levels) you've configured for those rules. +When you successfully [compose](./federated-types/composition) the schemas provided by your [subgraphs](./building-supergraphs/subgraphs-overview/) into a supergraph schema, the composition process can flag potential improvements. Specifically, composition can output violations of the [GraphOS schema linter's](/graphos/delivery/schema-linter) [composition rules](/graphos/delivery/linter-rules#composition-rules) in GraphOS Studio or via the [Rover CLI](/rover/). The [`rover subgraph check`](/rover/commands/subgraphs#subgraph-check) command outputs rule violations with the [severity levels](/graphos/delivery/schema-linter/#setting-severity-levels) you've configured for your graph variant. [`rover supergraph compose`](/rover/commands/supergraphs#supergraph-compose) outputs all rule violations. See below for a list of composition rules with the code that GraphOS returns for each rule violation. Rules are categorized by rule type. Refer to the [rules reference page](./linter-rules) for a comprehensive list of linter rules. From ac4c88d79aaa74ff50d86e1007d30679b5a0ec66 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Thu, 24 Aug 2023 16:11:24 -0600 Subject: [PATCH 39/95] Copy edit --- docs/source/hints.mdx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/source/hints.mdx b/docs/source/hints.mdx index 751020ad8..eb63e1aad 100644 --- a/docs/source/hints.mdx +++ b/docs/source/hints.mdx @@ -2,9 +2,11 @@ title: Composition hints --- -When you successfully [compose](./federated-types/composition) the schemas provided by your [subgraphs](./building-supergraphs/subgraphs-overview/) into a supergraph schema, the composition process can flag potential improvements. Specifically, composition can output violations of the [GraphOS schema linter's](/graphos/delivery/schema-linter) [composition rules](/graphos/delivery/linter-rules#composition-rules) in GraphOS Studio or via the [Rover CLI](/rover/). The [`rover subgraph check`](/rover/commands/subgraphs#subgraph-check) command outputs rule violations with the [severity levels](/graphos/delivery/schema-linter/#setting-severity-levels) you've configured for your graph variant. [`rover supergraph compose`](/rover/commands/supergraphs#supergraph-compose) outputs all rule violations. +When you successfully [compose](./federated-types/composition) the schemas provided by your [subgraphs](./building-supergraphs/subgraphs-overview/) into a supergraph schema, the composition process can flag potential improvements. Specifically, composition can output violations of the [GraphOS schema linter's](/graphos/delivery/schema-linter) [composition rules](/graphos/delivery/linter-rules#composition-rules) in GraphOS Studio or via the [Rover CLI](/rover/). -See below for a list of composition rules with the code that GraphOS returns for each rule violation. Rules are categorized by rule type. Refer to the [rules reference page](./linter-rules) for a comprehensive list of linter rules. +The [`rover subgraph check`](/rover/commands/subgraphs#subgraph-check) command outputs rule violations with the [severity levels](/graphos/delivery/schema-linter/#setting-severity-levels) you've configured for your graph variant. The [`rover supergraph compose`](/rover/commands/supergraphs#supergraph-compose) command outputs rule violations for all local subgraph schemas. + +See below for a list of composition rules categorized by rule type. The heading for each rule is the code that GraphOS returns for the rule violation. Refer to the [rules reference page](./linter-rules) for a comprehensive list of linter rules. ### Inconsistent elements From 14ba55318161909a2a53cf5104cf0d49484751b0 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Thu, 24 Aug 2023 16:14:13 -0600 Subject: [PATCH 40/95] Fix link --- docs/source/hints.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/hints.mdx b/docs/source/hints.mdx index eb63e1aad..18720224d 100644 --- a/docs/source/hints.mdx +++ b/docs/source/hints.mdx @@ -6,7 +6,7 @@ When you successfully [compose](./federated-types/composition) the schemas provi The [`rover subgraph check`](/rover/commands/subgraphs#subgraph-check) command outputs rule violations with the [severity levels](/graphos/delivery/schema-linter/#setting-severity-levels) you've configured for your graph variant. The [`rover supergraph compose`](/rover/commands/supergraphs#supergraph-compose) command outputs rule violations for all local subgraph schemas. -See below for a list of composition rules categorized by rule type. The heading for each rule is the code that GraphOS returns for the rule violation. Refer to the [rules reference page](./linter-rules) for a comprehensive list of linter rules. +See below for a list of composition rules categorized by rule type. The heading for each rule is the code that GraphOS returns for the rule violation. Refer to the [rules reference page](/graphos/delivery/linter-rules) for a comprehensive list of linter rules. ### Inconsistent elements From 4873994debea503f7c5028acbda05d58434ed437 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Thu, 24 Aug 2023 17:09:46 -0600 Subject: [PATCH 41/95] Add implications of querying with mismatched keys --- docs/source/entities-advanced.mdx | 90 ++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/docs/source/entities-advanced.mdx b/docs/source/entities-advanced.mdx index 3488ec89a..512761ca8 100644 --- a/docs/source/entities-advanced.mdx +++ b/docs/source/entities-advanced.mdx @@ -81,7 +81,8 @@ type Product @key(fields: "upc") { -To merge entities between subgraphs, the entity must have at least one shared field between subgraphs. For example, queries can't merge the `Product` entity defined in the following subgraphs because they don't share any fields specified in `@key` selection set: + +To merge entities between subgraphs, the entity must have at least one shared field between subgraphs. For example, operations can't merge the `Product` entity defined in the following subgraphs because they don't share any fields specified in the `@key` selection set:

@@ -104,6 +105,93 @@ type Product @key(fields: "upc") { +#### Operations with mismatched `@key`s + +Mismatched keys also affect which fields can be resolved. Requests can resolve an entity's fields _if there is a traversable path from the root query to the fields_. + +Take these subgraph schemas as an example: + + + +```graphql title="Products subgraph" +type Product @key(fields: "sku") { + sku: ID! + upc: String! + name: String! + price: Int +} + +type Query { + product(sku: ID!): Product + products: [Product!]! +} + +``` + +```graphql title="Inventory subgraph" +type Product @key(fields: "upc") { + upc: String! + inStock: Boolean! +} +``` + + +The queries defined in the products subgraph can always resolve all product fields because the product entity can be joined via the `upc` field present in both schemas. + +On the other hand, queries added to the inventory subgraph can't resolve fields from the products subgraph. + + + +```graphql title="Products subgraph" +type Product @key(fields: "sku") { + sku: ID! + upc: String! + name: String! + price: Int +} +``` + +```graphql title="Inventory subgraph" +type Product @key(fields: "upc") { + upc: String! + inStock: Boolean! +} + +type Query { + productsInStock: [Product!]! +} +``` + + + +The `productsInStock` query can't resolve fields from the product subgraph since its `Product` type definition doesn't have `@key(fields: "upc")`, and the `sku` field isn't present in the inventory subgraph. + +If the product subgraph includes `@key(fields: "upc")`, all queries from the inventory subgraph can resolve all product fields: + + + +```graphql title="Products subgraph" +type Product @key(fields: "sku") @key(fields: "upc") { + sku: ID! + upc: String! + name: String! + price: Int +} +``` + +```graphql title="Inventory subgraph" +type Product @key(fields: "upc") { + upc: String! + inStock: Boolean! +} + +type Query { + productsInStock: [Product!]! +} +``` + + + ## Migrating entities and fields As your supergraph grows, you might want to move parts of an entity to a different subgraph. This section describes how to perform these migrations safely. From de5575276bb5d42599a11f52a9c0a10d548c29e2 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Thu, 24 Aug 2023 17:21:53 -0600 Subject: [PATCH 42/95] Copy edit --- docs/source/entities-advanced.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/entities-advanced.mdx b/docs/source/entities-advanced.mdx index 512761ca8..2f1c8625c 100644 --- a/docs/source/entities-advanced.mdx +++ b/docs/source/entities-advanced.mdx @@ -107,7 +107,7 @@ type Product @key(fields: "upc") { #### Operations with mismatched `@key`s -Mismatched keys also affect which fields can be resolved. Requests can resolve an entity's fields _if there is a traversable path from the root query to the fields_. +Mismatched keys affect which fields from an entity can be resolved. Requests can resolve an entity's fields _if there is a traversable path from the root query to the fields_. Take these subgraph schemas as an example: @@ -138,7 +138,7 @@ type Product @key(fields: "upc") { The queries defined in the products subgraph can always resolve all product fields because the product entity can be joined via the `upc` field present in both schemas. -On the other hand, queries added to the inventory subgraph can't resolve fields from the products subgraph. +On the other hand, queries added to the inventory subgraph can't resolve fields from the products subgraph: From 8e75f0006206f1d4bf9a1e5f1e369c8c7167c5a6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 25 Aug 2023 00:47:50 +0000 Subject: [PATCH 43/95] chore(deps): update all non-major dependencies (#2748) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 691 ++++++++++++++++++++++++---------------------- package.json | 14 +- 2 files changed, 365 insertions(+), 340 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4268f8012..0d3bd3f4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "devDependencies": { "@apollo/cache-control-types": "1.0.3", "@apollo/client": "3.8.1", - "@apollo/server": "4.9.1", + "@apollo/server": "4.9.2", "@apollo/utils.fetcher": "2.0.1", "@changesets/changelog-github": "0.4.8", "@changesets/cli": "2.26.2", @@ -31,12 +31,12 @@ "@types/async-retry": "1.4.5", "@types/bunyan": "1.8.8", "@types/deep-equal": "1.0.1", - "@types/jest": "29.5.3", + "@types/jest": "29.5.4", "@types/js-levenshtein": "1.1.1", "@types/loglevel": "1.5.4", "@types/make-fetch-happen": "10.0.1", "@types/nock": "10.0.3", - "@types/node": "14.18.54", + "@types/node": "14.18.56", "@types/node-fetch": "2.6.4", "@types/uuid": "9.0.2", "@typescript-eslint/eslint-plugin": "5.62.0", @@ -45,20 +45,20 @@ "graphql": "16.8.0", "graphql-http": "1.21.0", "graphql-tag": "2.12.6", - "jest": "29.6.2", - "jest-config": "29.6.2", + "jest": "29.6.4", + "jest-config": "29.6.4", "jest-cucumber": "3.0.1", "jest-junit": "16.0.0", "log4js": "6.9.1", "mocked-env": "1.3.5", "nock": "13.3.3", - "node-fetch": "2.6.13", + "node-fetch": "2.7.0", "prettier": "3.0.2", "semver": "7.5.4", "strip-indent": "3.0.0", "ts-jest": "29.1.1", "ts-node": "10.9.1", - "typescript": "5.1.6", + "typescript": "5.2.2", "winston": "3.10.0", "winston-transport": "4.5.0" }, @@ -281,9 +281,9 @@ "link": true }, "node_modules/@apollo/server": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@apollo/server/-/server-4.9.1.tgz", - "integrity": "sha512-uUzkHt7DU/NEdwMvkb4GZq8ho2EYJAJXTiBq0HUhhjOuxMVfZ7fbKgOIcSF33Ur7c67fLdWwulXMAvv89Cyv0w==", + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/@apollo/server/-/server-4.9.2.tgz", + "integrity": "sha512-DXARzsL7gvBfhUL2gTCpGduaH5wQFZi72/6ZOalpzT9InepIz0wL9TffSNVuaYla5u6JB9kX8WtnVUKf7IuHTA==", "dev": true, "dependencies": { "@apollo/cache-control-types": "^1.0.3", @@ -2983,16 +2983,16 @@ } }, "node_modules/@jest/console": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.6.2.tgz", - "integrity": "sha512-0N0yZof5hi44HAR2pPS+ikJ3nzKNoZdVu8FffRf3wy47I7Dm7etk/3KetMdRUqzVd16V4O2m2ISpNTbnIuqy1w==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.6.4.tgz", + "integrity": "sha512-wNK6gC0Ha9QeEPSkeJedQuTQqxZYnDPuDcDhVuVatRvMkL4D0VTvFVZj+Yuh6caG2aOfzkUZ36KtCmLNtR02hw==", "dev": true, "dependencies": { - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^29.6.2", - "jest-util": "^29.6.2", + "jest-message-util": "^29.6.3", + "jest-util": "^29.6.3", "slash": "^3.0.0" }, "engines": { @@ -3015,37 +3015,37 @@ } }, "node_modules/@jest/core": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.6.2.tgz", - "integrity": "sha512-Oj+5B+sDMiMWLhPFF+4/DvHOf+U10rgvCLGPHP8Xlsy/7QxS51aU/eBngudHlJXnaWD5EohAgJ4js+T6pa+zOg==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.6.4.tgz", + "integrity": "sha512-U/vq5ccNTSVgYH7mHnodHmCffGWHJnz/E1BEWlLuK5pM4FZmGfBn/nrJGLjUsSmyx3otCeqc1T31F4y08AMDLg==", "dev": true, "dependencies": { - "@jest/console": "^29.6.2", - "@jest/reporters": "^29.6.2", - "@jest/test-result": "^29.6.2", - "@jest/transform": "^29.6.2", - "@jest/types": "^29.6.1", + "@jest/console": "^29.6.4", + "@jest/reporters": "^29.6.4", + "@jest/test-result": "^29.6.4", + "@jest/transform": "^29.6.4", + "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.5.0", - "jest-config": "^29.6.2", - "jest-haste-map": "^29.6.2", - "jest-message-util": "^29.6.2", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.6.2", - "jest-resolve-dependencies": "^29.6.2", - "jest-runner": "^29.6.2", - "jest-runtime": "^29.6.2", - "jest-snapshot": "^29.6.2", - "jest-util": "^29.6.2", - "jest-validate": "^29.6.2", - "jest-watcher": "^29.6.2", + "jest-changed-files": "^29.6.3", + "jest-config": "^29.6.4", + "jest-haste-map": "^29.6.4", + "jest-message-util": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.6.4", + "jest-resolve-dependencies": "^29.6.4", + "jest-runner": "^29.6.4", + "jest-runtime": "^29.6.4", + "jest-snapshot": "^29.6.4", + "jest-util": "^29.6.3", + "jest-validate": "^29.6.3", + "jest-watcher": "^29.6.4", "micromatch": "^4.0.4", - "pretty-format": "^29.6.2", + "pretty-format": "^29.6.3", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, @@ -3077,88 +3077,88 @@ } }, "node_modules/@jest/environment": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.6.2.tgz", - "integrity": "sha512-AEcW43C7huGd/vogTddNNTDRpO6vQ2zaQNrttvWV18ArBx9Z56h7BIsXkNFJVOO4/kblWEQz30ckw0+L3izc+Q==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.6.4.tgz", + "integrity": "sha512-sQ0SULEjA1XUTHmkBRl7A1dyITM9yb1yb3ZNKPX3KlTd6IG7mWUe3e2yfExtC2Zz1Q+mMckOLHmL/qLiuQJrBQ==", "dev": true, "dependencies": { - "@jest/fake-timers": "^29.6.2", - "@jest/types": "^29.6.1", + "@jest/fake-timers": "^29.6.4", + "@jest/types": "^29.6.3", "@types/node": "*", - "jest-mock": "^29.6.2" + "jest-mock": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.6.2.tgz", - "integrity": "sha512-m6DrEJxVKjkELTVAztTLyS/7C92Y2b0VYqmDROYKLLALHn8T/04yPs70NADUYPrV3ruI+H3J0iUIuhkjp7vkfg==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.6.4.tgz", + "integrity": "sha512-Warhsa7d23+3X5bLbrbYvaehcgX5TLYhI03JKoedTiI8uJU4IhqYBWF7OSSgUyz4IgLpUYPkK0AehA5/fRclAA==", "dev": true, "dependencies": { - "expect": "^29.6.2", - "jest-snapshot": "^29.6.2" + "expect": "^29.6.4", + "jest-snapshot": "^29.6.4" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.6.2.tgz", - "integrity": "sha512-6zIhM8go3RV2IG4aIZaZbxwpOzz3ZiM23oxAlkquOIole+G6TrbeXnykxWYlqF7kz2HlBjdKtca20x9atkEQYg==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.6.4.tgz", + "integrity": "sha512-FEhkJhqtvBwgSpiTrocquJCdXPsyvNKcl/n7A3u7X4pVoF4bswm11c9d4AV+kfq2Gpv/mM8x7E7DsRvH+djkrg==", "dev": true, "dependencies": { - "jest-get-type": "^29.4.3" + "jest-get-type": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/fake-timers": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.6.2.tgz", - "integrity": "sha512-euZDmIlWjm1Z0lJ1D0f7a0/y5Kh/koLFMUBE5SUYWrmy8oNhJpbTBDAP6CxKnadcMLDoDf4waRYCe35cH6G6PA==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.6.4.tgz", + "integrity": "sha512-6UkCwzoBK60edXIIWb0/KWkuj7R7Qq91vVInOe3De6DSpaEiqjKcJw4F7XUet24Wupahj9J6PlR09JqJ5ySDHw==", "dev": true, "dependencies": { - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", - "jest-message-util": "^29.6.2", - "jest-mock": "^29.6.2", - "jest-util": "^29.6.2" + "jest-message-util": "^29.6.3", + "jest-mock": "^29.6.3", + "jest-util": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/globals": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.6.2.tgz", - "integrity": "sha512-cjuJmNDjs6aMijCmSa1g2TNG4Lby/AeU7/02VtpW+SLcZXzOLK2GpN2nLqcFjmhy3B3AoPeQVx7BnyOf681bAw==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.6.4.tgz", + "integrity": "sha512-wVIn5bdtjlChhXAzVXavcY/3PEjf4VqM174BM3eGL5kMxLiZD5CLnbmkEyA1Dwh9q8XjP6E8RwjBsY/iCWrWsA==", "dev": true, "dependencies": { - "@jest/environment": "^29.6.2", - "@jest/expect": "^29.6.2", - "@jest/types": "^29.6.1", - "jest-mock": "^29.6.2" + "@jest/environment": "^29.6.4", + "@jest/expect": "^29.6.4", + "@jest/types": "^29.6.3", + "jest-mock": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/reporters": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.6.2.tgz", - "integrity": "sha512-sWtijrvIav8LgfJZlrGCdN0nP2EWbakglJY49J1Y5QihcQLfy7ovyxxjJBRXMNltgt4uPtEcFmIMbVshEDfFWw==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.6.4.tgz", + "integrity": "sha512-sxUjWxm7QdchdrD3NfWKrL8FBsortZeibSJv4XLjESOOjSUOkjQcb0ZHJwfhEGIvBvTluTzfG2yZWZhkrXJu8g==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.6.2", - "@jest/test-result": "^29.6.2", - "@jest/transform": "^29.6.2", - "@jest/types": "^29.6.1", + "@jest/console": "^29.6.4", + "@jest/test-result": "^29.6.4", + "@jest/transform": "^29.6.4", + "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "@types/node": "*", "chalk": "^4.0.0", @@ -3167,13 +3167,13 @@ "glob": "^7.1.3", "graceful-fs": "^4.2.9", "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.6.2", - "jest-util": "^29.6.2", - "jest-worker": "^29.6.2", + "jest-message-util": "^29.6.3", + "jest-util": "^29.6.3", + "jest-worker": "^29.6.4", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", @@ -3207,10 +3207,26 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.0.tgz", + "integrity": "sha512-x58orMzEVfzPUKqlbLd1hXCnySCxKdDKa6Rjg97CwuLLRI4g3FHTdnExu1OqffVFay6zeMW+T6/DowFLndWnIw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@jest/schemas": { - "version": "29.6.0", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.0.tgz", - "integrity": "sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dependencies": { "@sinclair/typebox": "^0.27.8" }, @@ -3219,9 +3235,9 @@ } }, "node_modules/@jest/source-map": { - "version": "29.6.0", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.0.tgz", - "integrity": "sha512-oA+I2SHHQGxDCZpbrsCQSoMLb3Bz547JnM+jUr9qEbuw0vQlWZfpPS7CO9J7XiwKicEz9OFn/IYoLkkiUD7bzA==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", @@ -3233,13 +3249,13 @@ } }, "node_modules/@jest/test-result": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.6.2.tgz", - "integrity": "sha512-3VKFXzcV42EYhMCsJQURptSqnyjqCGbtLuX5Xxb6Pm6gUf1wIRIl+mandIRGJyWKgNKYF9cnstti6Ls5ekduqw==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.6.4.tgz", + "integrity": "sha512-uQ1C0AUEN90/dsyEirgMLlouROgSY+Wc/JanVVk0OiUKa5UFh7sJpMEM3aoUBAz2BRNvUJ8j3d294WFuRxSyOQ==", "dev": true, "dependencies": { - "@jest/console": "^29.6.2", - "@jest/types": "^29.6.1", + "@jest/console": "^29.6.4", + "@jest/types": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" }, @@ -3248,14 +3264,14 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.6.2.tgz", - "integrity": "sha512-GVYi6PfPwVejO7slw6IDO0qKVum5jtrJ3KoLGbgBWyr2qr4GaxFV6su+ZAjdTX75Sr1DkMFRk09r2ZVa+wtCGw==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.6.4.tgz", + "integrity": "sha512-E84M6LbpcRq3fT4ckfKs9ryVanwkaIB0Ws9bw3/yP4seRLg/VaCZ/LgW0MCq5wwk4/iP/qnilD41aj2fsw2RMg==", "dev": true, "dependencies": { - "@jest/test-result": "^29.6.2", + "@jest/test-result": "^29.6.4", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.6.2", + "jest-haste-map": "^29.6.4", "slash": "^3.0.0" }, "engines": { @@ -3263,22 +3279,22 @@ } }, "node_modules/@jest/transform": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.6.2.tgz", - "integrity": "sha512-ZqCqEISr58Ce3U+buNFJYUktLJZOggfyvR+bZMaiV1e8B1SIvJbwZMrYz3gx/KAPn9EXmOmN+uB08yLCjWkQQg==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.6.4.tgz", + "integrity": "sha512-8thgRSiXUqtr/pPGY/OsyHuMjGyhVnWrFAwoxmIemlBuiMyU1WFs0tXoNxzcr4A4uErs/ABre76SGmrr5ab/AA==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.6.2", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.6.2", + "jest-haste-map": "^29.6.4", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.6.3", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", @@ -3309,12 +3325,12 @@ "license": "MIT" }, "node_modules/@jest/types": { - "version": "29.6.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.1.tgz", - "integrity": "sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, "dependencies": { - "@jest/schemas": "^29.6.0", + "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", @@ -3932,9 +3948,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.3", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.3.tgz", - "integrity": "sha512-1Nq7YrO/vJE/FYnqYyw0FS8LdrjExSgIiHyKg7xPpn+yi8Q4huZryKnkJatN1ZRH89Kw2v33/8ZMB7DuZeSLlA==", + "version": "29.5.4", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.4.tgz", + "integrity": "sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -4005,9 +4021,9 @@ } }, "node_modules/@types/node": { - "version": "14.18.54", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.54.tgz", - "integrity": "sha512-uq7O52wvo2Lggsx1x21tKZgqkJpvwCseBBPtX/nKQfpVlEsLOb11zZ1CRsWUKvJF0+lzuA9jwvA7Pr2Wt7i3xw==" + "version": "14.18.56", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.56.tgz", + "integrity": "sha512-+k+57NVS9opgrEn5l9c0gvD1r6C+PtyhVE4BTnMMRwiEA8ZO8uFcs6Yy2sXIy0eC95ZurBtRSvhZiHXBysbl6w==" }, "node_modules/@types/node-fetch": { "version": "2.6.4", @@ -4471,9 +4487,9 @@ } }, "node_modules/@whatwg-node/fetch/node_modules/@types/node": { - "version": "18.17.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.6.tgz", - "integrity": "sha512-fGmT/P7z7ecA6bv/ia5DlaWCH4YeZvAQMNpUhrJjtAhOhZfoxS1VLUgU2pdk63efSjQaOJWdXMuAJsws+8I6dg==", + "version": "18.17.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.11.tgz", + "integrity": "sha512-r3hjHPBu+3LzbGBa8DHnr/KAeTEEOrahkcL+cZc4MaBMTM+mk8LtXR+zw+nqfjuDZZzYTYgTcpHuP+BEQk069g==", "dev": true, "peer": true }, @@ -4893,15 +4909,15 @@ } }, "node_modules/babel-jest": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.6.2.tgz", - "integrity": "sha512-BYCzImLos6J3BH/+HvUCHG1dTf2MzmAB4jaVxHV+29RZLjR29XuYTmsf2sdDwkrb+FczkGo3kOhE7ga6sI0P4A==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.6.4.tgz", + "integrity": "sha512-meLj23UlSLddj6PC+YTOFRgDAtjnZom8w/ACsrx0gtPtv5cJZk0A5Unk5bV4wixD7XaPCN1fQvpww8czkZURmw==", "dev": true, "dependencies": { - "@jest/transform": "^29.6.2", + "@jest/transform": "^29.6.4", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.5.0", + "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" @@ -4944,9 +4960,10 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "29.5.0", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", @@ -5022,11 +5039,12 @@ } }, "node_modules/babel-preset-jest": { - "version": "29.5.0", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", "dev": true, - "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "^29.5.0", + "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "engines": { @@ -6752,9 +6770,9 @@ } }, "node_modules/diff-sequences": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", - "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -7479,8 +7497,9 @@ }, "node_modules/execa": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, - "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -7632,17 +7651,16 @@ "license": "MIT" }, "node_modules/expect": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.6.2.tgz", - "integrity": "sha512-iAErsLxJ8C+S02QbLAwgSGSezLQK+XXRDt8IuFXFpwCNw2ECmzZSmjKcCaFVp5VRMk+WAvz6h6jokzEzBFZEuA==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.6.4.tgz", + "integrity": "sha512-F2W2UyQ8XYyftHT57dtfg8Ue3X5qLgm2sSug0ivvLRH/VKNRL/pDxg/TH7zVzbQB0tu80clNFy6LU7OS/VSEKA==", "dev": true, "dependencies": { - "@jest/expect-utils": "^29.6.2", - "@types/node": "*", - "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.6.2", - "jest-message-util": "^29.6.2", - "jest-util": "^29.6.2" + "@jest/expect-utils": "^29.6.4", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.6.4", + "jest-message-util": "^29.6.3", + "jest-util": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -8202,8 +8220,9 @@ }, "node_modules/get-stream": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -8731,8 +8750,9 @@ }, "node_modules/human-signals": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=10.17.0" } @@ -9563,15 +9583,15 @@ } }, "node_modules/jest": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.6.2.tgz", - "integrity": "sha512-8eQg2mqFbaP7CwfsTpCxQ+sHzw1WuNWL5UUvjnWP4hx2riGz9fPSzYOaU5q8/GqWn1TfgZIVTqYJygbGbWAANg==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.6.4.tgz", + "integrity": "sha512-tEFhVQFF/bzoYV1YuGyzLPZ6vlPrdfvDmmAxudA1dLEuiztqg2Rkx20vkKY32xiDROcD2KXlgZ7Cu8RPeEHRKw==", "dev": true, "dependencies": { - "@jest/core": "^29.6.2", - "@jest/types": "^29.6.1", + "@jest/core": "^29.6.4", + "@jest/types": "^29.6.3", "import-local": "^3.0.2", - "jest-cli": "^29.6.2" + "jest-cli": "^29.6.4" }, "bin": { "jest": "bin/jest.js" @@ -9589,11 +9609,13 @@ } }, "node_modules/jest-changed-files": { - "version": "29.5.0", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.6.3.tgz", + "integrity": "sha512-G5wDnElqLa4/c66ma5PG9eRjE342lIbF6SUnTJi26C3J28Fv2TVY2rOyKB9YGbSA5ogwevgmxc4j4aVjrEK6Yg==", "dev": true, - "license": "MIT", "dependencies": { "execa": "^5.0.0", + "jest-util": "^29.6.3", "p-limit": "^3.1.0" }, "engines": { @@ -9602,8 +9624,9 @@ }, "node_modules/jest-changed-files/node_modules/p-limit": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, - "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -9615,28 +9638,28 @@ } }, "node_modules/jest-circus": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.6.2.tgz", - "integrity": "sha512-G9mN+KOYIUe2sB9kpJkO9Bk18J4dTDArNFPwoZ7WKHKel55eKIS/u2bLthxgojwlf9NLCVQfgzM/WsOVvoC6Fw==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.6.4.tgz", + "integrity": "sha512-YXNrRyntVUgDfZbjXWBMPslX1mQ8MrSG0oM/Y06j9EYubODIyHWP8hMUbjbZ19M3M+zamqEur7O80HODwACoJw==", "dev": true, "dependencies": { - "@jest/environment": "^29.6.2", - "@jest/expect": "^29.6.2", - "@jest/test-result": "^29.6.2", - "@jest/types": "^29.6.1", + "@jest/environment": "^29.6.4", + "@jest/expect": "^29.6.4", + "@jest/test-result": "^29.6.4", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", "dedent": "^1.0.0", "is-generator-fn": "^2.0.0", - "jest-each": "^29.6.2", - "jest-matcher-utils": "^29.6.2", - "jest-message-util": "^29.6.2", - "jest-runtime": "^29.6.2", - "jest-snapshot": "^29.6.2", - "jest-util": "^29.6.2", + "jest-each": "^29.6.3", + "jest-matcher-utils": "^29.6.4", + "jest-message-util": "^29.6.3", + "jest-runtime": "^29.6.4", + "jest-snapshot": "^29.6.4", + "jest-util": "^29.6.3", "p-limit": "^3.1.0", - "pretty-format": "^29.6.2", + "pretty-format": "^29.6.3", "pure-rand": "^6.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" @@ -9675,21 +9698,21 @@ } }, "node_modules/jest-cli": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.6.2.tgz", - "integrity": "sha512-TT6O247v6dCEX2UGHGyflMpxhnrL0DNqP2fRTKYm3nJJpCTfXX3GCMQPGFjXDoj0i5/Blp3jriKXFgdfmbYB6Q==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.6.4.tgz", + "integrity": "sha512-+uMCQ7oizMmh8ZwRfZzKIEszFY9ksjjEQnTEMTaL7fYiL3Kw4XhqT9bYh+A4DQKUb67hZn2KbtEnDuHvcgK4pQ==", "dev": true, "dependencies": { - "@jest/core": "^29.6.2", - "@jest/test-result": "^29.6.2", - "@jest/types": "^29.6.1", + "@jest/core": "^29.6.4", + "@jest/test-result": "^29.6.4", + "@jest/types": "^29.6.3", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "import-local": "^3.0.2", - "jest-config": "^29.6.2", - "jest-util": "^29.6.2", - "jest-validate": "^29.6.2", + "jest-config": "^29.6.4", + "jest-util": "^29.6.3", + "jest-validate": "^29.6.3", "prompts": "^2.0.1", "yargs": "^17.3.1" }, @@ -9724,31 +9747,31 @@ } }, "node_modules/jest-config": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.6.2.tgz", - "integrity": "sha512-VxwFOC8gkiJbuodG9CPtMRjBUNZEHxwfQXmIudSTzFWxaci3Qub1ddTRbFNQlD/zUeaifLndh/eDccFX4wCMQw==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.6.4.tgz", + "integrity": "sha512-JWohr3i9m2cVpBumQFv2akMEnFEPVOh+9L2xIBJhJ0zOaci2ZXuKJj0tgMKQCBZAKA09H049IR4HVS/43Qb19A==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.6.2", - "@jest/types": "^29.6.1", - "babel-jest": "^29.6.2", + "@jest/test-sequencer": "^29.6.4", + "@jest/types": "^29.6.3", + "babel-jest": "^29.6.4", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-circus": "^29.6.2", - "jest-environment-node": "^29.6.2", - "jest-get-type": "^29.4.3", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.6.2", - "jest-runner": "^29.6.2", - "jest-util": "^29.6.2", - "jest-validate": "^29.6.2", + "jest-circus": "^29.6.4", + "jest-environment-node": "^29.6.4", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.6.4", + "jest-runner": "^29.6.4", + "jest-util": "^29.6.3", + "jest-validate": "^29.6.3", "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "^29.6.2", + "pretty-format": "^29.6.3", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -10781,15 +10804,15 @@ } }, "node_modules/jest-diff": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.6.2.tgz", - "integrity": "sha512-t+ST7CB9GX5F2xKwhwCf0TAR17uNDiaPTZnVymP9lw0lssa9vG+AFyDZoeIHStU3WowFFwT+ky+er0WVl2yGhA==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.6.4.tgz", + "integrity": "sha512-9F48UxR9e4XOEZvoUXEHSWY4qC4zERJaOfrbBg9JpbJOO43R1vN76REt/aMGZoY6GD5g84nnJiBIVlscegefpw==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "diff-sequences": "^29.4.3", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.6.2" + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -10812,9 +10835,9 @@ } }, "node_modules/jest-docblock": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.4.3.tgz", - "integrity": "sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.6.3.tgz", + "integrity": "sha512-2+H+GOTQBEm2+qFSQ7Ma+BvyV+waiIFxmZF5LdpBsAEjWX8QYjSCa4FrkIYtbfXUJJJnFCYrOtt6TZ+IAiTjBQ==", "dev": true, "dependencies": { "detect-newline": "^3.0.0" @@ -10824,16 +10847,16 @@ } }, "node_modules/jest-each": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.6.2.tgz", - "integrity": "sha512-MsrsqA0Ia99cIpABBc3izS1ZYoYfhIy0NNWqPSE0YXbQjwchyt6B1HD2khzyPe1WiJA7hbxXy77ZoUQxn8UlSw==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.6.3.tgz", + "integrity": "sha512-KoXfJ42k8cqbkfshW7sSHcdfnv5agDdHCPA87ZBdmHP+zJstTJc0ttQaJ/x7zK6noAL76hOuTIJ6ZkQRS5dcyg==", "dev": true, "dependencies": { - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "chalk": "^4.0.0", - "jest-get-type": "^29.4.3", - "jest-util": "^29.6.2", - "pretty-format": "^29.6.2" + "jest-get-type": "^29.6.3", + "jest-util": "^29.6.3", + "pretty-format": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -11039,45 +11062,46 @@ "license": "MIT" }, "node_modules/jest-environment-node": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.6.2.tgz", - "integrity": "sha512-YGdFeZ3T9a+/612c5mTQIllvWkddPbYcN2v95ZH24oWMbGA4GGS2XdIF92QMhUhvrjjuQWYgUGW2zawOyH63MQ==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.6.4.tgz", + "integrity": "sha512-i7SbpH2dEIFGNmxGCpSc2w9cA4qVD+wfvg2ZnfQ7XVrKL0NA5uDVBIiGH8SR4F0dKEv/0qI5r+aDomDf04DpEQ==", "dev": true, "dependencies": { - "@jest/environment": "^29.6.2", - "@jest/fake-timers": "^29.6.2", - "@jest/types": "^29.6.1", + "@jest/environment": "^29.6.4", + "@jest/fake-timers": "^29.6.4", + "@jest/types": "^29.6.3", "@types/node": "*", - "jest-mock": "^29.6.2", - "jest-util": "^29.6.2" + "jest-mock": "^29.6.3", + "jest-util": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-get-type": { - "version": "29.4.3", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, - "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-haste-map": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.6.2.tgz", - "integrity": "sha512-+51XleTDAAysvU8rT6AnS1ZJ+WHVNqhj1k6nTvN2PYP+HjU3kqlaKQ1Lnw3NYW3bm2r8vq82X0Z1nDDHZMzHVA==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.6.4.tgz", + "integrity": "sha512-12Ad+VNTDHxKf7k+M65sviyynRoZYuL1/GTuhEVb8RYsNSNln71nANRb/faSyWvx0j+gHcivChXHIoMJrGYjog==", "dev": true, "dependencies": { - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.6.2", - "jest-worker": "^29.6.2", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.6.3", + "jest-worker": "^29.6.4", "micromatch": "^4.0.4", "walker": "^1.0.8" }, @@ -11865,28 +11889,28 @@ } }, "node_modules/jest-leak-detector": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.6.2.tgz", - "integrity": "sha512-aNqYhfp5uYEO3tdWMb2bfWv6f0b4I0LOxVRpnRLAeque2uqOVVMLh6khnTcE2qJ5wAKop0HcreM1btoysD6bPQ==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.6.3.tgz", + "integrity": "sha512-0kfbESIHXYdhAdpLsW7xdwmYhLf1BRu4AA118/OxFm0Ho1b2RcTmO4oF6aAMaxpxdxnJ3zve2rgwzNBD4Zbm7Q==", "dev": true, "dependencies": { - "jest-get-type": "^29.4.3", - "pretty-format": "^29.6.2" + "jest-get-type": "^29.6.3", + "pretty-format": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.6.2.tgz", - "integrity": "sha512-4LiAk3hSSobtomeIAzFTe+N8kL6z0JtF3n6I4fg29iIW7tt99R7ZcIFW34QkX+DuVrf+CUe6wuVOpm7ZKFJzZQ==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.6.4.tgz", + "integrity": "sha512-KSzwyzGvK4HcfnserYqJHYi7sZVqdREJ9DMPAKVbS98JsIAvumihaNUbjrWw0St7p9IY7A9UskCW5MYlGmBQFQ==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "jest-diff": "^29.6.2", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.6.2" + "jest-diff": "^29.6.4", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -11908,18 +11932,18 @@ } }, "node_modules/jest-message-util": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.6.2.tgz", - "integrity": "sha512-vnIGYEjoPSuRqV8W9t+Wow95SDp6KPX2Uf7EoeG9G99J2OVh7OSwpS4B6J0NfpEIpfkBNHlBZpA2rblEuEFhZQ==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.6.3.tgz", + "integrity": "sha512-FtzaEEHzjDpQp51HX4UMkPZjy46ati4T5pEMyM6Ik48ztu4T9LQplZ6OsimHx7EuM9dfEh5HJa6D3trEftu3dA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", - "pretty-format": "^29.6.2", + "pretty-format": "^29.6.3", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, @@ -11943,14 +11967,14 @@ } }, "node_modules/jest-mock": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.6.2.tgz", - "integrity": "sha512-hoSv3lb3byzdKfwqCuT6uTscan471GUECqgNYykg6ob0yiAw3zYc7OrPnI9Qv8Wwoa4lC7AZ9hyS4AiIx5U2zg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.6.3.tgz", + "integrity": "sha512-Z7Gs/mOyTSR4yPsaZ72a/MtuK6RnC3JYqWONe48oLaoEcYwEDxqvbXz85G4SJrm2Z5Ar9zp6MiHF4AlFlRM4Pg==", "dev": true, "dependencies": { - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "@types/node": "*", - "jest-util": "^29.6.2" + "jest-util": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -11973,25 +11997,26 @@ } }, "node_modules/jest-regex-util": { - "version": "29.4.3", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, - "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-resolve": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.6.2.tgz", - "integrity": "sha512-G/iQUvZWI5e3SMFssc4ug4dH0aZiZpsDq9o1PtXTV1210Ztyb2+w+ZgQkB3iOiC5SmAEzJBOHWz6Hvrd+QnNPw==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.6.4.tgz", + "integrity": "sha512-fPRq+0vcxsuGlG0O3gyoqGTAxasagOxEuyoxHeyxaZbc9QNek0AmJWSkhjlMG+mTsj+8knc/mWb3fXlRNVih7Q==", "dev": true, "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.6.2", + "jest-haste-map": "^29.6.4", "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.6.2", - "jest-validate": "^29.6.2", + "jest-util": "^29.6.3", + "jest-validate": "^29.6.3", "resolve": "^1.20.0", "resolve.exports": "^2.0.0", "slash": "^3.0.0" @@ -12001,13 +12026,13 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.6.2.tgz", - "integrity": "sha512-LGqjDWxg2fuQQm7ypDxduLu/m4+4Lb4gczc13v51VMZbVP5tSBILqVx8qfWcsdP8f0G7aIqByIALDB0R93yL+w==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.6.4.tgz", + "integrity": "sha512-7+6eAmr1ZBF3vOAJVsfLj1QdqeXG+WYhidfLHBRZqGN24MFRIiKG20ItpLw2qRAsW/D2ZUUmCNf6irUr/v6KHA==", "dev": true, "dependencies": { - "jest-regex-util": "^29.4.3", - "jest-snapshot": "^29.6.2" + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.6.4" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -12029,30 +12054,30 @@ } }, "node_modules/jest-runner": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.6.2.tgz", - "integrity": "sha512-wXOT/a0EspYgfMiYHxwGLPCZfC0c38MivAlb2lMEAlwHINKemrttu1uSbcGbfDV31sFaPWnWJPmb2qXM8pqZ4w==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.6.4.tgz", + "integrity": "sha512-SDaLrMmtVlQYDuG0iSPYLycG8P9jLI+fRm8AF/xPKhYDB2g6xDWjXBrR5M8gEWsK6KVFlebpZ4QsrxdyIX1Jaw==", "dev": true, "dependencies": { - "@jest/console": "^29.6.2", - "@jest/environment": "^29.6.2", - "@jest/test-result": "^29.6.2", - "@jest/transform": "^29.6.2", - "@jest/types": "^29.6.1", + "@jest/console": "^29.6.4", + "@jest/environment": "^29.6.4", + "@jest/test-result": "^29.6.4", + "@jest/transform": "^29.6.4", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "emittery": "^0.13.1", "graceful-fs": "^4.2.9", - "jest-docblock": "^29.4.3", - "jest-environment-node": "^29.6.2", - "jest-haste-map": "^29.6.2", - "jest-leak-detector": "^29.6.2", - "jest-message-util": "^29.6.2", - "jest-resolve": "^29.6.2", - "jest-runtime": "^29.6.2", - "jest-util": "^29.6.2", - "jest-watcher": "^29.6.2", - "jest-worker": "^29.6.2", + "jest-docblock": "^29.6.3", + "jest-environment-node": "^29.6.4", + "jest-haste-map": "^29.6.4", + "jest-leak-detector": "^29.6.3", + "jest-message-util": "^29.6.3", + "jest-resolve": "^29.6.4", + "jest-runtime": "^29.6.4", + "jest-util": "^29.6.3", + "jest-watcher": "^29.6.4", + "jest-worker": "^29.6.4", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -12102,31 +12127,31 @@ } }, "node_modules/jest-runtime": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.6.2.tgz", - "integrity": "sha512-2X9dqK768KufGJyIeLmIzToDmsN0m7Iek8QNxRSI/2+iPFYHF0jTwlO3ftn7gdKd98G/VQw9XJCk77rbTGZnJg==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.6.2", - "@jest/fake-timers": "^29.6.2", - "@jest/globals": "^29.6.2", - "@jest/source-map": "^29.6.0", - "@jest/test-result": "^29.6.2", - "@jest/transform": "^29.6.2", - "@jest/types": "^29.6.1", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.6.4.tgz", + "integrity": "sha512-s/QxMBLvmwLdchKEjcLfwzP7h+jsHvNEtxGP5P+Fl1FMaJX2jMiIqe4rJw4tFprzCwuSvVUo9bn0uj4gNRXsbA==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.6.4", + "@jest/fake-timers": "^29.6.4", + "@jest/globals": "^29.6.4", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.6.4", + "@jest/transform": "^29.6.4", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.6.2", - "jest-message-util": "^29.6.2", - "jest-mock": "^29.6.2", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.6.2", - "jest-snapshot": "^29.6.2", - "jest-util": "^29.6.2", + "jest-haste-map": "^29.6.4", + "jest-message-util": "^29.6.3", + "jest-mock": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.6.4", + "jest-snapshot": "^29.6.4", + "jest-util": "^29.6.3", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -12162,9 +12187,9 @@ } }, "node_modules/jest-snapshot": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.6.2.tgz", - "integrity": "sha512-1OdjqvqmRdGNvWXr/YZHuyhh5DeaLp1p/F8Tht/MrMw4Kr1Uu/j4lRG+iKl1DAqUJDWxtQBMk41Lnf/JETYBRA==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.6.4.tgz", + "integrity": "sha512-VC1N8ED7+4uboUKGIDsbvNAZb6LakgIPgAF4RSpF13dN6YaMokfRqO+BaqK4zIh6X3JffgwbzuGqDEjHm/MrvA==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", @@ -12172,20 +12197,20 @@ "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.6.2", - "@jest/transform": "^29.6.2", - "@jest/types": "^29.6.1", + "@jest/expect-utils": "^29.6.4", + "@jest/transform": "^29.6.4", + "@jest/types": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "expect": "^29.6.2", + "expect": "^29.6.4", "graceful-fs": "^4.2.9", - "jest-diff": "^29.6.2", - "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.6.2", - "jest-message-util": "^29.6.2", - "jest-util": "^29.6.2", + "jest-diff": "^29.6.4", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.6.4", + "jest-message-util": "^29.6.3", + "jest-util": "^29.6.3", "natural-compare": "^1.4.0", - "pretty-format": "^29.6.2", + "pretty-format": "^29.6.3", "semver": "^7.5.3" }, "engines": { @@ -12208,12 +12233,12 @@ } }, "node_modules/jest-util": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.6.2.tgz", - "integrity": "sha512-3eX1qb6L88lJNCFlEADKOkjpXJQyZRiavX1INZ4tRnrBVr2COd3RgcTLyUiEXMNBlDU/cgYq6taUS0fExrWW4w==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.6.3.tgz", + "integrity": "sha512-QUjna/xSy4B32fzcKTSz1w7YYzgiHrjjJjevdRf61HYk998R5vVMMNmrHESYZVDS5DSWs+1srPLPKxXPkeSDOA==", "dev": true, "dependencies": { - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -12240,17 +12265,17 @@ } }, "node_modules/jest-validate": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.6.2.tgz", - "integrity": "sha512-vGz0yMN5fUFRRbpJDPwxMpgSXW1LDKROHfBopAvDcmD6s+B/s8WJrwi+4bfH4SdInBA5C3P3BI19dBtKzx1Arg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.6.3.tgz", + "integrity": "sha512-e7KWZcAIX+2W1o3cHfnqpGajdCs1jSM3DkXjGeLSNmCazv1EeI1ggTeK5wdZhF+7N+g44JI2Od3veojoaumlfg==", "dev": true, "dependencies": { - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", - "jest-get-type": "^29.4.3", + "jest-get-type": "^29.6.3", "leven": "^3.1.0", - "pretty-format": "^29.6.2" + "pretty-format": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -12285,18 +12310,18 @@ } }, "node_modules/jest-watcher": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.6.2.tgz", - "integrity": "sha512-GZitlqkMkhkefjfN/p3SJjrDaxPflqxEAv3/ik10OirZqJGYH5rPiIsgVcfof0Tdqg3shQGdEIxDBx+B4tuLzA==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.6.4.tgz", + "integrity": "sha512-oqUWvx6+On04ShsT00Ir9T4/FvBeEh2M9PTubgITPxDa739p4hoQweWPRGyYeaojgT0xTpZKF0Y/rSY1UgMxvQ==", "dev": true, "dependencies": { - "@jest/test-result": "^29.6.2", - "@jest/types": "^29.6.1", + "@jest/test-result": "^29.6.4", + "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.13.1", - "jest-util": "^29.6.2", + "jest-util": "^29.6.3", "string-length": "^4.0.1" }, "engines": { @@ -12320,13 +12345,13 @@ } }, "node_modules/jest-worker": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.6.2.tgz", - "integrity": "sha512-l3ccBOabTdkng8I/ORCkADz4eSMKejTYv1vB/Z83UiubqhC1oQ5Li6dWCyqOIvSifGjUBxuvxvlm6KGK2DtuAQ==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.6.4.tgz", + "integrity": "sha512-6dpvFV4WjcWbDVGgHTWo/aupl8/LbBx2NSKfiwqf79xC/yeJjKHT1+StcKy/2KTmW16hE68ccKVOtXf+WZGz7Q==", "dev": true, "dependencies": { "@types/node": "*", - "jest-util": "^29.6.2", + "jest-util": "^29.6.3", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -13530,9 +13555,9 @@ "license": "MIT" }, "node_modules/node-fetch": { - "version": "2.6.13", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz", - "integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -14292,11 +14317,11 @@ } }, "node_modules/pretty-format": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.2.tgz", - "integrity": "sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.3.tgz", + "integrity": "sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==", "dependencies": { - "@jest/schemas": "^29.6.0", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -16794,9 +16819,9 @@ } }, "node_modules/typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index a4b65d16e..2b3bcbcc7 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "devDependencies": { "@apollo/cache-control-types": "1.0.3", "@apollo/client": "3.8.1", - "@apollo/server": "4.9.1", + "@apollo/server": "4.9.2", "@apollo/utils.fetcher": "2.0.1", "@changesets/changelog-github": "0.4.8", "@changesets/cli": "2.26.2", @@ -57,12 +57,12 @@ "@types/async-retry": "1.4.5", "@types/bunyan": "1.8.8", "@types/deep-equal": "1.0.1", - "@types/jest": "29.5.3", + "@types/jest": "29.5.4", "@types/js-levenshtein": "1.1.1", "@types/loglevel": "1.5.4", "@types/make-fetch-happen": "10.0.1", "@types/nock": "10.0.3", - "@types/node": "14.18.54", + "@types/node": "14.18.56", "@types/node-fetch": "2.6.4", "@types/uuid": "9.0.2", "@typescript-eslint/eslint-plugin": "5.62.0", @@ -71,20 +71,20 @@ "graphql": "16.8.0", "graphql-http": "1.21.0", "graphql-tag": "2.12.6", - "jest": "29.6.2", - "jest-config": "29.6.2", + "jest": "29.6.4", + "jest-config": "29.6.4", "jest-cucumber": "3.0.1", "jest-junit": "16.0.0", "log4js": "6.9.1", "mocked-env": "1.3.5", "nock": "13.3.3", - "node-fetch": "2.6.13", + "node-fetch": "2.7.0", "prettier": "3.0.2", "semver": "7.5.4", "strip-indent": "3.0.0", "ts-jest": "29.1.1", "ts-node": "10.9.1", - "typescript": "5.1.6", + "typescript": "5.2.2", "winston": "3.10.0", "winston-transport": "4.5.0" }, From 42c96cc510ead78d288813714333bb29df1f22d2 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 25 Aug 2023 15:41:22 +0200 Subject: [PATCH 44/95] docs: typo requiresScope -> requiresScopes (#2750) --- docs/source/federated-types/federated-directives.mdx | 4 ++-- docs/source/federation-versions.mdx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/federated-types/federated-directives.mdx b/docs/source/federated-types/federated-directives.mdx index 930d8fea3..14457bdf8 100644 --- a/docs/source/federated-types/federated-directives.mdx +++ b/docs/source/federated-types/federated-directives.mdx @@ -367,9 +367,9 @@ directive @authenticated on | ENUM ``` -Indicates to composition that the target element is accessible only to the authenticated supergraph users. For more granular access control, see the [`@requiresScope`](#requiresScope) directive below. Refer to the [Apollo Router article](/router/configuration/authorization#authenticated) for additional details. +Indicates to composition that the target element is accessible only to the authenticated supergraph users. For more granular access control, see the [`@requiresScopes`](#requiresScopes) directive below. Refer to the [Apollo Router article](/router/configuration/authorization#authenticated) for additional details. -### `@requiresScope` +### `@requiresScopes` > ⚠️ **This directive is available in Apollo Federation 2.5 and later.** > **Is is an [Enterprise feature](/router/enterprise-features) of the Apollo Router** and requires an organization with a [GraphOS Enterprise plan](https://www.apollographql.com/pricing/).
diff --git a/docs/source/federation-versions.mdx b/docs/source/federation-versions.mdx index cd27fec28..6b1292521 100644 --- a/docs/source/federation-versions.mdx +++ b/docs/source/federation-versions.mdx @@ -92,7 +92,7 @@ directive @authenticated on -##### `@requiresScope` +##### `@requiresScopes` @@ -134,7 +134,7 @@ Scope -- Custom scalar representing a JWT scope. Used by new `@requiresScope` directive. +- Custom scalar representing a JWT scope. Used by new `@requiresScopes` directive. From 7a1f2994d9400d23f1ff09b6d804222a962b8cb3 Mon Sep 17 00:00:00 2001 From: Samuel Vazquez Date: Fri, 25 Aug 2023 08:00:01 -0700 Subject: [PATCH 45/95] feat: include query plan object in extensions (#2724) * feat: include query plan object in extensions * feat: query plan format * Create loud-camels-doubt.md --------- Co-authored-by: Samuel Vazquez Co-authored-by: Sylvain Lebresne Co-authored-by: Sylvain Lebresne --- .changeset/loud-camels-doubt.md | 7 +++++++ gateway-js/src/index.ts | 14 +++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 .changeset/loud-camels-doubt.md diff --git a/.changeset/loud-camels-doubt.md b/.changeset/loud-camels-doubt.md new file mode 100644 index 000000000..d74d9253f --- /dev/null +++ b/.changeset/loud-camels-doubt.md @@ -0,0 +1,7 @@ +--- +"@apollo/gateway": patch +--- + +Adds header to change the format of exposed query plans, and allows formatting it as json. + +When the gateway is configured to allow it, adding the `Apollo-Query-Plan-Experimental` header to a request already allowed a "prettified" text version of the query plan used for the query is returned in the response extension. This changes adds support for a new (optional) accompanying header, `Apollo-Query-Plan-Experimental-Format`, which can be set to the value "internal" to have the query plan returned as a json object (that correspond to the internal representation of that query plan) instead of the text version otherwise sent. Note that if that new header is not provided, then the query plan continues to be send in the previous prettified text version. diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 18438f01a..fb9c56db9 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -858,6 +858,13 @@ export class ApolloGateway implements GatewayInterface { } if (shouldShowQueryPlan) { + const queryPlanFormat = + request.http && + request.http.headers && + request.http.headers.has('Apollo-Query-Plan-Experimental-Format') + ? request.http.headers.get('Apollo-Query-Plan-Experimental-Format') + : 'prettified' + // TODO: expose the query plan in a more flexible JSON format in the future // and rename this to `queryPlan`. Playground should cutover to use the new // option once we've built a way to print that representation. @@ -866,7 +873,12 @@ export class ApolloGateway implements GatewayInterface { // still want to respond to Playground with something truthy since it depends // on this to decide that query plans are supported by this gateway. response.extensions = { - __queryPlanExperimental: serializedQueryPlan || true, + __queryPlanExperimental: + queryPlanFormat === 'prettified' + ? serializedQueryPlan || true + : queryPlanFormat === 'internal' + ? queryPlan + : true }; } if (response.errors) { From 203b0a444782f6d6ca2f2dbb68c6e4eb59de6d45 Mon Sep 17 00:00:00 2001 From: Sylvain Lebresne Date: Tue, 29 Aug 2023 16:48:36 +0200 Subject: [PATCH 46/95] Fix incorrect removal of dependencies between groups in some rare `@requires` (#2726) Fulfilling some `@requires` can necessitate querying group(s) (subgraphs) that are not the direct parent of the group with the `@requires`. To generate plan that are as optimal as possible, the code needs to figure out the proper parent of those new groups, that is what is the "earliest" group those new groups truly depend on. This is done by starting to look at the parent of the `@requires` group, see if the new groups genuinely depend on anything that parent fetches, and if not, iterate the same logic on the grand-parent (until we either find a parent the new groups depend on, or we reach the beginning of the plan. But the code that checked if the new groups truly depend on a given parent was incomplete: it was sometimes responding that there were no dependency when that was not true. The result is that for some specific configuration of a `@requires`, we could end up with a plan whereby some group/fetch A depended on the results of another group/fetch B, but A was performed in parallel of B instead of after it. Which ultimately resulted in not getting the proper data for the required fields, leading to incorrect executions (the exact detail of how this would show up depends a bit on the exact service implementation, but typically the resolver of the field on which there is a `@requires` would get `null` for its requirements, which might lead that resolver to throw an error as this is unexpected). Fixes #2683 Co-authored-by: Chris Lenfest --- .changeset/strange-moons-prove.md | 15 + .../src/__tests__/executeQueryPlan.test.ts | 367 +++++++++++++++++- query-planner-js/src/buildPlan.ts | 9 + 3 files changed, 390 insertions(+), 1 deletion(-) create mode 100644 .changeset/strange-moons-prove.md diff --git a/.changeset/strange-moons-prove.md b/.changeset/strange-moons-prove.md new file mode 100644 index 000000000..0787c3670 --- /dev/null +++ b/.changeset/strange-moons-prove.md @@ -0,0 +1,15 @@ +--- +"@apollo/query-planner": major +"@apollo/gateway": major +--- + +Fix some potentially incorrect query plans with `@requires` when some dependencies are involved. + +In some rare case of `@requires`, an over-eager optimisation was incorrectly considering that +a dependency between 2 subgraph fetches was unnecessary, leading to doing 2 subgraphs queries +in parallel when those should be done sequentially (because the 2nd query rely on results +from the 1st one). This effectively resulted in the required fields not being provided (the +consequence of which depends a bit on the resolver detail, but if the resolver expected +the required fields to be populated (as they should), then this could typically result +in a message of the form `GraphQLError: Cannot read properties of null`). + \ No newline at end of file diff --git a/gateway-js/src/__tests__/executeQueryPlan.test.ts b/gateway-js/src/__tests__/executeQueryPlan.test.ts index 1980fecd5..40836faf6 100644 --- a/gateway-js/src/__tests__/executeQueryPlan.test.ts +++ b/gateway-js/src/__tests__/executeQueryPlan.test.ts @@ -3545,6 +3545,371 @@ describe('executeQueryPlan', () => { } `); }); + + test('requires with nested field example from #2683 is fixed', async () => { + const parentItems = [ + { "__typename": "ParentItem", "id": '1', "name": "Parent Item #1" }, + { "__typename": "ParentItem", "id": '2', "name": "Parent Item #2" }, + { "__typename": "ParentItem", "id": '3', "name": "Parent Item #3" }, + ]; + + const childItems = [ + { "__typename": "ChildItem", "id": '1', "name": "Child Item #1" }, + { "__typename": "ChildItem", "id": '2', "name": "Child Item #2" }, + { "__typename": "ChildItem", "id": '3', "name": "Child Item #3" }, + ]; + + const s1 = { + name: 'S1', + typeDefs: gql` + type Query { + parentItems: [ParentItem!]! + } + + type ParentItem @key(fields: "id") { + id: ID! + name: String! + } + `, + resolvers: { + Query: { + parentItems() { + return parentItems; + }, + }, + ParentItem: { + __resolveReference(ref: { id: string }) { + return parentItems.find(({id}) => ref.id === id); + }, + } + } + } + + const s2 = { + name: 'S2', + typeDefs: gql` + type ChildItem @key(fields: "id") { + id: ID! + name: String! + parentItem: ParentItem + } + + type ParentItem @key(fields: "id") { + id: ID! + childItems: [ChildItem!]! + } + `, + resolvers: { + ChildItem: { + __resolveReference(ref: { id: string }) { + return childItems.find(({id}) => ref.id === id); + }, + parentItem(ref: { id: string }) { + return parentItems.find(({id}) => ref.id === id); + } + }, + ParentItem: { + __resolveReference(ref: { id: string }) { + return parentItems.find(({id}) => ref.id === id); + }, + childItems(ref: { id: string }) { + return [childItems.find(({id}) => ref.id === id)]; + } + }, + } + } + + const s3 = { + name: 'S3', + typeDefs: gql` + type ChildItem @key(fields: "id") { + id: ID! + name: String! @external + parentItem: ParentItem @external + message: String! @requires(fields: "name parentItem { name }") + } + + type ParentItem { + name: String! @external + } + `, + resolvers: { + ChildItem: { + __resolveReference(ref: { id: string, name: string, parentItem: { name: string }}) { + console.log(ref); + return { + ...ref, + message: `${ref.parentItem.name} | ${ref.name}`, + }; + } + } + } + } + + const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2, s3 ]); + let operation = parseOp(` + query { + parentItems { + childItems { + message + } + } + } + `, schema); + + let queryPlan = buildPlan(operation, queryPlanner); + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "S1") { + { + parentItems { + __typename + id + } + } + }, + Flatten(path: "parentItems.@") { + Fetch(service: "S2") { + { + ... on ParentItem { + __typename + id + } + } => + { + ... on ParentItem { + childItems { + __typename + id + name + parentItem { + __typename + id + } + } + } + } + }, + }, + Flatten(path: "parentItems.@.childItems.@.parentItem") { + Fetch(service: "S1") { + { + ... on ParentItem { + __typename + id + } + } => + { + ... on ParentItem { + name + } + } + }, + }, + Flatten(path: "parentItems.@.childItems.@") { + Fetch(service: "S3") { + { + ... on ChildItem { + __typename + name + parentItem { + name + } + id + } + } => + { + ... on ChildItem { + message + } + } + }, + }, + }, + } + `); + let response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + expect(response.errors).toBeUndefined(); + expect(response.data).toMatchInlineSnapshot(` + Object { + "parentItems": Array [ + Object { + "childItems": Array [ + Object { + "message": "Parent Item #1 | Child Item #1", + }, + ], + }, + Object { + "childItems": Array [ + Object { + "message": "Parent Item #2 | Child Item #2", + }, + ], + }, + Object { + "childItems": Array [ + Object { + "message": "Parent Item #3 | Child Item #3", + }, + ], + }, + ], + } + `); + + operation = parseOp(` + query { + parentItems { + childItems { + ParentItem: parentItem { + name + id + } + message + } + } + } + `, schema); + + queryPlan = buildPlan(operation, queryPlanner); + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "S1") { + { + parentItems { + __typename + id + } + } + }, + Flatten(path: "parentItems.@") { + Fetch(service: "S2") { + { + ... on ParentItem { + __typename + id + } + } => + { + ... on ParentItem { + childItems { + __typename + id + ParentItem: parentItem { + __typename + id + } + name + parentItem { + __typename + id + } + } + } + } + }, + }, + Parallel { + Flatten(path: "parentItems.@.childItems.@.ParentItem") { + Fetch(service: "S1") { + { + ... on ParentItem { + __typename + id + } + } => + { + ... on ParentItem { + name + } + } + }, + }, + Sequence { + Flatten(path: "parentItems.@.childItems.@.parentItem") { + Fetch(service: "S1") { + { + ... on ParentItem { + __typename + id + } + } => + { + ... on ParentItem { + name + } + } + }, + }, + Flatten(path: "parentItems.@.childItems.@") { + Fetch(service: "S3") { + { + ... on ChildItem { + __typename + name + parentItem { + name + } + id + } + } => + { + ... on ChildItem { + message + } + } + }, + }, + }, + }, + }, + } + `); + response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + expect(response.errors).toBeUndefined(); + expect(response.data).toMatchInlineSnapshot(` + Object { + "parentItems": Array [ + Object { + "childItems": Array [ + Object { + "ParentItem": Object { + "id": "1", + "name": "Parent Item #1", + }, + "message": "Parent Item #1 | Child Item #1", + }, + ], + }, + Object { + "childItems": Array [ + Object { + "ParentItem": Object { + "id": "2", + "name": "Parent Item #2", + }, + "message": "Parent Item #2 | Child Item #2", + }, + ], + }, + Object { + "childItems": Array [ + Object { + "ParentItem": Object { + "id": "3", + "name": "Parent Item #3", + }, + "message": "Parent Item #3 | Child Item #3", + }, + ], + }, + ], + } + `); + }); }); describe('@key', () => { @@ -6033,7 +6398,7 @@ describe('executeQueryPlan', () => { }); }); - it(`surface post-processing errors as extensions in the response`, async () => { + it('surface post-processing errors as extensions in the response', async () => { // This test is such that the first subgraph return some object with a key, but // then the 2nd one is queried for additional field `x`, but the reference resolver // returns `null`, so that response is ignored, and the data internally to the diff --git a/query-planner-js/src/buildPlan.ts b/query-planner-js/src/buildPlan.ts index 57f61aea2..fc2a247e3 100644 --- a/query-planner-js/src/buildPlan.ts +++ b/query-planner-js/src/buildPlan.ts @@ -1109,6 +1109,15 @@ class FetchGroup { return true; } + // If we do have inputs, then we first look at the path to `maybeParent`, and it needs to be + // essentially empty, "essentially" is because path can sometimes have some leading fragment(s) + // and those are fine to ignore. But if the path has some field, then this implies that the inputs + // of `this` are based on something at a deeper level than those of `maybeParent`, and the "contains" + // comparison we do below would not make sense. + if (relation.path.some((elt) => elt.kind === 'Field')) { + return false; + } + // In theory, the most general test we could have here is to check if `this.inputs` "intersects" // `maybeParent.selection. As if it doesn't, we know our inputs don't depend on anything the // parent fetches. However, selection set intersection is a bit tricky to implement (due to fragments, From ce44c19a87395ff4980f6ef28d5591b124a452ae Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Wed, 30 Aug 2023 12:28:07 -0600 Subject: [PATCH 47/95] Specify minVersion for composition hints --- docs/source/hints.mdx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/source/hints.mdx b/docs/source/hints.mdx index 18720224d..053a94b69 100644 --- a/docs/source/hints.mdx +++ b/docs/source/hints.mdx @@ -2,9 +2,11 @@ title: Composition hints --- -When you successfully [compose](./federated-types/composition) the schemas provided by your [subgraphs](./building-supergraphs/subgraphs-overview/) into a supergraph schema, the composition process can flag potential improvements. Specifically, composition can output violations of the [GraphOS schema linter's](/graphos/delivery/schema-linter) [composition rules](/graphos/delivery/linter-rules#composition-rules) in GraphOS Studio or via the [Rover CLI](/rover/). +When you successfully [compose](./federated-types/composition) the schemas provided by your [subgraphs](./building-supergraphs/subgraphs-overview/) into a supergraph schema, the composition process can flag potential improvements or **hints**. Hints are violations of the [GraphOS schema linter's](/graphos/delivery/schema-linter) [composition rules](/graphos/delivery/linter-rules#composition-rules). You can review them on the [Checks](/graphos/delivery/schema-checks) page in GraphOS Studio or via the [Rover CLI](/rover/). -The [`rover subgraph check`](/rover/commands/subgraphs#subgraph-check) command outputs rule violations with the [severity levels](/graphos/delivery/schema-linter/#setting-severity-levels) you've configured for your graph variant. The [`rover supergraph compose`](/rover/commands/supergraphs#supergraph-compose) command outputs rule violations for all local subgraph schemas. +> Composition hints only appear in GraphOS Studio and via the `rover subgraph check` command for graphs on [federation version `2.4`](/federation/federation-versions/#v24) or later. You can update a graph's version from its **Settings** page in [GraphOS Studio](https://studio.apollographql.com?referrer=docs-content). + +The [`rover subgraph check`](/rover/commands/subgraphs#subgraph-check) command outputs rule violations with the [severity levels](/graphos/delivery/schema-linter/#setting-severity-levels) you've configured for your graph variant. The [`rover supergraph compose`](/rover/commands/supergraphs#supergraph-compose) command outputs rule violations for _all_ local subgraph schemas. See below for a list of composition rules categorized by rule type. The heading for each rule is the code that GraphOS returns for the rule violation. Refer to the [rules reference page](/graphos/delivery/linter-rules) for a comprehensive list of linter rules. From 73d9bdf5ff5155f6183b5d03ed6c1d4dde4d17b6 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Wed, 30 Aug 2023 12:56:37 -0600 Subject: [PATCH 48/95] Update broken or redirected links --- docs/source/entities-advanced.mdx | 2 +- docs/source/entities.mdx | 2 +- docs/source/managed-federation/deployment.mdx | 8 ++++---- docs/source/managed-federation/uplink.mdx | 2 +- docs/source/performance/monitoring.mdx | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/source/entities-advanced.mdx b/docs/source/entities-advanced.mdx index 2f1c8625c..a9d24b623 100644 --- a/docs/source/entities-advanced.mdx +++ b/docs/source/entities-advanced.mdx @@ -627,4 +627,4 @@ const resolvers = { A basic implementation of the `fetchProductByID` function might make a database call each time it's called. If we need to resolve `Product.price` for `N` different products, this results in `N` database calls. These calls are made _in addition to_ the call made by the Reviews subgraph to fetch the initial list of reviews (and the `id` of each product). This is where the "N+1" problem gets its name. If not prevented, this problem can cause performance problems or even enable denial-of-service attacks. -This problem is not limited to reference resolvers! In fact, it can occur with any resolver that fetches from a data store. To handle this problem, we strongly recommend using [the dataloader pattern](https://github.com/graphql/dataloader). Nearly every GraphQL server library provides a dataloader implementation, and you should use it in **every resolver**. This is true even for resolvers that _aren't_ for entities and that _don't_ return a list. These resolvers can _still_ cause N+1 issues via [batched requests](/enterprise-guide/graph-security/#batched-requests). +This problem is not limited to reference resolvers! In fact, it can occur with any resolver that fetches from a data store. To handle this problem, we strongly recommend using [the dataloader pattern](https://github.com/graphql/dataloader). Nearly every GraphQL server library provides a dataloader implementation, and you should use it in **every resolver**. This is true even for resolvers that _aren't_ for entities and that _don't_ return a list. These resolvers can _still_ cause N+1 issues via [batched requests](/tutorials/voyage-part2/02-monolith-graph-setup). diff --git a/docs/source/entities.mdx b/docs/source/entities.mdx index 9c9d134e5..33dab2243 100644 --- a/docs/source/entities.mdx +++ b/docs/source/entities.mdx @@ -81,7 +81,7 @@ type Product @key(fields: "id") { > * Fields that return a union or interface > * Fields that take arguments -For more information on advanced key options, like how to define [multiple keys](./entities-advanced#multiple-keys) or [compound keys](/entities-advanced#compound-keys), see [Advanced topics for federation entities](./entities-advanced). +For more information on advanced key options, like how to define [multiple keys](./entities-advanced/#multiple-keys) or [compound keys](/entities-advanced/#compound-keys), see [Advanced topics for federation entities](./entities-advanced). ### 2. Define a reference resolver diff --git a/docs/source/managed-federation/deployment.mdx b/docs/source/managed-federation/deployment.mdx index 0b8b5c884..48d6b7ce0 100644 --- a/docs/source/managed-federation/deployment.mdx +++ b/docs/source/managed-federation/deployment.mdx @@ -5,11 +5,11 @@ description: Best practices When rolling out changes to a [subgraph](../building-supergraphs/subgraphs-overview/), we recommend the following workflow: -1. Confirm the backward compatibility of each change by [running `rover subgraph check`](/rover/commands/subgraphs/#checking-subgraph-schema-changes) in your CI pipeline. +1. Confirm the backward compatibility of each change by [running `rover subgraph check`](/rover/commands/subgraphs/#validating-subgraph-schema-changes) in your CI pipeline. 2. Merge backward compatible changes that successfully pass schema checks. 3. Deploy changes to the subgraph in your infrastructure. 4. Wait until all replicas finish deploying. -5. Run [`rover subgraph publish`](/rover/commands/subgraphs/#publishing-a-subgraph-schema-to-apollo-studio) to update your managed federation configuration: +5. Run [`rover subgraph publish`](/rover/commands/subgraphs/#publishing-a-subgraph-schema-to-graphos) to update your managed federation configuration: ```bash rover subgraph publish my-supergraph@my-variant \ @@ -119,7 +119,7 @@ The next time it starts up or polls, your router obtains an updated configuratio With managed federation, you can control which version of your graph a fleet of routers are using. In the majority of cases, rolling over all of your router instances to a new schema version is safe, assuming you've used [schema checks](./federated-schema-checks/) to confirm that your changes are backward compatible. -However, changes at the router level might involve a variety of different updates, such as [migrating entities](../entities-advanced/#migrating-entities-and-fields) from one subgraph to another. If your infrastructure requires a more advanced deployment process, you can use [graph variants](/graphos/graphs/overview/#variants) to manage different fleets of routers running with different configurations. +However, changes at the router level might involve a variety of different updates, such as [migrating entities](../entities-advanced/#migrating-entities-and-fields) from one subgraph to another. If your infrastructure requires a more advanced deployment process, you can use [graph variants](/graphos/graphs/#variants) to manage different fleets of routers running with different configurations. ### Example @@ -143,7 +143,7 @@ To configure a canary deployment, you might maintain two production graph varian rover subgraph publish my-supergraph@prod --name=launches --schema ./launches/schema.graphql ``` -If your canary variant [reports metrics to GraphOS](/graphos/metrics/usage-reporting/), you can use [Apollo Studio](https://studio.apollographql.com) to verify a canary's performance before rolling out changes to the rest of the graph. You can also use variants to support a variety of other advanced deployment workflows, such as blue/green deployments. +If your canary variant [reports metrics to GraphOS](/graphos/metrics/), you can use [Apollo Studio](https://studio.apollographql.com?referrer=docs-content) to verify a canary's performance before rolling out changes to the rest of the graph. You can also use variants to support a variety of other advanced deployment workflows, such as blue/green deployments. ## Modifying query-planning logic diff --git a/docs/source/managed-federation/uplink.mdx b/docs/source/managed-federation/uplink.mdx index 0b27cf959..5ed0e84ca 100644 --- a/docs/source/managed-federation/uplink.mdx +++ b/docs/source/managed-federation/uplink.mdx @@ -140,4 +140,4 @@ APOLLO_SCHEMA_CONFIG_DELIVERY_ENDPOINT=https://aws.uplink.api.apollographql.com/ Supergraph schemas provided by Uplink cannot exceed 6MB in size. The _vast_ majority of supergraph schemas are well below this limit. -If your supergraph schema _does_ exceed 6MB, you can set up a [build status webhook](/graphos/notifications/build-status-notification/) for your graph. Whenever you're notified of a successful supergraph schema composition, your webhook can fetch the latest supergraph schema [via the Rover CLI](/rover/commands/supergraphs#supergraph-fetch). +If your supergraph schema _does_ exceed 6MB, you can set up a [build status webhook](/graphos/metrics/notifications/build-status-notification/) for your graph. Whenever you're notified of a successful supergraph schema composition, your webhook can fetch the latest supergraph schema [via the Rover CLI](/rover/commands/supergraphs#supergraph-fetch). diff --git a/docs/source/performance/monitoring.mdx b/docs/source/performance/monitoring.mdx index 305f6b8a9..2870ba2bd 100644 --- a/docs/source/performance/monitoring.mdx +++ b/docs/source/performance/monitoring.mdx @@ -39,7 +39,7 @@ Operation-level statistics are still collected for operations sent by clients, a ### Setup -To enable federated tracing, you [set the `APOLLO_KEY` environment variable](/apollo-server/monitoring/metrics/#connecting-to-apollo-studio) in your _gateway's_ environment. **Do not** set this environment variable for your subgraph servers. +To enable federated tracing, you [set the `APOLLO_KEY` environment variable](/apollo-server/monitoring/metrics/#connecting-to-graphos) in your _gateway's_ environment. **Do not** set this environment variable for your subgraph servers. > If _other_ features require you to set `APOLLO_KEY` in your subgraph servers, [disable usage reporting](/apollo-server/api/plugin/usage-reporting/#disabling-the-plugin) in those servers. From 33530522bcebb41eaa04ee9ec80a963b2f9a61b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 31 Aug 2023 00:55:51 +0300 Subject: [PATCH 49/95] chore(deps): update dependency @apollo/server to v4.9.3 [security] (#2758) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 21 +++++++++++---------- package.json | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0d3bd3f4f..ad0222388 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "devDependencies": { "@apollo/cache-control-types": "1.0.3", "@apollo/client": "3.8.1", - "@apollo/server": "4.9.2", + "@apollo/server": "4.9.3", "@apollo/utils.fetcher": "2.0.1", "@changesets/changelog-github": "0.4.8", "@changesets/cli": "2.26.2", @@ -281,9 +281,9 @@ "link": true }, "node_modules/@apollo/server": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/@apollo/server/-/server-4.9.2.tgz", - "integrity": "sha512-DXARzsL7gvBfhUL2gTCpGduaH5wQFZi72/6ZOalpzT9InepIz0wL9TffSNVuaYla5u6JB9kX8WtnVUKf7IuHTA==", + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/@apollo/server/-/server-4.9.3.tgz", + "integrity": "sha512-U56Sx/UmzR3Es344hQ/Ptf2EJrH+kV4ZPoLmgGjWoiwf2wYQ/pRSvkSXgjOvoyE34wSa8Gh7f92ljfLfY+6q1w==", "dev": true, "dependencies": { "@apollo/cache-control-types": "^1.0.3", @@ -4487,9 +4487,9 @@ } }, "node_modules/@whatwg-node/fetch/node_modules/@types/node": { - "version": "18.17.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.11.tgz", - "integrity": "sha512-r3hjHPBu+3LzbGBa8DHnr/KAeTEEOrahkcL+cZc4MaBMTM+mk8LtXR+zw+nqfjuDZZzYTYgTcpHuP+BEQk069g==", + "version": "18.17.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.12.tgz", + "integrity": "sha512-d6xjC9fJ/nSnfDeU0AMDsaJyb1iHsqCSOdi84w4u+SlN/UgQdY5tRhpMzaFYsI4mnpvgTivEaQd0yOUhAtOnEQ==", "dev": true, "peer": true }, @@ -6485,9 +6485,10 @@ } }, "node_modules/cucumber-messages/node_modules/@types/uuid": { - "version": "3.4.10", - "dev": true, - "license": "MIT" + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.11.tgz", + "integrity": "sha512-CJNkbEu4IdVuBMRVaNC2GjASgJK7ziqDlVXWuJ1pvhOLADl7nzxhTKjHRdOmo2SuXuygcWBmzgYgn9foTX0UiA==", + "dev": true }, "node_modules/cucumber-messages/node_modules/uuid": { "version": "3.4.0", diff --git a/package.json b/package.json index 2b3bcbcc7..90966b354 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "devDependencies": { "@apollo/cache-control-types": "1.0.3", "@apollo/client": "3.8.1", - "@apollo/server": "4.9.2", + "@apollo/server": "4.9.3", "@apollo/utils.fetcher": "2.0.1", "@changesets/changelog-github": "0.4.8", "@changesets/cli": "2.26.2", From 55f99c27deb06ed79488e6491c2a262c0776e42e Mon Sep 17 00:00:00 2001 From: Chris Lenfest Date: Thu, 31 Aug 2023 12:50:58 -0500 Subject: [PATCH 50/95] modifying .changeset for #2726 to be a patch (#2760) --- .changeset/strange-moons-prove.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/strange-moons-prove.md b/.changeset/strange-moons-prove.md index 0787c3670..ce0ea007c 100644 --- a/.changeset/strange-moons-prove.md +++ b/.changeset/strange-moons-prove.md @@ -1,6 +1,6 @@ --- -"@apollo/query-planner": major -"@apollo/gateway": major +"@apollo/query-planner": patch +"@apollo/gateway": patch --- Fix some potentially incorrect query plans with `@requires` when some dependencies are involved. @@ -12,4 +12,4 @@ from the 1st one). This effectively resulted in the required fields not being pr consequence of which depends a bit on the resolver detail, but if the resolver expected the required fields to be populated (as they should), then this could typically result in a message of the form `GraphQLError: Cannot read properties of null`). - \ No newline at end of file + From d8930ecec56fcfaded836ff5248562eba2d96786 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 31 Aug 2023 12:56:27 -0500 Subject: [PATCH 51/95] Version Packages (#2751) Co-authored-by: github-actions[bot] --- .changeset/loud-camels-doubt.md | 7 ---- .changeset/strange-moons-prove.md | 15 --------- composition-js/CHANGELOG.md | 7 ++++ composition-js/package.json | 6 ++-- .../CHANGELOG.md | 2 ++ .../package.json | 2 +- gateway-js/CHANGELOG.md | 22 +++++++++++++ gateway-js/package.json | 8 ++--- internals-js/CHANGELOG.md | 2 ++ internals-js/package.json | 2 +- package-lock.json | 32 +++++++++---------- query-graphs-js/CHANGELOG.md | 6 ++++ query-graphs-js/package.json | 4 +-- query-planner-js/CHANGELOG.md | 17 ++++++++++ query-planner-js/package.json | 6 ++-- subgraph-js/CHANGELOG.md | 6 ++++ subgraph-js/package.json | 4 +-- 17 files changed, 94 insertions(+), 54 deletions(-) delete mode 100644 .changeset/loud-camels-doubt.md delete mode 100644 .changeset/strange-moons-prove.md diff --git a/.changeset/loud-camels-doubt.md b/.changeset/loud-camels-doubt.md deleted file mode 100644 index d74d9253f..000000000 --- a/.changeset/loud-camels-doubt.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@apollo/gateway": patch ---- - -Adds header to change the format of exposed query plans, and allows formatting it as json. - -When the gateway is configured to allow it, adding the `Apollo-Query-Plan-Experimental` header to a request already allowed a "prettified" text version of the query plan used for the query is returned in the response extension. This changes adds support for a new (optional) accompanying header, `Apollo-Query-Plan-Experimental-Format`, which can be set to the value "internal" to have the query plan returned as a json object (that correspond to the internal representation of that query plan) instead of the text version otherwise sent. Note that if that new header is not provided, then the query plan continues to be send in the previous prettified text version. diff --git a/.changeset/strange-moons-prove.md b/.changeset/strange-moons-prove.md deleted file mode 100644 index ce0ea007c..000000000 --- a/.changeset/strange-moons-prove.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@apollo/query-planner": patch -"@apollo/gateway": patch ---- - -Fix some potentially incorrect query plans with `@requires` when some dependencies are involved. - -In some rare case of `@requires`, an over-eager optimisation was incorrectly considering that -a dependency between 2 subgraph fetches was unnecessary, leading to doing 2 subgraphs queries -in parallel when those should be done sequentially (because the 2nd query rely on results -from the 1st one). This effectively resulted in the required fields not being provided (the -consequence of which depends a bit on the resolver detail, but if the resolver expected -the required fields to be populated (as they should), then this could typically result -in a message of the form `GraphQLError: Cannot read properties of null`). - diff --git a/composition-js/CHANGELOG.md b/composition-js/CHANGELOG.md index e5397b342..dfbe9135a 100644 --- a/composition-js/CHANGELOG.md +++ b/composition-js/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG for `@apollo/composition` +## 2.5.4 +### Patch Changes + +- Updated dependencies []: + - @apollo/federation-internals@2.5.4 + - @apollo/query-graphs@2.5.4 + ## 2.5.3 ### Patch Changes diff --git a/composition-js/package.json b/composition-js/package.json index 49491ff40..df160b160 100644 --- a/composition-js/package.json +++ b/composition-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/composition", - "version": "2.5.3", + "version": "2.5.4", "description": "Apollo Federation composition utilities", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -27,8 +27,8 @@ "access": "public" }, "dependencies": { - "@apollo/federation-internals": "2.5.3", - "@apollo/query-graphs": "2.5.3" + "@apollo/federation-internals": "2.5.4", + "@apollo/query-graphs": "2.5.4" }, "peerDependencies": { "graphql": "^16.5.0" diff --git a/federation-integration-testsuite-js/CHANGELOG.md b/federation-integration-testsuite-js/CHANGELOG.md index ab8ff1d03..164bc7085 100644 --- a/federation-integration-testsuite-js/CHANGELOG.md +++ b/federation-integration-testsuite-js/CHANGELOG.md @@ -1,5 +1,7 @@ # CHANGELOG for `federation-integration-testsuite-js` +## 2.5.4 + ## 2.5.3 ## 2.5.2 diff --git a/federation-integration-testsuite-js/package.json b/federation-integration-testsuite-js/package.json index 04a44a175..4e3d3903e 100644 --- a/federation-integration-testsuite-js/package.json +++ b/federation-integration-testsuite-js/package.json @@ -1,7 +1,7 @@ { "name": "apollo-federation-integration-testsuite", "private": true, - "version": "2.5.3", + "version": "2.5.4", "description": "Apollo Federation Integrations / Test Fixtures", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/gateway-js/CHANGELOG.md b/gateway-js/CHANGELOG.md index 4ad7f3c8a..a39ff2e58 100644 --- a/gateway-js/CHANGELOG.md +++ b/gateway-js/CHANGELOG.md @@ -1,5 +1,27 @@ # CHANGELOG for `@apollo/gateway` +## 2.5.4 +### Patch Changes + + +- Adds header to change the format of exposed query plans, and allows formatting it as json. ([#2724](https://github.com/apollographql/federation/pull/2724)) + + When the gateway is configured to allow it, adding the `Apollo-Query-Plan-Experimental` header to a request already allowed a "prettified" text version of the query plan used for the query is returned in the response extension. This changes adds support for a new (optional) accompanying header, `Apollo-Query-Plan-Experimental-Format`, which can be set to the value "internal" to have the query plan returned as a json object (that correspond to the internal representation of that query plan) instead of the text version otherwise sent. Note that if that new header is not provided, then the query plan continues to be send in the previous prettified text version. + +- Fix some potentially incorrect query plans with `@requires` when some dependencies are involved. ([#2726](https://github.com/apollographql/federation/pull/2726)) + + In some rare case of `@requires`, an over-eager optimisation was incorrectly considering that + a dependency between 2 subgraph fetches was unnecessary, leading to doing 2 subgraphs queries + in parallel when those should be done sequentially (because the 2nd query rely on results + from the 1st one). This effectively resulted in the required fields not being provided (the + consequence of which depends a bit on the resolver detail, but if the resolver expected + the required fields to be populated (as they should), then this could typically result + in a message of the form `GraphQLError: Cannot read properties of null`). +- Updated dependencies [[`203b0a44`](https://github.com/apollographql/federation/commit/203b0a444782f6d6ca2f2dbb68c6e4eb59de6d45)]: + - @apollo/query-planner@2.5.4 + - @apollo/composition@2.5.4 + - @apollo/federation-internals@2.5.4 + ## 2.5.3 ### Patch Changes diff --git a/gateway-js/package.json b/gateway-js/package.json index b4390af3a..f46d97d44 100644 --- a/gateway-js/package.json +++ b/gateway-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/gateway", - "version": "2.5.3", + "version": "2.5.4", "description": "Apollo Gateway", "author": "Apollo ", "main": "dist/index.js", @@ -25,9 +25,9 @@ "access": "public" }, "dependencies": { - "@apollo/composition": "2.5.3", - "@apollo/federation-internals": "2.5.3", - "@apollo/query-planner": "2.5.3", + "@apollo/composition": "2.5.4", + "@apollo/federation-internals": "2.5.4", + "@apollo/query-planner": "2.5.4", "@apollo/server-gateway-interface": "^1.1.0", "@apollo/usage-reporting-protobuf": "^4.1.0", "@apollo/utils.createhash": "^2.0.0", diff --git a/internals-js/CHANGELOG.md b/internals-js/CHANGELOG.md index a00cf6e1a..6532a4618 100644 --- a/internals-js/CHANGELOG.md +++ b/internals-js/CHANGELOG.md @@ -1,5 +1,7 @@ # CHANGELOG for `@apollo/federation-internals` +## 2.5.4 + ## 2.5.3 ### Patch Changes diff --git a/internals-js/package.json b/internals-js/package.json index 9f18e7d94..caa497ec3 100644 --- a/internals-js/package.json +++ b/internals-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/federation-internals", - "version": "2.5.3", + "version": "2.5.4", "description": "Apollo Federation internal utilities", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/package-lock.json b/package-lock.json index ad0222388..7002ac6c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,11 +69,11 @@ }, "composition-js": { "name": "@apollo/composition", - "version": "2.5.3", + "version": "2.5.4", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { - "@apollo/federation-internals": "2.5.3", - "@apollo/query-graphs": "2.5.3" + "@apollo/federation-internals": "2.5.4", + "@apollo/query-graphs": "2.5.4" }, "engines": { "node": ">=14.15.0" @@ -84,7 +84,7 @@ }, "federation-integration-testsuite-js": { "name": "apollo-federation-integration-testsuite", - "version": "2.5.3", + "version": "2.5.4", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { "graphql-tag": "^2.12.6", @@ -93,12 +93,12 @@ }, "gateway-js": { "name": "@apollo/gateway", - "version": "2.5.3", + "version": "2.5.4", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { - "@apollo/composition": "2.5.3", - "@apollo/federation-internals": "2.5.3", - "@apollo/query-planner": "2.5.3", + "@apollo/composition": "2.5.4", + "@apollo/federation-internals": "2.5.4", + "@apollo/query-planner": "2.5.4", "@apollo/server-gateway-interface": "^1.1.0", "@apollo/usage-reporting-protobuf": "^4.1.0", "@apollo/utils.createhash": "^2.0.0", @@ -124,7 +124,7 @@ }, "internals-js": { "name": "@apollo/federation-internals", - "version": "2.5.3", + "version": "2.5.4", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { "@types/uuid": "^9.0.0", @@ -17548,10 +17548,10 @@ }, "query-graphs-js": { "name": "@apollo/query-graphs", - "version": "2.5.3", + "version": "2.5.4", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { - "@apollo/federation-internals": "2.5.3", + "@apollo/federation-internals": "2.5.4", "deep-equal": "^2.0.5", "ts-graphviz": "^1.5.4", "uuid": "^9.0.0" @@ -17565,11 +17565,11 @@ }, "query-planner-js": { "name": "@apollo/query-planner", - "version": "2.5.3", + "version": "2.5.4", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { - "@apollo/federation-internals": "2.5.3", - "@apollo/query-graphs": "2.5.3", + "@apollo/federation-internals": "2.5.4", + "@apollo/query-graphs": "2.5.4", "@apollo/utils.keyvaluecache": "^2.1.0", "chalk": "^4.1.0", "deep-equal": "^2.0.5", @@ -17598,11 +17598,11 @@ }, "subgraph-js": { "name": "@apollo/subgraph", - "version": "2.5.3", + "version": "2.5.4", "license": "MIT", "dependencies": { "@apollo/cache-control-types": "^1.0.2", - "@apollo/federation-internals": "2.5.3" + "@apollo/federation-internals": "2.5.4" }, "engines": { "node": ">=14.15.0" diff --git a/query-graphs-js/CHANGELOG.md b/query-graphs-js/CHANGELOG.md index 7aa1f6fd0..2aa2035b8 100644 --- a/query-graphs-js/CHANGELOG.md +++ b/query-graphs-js/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG for `@apollo/query-graphs` +## 2.5.4 +### Patch Changes + +- Updated dependencies []: + - @apollo/federation-internals@2.5.4 + ## 2.5.3 ### Patch Changes diff --git a/query-graphs-js/package.json b/query-graphs-js/package.json index c39d7f425..3d71e26b7 100644 --- a/query-graphs-js/package.json +++ b/query-graphs-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/query-graphs", - "version": "2.5.3", + "version": "2.5.4", "description": "Apollo Federation library to work with 'query graphs'", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -23,7 +23,7 @@ "node": ">=14.15.0" }, "dependencies": { - "@apollo/federation-internals": "2.5.3", + "@apollo/federation-internals": "2.5.4", "deep-equal": "^2.0.5", "ts-graphviz": "^1.5.4", "uuid": "^9.0.0" diff --git a/query-planner-js/CHANGELOG.md b/query-planner-js/CHANGELOG.md index 171bb0e4a..94bb399fe 100644 --- a/query-planner-js/CHANGELOG.md +++ b/query-planner-js/CHANGELOG.md @@ -1,5 +1,22 @@ # CHANGELOG for `@apollo/query-planner` +## 2.5.4 +### Patch Changes + + +- Fix some potentially incorrect query plans with `@requires` when some dependencies are involved. ([#2726](https://github.com/apollographql/federation/pull/2726)) + + In some rare case of `@requires`, an over-eager optimisation was incorrectly considering that + a dependency between 2 subgraph fetches was unnecessary, leading to doing 2 subgraphs queries + in parallel when those should be done sequentially (because the 2nd query rely on results + from the 1st one). This effectively resulted in the required fields not being provided (the + consequence of which depends a bit on the resolver detail, but if the resolver expected + the required fields to be populated (as they should), then this could typically result + in a message of the form `GraphQLError: Cannot read properties of null`). +- Updated dependencies []: + - @apollo/federation-internals@2.5.4 + - @apollo/query-graphs@2.5.4 + ## 2.5.3 ### Patch Changes diff --git a/query-planner-js/package.json b/query-planner-js/package.json index 4ca7b8179..f5ce55c2e 100644 --- a/query-planner-js/package.json +++ b/query-planner-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/query-planner", - "version": "2.5.3", + "version": "2.5.4", "description": "Apollo Query Planner", "author": "Apollo ", "main": "dist/index.js", @@ -25,8 +25,8 @@ "access": "public" }, "dependencies": { - "@apollo/federation-internals": "2.5.3", - "@apollo/query-graphs": "2.5.3", + "@apollo/federation-internals": "2.5.4", + "@apollo/query-graphs": "2.5.4", "@apollo/utils.keyvaluecache": "^2.1.0", "chalk": "^4.1.0", "deep-equal": "^2.0.5", diff --git a/subgraph-js/CHANGELOG.md b/subgraph-js/CHANGELOG.md index 8b027e2a0..2199f7334 100644 --- a/subgraph-js/CHANGELOG.md +++ b/subgraph-js/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG for `@apollo/subgraph` +## 2.5.4 +### Patch Changes + +- Updated dependencies []: + - @apollo/federation-internals@2.5.4 + ## 2.5.3 ### Patch Changes diff --git a/subgraph-js/package.json b/subgraph-js/package.json index b65d236aa..51d7db72c 100644 --- a/subgraph-js/package.json +++ b/subgraph-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/subgraph", - "version": "2.5.3", + "version": "2.5.4", "description": "Apollo Subgraph Utilities", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -25,7 +25,7 @@ }, "dependencies": { "@apollo/cache-control-types": "^1.0.2", - "@apollo/federation-internals": "2.5.3" + "@apollo/federation-internals": "2.5.4" }, "peerDependencies": { "graphql": "^16.5.0" From 81c5612e50f10c3342547b913ffee58625ecaa73 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 1 Sep 2023 10:56:55 -0700 Subject: [PATCH 52/95] Pin npm@9 for now, npm@10 drops support for node@<18.17 (#2762) --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 72cdab40e..071b82f87 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,7 +19,7 @@ jobs: node-version: << parameters.node-version >> # node v14 defaults to npm 6, which is too old for our package-lock.json # should be able to remove this step when we drop node v14 - - run: npm install -g npm@latest + - run: npm install -g npm@9 - node/install-packages - run: name: Run tests From 11f6e92056490aa7c9e8070ee479f9f0224b4fa7 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 1 Sep 2023 10:59:34 -0700 Subject: [PATCH 53/95] Use `prettier@2` for jest snapshots (#2761) --- jest.config.base.js | 3 ++- package-lock.json | 17 +++++++++++++++++ package.json | 1 + 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/jest.config.base.js b/jest.config.base.js index 359a5d884..485f148b9 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -22,5 +22,6 @@ module.exports = { tsconfig: "/tsconfig.test.json", diagnostics: false } - } + }, + prettierPath: require.resolve("prettier-2"), }; diff --git a/package-lock.json b/package-lock.json index 7002ac6c9..cfe5559ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,7 @@ "nock": "13.3.3", "node-fetch": "2.7.0", "prettier": "3.0.2", + "prettier-2": "npm:prettier@2.8.8", "semver": "7.5.4", "strip-indent": "3.0.0", "ts-jest": "29.1.1", @@ -14317,6 +14318,22 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-2": { + "name": "prettier", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.3.tgz", diff --git a/package.json b/package.json index 90966b354..600c839d1 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "nock": "13.3.3", "node-fetch": "2.7.0", "prettier": "3.0.2", + "prettier-2": "npm:prettier@2.8.8", "semver": "7.5.4", "strip-indent": "3.0.0", "ts-jest": "29.1.1", From 717ef87c2e3c2709e3172cc0b2f9ef87ae98028d Mon Sep 17 00:00:00 2001 From: Dariusz Kuc <9501705+dariuszkuc@users.noreply.github.com> Date: Fri, 1 Sep 2023 15:11:36 -0500 Subject: [PATCH 54/95] docs: latest subgraph support updates (#2763) Latest subgraph compatibility results --- .cspell/cspell-dict.txt | 1 + .../supported-subgraphs.md | 50 +++++++++++-------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/.cspell/cspell-dict.txt b/.cspell/cspell-dict.txt index 215e120a6..fe37da297 100644 --- a/.cspell/cspell-dict.txt +++ b/.cspell/cspell-dict.txt @@ -43,6 +43,7 @@ comple condiditions correcly cyclesf +Cypher dedented Dedupe defered diff --git a/docs/source/building-supergraphs/supported-subgraphs.md b/docs/source/building-supergraphs/supported-subgraphs.md index 577df6449..8a85c0375 100644 --- a/docs/source/building-supergraphs/supported-subgraphs.md +++ b/docs/source/building-supergraphs/supported-subgraphs.md @@ -25,11 +25,12 @@ The following open-source GraphQL server libraries and hosted solutions support Ballerina GraphQL Module A spec-compliant, production-ready, Standard Library module for building and interacting with GraphQL APIs using Ballerina.

Github: ballerina-platform/module-ballerina-graphql
Type: Code first
-Stars: 51 ⭐
+Stars: 83 ⭐
Last Release: 2023-06-30

_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🔲
@provides🔲
federated tracing🔲
@link🟢
@shareable🔲
@tag🔲
@override🔲
@inaccessible🔲
@composeDirective🔲
@interfaceObject🔲
+ ## C# / .NET @@ -41,12 +42,12 @@ Last Release: 2023-06-30

+Stars: 13.5k ⭐
+Last Release: 2023-08-24

Core Library: GraphQL.js
Federation Library: Apollo Subgraph
+Last Release: 2023-08-22

Core Library: GraphQL.js
Federation Library: Apollo Subgraph
+Last Release: 2023-08-11

Core Library: GraphQL.js
+Last Release: 2023-08-11

+Stars: 4.6k ⭐
+Last Release: 2023-08-25

_service<
GraphQL for .NET

Github: graphql-dotnet/graphql-dotnet
Type: Code first | SDL first
Stars: 5.6k ⭐
-Last Release: 2023-06-17

_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🔲
@requires🟢
@provides🟢
federated tracing🔲
@link
@shareable🔲
@tag🔲
@override🔲
@inaccessible🔲
@composeDirective🔲
@interfaceObject🔲
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🔲
@requires🟢
@provides🟢
federated tracing🔲
@link
@shareable🔲
@tag🔲
@override🔲
@inaccessible🔲
@composeDirective🔲
@interfaceObject🔲
Hot Chocolate
Open-source GraphQL server for the Microsoft .NET platform that takes the complexity away and lets you focus on delivering the next big thing.

Github: ChilliCream/graphql-platform
Type: Code first | SDL first
-Stars: 4.5k ⭐
-Last Release: 2023-07-28

_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🔲
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link
@shareable🔲
@tag🔲
@override🔲
@inaccessible🔲
@composeDirective🔲
@interfaceObject🔲
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🔲
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link
@shareable🔲
@tag🔲
@override🔲
@inaccessible🔲
@composeDirective🔲
@interfaceObject🔲
@@ -125,8 +126,8 @@ Last Release: 2023-07-18

Core Library: Apollo Server
🌍  Spec-compliant and production ready JavaScript GraphQL server that lets you develop in a schema-first way. Built for Express, Connect, Hapi, Koa, and more.

Github: apollographql/apollo-server
Type: SDL first
-Stars: 13.4k ⭐
-Last Release: 2023-07-27

Core Library: GraphQL.js
Federation Library: Apollo Subgraph
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
express-graphql
Create a GraphQL HTTP server with Express.

Github: graphql/express-graphql
Type: SDL first
@@ -136,11 +137,11 @@ Last Release: 2020-11-19

Core Library: dotansimha/graphql-yoga
Type: SDL first
Stars: 7.7k ⭐
-Last Release: 2023-07-24

Core Library: GraphQL.js
Federation Library: Apollo Subgraph
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
GraphQL Helix
A highly evolved and framework-agnostic GraphQL HTTP server.

Github: contra/graphql-helix
Type: SDL first
-Stars: 823 ⭐
+Stars: 822 ⭐
Last Release: 2022-07-09

Core Library: GraphQL.js
Federation Library: Apollo Subgraph
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
Mercurius
Implement GraphQL servers and gateways with Fastify

Github: mercurius-js/mercurius
@@ -161,7 +162,7 @@ Last Release: 2023-06-16

Core Library: hayes/pothos
Type: Code first
Stars: 2.0k ⭐
-Last Release: 2023-07-25

Core Library: GraphQL.js
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
@@ -176,12 +177,12 @@ Last Release: 2023-07-25

Core Library: nuwave/lighthouse
Type: SDL first
Stars: 3.2k ⭐
-Last Release: 2023-07-20

Core Library: webonyx/graphql-php
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link
@shareable🔲
@tag🔲
@override🔲
@inaccessible🔲
@composeDirective🔲
@interfaceObject🔲
+Last Release: 2023-08-04

Core Library: webonyx/graphql-php
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link
@shareable🔲
@tag🔲
@override🔲
@inaccessible🔲
@composeDirective🔲
@interfaceObject🔲
GraphQL PHP PHP implementation of the GraphQL specification based on the reference implementation in JavaScript

Github: webonyx/graphql-php
Type: Code first
Stars: 4.5k ⭐
-Last Release: 2023-07-24

Federation Library: Skillshare/apollo-federation-php
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link
@shareable🔲
@tag🔲
@override🔲
@inaccessible🔲
@composeDirective🔲
@interfaceObject🔲
+Last Release: 2023-08-08

Federation Library: Skillshare/apollo-federation-php
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link
@shareable🔲
@tag🔲
@override🔲
@inaccessible🔲
@composeDirective🔲
@interfaceObject🔲
@@ -200,13 +201,13 @@ Last Release: 2023-06-27

Core Library: Graphene GraphQL framework for Python

Github: graphql-python/graphene
Type: Code first
-Stars: 7.7k ⭐
+Stars: 7.8k ⭐
Last Release: 2023-07-26

Core Library: GraphQL-core 3
Federation Library: graphql-python/graphene-federation
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
Strawberry A GraphQL library for Python that leverages type annotations 🍓

Github: strawberry-graphql/strawberry
Type: Code first
-Stars: 3.3k ⭐
-Last Release: 2023-07-28

Core Library: GraphQL-core 3
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
+Stars: 3.4k ⭐
+Last Release: 2023-08-24

Core Library: GraphQL-core 3
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
@@ -235,7 +236,7 @@ Last Release: 2021-02-12

Federation Library: async-graphql A GraphQL server library implemented in Rust

Github: async-graphql/async-graphql
Type: Code first
-Stars: 2.9k ⭐
+Stars: 3.0k ⭐
Last Release: 2022-11-28

_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
@@ -250,12 +251,12 @@ Last Release: 2022-11-28

+Stars: 880 ⭐
+Last Release: 2023-08-06

_service<
Caliban
Functional GraphQL library for Scala

Github: ghostdogpr/caliban
Type: Code first
-Stars: 878 ⭐
-Last Release: 2023-05-13

_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🔲
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🔲
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
Sangria
Scala GraphQL implementation

Github: sangria-graphql/sangria
Type: Code first
-Stars: 1.9k ⭐
+Stars: 2.0k ⭐
Last Release: 2023-06-20

Federation Library: sangria-graphql/sangria-federated
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
@@ -271,7 +272,7 @@ Last Release: 2023-06-20

Federation Library: GraphQLSwift/Graphiti
Type: SDL first
Stars: 507 ⭐
-Last Release: 2023-06-12

_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
+Last Release: 2023-07-31

_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
@@ -289,9 +290,14 @@ Last Release: 2023-06-12

+Stars: 3.0k ⭐
+Last Release: 2023-08-24

+ + - +
_service<
GraphQL Mesh
Executable GraphQL schema from multiple data sources, query anything, run anywhere.

Github: Urigo/graphql-mesh
Type: undefined
-Stars: 2.9k ⭐
-Last Release: 2023-07-28

_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
Neo4J Graph Database
A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations.

Github: neo4j/graphql
+Type: Code first | SDL first
+Stars: 445 ⭐
+Last Release: 2023-08-22

Core Library: GraphQL.js
Federation Library: Apollo Subgraph
_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🟢
repeatable @key🟢
@requires🟢
@provides🟢
federated tracing🟢
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🟢
@interfaceObject🟢
StepZen, an IBM Company
Build GraphQL APIs for all your data in a declarative way. Federate across any data source, including GraphQL.

_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🔲
repeatable @key🔲
@requires🟢
@provides🟢
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
Build GraphQL APIs for all your data in a declarative way. Federate across any data source, including GraphQL.

_service🟢
@key (single)🟢
@key (multi)🟢
@key (composite)🔲
repeatable @key🔲
@requires🟢
@provides🔲
federated tracing🔲
@link🟢
@shareable🟢
@tag🟢
@override🟢
@inaccessible🟢
@composeDirective🔲
@interfaceObject🔲
From 30a8498b76ea54b049c4a12323e30eaac9b8afe9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 2 Sep 2023 00:38:28 +0000 Subject: [PATCH 55/95] chore(deps): update all non-major dependencies (#2764) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 46 +++++++++++++++++++++++----------------------- package.json | 10 +++++----- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index cfe5559ea..0bd1c3143 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ ], "devDependencies": { "@apollo/cache-control-types": "1.0.3", - "@apollo/client": "3.8.1", + "@apollo/client": "3.8.2", "@apollo/server": "4.9.3", "@apollo/utils.fetcher": "2.0.1", "@changesets/changelog-github": "0.4.8", @@ -36,14 +36,14 @@ "@types/loglevel": "1.5.4", "@types/make-fetch-happen": "10.0.1", "@types/nock": "10.0.3", - "@types/node": "14.18.56", + "@types/node": "14.18.57", "@types/node-fetch": "2.6.4", - "@types/uuid": "9.0.2", + "@types/uuid": "9.0.3", "@typescript-eslint/eslint-plugin": "5.62.0", "bunyan": "1.8.15", "cspell": "6.31.3", "graphql": "16.8.0", - "graphql-http": "1.21.0", + "graphql-http": "1.22.0", "graphql-tag": "2.12.6", "jest": "29.6.4", "jest-config": "29.6.4", @@ -53,7 +53,7 @@ "mocked-env": "1.3.5", "nock": "13.3.3", "node-fetch": "2.7.0", - "prettier": "3.0.2", + "prettier": "3.0.3", "prettier-2": "npm:prettier@2.8.8", "semver": "7.5.4", "strip-indent": "3.0.0", @@ -197,9 +197,9 @@ } }, "node_modules/@apollo/client": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.8.1.tgz", - "integrity": "sha512-JGGj/9bdoLEqzatRikDeN8etseY5qeFAY0vSAx/Pd0ePNsaflKzHx6V2NZ0NsGkInq+9IXXX3RLVDf0EotizMA==", + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.8.2.tgz", + "integrity": "sha512-SSxRTHlHdlR65mvV5j5e3JkYs9z/eFQfJPgSfqTeKa3jgHKofBaMb+UWxJPInqV5MqBFAkPFt8fYEBZwM7oGZA==", "dev": true, "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", @@ -4022,9 +4022,9 @@ } }, "node_modules/@types/node": { - "version": "14.18.56", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.56.tgz", - "integrity": "sha512-+k+57NVS9opgrEn5l9c0gvD1r6C+PtyhVE4BTnMMRwiEA8ZO8uFcs6Yy2sXIy0eC95ZurBtRSvhZiHXBysbl6w==" + "version": "14.18.57", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.57.tgz", + "integrity": "sha512-AV54VmsNLLPoUm+EBroYLyXQUI8kfG+oFIocRw5DjA6R+8wg7VV1Vqoi9sdjbjk1Zmxhxp+ecLqLdybnfV6MGQ==" }, "node_modules/@types/node-fetch": { "version": "2.6.4", @@ -4099,9 +4099,9 @@ "license": "MIT" }, "node_modules/@types/uuid": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.2.tgz", - "integrity": "sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==" + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.3.tgz", + "integrity": "sha512-taHQQH/3ZyI3zP8M/puluDEIEvtQHVYcC6y3N8ijFtAd28+Ey/G4sg1u2gB01S8MwybLOKAp9/yCMu/uR5l3Ug==" }, "node_modules/@types/ws": { "version": "8.5.4", @@ -4488,9 +4488,9 @@ } }, "node_modules/@whatwg-node/fetch/node_modules/@types/node": { - "version": "18.17.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.12.tgz", - "integrity": "sha512-d6xjC9fJ/nSnfDeU0AMDsaJyb1iHsqCSOdi84w4u+SlN/UgQdY5tRhpMzaFYsI4mnpvgTivEaQd0yOUhAtOnEQ==", + "version": "18.17.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.13.tgz", + "integrity": "sha512-SlLPDDe6YQl1JnQQy4hgsuJeo5q5c1TBU4be4jeBLXsqpjoDbfb0HesSfhMwnaxfSJ4txtfzJzW5/x/43fkkfQ==", "dev": true, "peer": true }, @@ -8457,9 +8457,9 @@ } }, "node_modules/graphql-http": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/graphql-http/-/graphql-http-1.21.0.tgz", - "integrity": "sha512-yrItPfHj5WeT4n7iusbVin+vGSQjXFAX6U/GnYytdCJRXVad1TWGtYFDZ2ROjCKpXQzIwvfbiWCEwfuXgR3B6A==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/graphql-http/-/graphql-http-1.22.0.tgz", + "integrity": "sha512-9RBUlGJWBFqz9LwfpmAbjJL/8j/HCNkZwPBU5+Bfmwez+1Ay43DocMNQYpIWsWqH0Ftv6PTNAh2aRnnMCBJgLw==", "dev": true, "engines": { "node": ">=12" @@ -14304,9 +14304,9 @@ } }, "node_modules/prettier": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.2.tgz", - "integrity": "sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" diff --git a/package.json b/package.json index 600c839d1..b06fdb202 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ ], "devDependencies": { "@apollo/cache-control-types": "1.0.3", - "@apollo/client": "3.8.1", + "@apollo/client": "3.8.2", "@apollo/server": "4.9.3", "@apollo/utils.fetcher": "2.0.1", "@changesets/changelog-github": "0.4.8", @@ -62,14 +62,14 @@ "@types/loglevel": "1.5.4", "@types/make-fetch-happen": "10.0.1", "@types/nock": "10.0.3", - "@types/node": "14.18.56", + "@types/node": "14.18.57", "@types/node-fetch": "2.6.4", - "@types/uuid": "9.0.2", + "@types/uuid": "9.0.3", "@typescript-eslint/eslint-plugin": "5.62.0", "bunyan": "1.8.15", "cspell": "6.31.3", "graphql": "16.8.0", - "graphql-http": "1.21.0", + "graphql-http": "1.22.0", "graphql-tag": "2.12.6", "jest": "29.6.4", "jest-config": "29.6.4", @@ -79,7 +79,7 @@ "mocked-env": "1.3.5", "nock": "13.3.3", "node-fetch": "2.7.0", - "prettier": "3.0.2", + "prettier": "3.0.3", "prettier-2": "npm:prettier@2.8.8", "semver": "7.5.4", "strip-indent": "3.0.0", From dece93bc23022af9eda5b5ade181399b262f4948 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 3 Sep 2023 02:21:42 +0000 Subject: [PATCH 56/95] chore(deps): update dependency @types/node to v14.18.58 (#2768) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0bd1c3143..8e908e405 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "@types/loglevel": "1.5.4", "@types/make-fetch-happen": "10.0.1", "@types/nock": "10.0.3", - "@types/node": "14.18.57", + "@types/node": "14.18.58", "@types/node-fetch": "2.6.4", "@types/uuid": "9.0.3", "@typescript-eslint/eslint-plugin": "5.62.0", @@ -4022,9 +4022,9 @@ } }, "node_modules/@types/node": { - "version": "14.18.57", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.57.tgz", - "integrity": "sha512-AV54VmsNLLPoUm+EBroYLyXQUI8kfG+oFIocRw5DjA6R+8wg7VV1Vqoi9sdjbjk1Zmxhxp+ecLqLdybnfV6MGQ==" + "version": "14.18.58", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.58.tgz", + "integrity": "sha512-Y8ETZc8afYf6lQ/mVp096phIVsgD/GmDxtm3YaPcc+71jmi/J6zdwbwaUU4JvS56mq6aSfbpkcKhQ5WugrWFPw==" }, "node_modules/@types/node-fetch": { "version": "2.6.4", @@ -4488,9 +4488,9 @@ } }, "node_modules/@whatwg-node/fetch/node_modules/@types/node": { - "version": "18.17.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.13.tgz", - "integrity": "sha512-SlLPDDe6YQl1JnQQy4hgsuJeo5q5c1TBU4be4jeBLXsqpjoDbfb0HesSfhMwnaxfSJ4txtfzJzW5/x/43fkkfQ==", + "version": "18.17.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.14.tgz", + "integrity": "sha512-ZE/5aB73CyGqgQULkLG87N9GnyGe5TcQjv34pwS8tfBs1IkCh0ASM69mydb2znqd6v0eX+9Ytvk6oQRqu8T1Vw==", "dev": true, "peer": true }, diff --git a/package.json b/package.json index b06fdb202..a086da16b 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@types/loglevel": "1.5.4", "@types/make-fetch-happen": "10.0.1", "@types/nock": "10.0.3", - "@types/node": "14.18.57", + "@types/node": "14.18.58", "@types/node-fetch": "2.6.4", "@types/uuid": "9.0.3", "@typescript-eslint/eslint-plugin": "5.62.0", From 21631aac5128b704c6b05b90a1ee0443d41ec377 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 5 Sep 2023 13:49:08 -0600 Subject: [PATCH 57/95] Add note about query planner key usage --- docs/source/entities-advanced.mdx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/source/entities-advanced.mdx b/docs/source/entities-advanced.mdx index a9d24b623..73463913b 100644 --- a/docs/source/entities-advanced.mdx +++ b/docs/source/entities-advanced.mdx @@ -25,6 +25,20 @@ type Product @key(fields: "id") @key(fields: "sku") { This pattern is helpful when different subgraphs interact with different fields of an entity. For example, a Reviews subgraph might refer to products by their ID, whereas an Inventory subgraph might use SKUs. +
+ +**Note:** If you include multiple `@key` fields sets, the query planner always uses the most efficient set for entity resolution. For example, if you identify a type by `@key(fields: "id")` _or_ `@key(fields: "id sku")`: + +```graphql +type Product @key(fields: "id") @key(fields: "id sku") { + # ... +} +``` + +It means that either `id` or (`id` _and_ `sku`) are enough to uniquely identify the entity. And since only `id` is enough, the query planner always uses only that field to resolve the entity. + +
+ A subgraph that [references an entity without contributing any fields](./entities/#referencing-an-entity-without-contributing-fields) can include the fields of any `@key` in its stub definition: ```graphql title="Reviews subgraph" @@ -81,7 +95,6 @@ type Product @key(fields: "upc") {
- To merge entities between subgraphs, the entity must have at least one shared field between subgraphs. For example, operations can't merge the `Product` entity defined in the following subgraphs because they don't share any fields specified in the `@key` selection set:

From 687bc42e509e3c75affe7cd3243de4bb1c407002 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 5 Sep 2023 14:36:37 -0600 Subject: [PATCH 58/95] Restructure --- docs/source/entities-advanced.mdx | 62 ++++++++++++++++--------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/docs/source/entities-advanced.mdx b/docs/source/entities-advanced.mdx index 73463913b..cf5d73852 100644 --- a/docs/source/entities-advanced.mdx +++ b/docs/source/entities-advanced.mdx @@ -6,13 +6,39 @@ This article describes complex behaviors of federated entities beyond those cove ## Advanced `@key`s -A single entity can have multiple `@key`s. Additionally, a `@key` can include multiple fields, and even arbitrarily nested fields. +A single `@key` can require multiple fields to uniquely identify an entity—this is called a compound or composite key. Additionally, an entity can have multiple `@key`s if multiple fields or combinations of fields can independently serve as unique identifiers. + +### Compound `@key`s + +A single `@key` can consist of multiple fields, the combination of which is required to uniquely identify an entity. In the following example, the combination of both `username` and `domain` is required to uniquely identify a `User` entity: + +```graphql {1} title="Users subgraph" +type User @key(fields: "username domain") { + username: String! + domain: String! +} +``` + +#### Nested fields in compound `@key`s + +Compound keys can also include _nested_ fields. In the following example, the `User` entity's primary key consists of both a user's `id` _and_ the `id` of that user's associated `Organization`: + +```graphql {1} title="Users subgraph" +type User @key(fields: "id organization { id }") { + id: ID! + organization: Organization! +} + +type Organization { + id: ID! +} +``` ### Multiple `@key`s -You can define more than one `@key` for an entity, when applicable. +When different subgraphs interact with different fields of an entity, you may need to define multiple `@key`s for an entity. For example, a Products subgraph might refer to products by their ID, whereas an Inventory subgraph might use SKUs. -In this example, a `Product` entity can be uniquely identified by either its `id` _or_ its `sku`: +In the following example, the `Product` entity can be uniquely identified by _either_ its `id` _or_ its `sku`: ```graphql {1} title="Products subgraph" type Product @key(fields: "id") @key(fields: "sku") { @@ -22,9 +48,6 @@ type Product @key(fields: "id") @key(fields: "sku") { price: Int } ``` - -This pattern is helpful when different subgraphs interact with different fields of an entity. For example, a Reviews subgraph might refer to products by their ID, whereas an Inventory subgraph might use SKUs. -
**Note:** If you include multiple `@key` fields sets, the query planner always uses the most efficient set for entity resolution. For example, if you identify a type by `@key(fields: "id")` _or_ `@key(fields: "id sku")`: @@ -35,11 +58,13 @@ type Product @key(fields: "id") @key(fields: "id sku") { } ``` -It means that either `id` or (`id` _and_ `sku`) are enough to uniquely identify the entity. And since only `id` is enough, the query planner always uses only that field to resolve the entity. +It means that either `id` or (`id` _and_ `sku`) are enough to uniquely identify the entity. Since only `id` is enough, the query planner always uses only that field to resolve the entity.
-A subgraph that [references an entity without contributing any fields](./entities/#referencing-an-entity-without-contributing-fields) can include the fields of any `@key` in its stub definition: +#### Referencing entities with multiple keys + +A subgraph that [references an entity without contributing any fields](./entities/#referencing-an-entity-without-contributing-fields) can include the fields of _any_ `@key` fields in its stub definition: ```graphql title="Reviews subgraph" # Either: @@ -54,29 +79,10 @@ type Product @key(fields: "sku", resolvable: false) { ``` -### Compound `@key`s - -A single `@key` can consist of multiple fields, and even _nested_ fields. - -In this example, the `User` entity's primary key consists of both a user's `id` _and_ the `id` of that user's associated `Organization`: - -```graphql {1} title="Users subgraph" -type User @key(fields: "id organization { id }") { - id: ID! - organization: Organization! -} - -type Organization { - id: ID! -} -``` - ### Differing `@key`s across subgraphs An entity often has the exact same `@key` field(s) across subgraphs, but this isn't required. For example, you can define a `Product` entity shared between subgraphs, one with `sku` and `upc` as the `@key`s and one with only `upc` as the `@key` field: - - ```graphql title="Products subgraph" type Product @key(fields: "sku") @key(fields: "upc") { sku: ID! @@ -93,8 +99,6 @@ type Product @key(fields: "upc") { } ``` - - To merge entities between subgraphs, the entity must have at least one shared field between subgraphs. For example, operations can't merge the `Product` entity defined in the following subgraphs because they don't share any fields specified in the `@key` selection set:

From e62476e770478ba4ab21e038ea207f42e855c084 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 5 Sep 2023 14:53:06 -0600 Subject: [PATCH 59/95] Copyedit --- docs/source/entities-advanced.mdx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/docs/source/entities-advanced.mdx b/docs/source/entities-advanced.mdx index cf5d73852..974a56b42 100644 --- a/docs/source/entities-advanced.mdx +++ b/docs/source/entities-advanced.mdx @@ -36,7 +36,7 @@ type Organization { ### Multiple `@key`s -When different subgraphs interact with different fields of an entity, you may need to define multiple `@key`s for an entity. For example, a Products subgraph might refer to products by their ID, whereas an Inventory subgraph might use SKUs. +When different subgraphs interact with different fields of an entity, you may need to define multiple `@key`s for an entity. For example, a Reviews subgraph might refer to products by their ID, whereas an Inventory subgraph might use SKUs. In the following example, the `Product` entity can be uniquely identified by _either_ its `id` _or_ its `sku`: @@ -79,7 +79,7 @@ type Product @key(fields: "sku", resolvable: false) { ``` -### Differing `@key`s across subgraphs +### Mismatched `@key`s across subgraphs An entity often has the exact same `@key` field(s) across subgraphs, but this isn't required. For example, you can define a `Product` entity shared between subgraphs, one with `sku` and `upc` as the `@key`s and one with only `upc` as the `@key` field: @@ -153,9 +153,9 @@ type Product @key(fields: "upc") { ``` -The queries defined in the products subgraph can always resolve all product fields because the product entity can be joined via the `upc` field present in both schemas. +The queries defined in the Products subgraph can always resolve all product fields because the product entity can be joined via the `upc` field present in both schemas. -On the other hand, queries added to the inventory subgraph can't resolve fields from the products subgraph: +On the other hand, queries added to the Inventory subgraph can't resolve fields from the Products subgraph: @@ -181,11 +181,9 @@ type Query { -The `productsInStock` query can't resolve fields from the product subgraph since its `Product` type definition doesn't have `@key(fields: "upc")`, and the `sku` field isn't present in the inventory subgraph. +The `productsInStock` query can't resolve fields from the Products subgraph since its `Product` type definition doesn't have `@key(fields: "upc")`, and the `sku` field isn't present in the Inventory subgraph. -If the product subgraph includes `@key(fields: "upc")`, all queries from the inventory subgraph can resolve all product fields: - - +If the Products subgraph includes `@key(fields: "upc")`, all queries from the Inventory subgraph can resolve all product fields: ```graphql title="Products subgraph" type Product @key(fields: "sku") @key(fields: "upc") { @@ -207,8 +205,6 @@ type Query { } ``` - - ## Migrating entities and fields As your supergraph grows, you might want to move parts of an entity to a different subgraph. This section describes how to perform these migrations safely. From 8910f9dbb0a91066c4c20afadff332664711c17c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 21:50:33 +0000 Subject: [PATCH 60/95] chore(deps): update dependency @apollo/client to v3.8.3 (#2772) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8e908e405..0bc3e3dfd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ ], "devDependencies": { "@apollo/cache-control-types": "1.0.3", - "@apollo/client": "3.8.2", + "@apollo/client": "3.8.3", "@apollo/server": "4.9.3", "@apollo/utils.fetcher": "2.0.1", "@changesets/changelog-github": "0.4.8", @@ -197,9 +197,9 @@ } }, "node_modules/@apollo/client": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.8.2.tgz", - "integrity": "sha512-SSxRTHlHdlR65mvV5j5e3JkYs9z/eFQfJPgSfqTeKa3jgHKofBaMb+UWxJPInqV5MqBFAkPFt8fYEBZwM7oGZA==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.8.3.tgz", + "integrity": "sha512-mK86JM6hCpMEBGDgdO9U8ZYS8r9lPjXE1tVGpJMdSFUsIcXpmEfHUAbbFpPtYmxn8Qa7XsYy0dwDaDhpf4UUPw==", "dev": true, "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", diff --git a/package.json b/package.json index a086da16b..b7a486621 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ ], "devDependencies": { "@apollo/cache-control-types": "1.0.3", - "@apollo/client": "3.8.2", + "@apollo/client": "3.8.3", "@apollo/server": "4.9.3", "@apollo/utils.fetcher": "2.0.1", "@changesets/changelog-github": "0.4.8", From 7034745482f5d4e3b970b6a44bed54367ff6ccfa Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Wed, 6 Sep 2023 09:18:15 -0600 Subject: [PATCH 61/95] Copyedit --- docs/source/entities-advanced.mdx | 35 ++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/docs/source/entities-advanced.mdx b/docs/source/entities-advanced.mdx index 974a56b42..43b31bb58 100644 --- a/docs/source/entities-advanced.mdx +++ b/docs/source/entities-advanced.mdx @@ -6,11 +6,11 @@ This article describes complex behaviors of federated entities beyond those cove ## Advanced `@key`s -A single `@key` can require multiple fields to uniquely identify an entity—this is called a compound or composite key. Additionally, an entity can have multiple `@key`s if multiple fields or combinations of fields can independently serve as unique identifiers. +A single `@key` can require a combination of fields to uniquely identify an entity. Additionally, an entity can have multiple separate `@key`s if multiple fields or combinations of fields can independently serve as unique identifiers. ### Compound `@key`s -A single `@key` can consist of multiple fields, the combination of which is required to uniquely identify an entity. In the following example, the combination of both `username` and `domain` is required to uniquely identify a `User` entity: +A single `@key` can consist of multiple fields, the combination of which is required to uniquely identify an entity. This is called a **compound** or composite key. In the following example, the combination of both `username` and `domain` fields is required to uniquely identify the `User` entity: ```graphql {1} title="Users subgraph" type User @key(fields: "username domain") { @@ -36,7 +36,7 @@ type Organization { ### Multiple `@key`s -When different subgraphs interact with different fields of an entity, you may need to define multiple `@key`s for an entity. For example, a Reviews subgraph might refer to products by their ID, whereas an Inventory subgraph might use SKUs. +When different subgraphs interact with different fields of an entity, you may need to define multiple `@key`s for the entity. For example, a Reviews subgraph might refer to products by their ID, whereas an Inventory subgraph might use SKUs. In the following example, the `Product` entity can be uniquely identified by _either_ its `id` _or_ its `sku`: @@ -50,21 +50,32 @@ type Product @key(fields: "id") @key(fields: "sku") { ```
-**Note:** If you include multiple `@key` fields sets, the query planner always uses the most efficient set for entity resolution. For example, if you identify a type by `@key(fields: "id")` _or_ `@key(fields: "id sku")`: +**Note:** If you include multiple sets of `@key` fields, the query planner always uses the most efficient set for entity resolution. For example, suppose you allow a type to be identified by `@key(fields: "id")` _or_ `@key(fields: "id sku")`: -```graphql +```graphql {1} type Product @key(fields: "id") @key(fields: "id sku") { # ... } ``` -It means that either `id` or (`id` _and_ `sku`) are enough to uniquely identify the entity. Since only `id` is enough, the query planner always uses only that field to resolve the entity. +That means either `id` or (`id` _and_ `sku`) are enough to uniquely identify the entity. Since `id` alone is enough, the query planner always uses only that field to resolve the entity, and `@key(fields: "id sku")` is effectively ignored.
#### Referencing entities with multiple keys -A subgraph that [references an entity without contributing any fields](./entities/#referencing-an-entity-without-contributing-fields) can include the fields of _any_ `@key` fields in its stub definition: +A subgraph that [references an entity without contributing any fields](./entities/#referencing-an-entity-without-contributing-fields) can use any `@key` fields in its stub definition. For example, if the Products subgraph defines the `Product` entity like this: + +```graphql {1} title="Products subgraph" +type Product @key(fields: "id") @key(fields: "sku") { + id: ID! + sku: String! + name: String! + price: Int +} +``` + +Then, a Reviews subgraph can use either `id` or `sku` in the stub definition: ```graphql title="Reviews subgraph" # Either: @@ -79,9 +90,9 @@ type Product @key(fields: "sku", resolvable: false) { ``` -### Mismatched `@key`s across subgraphs +### Differing `@key`s across subgraphs -An entity often has the exact same `@key` field(s) across subgraphs, but this isn't required. For example, you can define a `Product` entity shared between subgraphs, one with `sku` and `upc` as the `@key`s and one with only `upc` as the `@key` field: +An entity often uses the exact same `@key` field(s) across subgraphs, but this isn't required. For example, you can define a `Product` entity shared between subgraphs, one with `sku` and `upc` as the `@key`s and one with only `upc` as the `@key` field: ```graphql title="Products subgraph" type Product @key(fields: "sku") @key(fields: "upc") { @@ -122,9 +133,9 @@ type Product @key(fields: "upc") { -#### Operations with mismatched `@key`s +#### Operations with differing `@key`s -Mismatched keys affect which fields from an entity can be resolved. Requests can resolve an entity's fields _if there is a traversable path from the root query to the fields_. +Differing keys across subgraphs affect which of the entity's fields can be resolved from each subgraph. Requests can resolve fields **if there is a traversable path from the root query to the fields**. Take these subgraph schemas as an example: @@ -181,7 +192,7 @@ type Query { -The `productsInStock` query can't resolve fields from the Products subgraph since its `Product` type definition doesn't have `@key(fields: "upc")`, and the `sku` field isn't present in the Inventory subgraph. +The `productsInStock` query can't resolve fields from the Products subgraph since the Products subgraph's `Product` type definition doesn't include `upc` as a key field, and `sku` isn't present in the Inventory subgraph. If the Products subgraph includes `@key(fields: "upc")`, all queries from the Inventory subgraph can resolve all product fields: From 8dbefd44f3bb9004556a4c34d4c40cad68ec00dc Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Wed, 6 Sep 2023 13:56:36 -0600 Subject: [PATCH 62/95] Apply suggestions from code review Co-authored-by: Edward Huang <18322228+shorgi@users.noreply.github.com> --- docs/source/entities-advanced.mdx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/source/entities-advanced.mdx b/docs/source/entities-advanced.mdx index 43b31bb58..85280002f 100644 --- a/docs/source/entities-advanced.mdx +++ b/docs/source/entities-advanced.mdx @@ -4,13 +4,17 @@ title: Advanced topics on federated entities This article describes complex behaviors of federated entities beyond those covered in [entity basics](./entities/). -## Advanced `@key`s +## Customizing `@key`s -A single `@key` can require a combination of fields to uniquely identify an entity. Additionally, an entity can have multiple separate `@key`s if multiple fields or combinations of fields can independently serve as unique identifiers. +You can customize the `@key` of an entity to best identify the entity across subgraphs: + +- Create a [compound `@key`](#compound-keys) with multiple fields. +- Define [multiple `@key`s](#multiple-keys) for an entity. +- Assign [different `@key`s across subgraphs](#differing-key-across-subgraphs) for an entity. ### Compound `@key`s -A single `@key` can consist of multiple fields, the combination of which is required to uniquely identify an entity. This is called a **compound** or composite key. In the following example, the combination of both `username` and `domain` fields is required to uniquely identify the `User` entity: +A single `@key` can consist of multiple fields, the combination of which uniquely identifies an entity. This is called a **compound** or composite key. In the following example, the combination of both `username` and `domain` fields is required to uniquely identify the `User` entity: ```graphql {1} title="Users subgraph" type User @key(fields: "username domain") { @@ -92,7 +96,7 @@ type Product @key(fields: "sku", resolvable: false) { ### Differing `@key`s across subgraphs -An entity often uses the exact same `@key` field(s) across subgraphs, but this isn't required. For example, you can define a `Product` entity shared between subgraphs, one with `sku` and `upc` as the `@key`s and one with only `upc` as the `@key` field: +Although an entity commonly uses the exact same `@key` field(s) across subgraphs, you can alternatively use different `@key`s with different fields. For example, you can define a `Product` entity shared between subgraphs, one with `sku` and `upc` as its `@key`s, and the other with only `upc` as the `@key` field: ```graphql title="Products subgraph" type Product @key(fields: "sku") @key(fields: "upc") { From 09856b15c4f068c3d3604ae732ecc9ced755f77c Mon Sep 17 00:00:00 2001 From: Dariusz Kuc <9501705+dariuszkuc@users.noreply.github.com> Date: Wed, 6 Sep 2023 14:56:58 -0500 Subject: [PATCH 63/95] chore: update fed 2.5 changelog (#2773) --- docs/source/federation-versions.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/federation-versions.mdx b/docs/source/federation-versions.mdx index 6b1292521..4d08c6446 100644 --- a/docs/source/federation-versions.mdx +++ b/docs/source/federation-versions.mdx @@ -39,7 +39,7 @@ First release Available in GraphOS? -**No** +**Yes** @@ -47,7 +47,7 @@ Available in GraphOS? Minimum router version -**`TBD`** +**`1.29.1`** From df834d31809f996a8c27331545fe08e5c92cbfd1 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Wed, 6 Sep 2023 14:16:33 -0600 Subject: [PATCH 64/95] Add non-nullability recommendation --- docs/source/entities.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/entities.mdx b/docs/source/entities.mdx index 33dab2243..1e70d07ed 100644 --- a/docs/source/entities.mdx +++ b/docs/source/entities.mdx @@ -81,6 +81,8 @@ type Product @key(fields: "id") { > * Fields that return a union or interface > * Fields that take arguments +Though not strictly required, it's best to use non-nullable fields for keys. If you use fields that return `null` values, GraphOS may encounter issues resolving the entity. + For more information on advanced key options, like how to define [multiple keys](./entities-advanced/#multiple-keys) or [compound keys](/entities-advanced/#compound-keys), see [Advanced topics for federation entities](./entities-advanced). ### 2. Define a reference resolver From 57d9c64dee91c2f808a5e4602243ec18389d4f1d Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Thu, 7 Sep 2023 16:13:30 -0600 Subject: [PATCH 65/95] Update links --- docs/source/entities-advanced.mdx | 2 +- docs/source/federated-types/federated-directives.mdx | 2 +- docs/source/federation-2/new-in-federation-2.mdx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/entities-advanced.mdx b/docs/source/entities-advanced.mdx index 85280002f..968fd433a 100644 --- a/docs/source/entities-advanced.mdx +++ b/docs/source/entities-advanced.mdx @@ -655,4 +655,4 @@ const resolvers = { A basic implementation of the `fetchProductByID` function might make a database call each time it's called. If we need to resolve `Product.price` for `N` different products, this results in `N` database calls. These calls are made _in addition to_ the call made by the Reviews subgraph to fetch the initial list of reviews (and the `id` of each product). This is where the "N+1" problem gets its name. If not prevented, this problem can cause performance problems or even enable denial-of-service attacks. -This problem is not limited to reference resolvers! In fact, it can occur with any resolver that fetches from a data store. To handle this problem, we strongly recommend using [the dataloader pattern](https://github.com/graphql/dataloader). Nearly every GraphQL server library provides a dataloader implementation, and you should use it in **every resolver**. This is true even for resolvers that _aren't_ for entities and that _don't_ return a list. These resolvers can _still_ cause N+1 issues via [batched requests](/tutorials/voyage-part2/02-monolith-graph-setup). +This problem is not limited to reference resolvers! In fact, it can occur with any resolver that fetches from a data store. To handle this problem, we strongly recommend using [the dataloader pattern](https://github.com/graphql/dataloader). Nearly every GraphQL server library provides a dataloader implementation, and you should use it in **every resolver**. This is true even for resolvers that _aren't_ for entities and that _don't_ return a list. These resolvers can _still_ cause N+1 issues via [batched requests](/technotes/TN0021-graph-security/#batched-requests). diff --git a/docs/source/federated-types/federated-directives.mdx b/docs/source/federated-types/federated-directives.mdx index 14457bdf8..230f6ae3d 100644 --- a/docs/source/federated-types/federated-directives.mdx +++ b/docs/source/federated-types/federated-directives.mdx @@ -142,7 +142,7 @@ Examples: * `"username region"` * `"name organization { id }"` -See also [Advanced `@key`s](../entities-advanced/#advanced-keys). +See also [Advanced `@key`s](../entities-advanced/#customizing-keys). diff --git a/docs/source/federation-2/new-in-federation-2.mdx b/docs/source/federation-2/new-in-federation-2.mdx index 7a0cc4158..7dc89c8c3 100644 --- a/docs/source/federation-2/new-in-federation-2.mdx +++ b/docs/source/federation-2/new-in-federation-2.mdx @@ -236,7 +236,7 @@ type Bill @key(fields: "id") { -For details, see [Entity migration](../entities-advanced/#entity-migration). +For details, see [Entity migration](../entities-advanced/#migrating-entities-and-fields). ## Interfaces implementing interfaces From c590c5af16a80bc3bab66a677799a3cdbc9491ea Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Thu, 7 Sep 2023 16:41:27 -0600 Subject: [PATCH 66/95] Rewrite intro --- docs/source/entities-advanced.mdx | 8 ++------ docs/source/federated-types/federated-directives.mdx | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/docs/source/entities-advanced.mdx b/docs/source/entities-advanced.mdx index 968fd433a..4f570710f 100644 --- a/docs/source/entities-advanced.mdx +++ b/docs/source/entities-advanced.mdx @@ -4,13 +4,9 @@ title: Advanced topics on federated entities This article describes complex behaviors of federated entities beyond those covered in [entity basics](./entities/). -## Customizing `@key`s +## Using advanced `@key`s -You can customize the `@key` of an entity to best identify the entity across subgraphs: - -- Create a [compound `@key`](#compound-keys) with multiple fields. -- Define [multiple `@key`s](#multiple-keys) for an entity. -- Assign [different `@key`s across subgraphs](#differing-key-across-subgraphs) for an entity. +Depending on your entities' fields and usage, you may need to use more advanced `@key`s. For example, you may need to define a [compound `@key`](#compound-keys) if multiple fields are required to uniquely identify an entity. If different subgraphs interact with different fields an entity, you may need to define [multiple](#multiple-keys)—and sometimes [differing](#differing-keys-across-subgraphs)—`@key`s for the entity. ### Compound `@key`s diff --git a/docs/source/federated-types/federated-directives.mdx b/docs/source/federated-types/federated-directives.mdx index 230f6ae3d..0ac36afe9 100644 --- a/docs/source/federated-types/federated-directives.mdx +++ b/docs/source/federated-types/federated-directives.mdx @@ -142,7 +142,7 @@ Examples: * `"username region"` * `"name organization { id }"` -See also [Advanced `@key`s](../entities-advanced/#customizing-keys). +See also [Advanced `@key`s](../entities-advanced/#using-advanced-keys). From 2caf312c0acacafa1ae45fd45901f67091de1f57 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 9 Sep 2023 09:28:18 +0000 Subject: [PATCH 67/95] chore(deps): update dependency @types/node to v14.18.59 (#2777) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0bc3e3dfd..d8d08562e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "@types/loglevel": "1.5.4", "@types/make-fetch-happen": "10.0.1", "@types/nock": "10.0.3", - "@types/node": "14.18.58", + "@types/node": "14.18.59", "@types/node-fetch": "2.6.4", "@types/uuid": "9.0.3", "@typescript-eslint/eslint-plugin": "5.62.0", @@ -4022,9 +4022,9 @@ } }, "node_modules/@types/node": { - "version": "14.18.58", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.58.tgz", - "integrity": "sha512-Y8ETZc8afYf6lQ/mVp096phIVsgD/GmDxtm3YaPcc+71jmi/J6zdwbwaUU4JvS56mq6aSfbpkcKhQ5WugrWFPw==" + "version": "14.18.59", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.59.tgz", + "integrity": "sha512-NWJMpBL2Xs3MY93yrD6YrrTKep8eIA6iMnfG4oIc6LrTRlBZgiSCGiY3V/Owlp6umIBLyKb4F8Q7hxWatjYH5A==" }, "node_modules/@types/node-fetch": { "version": "2.6.4", @@ -4488,9 +4488,9 @@ } }, "node_modules/@whatwg-node/fetch/node_modules/@types/node": { - "version": "18.17.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.14.tgz", - "integrity": "sha512-ZE/5aB73CyGqgQULkLG87N9GnyGe5TcQjv34pwS8tfBs1IkCh0ASM69mydb2znqd6v0eX+9Ytvk6oQRqu8T1Vw==", + "version": "18.17.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.15.tgz", + "integrity": "sha512-2yrWpBk32tvV/JAd3HNHWuZn/VDN1P+72hWirHnvsvTGSqbANi+kSeuQR9yAHnbvaBvHDsoTdXV0Fe+iRtHLKA==", "dev": true, "peer": true }, diff --git a/package.json b/package.json index b7a486621..e1c6d0c68 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@types/loglevel": "1.5.4", "@types/make-fetch-happen": "10.0.1", "@types/nock": "10.0.3", - "@types/node": "14.18.58", + "@types/node": "14.18.59", "@types/node-fetch": "2.6.4", "@types/uuid": "9.0.3", "@typescript-eslint/eslint-plugin": "5.62.0", From d931a1dc4bd0a9e6e1c218921a52c23555bbd5d7 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 11 Sep 2023 11:22:26 -0600 Subject: [PATCH 68/95] Copyedit efficiency note --- docs/source/entities-advanced.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/entities-advanced.mdx b/docs/source/entities-advanced.mdx index 4f570710f..2dbf14cfe 100644 --- a/docs/source/entities-advanced.mdx +++ b/docs/source/entities-advanced.mdx @@ -50,7 +50,7 @@ type Product @key(fields: "id") @key(fields: "sku") { ```
-**Note:** If you include multiple sets of `@key` fields, the query planner always uses the most efficient set for entity resolution. For example, suppose you allow a type to be identified by `@key(fields: "id")` _or_ `@key(fields: "id sku")`: +**Note:** If you include multiple sets of `@key` fields, the query planner uses the most efficient set for entity resolution. For example, suppose you allow a type to be identified by `@key(fields: "id")` _or_ `@key(fields: "id sku")`: ```graphql {1} type Product @key(fields: "id") @key(fields: "id sku") { @@ -58,7 +58,7 @@ type Product @key(fields: "id") @key(fields: "id sku") { } ``` -That means either `id` or (`id` _and_ `sku`) are enough to uniquely identify the entity. Since `id` alone is enough, the query planner always uses only that field to resolve the entity, and `@key(fields: "id sku")` is effectively ignored. +That means either `id` or (`id` _and_ `sku`) is enough to uniquely identify the entity. Since `id` alone is enough, the query planner will use only that field to resolve the entity, and `@key(fields: "id sku")` is effectively ignored.
From a26975a2d36721c860e6f2d67839e5209fb7df89 Mon Sep 17 00:00:00 2001 From: Sylvain Lebresne Date: Thu, 14 Sep 2023 10:32:47 +0200 Subject: [PATCH 69/95] Adds code to output mermaid graph for query graphs (#2779) --- .changeset/chilled-geckos-change.md | 6 ++ query-graphs-js/src/index.ts | 1 + query-graphs-js/src/mermaid.ts | 116 ++++++++++++++++++++++++++++ query-graphs-js/src/querygraph.ts | 32 +++++--- 4 files changed, 145 insertions(+), 10 deletions(-) create mode 100644 .changeset/chilled-geckos-change.md create mode 100644 query-graphs-js/src/mermaid.ts diff --git a/.changeset/chilled-geckos-change.md b/.changeset/chilled-geckos-change.md new file mode 100644 index 000000000..f0184f387 --- /dev/null +++ b/.changeset/chilled-geckos-change.md @@ -0,0 +1,6 @@ +--- + +--- + + + \ No newline at end of file diff --git a/query-graphs-js/src/index.ts b/query-graphs-js/src/index.ts index 3907233a7..e4ee41a36 100644 --- a/query-graphs-js/src/index.ts +++ b/query-graphs-js/src/index.ts @@ -6,3 +6,4 @@ export * from './transition'; export * from './pathContext'; export * from './conditionsCaching'; export * from './conditionsValidation'; +export * from './mermaid'; diff --git a/query-graphs-js/src/mermaid.ts b/query-graphs-js/src/mermaid.ts new file mode 100644 index 000000000..6eb5aa62d --- /dev/null +++ b/query-graphs-js/src/mermaid.ts @@ -0,0 +1,116 @@ +/* Functions used to output query graphs as [mermaid graphs](https://mermaid.js.org/syntax/flowchart.html). */ + +import { ObjectType } from "@apollo/federation-internals"; +import { Edge, FEDERATED_GRAPH_ROOT_SOURCE, QueryGraph, Vertex, isFederatedGraphRootType, simpleTraversal } from "./querygraph"; + +export type MermaidOptions = { + includeRootTypeLinks?: boolean, +} + +export class MermaidGraph { + private readonly before: string[] = []; + private readonly after: string[] = []; + private readonly subgraphs = new Map(); + + private isBuilt = false; + + constructor( + private readonly graph: QueryGraph, + private readonly options: MermaidOptions = {}, + ) { + for (const name of graph.sources.keys()) { + if (name === this.graph.name || name === FEDERATED_GRAPH_ROOT_SOURCE) { + continue; + } + this.subgraphs.set(name, []); + } + } + + private subgraphName(vertex: Vertex): string | undefined { + if (vertex.source === this.graph.name || vertex.source === FEDERATED_GRAPH_ROOT_SOURCE) { + return undefined; + } + return vertex.source; + } + + private vertexName(vertex: Vertex): string { + if (isFederatedGraphRootType(vertex.type)) { + return `root-${vertex.type.name.slice(1, vertex.type.name.length-1)}`; + } + const sg = this.subgraphName(vertex); + const n = sg ? `${vertex.type.name}-${sg}` : `${vertex.type.name}`; + return vertex.provideId ? `${n}-${vertex.provideId}` : n; + } + + addVertex(vertex: Vertex): void { + const sg = this.subgraphName(vertex); + const addTo = sg ? this.subgraphs.get(sg)! : this.before; + if (isFederatedGraphRootType(vertex.type)) { + addTo.push(`${this.vertexName(vertex)}(["root(${vertex.type.name.slice(1, vertex.type.name.length)})"])`); + } else { + addTo.push(`${this.vertexName(vertex)}["${vertex.toString()}"]`); + } + } + + addEdge(edge: Edge): boolean { + switch (edge.transition.kind) { + case 'FieldCollection': + if (edge.transition.definition.name.startsWith('_')) { + return false; + } + break; + case 'RootTypeResolution': + if (!(this.options.includeRootTypeLinks ?? true)) { + return false; + } + break; + case 'SubgraphEnteringTransition': + const rt = edge.tail.type as ObjectType; + if (rt.fields().filter((f) => !f.name.startsWith('_')).length === 0) { + return false; + } + break; + } + + const head = this.vertexName(edge.head); + const tail = this.vertexName(edge.tail); + const addTo = edge.head.source !== this.graph.name && edge.head.source === edge.tail.source + ? this.subgraphs.get(edge.head.source)! + : this.after; + const label = edge.label(); + if (label.length === 0) { + addTo.push(`${head} --> ${tail}`); + } else { + addTo.push(`${head} -->|"${label}"| ${tail}`); + } + return true; + } + + build(): void { + if (this.isBuilt) { + return; + } + + simpleTraversal( + this.graph, + (v) => this.addVertex(v), + (e) => this.addEdge(e), + ); + + this.isBuilt = true; + } + + toString(): string { + this.build(); + + const final = [ 'flowchart TD' ]; + this.before.forEach((b) => final.push(' ' + b)); + for (const [name, data] of this.subgraphs.entries()) { + final.push(` subgraph ${name}`); + data.forEach((d) => final.push(' ' + d)); + final.push(' end'); + } + this.after.forEach((a) => final.push(' ' + a)); + return final.join('\n'); + } +} diff --git a/query-graphs-js/src/querygraph.ts b/query-graphs-js/src/querygraph.ts index 7fc88525c..2a890c792 100644 --- a/query-graphs-js/src/querygraph.ts +++ b/query-graphs-js/src/querygraph.ts @@ -61,6 +61,13 @@ export function isFederatedGraphRootType(type: NamedType) { */ export class Vertex { hasReachableCrossSubgraphEdges: boolean = false; + // @provides works by creating duplicates of the vertex/type involved in the provides and adding the provided + // edges only to those copy. This means that with @provides, you can have more than one vertex per-type-and-subgraph + // in a query graph. Which is fined, but this `provideId` allows to distinguish if a vertex was created as part of + // this @provides duplication or not. The value of this field has no other meaning than to be unique per-@provide, + // and so all the vertex copied for a given @provides application will have the same `provideId`. Overall, this + // mostly exists for debugging visualization. + provideId: number | undefined; constructor( /** Index used for this vertex in the query graph it is part of. */ @@ -75,7 +82,8 @@ export class Vertex { ) {} toString(): string { - return `${this.type}(${this.source})`; + const label = `${this.type}(${this.source})`; + return this.provideId ? `${label}-${this.provideId}` : label; } } @@ -741,6 +749,7 @@ function federateSubgraphs(supergraph: Schema, subgraphs: QueryGraph[]): QueryGr ); } // Now we handle @provides + let provideId = 0; for (const [i, subgraph] of subgraphs.entries()) { const subgraphSchema = schemas[i]; const subgraphMetadata = federationMetadata(subgraphSchema); @@ -756,6 +765,7 @@ function federateSubgraphs(supergraph: Schema, subgraphs: QueryGraph[]): QueryGr const field = e.transition.definition; assert(isCompositeType(type), () => `Non composite type "${type}" should not have field collection edge ${e}`); for (const providesApplication of field.appliedDirectivesOf(providesDirective)) { + ++provideId; const fieldType = baseType(field.type!); assert(isCompositeType(fieldType), () => `Invalid @provide on field "${field}" whose type "${fieldType}" is not a composite type`) const provided = parseFieldSetArgument({ parentType: fieldType, directive: providesApplication }); @@ -766,9 +776,9 @@ function federateSubgraphs(supergraph: Schema, subgraphs: QueryGraph[]): QueryGr const copiedEdge = builder.edge(head, e.index); // We make a copy of the `fieldType` vertex (with all the same edges), and we change this particular edge to point to the // new copy. From that, we can add all the provides edges to the copy. - const copiedTail = builder.makeCopy(tail); + const copiedTail = builder.makeCopy(tail, provideId); builder.updateEdgeTail(copiedEdge, copiedTail); - addProvidesEdges(subgraphSchema, builder, copiedTail, provided); + addProvidesEdges(subgraphSchema, builder, copiedTail, provided, provideId); } } return true; // Always traverse edges @@ -832,7 +842,7 @@ function federateSubgraphs(supergraph: Schema, subgraphs: QueryGraph[]): QueryGr return builder.build(FEDERATED_GRAPH_ROOT_SOURCE); } -function addProvidesEdges(schema: Schema, builder: GraphBuilder, from: Vertex, provided: SelectionSet) { +function addProvidesEdges(schema: Schema, builder: GraphBuilder, from: Vertex, provided: SelectionSet, provideId: number) { const stack: [Vertex, SelectionSet][] = [[from, provided]]; const source = from.source; while (stack.length > 0) { @@ -847,7 +857,7 @@ function addProvidesEdges(schema: Schema, builder: GraphBuilder, from: Vertex, p // If this is a leaf field, then we don't really have anything to do. Otherwise, we need to copy // the tail and continue propagating the provides from there. if (selection.selectionSet) { - const copiedTail = builder.makeCopy(existingEdge.tail); + const copiedTail = builder.makeCopy(existingEdge.tail, provideId); builder.updateEdgeTail(existingEdge, copiedTail); stack.push([copiedTail, selection.selectionSet]); } @@ -860,7 +870,7 @@ function addProvidesEdges(schema: Schema, builder: GraphBuilder, from: Vertex, p // If the field is a leaf, then just create the new edge and we're done. Othewise, we // should copy the vertex (unless we just created it), add the edge and continue. if (selection.selectionSet) { - const copiedTail = existingTail ? builder.makeCopy(existingTail) : newTail; + const copiedTail = existingTail ? builder.makeCopy(existingTail, provideId) : newTail; builder.addEdge(v, copiedTail, new FieldCollection(fieldDef, true)); stack.push([copiedTail, selection.selectionSet]); } else { @@ -875,7 +885,7 @@ function addProvidesEdges(schema: Schema, builder: GraphBuilder, from: Vertex, p // @provides shouldn't have validated in the first place (another way to put it is, contrary to fields, there is no way currently // to mark a full type as @external). assert(existingEdge, () => `Shouldn't have ${selection} with no corresponding edge on ${v} (edges are: [${builder.edges(v)}])`); - const copiedTail = builder.makeCopy(existingEdge.tail); + const copiedTail = builder.makeCopy(existingEdge.tail, provideId); builder.updateEdgeTail(existingEdge, copiedTail); stack.push([copiedTail, selection.selectionSet!]); } else { @@ -1030,10 +1040,13 @@ class GraphBuilder { * Creates a new vertex that is a full copy of the provided one, including having the same out-edge, but with no incoming edges. * * @param vertex - the vertex to copy. + * @param provideId - if the vertex is copied for the sake of a `@provides`, an id that identify that provide and will be set on + * the newly copied vertex. * @returns the newly created copy. */ - makeCopy(vertex: Vertex): Vertex { + makeCopy(vertex: Vertex, provideId?: number): Vertex { const newVertex = this.createNewVertex(vertex.type, vertex.source, this.sources.get(vertex.source)!); + newVertex.provideId = provideId; newVertex.hasReachableCrossSubgraphEdges = vertex.hasReachableCrossSubgraphEdges; for (const edge of this.outEdges[vertex.index]) { this.addEdge(newVertex, edge.tail, edge.transition, edge.conditions); @@ -1092,8 +1105,7 @@ class GraphBuilderFromSchema extends GraphBuilder { private readonly supergraph?: { apiSchema: Schema, isFed1: boolean }, ) { super(); - this.isFederatedSubgraph = isFederationSubgraphSchema(schema); - assert(!this.isFederatedSubgraph || supergraph, `Missing supergraph schema for building the federated subgraph graph`); + this.isFederatedSubgraph = !!supergraph && isFederationSubgraphSchema(schema); } private hasDirective(elt: FieldDefinition | NamedType, directiveFct: (metadata: FederationMetadata) => DirectiveDefinition): boolean { From 4fd562be3a59be95c45f54846fb5fb91c53b6f14 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Thu, 14 Sep 2023 15:22:25 -0700 Subject: [PATCH 70/95] Reinstate `codecov` (#2780) --- .circleci/config.yml | 3 ++ .cspell/cspell-dict.txt | 1 + codecov.yml | 8 +++ package-lock.json | 112 ++++++++++++++++++++++++++++++++++++++++ package.json | 5 +- 5 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 codecov.yml diff --git a/.circleci/config.yml b/.circleci/config.yml index 071b82f87..8f787a3d9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,6 +24,9 @@ jobs: - run: name: Run tests command: npm run test:ci + - run: + name: Upload coverage + command: npm run coverage:upload - store_test_results: path: junit.xml diff --git a/.cspell/cspell-dict.txt b/.cspell/cspell-dict.txt index fe37da297..396cfd18d 100644 --- a/.cspell/cspell-dict.txt +++ b/.cspell/cspell-dict.txt @@ -35,6 +35,7 @@ caes cand Childs childs +codecov compabitle compatbility Compatiblity diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..fa6fee59b --- /dev/null +++ b/codecov.yml @@ -0,0 +1,8 @@ +coverage: + status: + project: + default: + threshold: 1% + patch: + default: + target: 0% \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d8d08562e..76f6923ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "@types/uuid": "9.0.3", "@typescript-eslint/eslint-plugin": "5.62.0", "bunyan": "1.8.15", + "codecov": "3.8.3", "cspell": "6.31.3", "graphql": "16.8.0", "graphql-http": "1.22.0", @@ -4740,6 +4741,16 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/argv": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz", + "integrity": "sha512-dEamhpPEwRUBpLNHeuCm/v+g0anFByHahxodVO/BbAarHVBBg2MccCwf9K+o1Pof+2btdnkJelYVUWjW/VrATw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "engines": { + "node": ">=0.6.10" + } + }, "node_modules/arr-diff": { "version": "4.0.0", "dev": true, @@ -5807,6 +5818,26 @@ "node": ">= 0.12.0" } }, + "node_modules/codecov": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.8.3.tgz", + "integrity": "sha512-Y8Hw+V3HgR7V71xWH2vQ9lyS358CbGCldWlJFR0JirqoGtOoas3R3/OclRTvgUYFK29mmJICDPauVKmpqbwhOA==", + "deprecated": "https://about.codecov.io/blog/codecov-uploader-deprecation-plan/", + "dev": true, + "dependencies": { + "argv": "0.0.2", + "ignore-walk": "3.0.4", + "js-yaml": "3.14.1", + "teeny-request": "7.1.1", + "urlgrey": "1.0.0" + }, + "bin": { + "codecov": "bin/codecov" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.1", "dev": true, @@ -8804,6 +8835,15 @@ "node": ">= 4" } }, + "node_modules/ignore-walk": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz", + "integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==", + "dev": true, + "dependencies": { + "minimatch": "^3.0.4" + } + }, "node_modules/immutable": { "version": "3.7.6", "dev": true, @@ -16126,6 +16166,15 @@ "node": ">= 0.4" } }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "dev": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, "node_modules/stream-transform": { "version": "2.1.3", "dev": true, @@ -16304,6 +16353,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "dev": true + }, "node_modules/supports-color": { "version": "7.2.0", "license": "MIT", @@ -16397,6 +16452,54 @@ "version": "4.0.0", "license": "ISC" }, + "node_modules/teeny-request": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.1.1.tgz", + "integrity": "sha512-iwY6rkW5DDGq8hE2YgNQlKbptYpY5Nn2xecjQiNjOXWbKzPGUfmeUBCSQbbr306d7Z7U2N0TPl+/SwYRfua1Dg==", + "dev": true, + "dependencies": { + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/teeny-request/node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/term-size": { "version": "2.2.1", "dev": true, @@ -17093,6 +17196,15 @@ "requires-port": "^1.0.0" } }, + "node_modules/urlgrey": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-1.0.0.tgz", + "integrity": "sha512-hJfIzMPJmI9IlLkby8QrsCykQ+SXDeO2W5Q9QTW3QpqZVTx4a/K7p8/5q+/isD8vsbVaFgql/gvAoQCRQ2Cb5w==", + "dev": true, + "dependencies": { + "fast-url-parser": "^1.1.3" + } + }, "node_modules/urlpattern-polyfill": { "version": "6.0.2", "dev": true, diff --git a/package.json b/package.json index e1c6d0c68..ab7fcf82e 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "test:clean": "jest --clearCache", "test:watch": "jest --verbose --watchAll", "testonly": "npm test", - "test:ci": "npm test -- --ci --maxWorkers=2 --reporters=default --reporters=jest-junit", + "test:ci": "npm run coverage -- --ci --maxWorkers=2 --reporters=default --reporters=jest-junit", + "coverage": "npm test -- --coverage", + "coverage:upload": "codecov", "codegen": "graphql-codegen --config codegen.yml", "codegen:check": "npm run codegen && git diff --exit-code", "lint": "eslint . --ext .ts", @@ -67,6 +69,7 @@ "@types/uuid": "9.0.3", "@typescript-eslint/eslint-plugin": "5.62.0", "bunyan": "1.8.15", + "codecov": "3.8.3", "cspell": "6.31.3", "graphql": "16.8.0", "graphql-http": "1.22.0", From 66d7e4ced9a6ccd170d5e228741332395a2dd553 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Thu, 14 Sep 2023 15:42:21 -0700 Subject: [PATCH 71/95] Don't treat entity interface `__typename` refinements as useless (#2775) Fix specific case for requesting `__typename` on interface entity type In certain cases, when resolving a `__typename` on an interface entity (due to it actual being requested in the operation), that fetch group could previously be trimmed / treated as useless. At a glance, it appears to be a redundant step, i.e.: ``` { ... on Product { __typename id }} => { ... on Product { __typename} } ``` It's actually necessary to preserve this in the case that we're coming from an interface object to an (entity) interface so that we can resolve the concrete `__typename` correctly. Ref: #2743 (this fixes one of the queries from the issue but not both) --- .changeset/bright-mails-travel.md | 14 ++ .../src/__tests__/executeQueryPlan.test.ts | 179 +++++++++++++++++- query-planner-js/src/buildPlan.ts | 30 +++ subgraph-js/src/types.ts | 47 +++-- 4 files changed, 253 insertions(+), 17 deletions(-) create mode 100644 .changeset/bright-mails-travel.md diff --git a/.changeset/bright-mails-travel.md b/.changeset/bright-mails-travel.md new file mode 100644 index 000000000..bcd21a3f4 --- /dev/null +++ b/.changeset/bright-mails-travel.md @@ -0,0 +1,14 @@ +--- +"@apollo/query-planner": patch +"@apollo/subgraph": patch +"@apollo/gateway": patch +--- + +Fix specific case for requesting __typename on interface entity type + +In certain cases, when resolving a __typename on an interface entity (due to it actual being requested in the operation), that fetch group could previously be trimmed / treated as useless. At a glance, it appears to be a redundant step, i.e.: +``` +{ ... on Product { __typename id }} => { ... on Product { __typename} } +``` +It's actually necessary to preserve this in the case that we're coming from an interface object to an (entity) interface so that we can resolve the concrete __typename correctly. + \ No newline at end of file diff --git a/gateway-js/src/__tests__/executeQueryPlan.test.ts b/gateway-js/src/__tests__/executeQueryPlan.test.ts index 40836faf6..b1c4c5f86 100644 --- a/gateway-js/src/__tests__/executeQueryPlan.test.ts +++ b/gateway-js/src/__tests__/executeQueryPlan.test.ts @@ -3636,7 +3636,6 @@ describe('executeQueryPlan', () => { resolvers: { ChildItem: { __resolveReference(ref: { id: string, name: string, parentItem: { name: string }}) { - console.log(ref); return { ...ref, message: `${ref.parentItem.name} | ${ref.name}`, @@ -5549,6 +5548,184 @@ describe('executeQueryPlan', () => { } `); }); + + test('handles resolving concrete `__typename`s of an @interfaceObject via the interface itself (issue #2743)', async () => { + const iList = [ + { + id: '1', + tField: 'field on a T' + }, + ]; + const S1 = { + name: 'S1', + typeDefs: gql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@interfaceObject"] + ) + + interface I @key(fields: "id") { + id: ID! + } + + type T implements I @key(fields: "id") { + id: ID! + tField: String + } + `, + resolvers: { + I: { + __resolveReference: ({ id }: { id: string }) => { + return iList.find((e) => e.id === id); + }, + __resolveType: () => { + return 'T'; + }, + }, + Query: { + allI: () => iList, + }, + }, + }; + + const S2 = { + name: 'S2', + typeDefs: gql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@interfaceObject"] + ) + + type I @key(fields: "id") @interfaceObject { + id: ID! + iField: String + } + `, + resolvers: { + I: { + __resolveReference: ({ id }: { id: string }) => + iList.find((e) => e.id === id), + iField: () => "field on I's", + }, + }, + }; + + const S3 = { + name: 'S3', + typeDefs: gql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@interfaceObject"] + ) + + type I @key(fields: "id") @interfaceObject { + id: ID! + } + + type Query { + searchIs: [I!]! + } + `, + resolvers: { + I: { + __resolveReference: ({ id }: { id: string }) => + iList.find((e) => e.id === id), + }, + Query: { + searchIs: () => iList, + }, + }, + }; + + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + S1, S2, S3, + ]); + const operation = parseOp( + `#graphql + { + searchIs { + __typename + id + iField + } + } + `, + schema, + ); + + const queryPlan = buildPlan(operation, queryPlanner); + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "S3") { + { + searchIs { + __typename + id + } + } + }, + Parallel { + Flatten(path: "searchIs.@") { + Fetch(service: "S1") { + { + ... on I { + __typename + id + } + } => + { + ... on I { + __typename + } + } + }, + }, + Flatten(path: "searchIs.@") { + Fetch(service: "S2") { + { + ... on I { + __typename + id + } + } => + { + ... on I { + iField + } + } + }, + }, + }, + }, + } + `); + + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); + + expect(response).toMatchInlineSnapshot(` + Object { + "data": Object { + "searchIs": Array [ + Object { + "__typename": "T", + "iField": "field on I's", + "id": "1", + }, + ], + }, + } + `); + }); }); describe('fields with conflicting types needing aliasing', () => { diff --git a/query-planner-js/src/buildPlan.ts b/query-planner-js/src/buildPlan.ts index fc2a247e3..c69e3bd37 100644 --- a/query-planner-js/src/buildPlan.ts +++ b/query-planner-js/src/buildPlan.ts @@ -1226,9 +1226,39 @@ class FetchGroup { return undefined; } + // This condition is specific to the case where we're resolving the _concrete_ + // `__typename` field of an interface when coming from an interfaceObject type. + // i.e. { ... on Product { __typename id }} => { ... on Product { __typename} } + // This is usually useless at a glance, but in this case we need to actually + // keep this since this is our only path to resolving the concrete `__typename`. + const isInterfaceTypeConditionOnInterfaceObject = ( + selection: Selection + ): boolean => { + if (selection.kind === "FragmentSelection") { + const parentType = selection.element.typeCondition; + if (parentType && isInterfaceType(parentType)) { + // Lastly, we just need to check that we're coming from a subgraph + // that has the type as an interface object in its schema. + return this.parents().some((p) => { + const typeInParent = this.dependencyGraph.subgraphSchemas + .get(p.group.subgraphName) + ?.type(parentType.name); + return typeInParent && isInterfaceObjectType(typeInParent); + }); + } + } + return false; + }; + const inputSelections = this.inputs.selectionSets().flatMap((s) => s.selections()); // Checks that every selection is contained in the input selections. const isUseless = this.selection.selections().every((selection) => { + // If we're coming from an interfaceObject _to_ an interface, we're + // "resolving" the concrete type of the interface and don't want to treat + // this as useless. + if (isInterfaceTypeConditionOnInterfaceObject(selection)) { + return false; + } const conditionInSupergraph = conditionInSupergraphIfInterfaceObject(selection); if (!conditionInSupergraph) { // We're not in the @interfaceObject case described above. We just check that an input selection contains the diff --git a/subgraph-js/src/types.ts b/subgraph-js/src/types.ts index f8964680a..13333e234 100644 --- a/subgraph-js/src/types.ts +++ b/subgraph-js/src/types.ts @@ -57,26 +57,41 @@ function isPromise(value: PromiseOrValue): value is Promise { return typeof (value as {then?: unknown})?.then === 'function'; } -function addTypeNameToPossibleReturn( - maybeObject: null | T, +async function maybeAddTypeNameToPossibleReturn( + maybeObject: PromiseOrValue, typename: string, -): null | (T & { __typename: string }) { - if (maybeObject !== null && typeof maybeObject === 'object') { - Object.defineProperty(maybeObject, '__typename', { +): Promise { + const objectOrNull = await maybeObject; + if ( + objectOrNull !== null + && typeof objectOrNull === 'object' + ) { + // If the object already has a __typename assigned, we're "refining" the + // type from an interface to an interfaceObject. + if ('__typename' in objectOrNull && objectOrNull.__typename !== typename) { + // XXX There's a really interesting nuance here in this condition. At a + // first glance, it looks like the code here and below could be simplified + // to just: + // ``` + // objectOrNull.__typename = typename; + // return objectOrNull; + // ``` + // But in this case, something internal to `graphql-js` depends on the + // identity of the object returned here. If we mutate in this case, we end + // up with errors from `graphql-js`. This might be worth investigating at + // some point, but for now creating a new object solves the problem and + // doesn't create any new ones. + return { + ...objectOrNull, + __typename: typename, + }; + } + + Object.defineProperty(objectOrNull, '__typename', { value: typename, }); } - return maybeObject as null | (T & { __typename: string }); -} - -function maybeAddTypeNameToPossibleReturn( - maybeObject: PromiseOrValue, - typename: string, -): PromiseOrValue { - if (isPromise(maybeObject)) { - return maybeObject.then((x: any) => addTypeNameToPossibleReturn(x, typename)); - } - return addTypeNameToPossibleReturn(maybeObject, typename); + return objectOrNull; } /** From 11d69981cbd22c7d5c90455d0d728549b705ba29 Mon Sep 17 00:00:00 2001 From: Dariusz Kuc <9501705+dariuszkuc@users.noreply.github.com> Date: Thu, 14 Sep 2023 18:33:11 -0500 Subject: [PATCH 72/95] fix: propagate type information when renaming entity fields (#2776) Fix reuse of existing __typename after entity fetch and ensure key renaming logic do nothing if the field to rename does not exists --------- Co-authored-by: Sylvain Lebresne --- .../src/__tests__/executeQueryPlan.test.ts | 155 ++++++++++++++++++ gateway-js/src/dataRewrites.ts | 7 +- gateway-js/src/executeQueryPlan.ts | 5 + 3 files changed, 165 insertions(+), 2 deletions(-) diff --git a/gateway-js/src/__tests__/executeQueryPlan.test.ts b/gateway-js/src/__tests__/executeQueryPlan.test.ts index b1c4c5f86..d724310de 100644 --- a/gateway-js/src/__tests__/executeQueryPlan.test.ts +++ b/gateway-js/src/__tests__/executeQueryPlan.test.ts @@ -6775,4 +6775,159 @@ describe('executeQueryPlan', () => { } `); }) + + it(`handles duplicate aliased fields on entities correctly`, async () => { + const s1Data = [ + { id: 't1', type: 'T1' }, + { id: 't2', type: 'T2' }, + ] + const s1 = { + name: 'S1', + typeDefs: gql` + type Query { + testQuery(id: String!): I + } + + interface I { + id: String! + } + + type T1 implements I @key(fields: "id", resolvable: false) { + id: String! + } + + type T2 implements I @key(fields: "id", resolvable: false) { + id: String! + } + `, + resolvers: { + Query: { + testQuery(_: any, args: any) { + return s1Data.find((obj) => obj.id === args.id); + }, + }, + I: { + __resolveType(obj: any) { + return obj.type + } + }, + }, + }; + + const t1 = { + id: 't1', + foo: { + field: "field1", + }, + }; + + const t2 = { + id: 't2', + bar: { + field: "field2", + }, + }; + const s2 = { + name: 'S2', + typeDefs: gql` + interface I { + id: String! + } + + type Test { + field: String! + } + + type T1 implements I @key(fields: "id") { + id: String! + foo: Test + } + + type T2 implements I @key(fields: "id") { + id: String! + bar: Test + } + `, + resolvers: { + Query: { + }, + Test: { + }, + T1: { + __resolveReference() { + return t1; + }, + }, + T2: { + __resolveReference() { + return t2; + }, + }, + }, + }; + + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); + + const operation = parseOp( + ` + query AliasedQuery($tId: String!) { + testQuery(id: $tId) { + ... on T1 { + foo { + field + } + } + ... on T2 { + foo: bar { + field + } + } + } + } + `, + schema, + ); + + const queryPlan = buildPlan(operation, queryPlanner); + const requestContext = buildRequestContext(); + requestContext.request.variables = { tId: "t1" }; + + let response = await executePlan( + queryPlan, + operation, + requestContext, + schema, + serviceMap, + ); + expect(response.data).toMatchObject( + { + "testQuery": { + "foo": { + "field": "field1" + } + } + } + ) + + requestContext.request.variables = { tId: "t2" }; + response = await executePlan( + queryPlan, + operation, + requestContext, + schema, + serviceMap, + ); + expect(response.data).toMatchObject( + { + "testQuery": { + "foo": { + "field": "field2" + } + } + } + ) + }); }); diff --git a/gateway-js/src/dataRewrites.ts b/gateway-js/src/dataRewrites.ts index 2e1dc08a1..fafc8f9c6 100644 --- a/gateway-js/src/dataRewrites.ts +++ b/gateway-js/src/dataRewrites.ts @@ -35,8 +35,11 @@ function rewriteAtPathFunction(rewrite: FetchDataRewrite, fieldAtPath: string): }; case 'KeyRenamer': return (obj) => { - obj[rewrite.renameKeyTo] = obj[fieldAtPath]; - obj[fieldAtPath] = undefined; + const objAtPath = obj[fieldAtPath]; + if (objAtPath) { + obj[rewrite.renameKeyTo] = obj[fieldAtPath]; + obj[fieldAtPath] = undefined; + } }; } } diff --git a/gateway-js/src/executeQueryPlan.ts b/gateway-js/src/executeQueryPlan.ts index 5440cdfed..17b745fb8 100644 --- a/gateway-js/src/executeQueryPlan.ts +++ b/gateway-js/src/executeQueryPlan.ts @@ -508,6 +508,11 @@ async function executeFetch( for (let i = 0; i < entities.length; i++) { const receivedEntity = receivedEntities[i]; + const existingEntity = entities[representationToEntity[i]]; + if (receivedEntity && !receivedEntity["__typename"]) { + const typename = existingEntity["__typename"]; + receivedEntity["__typename"] = typename; + } applyRewrites(context.supergraphSchema, fetch.outputRewrites, receivedEntity); deepMerge(entities[representationToEntity[i]], receivedEntity); } From a37bbbf6501032f16382ccb64d1dfa0dcddac789 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 15 Sep 2023 08:16:24 -0700 Subject: [PATCH 73/95] Correctly remove useless groups which "downgrade" `__typename` values (#2778) In certain cases, the query planner was preserving some fetches which were "useless" that would rewrite `__typename` from its already-resolved concrete type back to its interface type. This could result in (at least) requested fields being "filtered" from the final result due to the interface's `__typename` in the data where the concrete type's `__typename` was expected. Specifically, the solution was compute the path between newly created groups and their parents when we know that it's trivial (`[]`). Further along in the planning process, this allows to actually remove the known-useless group. Fixes #2743 (pt 2) Co-authored-by: Sylvain Lebresne --- .changeset/itchy-spiders-cry.md | 11 ++ .../src/__tests__/executeQueryPlan.test.ts | 186 +++++++++++++++++- query-planner-js/src/buildPlan.ts | 39 +++- 3 files changed, 220 insertions(+), 16 deletions(-) create mode 100644 .changeset/itchy-spiders-cry.md diff --git a/.changeset/itchy-spiders-cry.md b/.changeset/itchy-spiders-cry.md new file mode 100644 index 000000000..48581ade2 --- /dev/null +++ b/.changeset/itchy-spiders-cry.md @@ -0,0 +1,11 @@ +--- +"@apollo/query-planner": patch +"@apollo/gateway": patch +--- + +Don't preserve useless fetches which downgrade __typename from a concrete type back to its interface type. + +In certain cases, the query planner was preserving some fetches which were "useless" that would rewrite __typename from its already-resolved concrete type back to its interface type. This could result in (at least) requested fields being "filtered" from the final result due to the interface's __typename in the data where the concrete type's __typename was expected. + +Specifically, the solution was compute the path between newly created groups and their parents when we know that it's trivial (`[]`). Further along in the planning process, this allows to actually remove the known-useless group. + \ No newline at end of file diff --git a/gateway-js/src/__tests__/executeQueryPlan.test.ts b/gateway-js/src/__tests__/executeQueryPlan.test.ts index d724310de..659ee5b7e 100644 --- a/gateway-js/src/__tests__/executeQueryPlan.test.ts +++ b/gateway-js/src/__tests__/executeQueryPlan.test.ts @@ -5092,7 +5092,7 @@ describe('executeQueryPlan', () => { const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); - let operation = parseOp(` + const operation = parseOp(` { ts { v @@ -5100,7 +5100,7 @@ describe('executeQueryPlan', () => { } `, schema); - let queryPlan = buildPlan(operation, queryPlanner); + const queryPlan = buildPlan(operation, queryPlanner); const expectedPlan = ` QueryPlan { Sequence { @@ -5132,7 +5132,7 @@ describe('executeQueryPlan', () => { `; expect(queryPlan).toMatchInlineSnapshot(expectedPlan); - let response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); expect(response.data).toMatchInlineSnapshot(` Object { "ts": Array [ @@ -5229,7 +5229,7 @@ describe('executeQueryPlan', () => { const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2, s3 ]); - let operation = parseOp(` + const operation = parseOp(` { ts { v @@ -5237,7 +5237,7 @@ describe('executeQueryPlan', () => { } `, schema); - let queryPlan = buildPlan(operation, queryPlanner); + const queryPlan = buildPlan(operation, queryPlanner); const expectedPlan = ` QueryPlan { Sequence { @@ -5286,7 +5286,7 @@ describe('executeQueryPlan', () => { `; expect(queryPlan).toMatchInlineSnapshot(expectedPlan); - let response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); expect(response.data).toMatchInlineSnapshot(` Object { "ts": Array [ @@ -5473,7 +5473,6 @@ describe('executeQueryPlan', () => { } `, schema); - global.console = require('console'); queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -5726,6 +5725,179 @@ describe('executeQueryPlan', () => { } `); }); + + test("useless interface fetches aren't preserved when `__typename` has been resolved to its concrete type (issue #2743)", async () => { + const store = [ + { + id: '1', + tField: 'field on a T', + }, + ]; + + const S1 = { + name: 'S1', + typeDefs: gql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key"] + ) + + interface I @key(fields: "id") { + id: ID! + } + + type T implements I @key(fields: "id") { + id: ID! + tField: String + } + `, + resolvers: { + I: { + __resolveReference: ({ id }: { id: string }) => + store.find((e) => e.id === id), + __resolveType: () => 'T', + }, + }, + }; + + const S2 = { + name: 'S2', + typeDefs: gql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@interfaceObject"] + ) + + type I @key(fields: "id") @interfaceObject { + id: ID! + iField: String! + } + `, + resolvers: { + I: { + __resolveReference: ({ id }: { id: string }) => + store.find((e) => e.id === id), + iField: () => 'field on an I', + }, + }, + }; + + const S3 = { + name: 'S3', + typeDefs: gql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@interfaceObject"] + ) + + type I @key(fields: "id") @interfaceObject { + id: ID! + } + + type Query { + getIs: [I!]! + } + `, + resolvers: { + I: { + __resolveReference: ({ id }: { id: string }) => + store.find((e) => e.id === id), + }, + Query: { + getIs: () => store, + }, + }, + }; + + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + S1, + S2, + S3, + ]); + const operation = parseOp( + `#graphql + { + getIs { + id + ... on T { + iField + } + } + } + `, + schema, + ); + + const queryPlan = buildPlan(operation, queryPlanner); + expect(queryPlan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "S3") { + { + getIs { + __typename + id + } + } + }, + Flatten(path: "getIs.@") { + Fetch(service: "S1") { + { + ... on I { + __typename + id + } + } => + { + ... on I { + __typename + } + } + }, + }, + Flatten(path: "getIs.@") { + Fetch(service: "S2") { + { + ... on I { + __typename + id + } + } => + { + ... on I { + iField + } + } + }, + }, + }, + } + `); + + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); + + expect(response).toMatchInlineSnapshot(` + Object { + "data": Object { + "getIs": Array [ + Object { + "iField": "field on an I", + "id": "1", + }, + ], + }, + } + `); + }); }); describe('fields with conflicting types needing aliasing', () => { diff --git a/query-planner-js/src/buildPlan.ts b/query-planner-js/src/buildPlan.ts index c69e3bd37..d677c03a6 100644 --- a/query-planner-js/src/buildPlan.ts +++ b/query-planner-js/src/buildPlan.ts @@ -60,6 +60,7 @@ import { possibleRuntimeTypes, typesCanBeMerged, Supergraph, + sameType, } from "@apollo/federation-internals"; import { advanceSimultaneousPathsWithOperation, @@ -2435,13 +2436,16 @@ class FetchDependencyGraph { } if (group.isUseless()) { - // In general, removing a group is a bit tricky because we need to deal with the fact - // that the group can have multiple parents and children and no break the "path in parent" - // in all those cases. To keep thing relatively easily, we only handle the following - // cases (other cases will remain non-optimal, but hopefully this handle all the cases - // we care about in practice): - // 1. if the group has no children. In which case we can just remove it with no ceremony. - // 2. if the group has only a single parent and we have a path to that parent. + // In general, removing a group is a bit tricky because we need to deal + // with the fact that the group can have multiple parents, and we don't + // have the "path in parent" in all cases. To keep thing relatively + // easily, we only handle the following cases (other cases will remain + // non-optimal, but hopefully this handle all the cases we care about in + // practice): + // 1. if the group has no children. In which case we can just remove it + // with no ceremony. + // 2. if the group has only a single parent and we have a path to that + // parent. if (group.children().length === 0) { this.remove(group); } else { @@ -4080,7 +4084,7 @@ function computeGroupsForTree( deferContext: updatedDeferContext }; if (conditions) { - // We have some @requires. + // We have @requires or some other dependency to create groups for. const requireResult = handleRequires( dependencyGraph, edge, @@ -4495,7 +4499,24 @@ function handleRequires( subgraphName: group.subgraphName, mergeAt: path.inResponse(), }); - newGroup.addParents(createdGroups.map((group) => ({ group }))); + newGroup.addParents( + createdGroups.map((group) => ({ + group, + // Usually, computing the path of our new group into the created groups + // is not entirely trivial, but there is at least the relatively common + // case where the 2 groups we look at have: + // 1) the same `mergeAt`, and + // 2) the same parentType; in that case, we can basically infer those 2 + // groups apply at the same "place" and so the "path in parent" is + // empty. TODO: it should probably be possible to generalize this by + // checking the `mergeAt` plus analyzing the selection but that + // warrants some reflection... + path: sameMergeAt(group.mergeAt, newGroup.mergeAt) + && sameType(group.parentType, newGroup.parentType) + ? [] + : undefined, + })), + ); addPostRequireInputs( dependencyGraph, path, From 7be0e4dc07781a0b3213ec685f92de5bb9ebb236 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 16 Sep 2023 14:36:39 +0000 Subject: [PATCH 74/95] chore(deps): update all non-major dependencies (#2784) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 590 +++++++++++++++++++++++++--------------------- package.json | 14 +- 2 files changed, 330 insertions(+), 274 deletions(-) diff --git a/package-lock.json b/package-lock.json index 76f6923ef..b73309ec7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,16 +29,16 @@ "@opentelemetry/node": "0.24.0", "@svitejs/changesets-changelog-github-compact": "1.1.0", "@types/async-retry": "1.4.5", - "@types/bunyan": "1.8.8", + "@types/bunyan": "1.8.9", "@types/deep-equal": "1.0.1", - "@types/jest": "29.5.4", + "@types/jest": "29.5.5", "@types/js-levenshtein": "1.1.1", "@types/loglevel": "1.5.4", "@types/make-fetch-happen": "10.0.1", "@types/nock": "10.0.3", - "@types/node": "14.18.59", - "@types/node-fetch": "2.6.4", - "@types/uuid": "9.0.3", + "@types/node": "14.18.61", + "@types/node-fetch": "2.6.5", + "@types/uuid": "9.0.4", "@typescript-eslint/eslint-plugin": "5.62.0", "bunyan": "1.8.15", "codecov": "3.8.3", @@ -46,8 +46,8 @@ "graphql": "16.8.0", "graphql-http": "1.22.0", "graphql-tag": "2.12.6", - "jest": "29.6.4", - "jest-config": "29.6.4", + "jest": "29.7.0", + "jest-config": "29.7.0", "jest-cucumber": "3.0.1", "jest-junit": "16.0.0", "log4js": "6.9.1", @@ -2985,16 +2985,16 @@ } }, "node_modules/@jest/console": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.6.4.tgz", - "integrity": "sha512-wNK6gC0Ha9QeEPSkeJedQuTQqxZYnDPuDcDhVuVatRvMkL4D0VTvFVZj+Yuh6caG2aOfzkUZ36KtCmLNtR02hw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", "dev": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^29.6.3", - "jest-util": "^29.6.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0" }, "engines": { @@ -3017,15 +3017,15 @@ } }, "node_modules/@jest/core": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.6.4.tgz", - "integrity": "sha512-U/vq5ccNTSVgYH7mHnodHmCffGWHJnz/E1BEWlLuK5pM4FZmGfBn/nrJGLjUsSmyx3otCeqc1T31F4y08AMDLg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", "dev": true, "dependencies": { - "@jest/console": "^29.6.4", - "@jest/reporters": "^29.6.4", - "@jest/test-result": "^29.6.4", - "@jest/transform": "^29.6.4", + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", @@ -3033,21 +3033,21 @@ "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.6.3", - "jest-config": "^29.6.4", - "jest-haste-map": "^29.6.4", - "jest-message-util": "^29.6.3", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.6.4", - "jest-resolve-dependencies": "^29.6.4", - "jest-runner": "^29.6.4", - "jest-runtime": "^29.6.4", - "jest-snapshot": "^29.6.4", - "jest-util": "^29.6.3", - "jest-validate": "^29.6.3", - "jest-watcher": "^29.6.4", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", "micromatch": "^4.0.4", - "pretty-format": "^29.6.3", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, @@ -3079,37 +3079,37 @@ } }, "node_modules/@jest/environment": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.6.4.tgz", - "integrity": "sha512-sQ0SULEjA1XUTHmkBRl7A1dyITM9yb1yb3ZNKPX3KlTd6IG7mWUe3e2yfExtC2Zz1Q+mMckOLHmL/qLiuQJrBQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", "dev": true, "dependencies": { - "@jest/fake-timers": "^29.6.4", + "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", - "jest-mock": "^29.6.3" + "jest-mock": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.6.4.tgz", - "integrity": "sha512-Warhsa7d23+3X5bLbrbYvaehcgX5TLYhI03JKoedTiI8uJU4IhqYBWF7OSSgUyz4IgLpUYPkK0AehA5/fRclAA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", "dev": true, "dependencies": { - "expect": "^29.6.4", - "jest-snapshot": "^29.6.4" + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.6.4.tgz", - "integrity": "sha512-FEhkJhqtvBwgSpiTrocquJCdXPsyvNKcl/n7A3u7X4pVoF4bswm11c9d4AV+kfq2Gpv/mM8x7E7DsRvH+djkrg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dev": true, "dependencies": { "jest-get-type": "^29.6.3" @@ -3119,47 +3119,47 @@ } }, "node_modules/@jest/fake-timers": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.6.4.tgz", - "integrity": "sha512-6UkCwzoBK60edXIIWb0/KWkuj7R7Qq91vVInOe3De6DSpaEiqjKcJw4F7XUet24Wupahj9J6PlR09JqJ5ySDHw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", "dev": true, "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", - "jest-message-util": "^29.6.3", - "jest-mock": "^29.6.3", - "jest-util": "^29.6.3" + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/globals": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.6.4.tgz", - "integrity": "sha512-wVIn5bdtjlChhXAzVXavcY/3PEjf4VqM174BM3eGL5kMxLiZD5CLnbmkEyA1Dwh9q8XjP6E8RwjBsY/iCWrWsA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, "dependencies": { - "@jest/environment": "^29.6.4", - "@jest/expect": "^29.6.4", + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", "@jest/types": "^29.6.3", - "jest-mock": "^29.6.3" + "jest-mock": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/reporters": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.6.4.tgz", - "integrity": "sha512-sxUjWxm7QdchdrD3NfWKrL8FBsortZeibSJv4XLjESOOjSUOkjQcb0ZHJwfhEGIvBvTluTzfG2yZWZhkrXJu8g==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.6.4", - "@jest/test-result": "^29.6.4", - "@jest/transform": "^29.6.4", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "@types/node": "*", @@ -3173,9 +3173,9 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.6.3", - "jest-util": "^29.6.3", - "jest-worker": "^29.6.4", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", @@ -3251,12 +3251,12 @@ } }, "node_modules/@jest/test-result": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.6.4.tgz", - "integrity": "sha512-uQ1C0AUEN90/dsyEirgMLlouROgSY+Wc/JanVVk0OiUKa5UFh7sJpMEM3aoUBAz2BRNvUJ8j3d294WFuRxSyOQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", "dev": true, "dependencies": { - "@jest/console": "^29.6.4", + "@jest/console": "^29.7.0", "@jest/types": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" @@ -3266,14 +3266,14 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.6.4.tgz", - "integrity": "sha512-E84M6LbpcRq3fT4ckfKs9ryVanwkaIB0Ws9bw3/yP4seRLg/VaCZ/LgW0MCq5wwk4/iP/qnilD41aj2fsw2RMg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", "dev": true, "dependencies": { - "@jest/test-result": "^29.6.4", + "@jest/test-result": "^29.7.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.6.4", + "jest-haste-map": "^29.7.0", "slash": "^3.0.0" }, "engines": { @@ -3281,9 +3281,9 @@ } }, "node_modules/@jest/transform": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.6.4.tgz", - "integrity": "sha512-8thgRSiXUqtr/pPGY/OsyHuMjGyhVnWrFAwoxmIemlBuiMyU1WFs0tXoNxzcr4A4uErs/ABre76SGmrr5ab/AA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", @@ -3294,9 +3294,9 @@ "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.6.4", + "jest-haste-map": "^29.7.0", "jest-regex-util": "^29.6.3", - "jest-util": "^29.6.3", + "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", @@ -3862,9 +3862,10 @@ } }, "node_modules/@types/bunyan": { - "version": "1.8.8", + "version": "1.8.9", + "resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.9.tgz", + "integrity": "sha512-ZqS9JGpBxVOvsawzmVt30sP++gSQMTejCkIAQ3VdadOcRE8izTyW66hufvwLeH+YEGP6Js2AW7Gz+RMyvrEbmw==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -3950,9 +3951,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.4", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.4.tgz", - "integrity": "sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A==", + "version": "29.5.5", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.5.tgz", + "integrity": "sha512-ebylz2hnsWR9mYvmBFbXJXr+33UPc4+ZdxyDXh5w0FlPBTfCVN3wPL+kuOiQt3xvrK419v7XWeAs+AeOksafXg==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -4023,17 +4024,30 @@ } }, "node_modules/@types/node": { - "version": "14.18.59", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.59.tgz", - "integrity": "sha512-NWJMpBL2Xs3MY93yrD6YrrTKep8eIA6iMnfG4oIc6LrTRlBZgiSCGiY3V/Owlp6umIBLyKb4F8Q7hxWatjYH5A==" + "version": "14.18.61", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.61.tgz", + "integrity": "sha512-1mFT4DqS4/s9tlZbdkwEB/EnSykA9MDeDLIk3FHApGvIMGY//qgstB2gu9GKGESWyW/qiRUO+jhlLJ9bBJ8j+Q==" }, "node_modules/@types/node-fetch": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.4.tgz", - "integrity": "sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==", + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.5.tgz", + "integrity": "sha512-OZsUlr2nxvkqUFLSaY2ZbA+P1q22q+KrlxWOn/38RX+u5kTkYL2mTujEpzUhGkS+K/QCYp9oagfXG39XOzyySg==", "dependencies": { "@types/node": "*", - "form-data": "^3.0.0" + "form-data": "^4.0.0" + } + }, + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" } }, "node_modules/@types/normalize-package-data": { @@ -4100,9 +4114,9 @@ "license": "MIT" }, "node_modules/@types/uuid": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.3.tgz", - "integrity": "sha512-taHQQH/3ZyI3zP8M/puluDEIEvtQHVYcC6y3N8ijFtAd28+Ey/G4sg1u2gB01S8MwybLOKAp9/yCMu/uR5l3Ug==" + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.4.tgz", + "integrity": "sha512-zAuJWQflfx6dYJM62vna+Sn5aeSWhh3OB+wfUEACNcqUSc0AGc5JKl+ycL1vrH7frGTXhJchYjE1Hak8L819dA==" }, "node_modules/@types/ws": { "version": "8.5.4", @@ -4489,9 +4503,9 @@ } }, "node_modules/@whatwg-node/fetch/node_modules/@types/node": { - "version": "18.17.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.15.tgz", - "integrity": "sha512-2yrWpBk32tvV/JAd3HNHWuZn/VDN1P+72hWirHnvsvTGSqbANi+kSeuQR9yAHnbvaBvHDsoTdXV0Fe+iRtHLKA==", + "version": "18.17.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.17.tgz", + "integrity": "sha512-cOxcXsQ2sxiwkykdJqvyFS+MLQPLvIdwh5l6gNg8qF6s+C7XSkEWOZjK+XhUZd+mYvHV/180g2cnCcIl4l06Pw==", "dev": true, "peer": true }, @@ -4921,12 +4935,12 @@ } }, "node_modules/babel-jest": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.6.4.tgz", - "integrity": "sha512-meLj23UlSLddj6PC+YTOFRgDAtjnZom8w/ACsrx0gtPtv5cJZk0A5Unk5bV4wixD7XaPCN1fQvpww8czkZURmw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "dev": true, "dependencies": { - "@jest/transform": "^29.6.4", + "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.6.3", @@ -6079,6 +6093,43 @@ "node": ">=10" } }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/create-require": { "version": "1.1.1", "dev": true, @@ -7684,16 +7735,16 @@ "license": "MIT" }, "node_modules/expect": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.6.4.tgz", - "integrity": "sha512-F2W2UyQ8XYyftHT57dtfg8Ue3X5qLgm2sSug0ivvLRH/VKNRL/pDxg/TH7zVzbQB0tu80clNFy6LU7OS/VSEKA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "dev": true, "dependencies": { - "@jest/expect-utils": "^29.6.4", + "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.6.4", - "jest-message-util": "^29.6.3", - "jest-util": "^29.6.3" + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -8082,6 +8133,7 @@ }, "node_modules/form-data": { "version": "3.0.1", + "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -9625,15 +9677,15 @@ } }, "node_modules/jest": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.6.4.tgz", - "integrity": "sha512-tEFhVQFF/bzoYV1YuGyzLPZ6vlPrdfvDmmAxudA1dLEuiztqg2Rkx20vkKY32xiDROcD2KXlgZ7Cu8RPeEHRKw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "dependencies": { - "@jest/core": "^29.6.4", + "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", "import-local": "^3.0.2", - "jest-cli": "^29.6.4" + "jest-cli": "^29.7.0" }, "bin": { "jest": "bin/jest.js" @@ -9651,13 +9703,13 @@ } }, "node_modules/jest-changed-files": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.6.3.tgz", - "integrity": "sha512-G5wDnElqLa4/c66ma5PG9eRjE342lIbF6SUnTJi26C3J28Fv2TVY2rOyKB9YGbSA5ogwevgmxc4j4aVjrEK6Yg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", "dev": true, "dependencies": { "execa": "^5.0.0", - "jest-util": "^29.6.3", + "jest-util": "^29.7.0", "p-limit": "^3.1.0" }, "engines": { @@ -9680,28 +9732,28 @@ } }, "node_modules/jest-circus": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.6.4.tgz", - "integrity": "sha512-YXNrRyntVUgDfZbjXWBMPslX1mQ8MrSG0oM/Y06j9EYubODIyHWP8hMUbjbZ19M3M+zamqEur7O80HODwACoJw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", "dev": true, "dependencies": { - "@jest/environment": "^29.6.4", - "@jest/expect": "^29.6.4", - "@jest/test-result": "^29.6.4", + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", "dedent": "^1.0.0", "is-generator-fn": "^2.0.0", - "jest-each": "^29.6.3", - "jest-matcher-utils": "^29.6.4", - "jest-message-util": "^29.6.3", - "jest-runtime": "^29.6.4", - "jest-snapshot": "^29.6.4", - "jest-util": "^29.6.3", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", "p-limit": "^3.1.0", - "pretty-format": "^29.6.3", + "pretty-format": "^29.7.0", "pure-rand": "^6.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" @@ -9740,22 +9792,21 @@ } }, "node_modules/jest-cli": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.6.4.tgz", - "integrity": "sha512-+uMCQ7oizMmh8ZwRfZzKIEszFY9ksjjEQnTEMTaL7fYiL3Kw4XhqT9bYh+A4DQKUb67hZn2KbtEnDuHvcgK4pQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", "dev": true, "dependencies": { - "@jest/core": "^29.6.4", - "@jest/test-result": "^29.6.4", + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", "chalk": "^4.0.0", + "create-jest": "^29.7.0", "exit": "^0.1.2", - "graceful-fs": "^4.2.9", "import-local": "^3.0.2", - "jest-config": "^29.6.4", - "jest-util": "^29.6.3", - "jest-validate": "^29.6.3", - "prompts": "^2.0.1", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "yargs": "^17.3.1" }, "bin": { @@ -9789,31 +9840,31 @@ } }, "node_modules/jest-config": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.6.4.tgz", - "integrity": "sha512-JWohr3i9m2cVpBumQFv2akMEnFEPVOh+9L2xIBJhJ0zOaci2ZXuKJj0tgMKQCBZAKA09H049IR4HVS/43Qb19A==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.6.4", + "@jest/test-sequencer": "^29.7.0", "@jest/types": "^29.6.3", - "babel-jest": "^29.6.4", + "babel-jest": "^29.7.0", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-circus": "^29.6.4", - "jest-environment-node": "^29.6.4", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", "jest-get-type": "^29.6.3", "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.6.4", - "jest-runner": "^29.6.4", - "jest-util": "^29.6.3", - "jest-validate": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "^29.6.3", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -10846,15 +10897,15 @@ } }, "node_modules/jest-diff": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.6.4.tgz", - "integrity": "sha512-9F48UxR9e4XOEZvoUXEHSWY4qC4zERJaOfrbBg9JpbJOO43R1vN76REt/aMGZoY6GD5g84nnJiBIVlscegefpw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", - "pretty-format": "^29.6.3" + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -10877,9 +10928,9 @@ } }, "node_modules/jest-docblock": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.6.3.tgz", - "integrity": "sha512-2+H+GOTQBEm2+qFSQ7Ma+BvyV+waiIFxmZF5LdpBsAEjWX8QYjSCa4FrkIYtbfXUJJJnFCYrOtt6TZ+IAiTjBQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", "dev": true, "dependencies": { "detect-newline": "^3.0.0" @@ -10889,16 +10940,16 @@ } }, "node_modules/jest-each": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.6.3.tgz", - "integrity": "sha512-KoXfJ42k8cqbkfshW7sSHcdfnv5agDdHCPA87ZBdmHP+zJstTJc0ttQaJ/x7zK6noAL76hOuTIJ6ZkQRS5dcyg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", "dev": true, "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", - "jest-util": "^29.6.3", - "pretty-format": "^29.6.3" + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -11104,17 +11155,17 @@ "license": "MIT" }, "node_modules/jest-environment-node": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.6.4.tgz", - "integrity": "sha512-i7SbpH2dEIFGNmxGCpSc2w9cA4qVD+wfvg2ZnfQ7XVrKL0NA5uDVBIiGH8SR4F0dKEv/0qI5r+aDomDf04DpEQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", "dev": true, "dependencies": { - "@jest/environment": "^29.6.4", - "@jest/fake-timers": "^29.6.4", + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", - "jest-mock": "^29.6.3", - "jest-util": "^29.6.3" + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -11130,9 +11181,9 @@ } }, "node_modules/jest-haste-map": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.6.4.tgz", - "integrity": "sha512-12Ad+VNTDHxKf7k+M65sviyynRoZYuL1/GTuhEVb8RYsNSNln71nANRb/faSyWvx0j+gHcivChXHIoMJrGYjog==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dev": true, "dependencies": { "@jest/types": "^29.6.3", @@ -11142,8 +11193,8 @@ "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.6.3", - "jest-util": "^29.6.3", - "jest-worker": "^29.6.4", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "walker": "^1.0.8" }, @@ -11931,28 +11982,28 @@ } }, "node_modules/jest-leak-detector": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.6.3.tgz", - "integrity": "sha512-0kfbESIHXYdhAdpLsW7xdwmYhLf1BRu4AA118/OxFm0Ho1b2RcTmO4oF6aAMaxpxdxnJ3zve2rgwzNBD4Zbm7Q==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", "dev": true, "dependencies": { "jest-get-type": "^29.6.3", - "pretty-format": "^29.6.3" + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.6.4.tgz", - "integrity": "sha512-KSzwyzGvK4HcfnserYqJHYi7sZVqdREJ9DMPAKVbS98JsIAvumihaNUbjrWw0St7p9IY7A9UskCW5MYlGmBQFQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "jest-diff": "^29.6.4", + "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", - "pretty-format": "^29.6.3" + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -11974,9 +12025,9 @@ } }, "node_modules/jest-message-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.6.3.tgz", - "integrity": "sha512-FtzaEEHzjDpQp51HX4UMkPZjy46ati4T5pEMyM6Ik48ztu4T9LQplZ6OsimHx7EuM9dfEh5HJa6D3trEftu3dA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", @@ -11985,7 +12036,7 @@ "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", - "pretty-format": "^29.6.3", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, @@ -12009,14 +12060,14 @@ } }, "node_modules/jest-mock": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.6.3.tgz", - "integrity": "sha512-Z7Gs/mOyTSR4yPsaZ72a/MtuK6RnC3JYqWONe48oLaoEcYwEDxqvbXz85G4SJrm2Z5Ar9zp6MiHF4AlFlRM4Pg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", "dev": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", - "jest-util": "^29.6.3" + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -12048,17 +12099,17 @@ } }, "node_modules/jest-resolve": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.6.4.tgz", - "integrity": "sha512-fPRq+0vcxsuGlG0O3gyoqGTAxasagOxEuyoxHeyxaZbc9QNek0AmJWSkhjlMG+mTsj+8knc/mWb3fXlRNVih7Q==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dev": true, "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.6.4", + "jest-haste-map": "^29.7.0", "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.6.3", - "jest-validate": "^29.6.3", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "resolve": "^1.20.0", "resolve.exports": "^2.0.0", "slash": "^3.0.0" @@ -12068,13 +12119,13 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.6.4.tgz", - "integrity": "sha512-7+6eAmr1ZBF3vOAJVsfLj1QdqeXG+WYhidfLHBRZqGN24MFRIiKG20ItpLw2qRAsW/D2ZUUmCNf6irUr/v6KHA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", "dev": true, "dependencies": { "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.6.4" + "jest-snapshot": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -12096,30 +12147,30 @@ } }, "node_modules/jest-runner": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.6.4.tgz", - "integrity": "sha512-SDaLrMmtVlQYDuG0iSPYLycG8P9jLI+fRm8AF/xPKhYDB2g6xDWjXBrR5M8gEWsK6KVFlebpZ4QsrxdyIX1Jaw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "dev": true, "dependencies": { - "@jest/console": "^29.6.4", - "@jest/environment": "^29.6.4", - "@jest/test-result": "^29.6.4", - "@jest/transform": "^29.6.4", + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "emittery": "^0.13.1", "graceful-fs": "^4.2.9", - "jest-docblock": "^29.6.3", - "jest-environment-node": "^29.6.4", - "jest-haste-map": "^29.6.4", - "jest-leak-detector": "^29.6.3", - "jest-message-util": "^29.6.3", - "jest-resolve": "^29.6.4", - "jest-runtime": "^29.6.4", - "jest-util": "^29.6.3", - "jest-watcher": "^29.6.4", - "jest-worker": "^29.6.4", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -12169,17 +12220,17 @@ } }, "node_modules/jest-runtime": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.6.4.tgz", - "integrity": "sha512-s/QxMBLvmwLdchKEjcLfwzP7h+jsHvNEtxGP5P+Fl1FMaJX2jMiIqe4rJw4tFprzCwuSvVUo9bn0uj4gNRXsbA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", "dev": true, "dependencies": { - "@jest/environment": "^29.6.4", - "@jest/fake-timers": "^29.6.4", - "@jest/globals": "^29.6.4", + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.6.4", - "@jest/transform": "^29.6.4", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", @@ -12187,13 +12238,13 @@ "collect-v8-coverage": "^1.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.6.4", - "jest-message-util": "^29.6.3", - "jest-mock": "^29.6.3", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.6.4", - "jest-snapshot": "^29.6.4", - "jest-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -12229,9 +12280,9 @@ } }, "node_modules/jest-snapshot": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.6.4.tgz", - "integrity": "sha512-VC1N8ED7+4uboUKGIDsbvNAZb6LakgIPgAF4RSpF13dN6YaMokfRqO+BaqK4zIh6X3JffgwbzuGqDEjHm/MrvA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", @@ -12239,20 +12290,20 @@ "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.6.4", - "@jest/transform": "^29.6.4", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "expect": "^29.6.4", + "expect": "^29.7.0", "graceful-fs": "^4.2.9", - "jest-diff": "^29.6.4", + "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.6.4", - "jest-message-util": "^29.6.3", - "jest-util": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", "natural-compare": "^1.4.0", - "pretty-format": "^29.6.3", + "pretty-format": "^29.7.0", "semver": "^7.5.3" }, "engines": { @@ -12275,9 +12326,9 @@ } }, "node_modules/jest-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.6.3.tgz", - "integrity": "sha512-QUjna/xSy4B32fzcKTSz1w7YYzgiHrjjJjevdRf61HYk998R5vVMMNmrHESYZVDS5DSWs+1srPLPKxXPkeSDOA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, "dependencies": { "@jest/types": "^29.6.3", @@ -12307,9 +12358,9 @@ } }, "node_modules/jest-validate": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.6.3.tgz", - "integrity": "sha512-e7KWZcAIX+2W1o3cHfnqpGajdCs1jSM3DkXjGeLSNmCazv1EeI1ggTeK5wdZhF+7N+g44JI2Od3veojoaumlfg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", "dev": true, "dependencies": { "@jest/types": "^29.6.3", @@ -12317,7 +12368,7 @@ "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", - "pretty-format": "^29.6.3" + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -12352,18 +12403,18 @@ } }, "node_modules/jest-watcher": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.6.4.tgz", - "integrity": "sha512-oqUWvx6+On04ShsT00Ir9T4/FvBeEh2M9PTubgITPxDa739p4hoQweWPRGyYeaojgT0xTpZKF0Y/rSY1UgMxvQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dev": true, "dependencies": { - "@jest/test-result": "^29.6.4", + "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.13.1", - "jest-util": "^29.6.3", + "jest-util": "^29.7.0", "string-length": "^4.0.1" }, "engines": { @@ -12387,13 +12438,13 @@ } }, "node_modules/jest-worker": { - "version": "29.6.4", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.6.4.tgz", - "integrity": "sha512-6dpvFV4WjcWbDVGgHTWo/aupl8/LbBx2NSKfiwqf79xC/yeJjKHT1+StcKy/2KTmW16hE68ccKVOtXf+WZGz7Q==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, "dependencies": { "@types/node": "*", - "jest-util": "^29.6.3", + "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -14375,9 +14426,9 @@ } }, "node_modules/pretty-format": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.3.tgz", - "integrity": "sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -17235,8 +17286,13 @@ } }, "node_modules/uuid": { - "version": "9.0.0", - "license": "MIT", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } diff --git a/package.json b/package.json index ab7fcf82e..be044c4eb 100644 --- a/package.json +++ b/package.json @@ -57,16 +57,16 @@ "@opentelemetry/node": "0.24.0", "@svitejs/changesets-changelog-github-compact": "1.1.0", "@types/async-retry": "1.4.5", - "@types/bunyan": "1.8.8", + "@types/bunyan": "1.8.9", "@types/deep-equal": "1.0.1", - "@types/jest": "29.5.4", + "@types/jest": "29.5.5", "@types/js-levenshtein": "1.1.1", "@types/loglevel": "1.5.4", "@types/make-fetch-happen": "10.0.1", "@types/nock": "10.0.3", - "@types/node": "14.18.59", - "@types/node-fetch": "2.6.4", - "@types/uuid": "9.0.3", + "@types/node": "14.18.61", + "@types/node-fetch": "2.6.5", + "@types/uuid": "9.0.4", "@typescript-eslint/eslint-plugin": "5.62.0", "bunyan": "1.8.15", "codecov": "3.8.3", @@ -74,8 +74,8 @@ "graphql": "16.8.0", "graphql-http": "1.22.0", "graphql-tag": "2.12.6", - "jest": "29.6.4", - "jest-config": "29.6.4", + "jest": "29.7.0", + "jest-config": "29.7.0", "jest-cucumber": "3.0.1", "jest-junit": "16.0.0", "log4js": "6.9.1", From f4c9f80e35072ffaca58da6beec6635fa5caf73a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 13:44:58 -0700 Subject: [PATCH 75/95] Version Packages (#2781) Co-authored-by: github-actions[bot] --- .changeset/bright-mails-travel.md | 14 -------- .changeset/chilled-geckos-change.md | 6 ---- .changeset/itchy-spiders-cry.md | 11 ------- composition-js/CHANGELOG.md | 7 ++++ composition-js/package.json | 6 ++-- .../CHANGELOG.md | 2 ++ .../package.json | 2 +- gateway-js/CHANGELOG.md | 22 +++++++++++++ gateway-js/package.json | 8 ++--- internals-js/CHANGELOG.md | 2 ++ internals-js/package.json | 2 +- package-lock.json | 32 +++++++++---------- query-graphs-js/CHANGELOG.md | 6 ++++ query-graphs-js/package.json | 4 +-- query-planner-js/CHANGELOG.md | 21 ++++++++++++ query-planner-js/package.json | 6 ++-- subgraph-js/CHANGELOG.md | 14 ++++++++ subgraph-js/package.json | 4 +-- 18 files changed, 106 insertions(+), 63 deletions(-) delete mode 100644 .changeset/bright-mails-travel.md delete mode 100644 .changeset/chilled-geckos-change.md delete mode 100644 .changeset/itchy-spiders-cry.md diff --git a/.changeset/bright-mails-travel.md b/.changeset/bright-mails-travel.md deleted file mode 100644 index bcd21a3f4..000000000 --- a/.changeset/bright-mails-travel.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@apollo/query-planner": patch -"@apollo/subgraph": patch -"@apollo/gateway": patch ---- - -Fix specific case for requesting __typename on interface entity type - -In certain cases, when resolving a __typename on an interface entity (due to it actual being requested in the operation), that fetch group could previously be trimmed / treated as useless. At a glance, it appears to be a redundant step, i.e.: -``` -{ ... on Product { __typename id }} => { ... on Product { __typename} } -``` -It's actually necessary to preserve this in the case that we're coming from an interface object to an (entity) interface so that we can resolve the concrete __typename correctly. - \ No newline at end of file diff --git a/.changeset/chilled-geckos-change.md b/.changeset/chilled-geckos-change.md deleted file mode 100644 index f0184f387..000000000 --- a/.changeset/chilled-geckos-change.md +++ /dev/null @@ -1,6 +0,0 @@ ---- - ---- - - - \ No newline at end of file diff --git a/.changeset/itchy-spiders-cry.md b/.changeset/itchy-spiders-cry.md deleted file mode 100644 index 48581ade2..000000000 --- a/.changeset/itchy-spiders-cry.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@apollo/query-planner": patch -"@apollo/gateway": patch ---- - -Don't preserve useless fetches which downgrade __typename from a concrete type back to its interface type. - -In certain cases, the query planner was preserving some fetches which were "useless" that would rewrite __typename from its already-resolved concrete type back to its interface type. This could result in (at least) requested fields being "filtered" from the final result due to the interface's __typename in the data where the concrete type's __typename was expected. - -Specifically, the solution was compute the path between newly created groups and their parents when we know that it's trivial (`[]`). Further along in the planning process, this allows to actually remove the known-useless group. - \ No newline at end of file diff --git a/composition-js/CHANGELOG.md b/composition-js/CHANGELOG.md index dfbe9135a..a1f33397e 100644 --- a/composition-js/CHANGELOG.md +++ b/composition-js/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG for `@apollo/composition` +## 2.5.5 +### Patch Changes + +- Updated dependencies []: + - @apollo/federation-internals@2.5.5 + - @apollo/query-graphs@2.5.5 + ## 2.5.4 ### Patch Changes diff --git a/composition-js/package.json b/composition-js/package.json index df160b160..026ec4da9 100644 --- a/composition-js/package.json +++ b/composition-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/composition", - "version": "2.5.4", + "version": "2.5.5", "description": "Apollo Federation composition utilities", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -27,8 +27,8 @@ "access": "public" }, "dependencies": { - "@apollo/federation-internals": "2.5.4", - "@apollo/query-graphs": "2.5.4" + "@apollo/federation-internals": "2.5.5", + "@apollo/query-graphs": "2.5.5" }, "peerDependencies": { "graphql": "^16.5.0" diff --git a/federation-integration-testsuite-js/CHANGELOG.md b/federation-integration-testsuite-js/CHANGELOG.md index 164bc7085..17a0f03ac 100644 --- a/federation-integration-testsuite-js/CHANGELOG.md +++ b/federation-integration-testsuite-js/CHANGELOG.md @@ -1,5 +1,7 @@ # CHANGELOG for `federation-integration-testsuite-js` +## 2.5.5 + ## 2.5.4 ## 2.5.3 diff --git a/federation-integration-testsuite-js/package.json b/federation-integration-testsuite-js/package.json index 4e3d3903e..9f11662be 100644 --- a/federation-integration-testsuite-js/package.json +++ b/federation-integration-testsuite-js/package.json @@ -1,7 +1,7 @@ { "name": "apollo-federation-integration-testsuite", "private": true, - "version": "2.5.4", + "version": "2.5.5", "description": "Apollo Federation Integrations / Test Fixtures", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/gateway-js/CHANGELOG.md b/gateway-js/CHANGELOG.md index a39ff2e58..3d2da5a89 100644 --- a/gateway-js/CHANGELOG.md +++ b/gateway-js/CHANGELOG.md @@ -1,5 +1,27 @@ # CHANGELOG for `@apollo/gateway` +## 2.5.5 +### Patch Changes + + +- Fix specific case for requesting __typename on interface entity type ([#2775](https://github.com/apollographql/federation/pull/2775)) + + In certain cases, when resolving a __typename on an interface entity (due to it actual being requested in the operation), that fetch group could previously be trimmed / treated as useless. At a glance, it appears to be a redundant step, i.e.: + ``` + { ... on Product { __typename id }} => { ... on Product { __typename} } + ``` + It's actually necessary to preserve this in the case that we're coming from an interface object to an (entity) interface so that we can resolve the concrete __typename correctly. + +- Don't preserve useless fetches which downgrade __typename from a concrete type back to its interface type. ([#2778](https://github.com/apollographql/federation/pull/2778)) + + In certain cases, the query planner was preserving some fetches which were "useless" that would rewrite __typename from its already-resolved concrete type back to its interface type. This could result in (at least) requested fields being "filtered" from the final result due to the interface's __typename in the data where the concrete type's __typename was expected. + + Specifically, the solution was compute the path between newly created groups and their parents when we know that it's trivial (`[]`). Further along in the planning process, this allows to actually remove the known-useless group. +- Updated dependencies [[`66d7e4ce`](https://github.com/apollographql/federation/commit/66d7e4ced9a6ccd170d5e228741332395a2dd553), [`a37bbbf6`](https://github.com/apollographql/federation/commit/a37bbbf6501032f16382ccb64d1dfa0dcddac789)]: + - @apollo/query-planner@2.5.5 + - @apollo/composition@2.5.5 + - @apollo/federation-internals@2.5.5 + ## 2.5.4 ### Patch Changes diff --git a/gateway-js/package.json b/gateway-js/package.json index f46d97d44..409aea988 100644 --- a/gateway-js/package.json +++ b/gateway-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/gateway", - "version": "2.5.4", + "version": "2.5.5", "description": "Apollo Gateway", "author": "Apollo ", "main": "dist/index.js", @@ -25,9 +25,9 @@ "access": "public" }, "dependencies": { - "@apollo/composition": "2.5.4", - "@apollo/federation-internals": "2.5.4", - "@apollo/query-planner": "2.5.4", + "@apollo/composition": "2.5.5", + "@apollo/federation-internals": "2.5.5", + "@apollo/query-planner": "2.5.5", "@apollo/server-gateway-interface": "^1.1.0", "@apollo/usage-reporting-protobuf": "^4.1.0", "@apollo/utils.createhash": "^2.0.0", diff --git a/internals-js/CHANGELOG.md b/internals-js/CHANGELOG.md index 6532a4618..64db9168b 100644 --- a/internals-js/CHANGELOG.md +++ b/internals-js/CHANGELOG.md @@ -1,5 +1,7 @@ # CHANGELOG for `@apollo/federation-internals` +## 2.5.5 + ## 2.5.4 ## 2.5.3 diff --git a/internals-js/package.json b/internals-js/package.json index caa497ec3..177fe2b77 100644 --- a/internals-js/package.json +++ b/internals-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/federation-internals", - "version": "2.5.4", + "version": "2.5.5", "description": "Apollo Federation internal utilities", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/package-lock.json b/package-lock.json index b73309ec7..39bd7afff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,11 +71,11 @@ }, "composition-js": { "name": "@apollo/composition", - "version": "2.5.4", + "version": "2.5.5", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { - "@apollo/federation-internals": "2.5.4", - "@apollo/query-graphs": "2.5.4" + "@apollo/federation-internals": "2.5.5", + "@apollo/query-graphs": "2.5.5" }, "engines": { "node": ">=14.15.0" @@ -86,7 +86,7 @@ }, "federation-integration-testsuite-js": { "name": "apollo-federation-integration-testsuite", - "version": "2.5.4", + "version": "2.5.5", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { "graphql-tag": "^2.12.6", @@ -95,12 +95,12 @@ }, "gateway-js": { "name": "@apollo/gateway", - "version": "2.5.4", + "version": "2.5.5", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { - "@apollo/composition": "2.5.4", - "@apollo/federation-internals": "2.5.4", - "@apollo/query-planner": "2.5.4", + "@apollo/composition": "2.5.5", + "@apollo/federation-internals": "2.5.5", + "@apollo/query-planner": "2.5.5", "@apollo/server-gateway-interface": "^1.1.0", "@apollo/usage-reporting-protobuf": "^4.1.0", "@apollo/utils.createhash": "^2.0.0", @@ -126,7 +126,7 @@ }, "internals-js": { "name": "@apollo/federation-internals", - "version": "2.5.4", + "version": "2.5.5", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { "@types/uuid": "^9.0.0", @@ -17733,10 +17733,10 @@ }, "query-graphs-js": { "name": "@apollo/query-graphs", - "version": "2.5.4", + "version": "2.5.5", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { - "@apollo/federation-internals": "2.5.4", + "@apollo/federation-internals": "2.5.5", "deep-equal": "^2.0.5", "ts-graphviz": "^1.5.4", "uuid": "^9.0.0" @@ -17750,11 +17750,11 @@ }, "query-planner-js": { "name": "@apollo/query-planner", - "version": "2.5.4", + "version": "2.5.5", "license": "SEE LICENSE IN ./LICENSE", "dependencies": { - "@apollo/federation-internals": "2.5.4", - "@apollo/query-graphs": "2.5.4", + "@apollo/federation-internals": "2.5.5", + "@apollo/query-graphs": "2.5.5", "@apollo/utils.keyvaluecache": "^2.1.0", "chalk": "^4.1.0", "deep-equal": "^2.0.5", @@ -17783,11 +17783,11 @@ }, "subgraph-js": { "name": "@apollo/subgraph", - "version": "2.5.4", + "version": "2.5.5", "license": "MIT", "dependencies": { "@apollo/cache-control-types": "^1.0.2", - "@apollo/federation-internals": "2.5.4" + "@apollo/federation-internals": "2.5.5" }, "engines": { "node": ">=14.15.0" diff --git a/query-graphs-js/CHANGELOG.md b/query-graphs-js/CHANGELOG.md index 2aa2035b8..7d564e16d 100644 --- a/query-graphs-js/CHANGELOG.md +++ b/query-graphs-js/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG for `@apollo/query-graphs` +## 2.5.5 +### Patch Changes + +- Updated dependencies []: + - @apollo/federation-internals@2.5.5 + ## 2.5.4 ### Patch Changes diff --git a/query-graphs-js/package.json b/query-graphs-js/package.json index 3d71e26b7..dda831253 100644 --- a/query-graphs-js/package.json +++ b/query-graphs-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/query-graphs", - "version": "2.5.4", + "version": "2.5.5", "description": "Apollo Federation library to work with 'query graphs'", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -23,7 +23,7 @@ "node": ">=14.15.0" }, "dependencies": { - "@apollo/federation-internals": "2.5.4", + "@apollo/federation-internals": "2.5.5", "deep-equal": "^2.0.5", "ts-graphviz": "^1.5.4", "uuid": "^9.0.0" diff --git a/query-planner-js/CHANGELOG.md b/query-planner-js/CHANGELOG.md index 94bb399fe..0ec617fb1 100644 --- a/query-planner-js/CHANGELOG.md +++ b/query-planner-js/CHANGELOG.md @@ -1,5 +1,26 @@ # CHANGELOG for `@apollo/query-planner` +## 2.5.5 +### Patch Changes + + +- Fix specific case for requesting __typename on interface entity type ([#2775](https://github.com/apollographql/federation/pull/2775)) + + In certain cases, when resolving a __typename on an interface entity (due to it actual being requested in the operation), that fetch group could previously be trimmed / treated as useless. At a glance, it appears to be a redundant step, i.e.: + ``` + { ... on Product { __typename id }} => { ... on Product { __typename} } + ``` + It's actually necessary to preserve this in the case that we're coming from an interface object to an (entity) interface so that we can resolve the concrete __typename correctly. + +- Don't preserve useless fetches which downgrade __typename from a concrete type back to its interface type. ([#2778](https://github.com/apollographql/federation/pull/2778)) + + In certain cases, the query planner was preserving some fetches which were "useless" that would rewrite __typename from its already-resolved concrete type back to its interface type. This could result in (at least) requested fields being "filtered" from the final result due to the interface's __typename in the data where the concrete type's __typename was expected. + + Specifically, the solution was compute the path between newly created groups and their parents when we know that it's trivial (`[]`). Further along in the planning process, this allows to actually remove the known-useless group. +- Updated dependencies []: + - @apollo/federation-internals@2.5.5 + - @apollo/query-graphs@2.5.5 + ## 2.5.4 ### Patch Changes diff --git a/query-planner-js/package.json b/query-planner-js/package.json index f5ce55c2e..b247402e6 100644 --- a/query-planner-js/package.json +++ b/query-planner-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/query-planner", - "version": "2.5.4", + "version": "2.5.5", "description": "Apollo Query Planner", "author": "Apollo ", "main": "dist/index.js", @@ -25,8 +25,8 @@ "access": "public" }, "dependencies": { - "@apollo/federation-internals": "2.5.4", - "@apollo/query-graphs": "2.5.4", + "@apollo/federation-internals": "2.5.5", + "@apollo/query-graphs": "2.5.5", "@apollo/utils.keyvaluecache": "^2.1.0", "chalk": "^4.1.0", "deep-equal": "^2.0.5", diff --git a/subgraph-js/CHANGELOG.md b/subgraph-js/CHANGELOG.md index 2199f7334..cac11d486 100644 --- a/subgraph-js/CHANGELOG.md +++ b/subgraph-js/CHANGELOG.md @@ -1,5 +1,19 @@ # CHANGELOG for `@apollo/subgraph` +## 2.5.5 +### Patch Changes + + +- Fix specific case for requesting __typename on interface entity type ([#2775](https://github.com/apollographql/federation/pull/2775)) + + In certain cases, when resolving a __typename on an interface entity (due to it actual being requested in the operation), that fetch group could previously be trimmed / treated as useless. At a glance, it appears to be a redundant step, i.e.: + ``` + { ... on Product { __typename id }} => { ... on Product { __typename} } + ``` + It's actually necessary to preserve this in the case that we're coming from an interface object to an (entity) interface so that we can resolve the concrete __typename correctly. +- Updated dependencies []: + - @apollo/federation-internals@2.5.5 + ## 2.5.4 ### Patch Changes diff --git a/subgraph-js/package.json b/subgraph-js/package.json index 51d7db72c..b84e5888e 100644 --- a/subgraph-js/package.json +++ b/subgraph-js/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/subgraph", - "version": "2.5.4", + "version": "2.5.5", "description": "Apollo Subgraph Utilities", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -25,7 +25,7 @@ }, "dependencies": { "@apollo/cache-control-types": "^1.0.2", - "@apollo/federation-internals": "2.5.4" + "@apollo/federation-internals": "2.5.5" }, "peerDependencies": { "graphql": "^16.5.0" From 78b24a2b15e5ec38b24227915139c4bb58b278a5 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 18 Sep 2023 14:34:26 -0700 Subject: [PATCH 76/95] Wait for dist-tag updates to finish The logs from the `update-next-tags` script indicate that things aren't correctly being waited on. Some logs are missing entirely. Some packages don't get their appropriate update. This change should at least get us the logs if it doesn't fix the issue itself. --- scripts/update-next-tags.mjs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/scripts/update-next-tags.mjs b/scripts/update-next-tags.mjs index aa50617b7..f06f8f582 100644 --- a/scripts/update-next-tags.mjs +++ b/scripts/update-next-tags.mjs @@ -1,10 +1,13 @@ // @ts-check import { exec } from "child_process"; +import { promisify } from "util"; import { readFileSync } from "fs"; import fetch from "node-fetch"; import { resolve } from "path"; import semver from "semver"; +const asyncExec = promisify(exec); + let statusCode = 0; // Collect all the packages that we publish const workspaces = JSON.parse( @@ -37,14 +40,14 @@ await Promise.all( const command = `npm dist-tag add ${pkg}@${mostRecentVersion} next`; if (nextVersion !== mostRecentVersion) { console.log(`\`next\` tag is behind, updating...`); - exec(command, (e) => { - if (e) { - console.error(e); - throw e; - } else { - console.log("`next` tag updated successfully!"); - } - }); + try { + const { stdout, stderr } = await asyncExec(command) + if (stderr) console.error(stderr); + if (stdout) console.log(stdout); + } catch (e) { + console.error(e); + throw e; + } } else { console.log( "No action needed, `next` tag is pointed to most recent version" From 45a68e31a57a8aff4cf929adee45fe373294e72a Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 18 Sep 2023 15:04:24 -0700 Subject: [PATCH 77/95] Add missing changelog entry for #2776 --- gateway-js/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gateway-js/CHANGELOG.md b/gateway-js/CHANGELOG.md index 3d2da5a89..eb0c6f9c9 100644 --- a/gateway-js/CHANGELOG.md +++ b/gateway-js/CHANGELOG.md @@ -17,6 +17,11 @@ In certain cases, the query planner was preserving some fetches which were "useless" that would rewrite __typename from its already-resolved concrete type back to its interface type. This could result in (at least) requested fields being "filtered" from the final result due to the interface's __typename in the data where the concrete type's __typename was expected. Specifically, the solution was compute the path between newly created groups and their parents when we know that it's trivial (`[]`). Further along in the planning process, this allows to actually remove the known-useless group. + +- Propagate type information when renaming entity fields ([#2776](https://github.com/apollographql/federation/pull/2776)) + + Aliased entity fields might have been incorrectly overwritten if multiple fields/aliases shared the same name. Query planner automatically renames conflicting fields to ensure we can always generate a valid GraphQL query. The underlying issue was that this key rewriting logic was assuming the same type of an object. In case of entity queries asking for those aliased fields, we ended up always attempting to apply field renaming logic regardless, whether or not a given entity was of the correct type. This fix ensures that the query planner logic correctly accounts for the object type when applying field renaming logic. + - Updated dependencies [[`66d7e4ce`](https://github.com/apollographql/federation/commit/66d7e4ced9a6ccd170d5e228741332395a2dd553), [`a37bbbf6`](https://github.com/apollographql/federation/commit/a37bbbf6501032f16382ccb64d1dfa0dcddac789)]: - @apollo/query-planner@2.5.5 - @apollo/composition@2.5.5 From c1664823567afbc738604cca77d5e6a4c7ac4a2e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 20 Sep 2023 03:01:40 -0400 Subject: [PATCH 78/95] chore(deps): update all non-major dependencies (#2786) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 30 +++++++++++++++--------------- package.json | 8 ++++---- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index 39bd7afff..ad90fe107 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ ], "devDependencies": { "@apollo/cache-control-types": "1.0.3", - "@apollo/client": "3.8.3", + "@apollo/client": "3.8.4", "@apollo/server": "4.9.3", "@apollo/utils.fetcher": "2.0.1", "@changesets/changelog-github": "0.4.8", @@ -36,14 +36,14 @@ "@types/loglevel": "1.5.4", "@types/make-fetch-happen": "10.0.1", "@types/nock": "10.0.3", - "@types/node": "14.18.61", + "@types/node": "14.18.62", "@types/node-fetch": "2.6.5", "@types/uuid": "9.0.4", "@typescript-eslint/eslint-plugin": "5.62.0", "bunyan": "1.8.15", "codecov": "3.8.3", "cspell": "6.31.3", - "graphql": "16.8.0", + "graphql": "16.8.1", "graphql-http": "1.22.0", "graphql-tag": "2.12.6", "jest": "29.7.0", @@ -198,9 +198,9 @@ } }, "node_modules/@apollo/client": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.8.3.tgz", - "integrity": "sha512-mK86JM6hCpMEBGDgdO9U8ZYS8r9lPjXE1tVGpJMdSFUsIcXpmEfHUAbbFpPtYmxn8Qa7XsYy0dwDaDhpf4UUPw==", + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.8.4.tgz", + "integrity": "sha512-QFXE4ylSHUa6LgYoOGsPysJCm4YJOOM1NwHyF6msZdZXIerqUVpLvxQOdQEXgS0RWvYiBMC1wGOWKzJKSWBdAg==", "dev": true, "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", @@ -4024,9 +4024,9 @@ } }, "node_modules/@types/node": { - "version": "14.18.61", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.61.tgz", - "integrity": "sha512-1mFT4DqS4/s9tlZbdkwEB/EnSykA9MDeDLIk3FHApGvIMGY//qgstB2gu9GKGESWyW/qiRUO+jhlLJ9bBJ8j+Q==" + "version": "14.18.62", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.62.tgz", + "integrity": "sha512-53Fhb08qfKwSNCIUtysIqw0ye+v1d5QCdL2kl8liKQFlOZTAo+nEYr/FztzMaHBFwB5H0ugF0PF0gmtojaNNiQ==" }, "node_modules/@types/node-fetch": { "version": "2.6.5", @@ -4503,9 +4503,9 @@ } }, "node_modules/@whatwg-node/fetch/node_modules/@types/node": { - "version": "18.17.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.17.tgz", - "integrity": "sha512-cOxcXsQ2sxiwkykdJqvyFS+MLQPLvIdwh5l6gNg8qF6s+C7XSkEWOZjK+XhUZd+mYvHV/180g2cnCcIl4l06Pw==", + "version": "18.17.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.18.tgz", + "integrity": "sha512-/4QOuy3ZpV7Ya1GTRz5CYSz3DgkKpyUptXuQ5PPce7uuyJAOR7r9FhkmxJfvcNUXyklbC63a+YvB3jxy7s9ngw==", "dev": true, "peer": true }, @@ -8461,9 +8461,9 @@ "dev": true }, "node_modules/graphql": { - "version": "16.8.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.0.tgz", - "integrity": "sha512-0oKGaR+y3qcS5mCu1vb7KG+a89vjn06C7Ihq/dDl3jA+A8B3TKomvi3CiEcVLJQGalbu8F52LxkOym7U5sSfbg==", + "version": "16.8.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", + "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } diff --git a/package.json b/package.json index be044c4eb..1ed6699b5 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ ], "devDependencies": { "@apollo/cache-control-types": "1.0.3", - "@apollo/client": "3.8.3", + "@apollo/client": "3.8.4", "@apollo/server": "4.9.3", "@apollo/utils.fetcher": "2.0.1", "@changesets/changelog-github": "0.4.8", @@ -64,14 +64,14 @@ "@types/loglevel": "1.5.4", "@types/make-fetch-happen": "10.0.1", "@types/nock": "10.0.3", - "@types/node": "14.18.61", + "@types/node": "14.18.62", "@types/node-fetch": "2.6.5", "@types/uuid": "9.0.4", "@typescript-eslint/eslint-plugin": "5.62.0", "bunyan": "1.8.15", "codecov": "3.8.3", "cspell": "6.31.3", - "graphql": "16.8.0", + "graphql": "16.8.1", "graphql-http": "1.22.0", "graphql-tag": "2.12.6", "jest": "29.7.0", @@ -103,7 +103,7 @@ ] }, "volta": { - "node": "18.17.1", + "node": "18.18.0", "npm": "9.8.1" } } From f58071ac7c1fe8b1f3655e10ee4da9e507fca395 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 22 Sep 2023 10:59:29 -0700 Subject: [PATCH 79/95] Enforce prettier in test files to fix the DX for inline snapshotting Add opt-in repo pre-commit hook which enforces the same --- .circleci/config.yml | 4 +++- .git-blame-ignore-revs | 2 +- .git-hooks/pre-commit | 9 +++++++++ package.json | 6 +++++- 4 files changed, 18 insertions(+), 3 deletions(-) create mode 100755 .git-hooks/pre-commit diff --git a/.circleci/config.yml b/.circleci/config.yml index 8f787a3d9..16f8769d9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -31,7 +31,6 @@ jobs: path: junit.xml workflows: - version: 2 Build: jobs: - Test: @@ -56,3 +55,6 @@ workflows: - node/run: name: Check Spelling npm-run: spell:check + - node/run: + name: Check Prettier (tests) + npm-run: prettier:check diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index f02884cee..064966d92 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,2 +1,2 @@ # Run prettier -f2f1d6041cd67c67fdacabe0570fe2ce642c052c +f2f1d6041cd67c67fdacabe0570fe2ce642c052c \ No newline at end of file diff --git a/.git-hooks/pre-commit b/.git-hooks/pre-commit new file mode 100755 index 000000000..8912dd546 --- /dev/null +++ b/.git-hooks/pre-commit @@ -0,0 +1,9 @@ +#!/bin/bash + +# Grab prettier output, filter out garbage output lines (lines which aren't a +# file path), and remove the " 0ms" from the end of each line (everything after +# whitespace) +FILES=$(npm run prettier:fix | grep 'src' | sed 's/ .*//') + +# Add the files to the commit +echo "$FILES" | xargs git add \ No newline at end of file diff --git a/package.json b/package.json index be044c4eb..850d70df0 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,11 @@ "changeset-version": "changeset version && npm i", "build-and-publish": "npm run compile && changeset publish", "spell:check": "cspell lint --no-progress --config .cspell/cspell.yml || (echo 'Add any real words to ./cspell/cspell-dict.txt.'; exit 1)", - "use-repo-git-hooks": "git config core.hooksPath .git-hooks" + "//": "This only needs to use prettier@2 for as long as jest disallows using prettier@3", + "prettier:check": "node ./node_modules/prettier-2/bin-prettier.js --check ./**/__tests__/**/*.test.ts", + "prettier:fix": "node ./node_modules/prettier-2/bin-prettier.js --write ./**/__tests__/**/*.test.ts", + "//": "Optional: run this to configure git hooks and blame ignore revs", + "git:configure": "git config core.hooksPath .git-hooks; git config blame.ignoreRevsFile .git-blame-ignore-revs" }, "engines": { "node": ">=14.15.0", From dca3d464c8a699deaa1109ae69f376750b48c249 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 22 Sep 2023 11:16:14 -0700 Subject: [PATCH 80/95] run prettier --- .../executeQueryPlan.conditions.test.ts | 365 +- .../executeQueryPlan.introspection.test.ts | 70 +- .../src/__tests__/executeQueryPlan.test.ts | 2237 +++++++----- .../src/__tests__/gateway/endToEnd.test.ts | 14 +- .../src/__tests__/gateway/extensions.test.ts | 8 +- .../__tests__/gateway/lifecycle-hooks.test.ts | 4 +- .../__tests__/gateway/opentelemetry.test.ts | 36 +- .../__tests__/gateway/queryPlanCache.test.ts | 5 +- .../gateway/queryPlannerConfig.test.ts | 38 +- .../src/__tests__/gateway/reporting.test.ts | 36 +- .../__tests__/gateway/supergraphSdl.test.ts | 22 +- gateway-js/src/__tests__/httpSpec.test.ts | 2 +- .../integration/abstract-types.test.ts | 178 +- .../src/__tests__/integration/boolean.test.ts | 115 +- .../__tests__/integration/complex-key.test.ts | 9 +- .../integration/configuration.test.ts | 8 +- .../integration/custom-directives.test.ts | 2 +- .../__tests__/integration/fragments.test.ts | 2 +- .../__tests__/integration/list-key.test.ts | 7 +- .../src/__tests__/integration/logger.test.ts | 87 +- .../src/__tests__/integration/managed.test.ts | 2 +- .../integration/multiple-key.test.ts | 13 +- .../integration/networkRequests.test.ts | 10 +- .../__tests__/integration/provides.test.ts | 31 +- .../__tests__/integration/requires.test.ts | 5 +- .../src/__tests__/integration/scope.test.ts | 2 +- .../src/__tests__/integration/unions.test.ts | 58 +- .../__tests__/integration/variables.test.ts | 2 +- .../src/__tests__/queryPlanCucumber.test.ts | 28 +- .../src/__tests__/resultShaping.test.ts | 267 +- .../__tests__/LocalGraphQLDataSource.test.ts | 2 +- .../__tests__/RemoteGraphQLDataSource.test.ts | 6 +- .../__tests__/addExtensions.test.ts | 47 +- .../__tests__/IntrospectAndCompose.test.ts | 15 +- .../loadServicesFromRemoteEndpoint.test.ts | 2 +- .../loadSupergraphSdlFromStorage.test.ts | 4 +- .../src/__tests__/allFeatures.test.ts | 17 +- .../src/__tests__/buildPlan.defer.test.ts | 1222 ++++--- .../buildPlan.interfaceObject.test.ts | 208 +- .../__tests__/buildPlan.subscription.test.ts | 14 +- .../src/__tests__/buildPlan.test.ts | 3107 ++++++++++------- .../src/__tests__/generateAllPlans.test.ts | 74 +- .../supergraphBackwardCompatibility.test.ts | 179 +- .../src/__tests__/buildSubgraphSchema.test.ts | 98 +- 44 files changed, 5232 insertions(+), 3426 deletions(-) diff --git a/gateway-js/src/__tests__/executeQueryPlan.conditions.test.ts b/gateway-js/src/__tests__/executeQueryPlan.conditions.test.ts index 4881952a9..68ff85446 100644 --- a/gateway-js/src/__tests__/executeQueryPlan.conditions.test.ts +++ b/gateway-js/src/__tests__/executeQueryPlan.conditions.test.ts @@ -1,13 +1,23 @@ import gql from 'graphql-tag'; -import { getFederatedTestingSchema, ServiceDefinitionModule } from './execution-utils'; +import { + getFederatedTestingSchema, + ServiceDefinitionModule, +} from './execution-utils'; import { astSerializer, queryPlanSerializer, } from 'apollo-federation-integration-testsuite'; -import { Operation, parseOperation, Schema } from '@apollo/federation-internals'; +import { + Operation, + parseOperation, + Schema, +} from '@apollo/federation-internals'; import { QueryPlan } from '@apollo/query-planner'; import { LocalGraphQLDataSource } from '../datasources'; -import { GatewayExecutionResult, GatewayGraphQLRequestContext } from '@apollo/server-gateway-interface'; +import { + GatewayExecutionResult, + GatewayGraphQLRequestContext, +} from '@apollo/server-gateway-interface'; import { buildOperationContext } from '../operationContext'; import { executeQueryPlan } from '../executeQueryPlan'; @@ -15,7 +25,9 @@ expect.addSnapshotSerializer(astSerializer); expect.addSnapshotSerializer(queryPlanSerializer); describe('Execution tests for @include/@skip', () => { - function buildRequestContext(variables: Record): GatewayGraphQLRequestContext { + function buildRequestContext( + variables: Record, + ): GatewayGraphQLRequestContext { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return { @@ -28,7 +40,7 @@ describe('Execution tests for @include/@skip', () => { }; } - let s2Queries: {id : number}[] = []; + let s2Queries: { id: number }[] = []; /** * Simple subgraph schemas reused by a number of tests. This declares a simple interface `T` with 2 implems `T1` and `T2`. * There is a simple operation that returns a list of 3 simple objects: @@ -70,9 +82,9 @@ describe('Execution tests for @include/@skip', () => { { __typename: 'T2', id: 2, a2: 20 }, { __typename: 'T1', id: 3, a1: 30 }, ]; - } + }, }, - } + }, }, { name: 'S2', @@ -100,8 +112,8 @@ describe('Execution tests for @include/@skip', () => { return { ...ref, b2: 100 * ref.id }; }, }, - } - } + }, + }, ]; async function executePlan( @@ -114,7 +126,9 @@ describe('Execution tests for @include/@skip', () => { const apiSchema = schema.toAPISchema(); const operationContext = buildOperationContext({ schema: apiSchema.toGraphQLJSSchema(), - operationDocument: gql`${operation.toString()}`, + operationDocument: gql` + ${operation.toString()} + `, }); return executeQueryPlan( queryPlan, @@ -127,50 +141,71 @@ describe('Execution tests for @include/@skip', () => { } describe('Constant conditions optimisation', () => { - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema(simpleInterfaceSchemas); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema( + simpleInterfaceSchemas, + ); it('top-level @include never included', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t @include(if: false) { id } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(`QueryPlan {}`); - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(`Object {}`); }); it('top-level @skip always skipped', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t @skip(if: true) { id } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(`QueryPlan {}`); - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(`Object {}`); }); it('top-level @include always included', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t @include(if: true) { id } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); // Note that due to how we handle constant conditions, those don't get removed in the fetch nodes (which only matter when things are @@ -190,7 +225,12 @@ describe('Execution tests for @include/@skip', () => { } `); - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -210,13 +250,16 @@ describe('Execution tests for @include/@skip', () => { }); it('top-level @skip always included', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t @skip(if: false) { id } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); // Same as for @include: the @skip within the fetch is not cleared, but that's harmless. @@ -233,7 +276,12 @@ describe('Execution tests for @include/@skip', () => { } `); - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -253,7 +301,9 @@ describe('Execution tests for @include/@skip', () => { }); it('Non top-level @include never included', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t { ... on T1 { @@ -262,7 +312,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); // Importantly, we only bother querying S1 @@ -283,7 +334,12 @@ describe('Execution tests for @include/@skip', () => { } `); - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -301,7 +357,9 @@ describe('Execution tests for @include/@skip', () => { }); it('Non top-level @skip always skipped', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t { ... on T1 { @@ -310,7 +368,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); // Importantly, we only bother querying S1 @@ -331,7 +390,12 @@ describe('Execution tests for @include/@skip', () => { } `); - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -349,7 +413,9 @@ describe('Execution tests for @include/@skip', () => { }); it('Non top-level @include always included', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t { ... on T1 { @@ -358,7 +424,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); // Again, constantly included is the one case that still show up in the fetch, but that's harmless. The point here is @@ -397,7 +464,12 @@ describe('Execution tests for @include/@skip', () => { } `); - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -417,7 +489,9 @@ describe('Execution tests for @include/@skip', () => { }); it('Non top-level @skip always included', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t { ... on T1 { @@ -426,7 +500,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); // Again, constantly included is the one case that still show up in the fetch, but that's harmless. The point here is @@ -465,7 +540,12 @@ describe('Execution tests for @include/@skip', () => { } `); - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -486,10 +566,14 @@ describe('Execution tests for @include/@skip', () => { }); describe('Simple variable conditions handling', () => { - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema(simpleInterfaceSchemas); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema( + simpleInterfaceSchemas, + ); it('handles default values for condition variables', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query ($if: Boolean! = false){ t { id @@ -498,7 +582,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); // We validate that the condition has been extracted. @@ -540,7 +625,12 @@ describe('Execution tests for @include/@skip', () => { s2Queries = []; // No variables: the default (not included) should be used. - let response = await executePlan(queryPlan, operation, schema, serviceMap); + let response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -561,7 +651,9 @@ describe('Execution tests for @include/@skip', () => { s2Queries = []; // Checks that the overriding of the default does work. - response = await executePlan(queryPlan, operation, schema, serviceMap, { if: true }); + response = await executePlan(queryPlan, operation, schema, serviceMap, { + if: true, + }); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -584,7 +676,9 @@ describe('Execution tests for @include/@skip', () => { }); it('handles condition on named fragments spread', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query ($if: Boolean!){ t { id @@ -595,7 +689,8 @@ describe('Execution tests for @include/@skip', () => { fragment GetB1 on T1 { b1 } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` @@ -635,7 +730,13 @@ describe('Execution tests for @include/@skip', () => { `); s2Queries = []; - let response = await executePlan(queryPlan, operation, schema, serviceMap, { if: true }); + let response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + { if: true }, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -658,7 +759,9 @@ describe('Execution tests for @include/@skip', () => { s2Queries = []; // Checks that the overriding of the default does work. - response = await executePlan(queryPlan, operation, schema, serviceMap, { if: false }); + response = await executePlan(queryPlan, operation, schema, serviceMap, { + if: false, + }); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -679,7 +782,9 @@ describe('Execution tests for @include/@skip', () => { }); it('handles condition inside named fragments', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query ($if: Boolean!){ t { id @@ -690,7 +795,8 @@ describe('Execution tests for @include/@skip', () => { fragment OtherGetB1 on T1 { b1 @include(if: $if) } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` @@ -730,7 +836,13 @@ describe('Execution tests for @include/@skip', () => { `); s2Queries = []; - let response = await executePlan(queryPlan, operation, schema, serviceMap, { if: true }); + let response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + { if: true }, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -753,7 +865,9 @@ describe('Execution tests for @include/@skip', () => { s2Queries = []; // Checks that the overriding of the default does work. - response = await executePlan(queryPlan, operation, schema, serviceMap, { if: false }); + response = await executePlan(queryPlan, operation, schema, serviceMap, { + if: false, + }); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -772,13 +886,17 @@ describe('Execution tests for @include/@skip', () => { `); expect(s2Queries).toHaveLength(0); }); - }) + }); describe('Fetches with multiple top-level conditioned types', () => { - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema(simpleInterfaceSchemas); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema( + simpleInterfaceSchemas, + ); it('creates a condition node when a condition covers a whole fetch', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query ($if: Boolean!){ t { id @@ -790,7 +908,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); // We validate that the condition has been extracted. @@ -842,7 +961,13 @@ describe('Execution tests for @include/@skip', () => { `); s2Queries = []; - let response = await executePlan(queryPlan, operation, schema, serviceMap, { if: true }); + let response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + { if: true }, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -866,7 +991,9 @@ describe('Execution tests for @include/@skip', () => { expect(s2Queries).toHaveLength(3); s2Queries = []; - response = await executePlan(queryPlan, operation, schema, serviceMap, { if: false }); + response = await executePlan(queryPlan, operation, schema, serviceMap, { + if: false, + }); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -888,7 +1015,9 @@ describe('Execution tests for @include/@skip', () => { }); it('does _not_ creates a condition node when no single condition covers the whole fetch', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query ($if1: Boolean!, $if2: Boolean!){ t { id @@ -900,7 +1029,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); // We validate that the condition has been extracted. @@ -950,7 +1080,13 @@ describe('Execution tests for @include/@skip', () => { `); s2Queries = []; - let response = await executePlan(queryPlan, operation, schema, serviceMap, { if1: true, if2: true }); + let response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + { if1: true, if2: true }, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -974,7 +1110,10 @@ describe('Execution tests for @include/@skip', () => { expect(s2Queries).toHaveLength(3); s2Queries = []; - response = await executePlan(queryPlan, operation, schema, serviceMap, { if1: false, if2: false }); + response = await executePlan(queryPlan, operation, schema, serviceMap, { + if1: false, + if2: false, + }); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -999,7 +1138,10 @@ describe('Execution tests for @include/@skip', () => { expect(s2Queries).toHaveLength(3); s2Queries = []; - response = await executePlan(queryPlan, operation, schema, serviceMap, { if1: false, if2: true }); + response = await executePlan(queryPlan, operation, schema, serviceMap, { + if1: false, + if2: true, + }); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -1024,7 +1166,9 @@ describe('Execution tests for @include/@skip', () => { it('handles "nested" conditions', async () => { // Shows that as long as the condition matches, even "nested" @include gets handled as a condition node. - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query ($if1: Boolean!, $if2: Boolean!){ t { id @@ -1036,7 +1180,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); // We validate that the condition has been extracted. @@ -1090,7 +1235,13 @@ describe('Execution tests for @include/@skip', () => { `); s2Queries = []; - let response = await executePlan(queryPlan, operation, schema, serviceMap, { if1: true, if2: true }); + let response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + { if1: true, if2: true }, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -1114,7 +1265,10 @@ describe('Execution tests for @include/@skip', () => { expect(s2Queries).toHaveLength(3); s2Queries = []; - response = await executePlan(queryPlan, operation, schema, serviceMap, { if1: false, if2: true }); + response = await executePlan(queryPlan, operation, schema, serviceMap, { + if1: false, + if2: true, + }); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -1135,7 +1289,10 @@ describe('Execution tests for @include/@skip', () => { expect(s2Queries).toHaveLength(0); s2Queries = []; - response = await executePlan(queryPlan, operation, schema, serviceMap, { if1: true, if2: false }); + response = await executePlan(queryPlan, operation, schema, serviceMap, { + if1: true, + if2: false, + }); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -1157,7 +1314,9 @@ describe('Execution tests for @include/@skip', () => { describe('same element having both @skip and @include', () => { it('with constant conditions, neither excluding', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t { id @@ -1166,7 +1325,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` @@ -1208,7 +1368,12 @@ describe('Execution tests for @include/@skip', () => { `); s2Queries = []; - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -1231,7 +1396,9 @@ describe('Execution tests for @include/@skip', () => { }); it('with constant conditions, both excluding', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t { id @@ -1240,7 +1407,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` @@ -1261,7 +1429,12 @@ describe('Execution tests for @include/@skip', () => { `); s2Queries = []; - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -1282,7 +1455,9 @@ describe('Execution tests for @include/@skip', () => { }); it('with constant conditions, first excluding', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t { id @@ -1291,7 +1466,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` @@ -1312,7 +1488,12 @@ describe('Execution tests for @include/@skip', () => { `); s2Queries = []; - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -1333,7 +1514,9 @@ describe('Execution tests for @include/@skip', () => { }); it('with constant conditions, second excluding', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t { id @@ -1342,7 +1525,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` @@ -1363,7 +1547,12 @@ describe('Execution tests for @include/@skip', () => { `); s2Queries = []; - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -1384,7 +1573,9 @@ describe('Execution tests for @include/@skip', () => { }); it('with variable conditions', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query ($if1: Boolean!, $if2: Boolean!) { t { id @@ -1393,7 +1584,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); // Ensures both @skip and @include have condition nodes. @@ -1441,7 +1633,13 @@ describe('Execution tests for @include/@skip', () => { s2Queries = []; // With data included by both conditions - let response = await executePlan(queryPlan, operation, schema, serviceMap, { if1: true, if2: false }); + let response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + { if1: true, if2: false }, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -1464,7 +1662,10 @@ describe('Execution tests for @include/@skip', () => { s2Queries = []; // With data excluded by one condition - response = await executePlan(queryPlan, operation, schema, serviceMap, { if1: true, if2: true }); + response = await executePlan(queryPlan, operation, schema, serviceMap, { + if1: true, + if2: true, + }); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -1483,6 +1684,6 @@ describe('Execution tests for @include/@skip', () => { `); expect(s2Queries).toHaveLength(0); }); - }) - }) + }); + }); }); diff --git a/gateway-js/src/__tests__/executeQueryPlan.introspection.test.ts b/gateway-js/src/__tests__/executeQueryPlan.introspection.test.ts index 86e2e2baa..8202dcc32 100644 --- a/gateway-js/src/__tests__/executeQueryPlan.introspection.test.ts +++ b/gateway-js/src/__tests__/executeQueryPlan.introspection.test.ts @@ -1,13 +1,25 @@ import gql from 'graphql-tag'; -import { getFederatedTestingSchema, ServiceDefinitionModule } from './execution-utils'; -import { Operation, parseOperation, Schema } from "@apollo/federation-internals"; +import { + getFederatedTestingSchema, + ServiceDefinitionModule, +} from './execution-utils'; +import { + Operation, + parseOperation, + Schema, +} from '@apollo/federation-internals'; import { QueryPlan } from '@apollo/query-planner'; import { LocalGraphQLDataSource } from '../datasources'; -import { GatewayExecutionResult, GatewayGraphQLRequestContext } from '@apollo/server-gateway-interface'; +import { + GatewayExecutionResult, + GatewayGraphQLRequestContext, +} from '@apollo/server-gateway-interface'; import { buildOperationContext } from '../operationContext'; import { executeQueryPlan } from '../executeQueryPlan'; -function buildRequestContext(variables: Record): GatewayGraphQLRequestContext { +function buildRequestContext( + variables: Record, +): GatewayGraphQLRequestContext { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return { @@ -30,7 +42,9 @@ async function executePlan( const apiSchema = schema.toAPISchema(); const operationContext = buildOperationContext({ schema: apiSchema.toGraphQLJSSchema(), - operationDocument: gql`${operation.toString()}`, + operationDocument: gql` + ${operation.toString()} + `, }); return executeQueryPlan( queryPlan, @@ -67,20 +81,29 @@ describe('handling of introspection queries', () => { `, }, ]; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema(typeDefs); + const { serviceMap, schema, queryPlanner } = + getFederatedTestingSchema(typeDefs); it('it handles aliases on introspection fields', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { myAlias: __type(name: "T1") { kind name } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -93,17 +116,25 @@ describe('handling of introspection queries', () => { }); it('it handles aliases inside introspection fields', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { __type(name: "T1") { myKind: kind name } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -116,17 +147,26 @@ describe('handling of introspection queries', () => { }); it('it handles variables passed to introspection fields', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query ($name: String!) { __type(name: $name) { kind name } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); - const response = await executePlan(queryPlan, operation, schema, serviceMap, { name: "T1" }); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + { name: 'T1' }, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { diff --git a/gateway-js/src/__tests__/executeQueryPlan.test.ts b/gateway-js/src/__tests__/executeQueryPlan.test.ts index 659ee5b7e..160f4cf0e 100644 --- a/gateway-js/src/__tests__/executeQueryPlan.test.ts +++ b/gateway-js/src/__tests__/executeQueryPlan.test.ts @@ -18,12 +18,21 @@ import { QueryPlan, QueryPlanner } from '@apollo/query-planner'; import { ApolloGateway } from '..'; import { ApolloServer } from '@apollo/server'; import { getFederatedTestingSchema } from './execution-utils'; -import { Schema, Operation, parseOperation, arrayEquals, Supergraph } from '@apollo/federation-internals'; +import { + Schema, + Operation, + parseOperation, + arrayEquals, + Supergraph, +} from '@apollo/federation-internals'; import { addResolversToSchema, GraphQLResolverMap, } from '@apollo/subgraph/src/schema-helper'; -import {GatewayExecutionResult, GatewayGraphQLRequestContext} from '@apollo/server-gateway-interface'; +import { + GatewayExecutionResult, + GatewayGraphQLRequestContext, +} from '@apollo/server-gateway-interface'; import { unwrapSingleResultKind } from './gateway/testUtils'; expect.addSnapshotSerializer(astSerializer); @@ -35,26 +44,35 @@ describe('executeQueryPlan', () => { }; const parseOp = (operation: string, operationSchema?: Schema): Operation => { - return parseOperation((operationSchema ?? schema), operation); - } + return parseOperation(operationSchema ?? schema, operation); + }; - const buildPlan = (operation: string | Operation, operationQueryPlanner?: QueryPlanner, operationSchema?: Schema): QueryPlan => { - const op = typeof operation === 'string' ? parseOp(operation, operationSchema): operation; + const buildPlan = ( + operation: string | Operation, + operationQueryPlanner?: QueryPlanner, + operationSchema?: Schema, + ): QueryPlan => { + const op = + typeof operation === 'string' + ? parseOp(operation, operationSchema) + : operation; return (operationQueryPlanner ?? queryPlanner).buildQueryPlan(op); - } + }; async function executePlan( queryPlan: QueryPlan, operation: Operation, executeRequestContext?: GatewayGraphQLRequestContext, executeSchema?: Schema, - executeServiceMap?: { [serviceName: string]: LocalGraphQLDataSource } + executeServiceMap?: { [serviceName: string]: LocalGraphQLDataSource }, ): Promise { const supergraphSchema = executeSchema ?? schema; const apiSchema = supergraphSchema.toAPISchema(); const operationContext = buildOperationContext({ schema: apiSchema.toGraphQLJSSchema(), - operationDocument: gql`${operation.toString()}`, + operationDocument: gql` + ${operation.toString()} + `, }); return executeQueryPlan( queryPlan, @@ -66,10 +84,13 @@ describe('executeQueryPlan', () => { ); } - async function executeOperation(operationString: string, requestContext?: GatewayGraphQLRequestContext): Promise { - const operation = parseOp(operationString); - const queryPlan = buildPlan(operation); - return executePlan(queryPlan, operation, requestContext); + async function executeOperation( + operationString: string, + requestContext?: GatewayGraphQLRequestContext, + ): Promise { + const operation = parseOp(operationString); + const queryPlan = buildPlan(operation); + return executePlan(queryPlan, operation, requestContext); } function overrideResolversInService( @@ -155,7 +176,7 @@ describe('executeQueryPlan', () => { 'errors.0.message', 'Something went wrong', ); - expect(response).toHaveProperty('errors.0.path', ["me"]); + expect(response).toHaveProperty('errors.0.path', ['me']); expect(response).toHaveProperty( 'errors.0.extensions.code', 'UNAUTHENTICATED', @@ -1276,7 +1297,12 @@ describe('executeQueryPlan', () => { const operation = parseOp(`${getIntrospectionQuery()}`, schema); queryPlanner = new QueryPlanner(supergraph); const queryPlan = queryPlanner.buildQueryPlan(operation); - const response = await executePlan(queryPlan, operation, undefined, schema); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + ); expect(response.data).toHaveProperty('__schema'); expect(response.errors).toBeUndefined(); @@ -1310,7 +1336,12 @@ describe('executeQueryPlan', () => { queryPlanner = new QueryPlanner(supergraph); const queryPlan = queryPlanner.buildQueryPlan(operation); - const response = await executePlan(queryPlan, operation, undefined, schema); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + ); expect(response.data).toBeUndefined(); expect(response.errors).toMatchInlineSnapshot(` Array [ @@ -1390,7 +1421,12 @@ describe('executeQueryPlan', () => { queryPlanner = new QueryPlanner(supergraph); const queryPlan = queryPlanner.buildQueryPlan(operation); - const response = await executePlan(queryPlan, operation, undefined, schema); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + ); expect(response.data?.vehicle).toEqual(null); // This kind of error is only found by the post-processing of the gateway, and post-processing errors are currently not returned @@ -1425,24 +1461,24 @@ describe('executeQueryPlan', () => { } `; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ { name: 'S1', typeDefs: s1 }, - { name: 'S2', typeDefs: s2 } + { name: 'S2', typeDefs: s2 }, ]); addResolversToSchema(serviceMap['S1'].schema, { Query: { getA() { return { - getA: {} + getA: {}, }; }, }, A: { q() { return Object.create(null); - } - } + }, + }, }); addResolversToSchema(serviceMap['S2'].schema, { @@ -1453,7 +1489,8 @@ describe('executeQueryPlan', () => { }, }); - const operation = parseOp(` + const operation = parseOp( + ` query { getA { q { @@ -1461,7 +1498,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); @@ -1490,7 +1529,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { @@ -1501,66 +1546,69 @@ describe('executeQueryPlan', () => { }, } `); - }) + }); it('can query other subgraphs when the Query type is the type of a field after a mutation', async () => { - const s1 = gql` - type Query { - one: Int - } + const s1 = gql` + type Query { + one: Int + } - type Mutation { - mutateSomething: Query - } - `; + type Mutation { + mutateSomething: Query + } + `; - const s2 = gql` - type Query { - two: Int - } - `; + const s2 = gql` + type Query { + two: Int + } + `; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ - { name: 'S1', typeDefs: s1 }, - { name: 'S2', typeDefs: s2 } - ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + { name: 'S1', typeDefs: s1 }, + { name: 'S2', typeDefs: s2 }, + ]); - let hasMutated = false; + let hasMutated = false; - addResolversToSchema(serviceMap['S1'].schema, { - Query: { - one() { - return 1; - }, + addResolversToSchema(serviceMap['S1'].schema, { + Query: { + one() { + return 1; }, - Mutation: { - mutateSomething() { - hasMutated = true; - return {}; - }, - } - }); + }, + Mutation: { + mutateSomething() { + hasMutated = true; + return {}; + }, + }, + }); - addResolversToSchema(serviceMap['S2'].schema, { - Query: { - two() { - return 2; - }, + addResolversToSchema(serviceMap['S2'].schema, { + Query: { + two() { + return 2; }, - }); + }, + }); - const operation = parseOp(` + const operation = parseOp( + ` mutation { mutateSomething { one two } } - `, schema); + `, + schema, + ); - const queryPlan = buildPlan(operation, queryPlanner); + const queryPlan = buildPlan(operation, queryPlanner); - expect(queryPlan).toMatchInlineSnapshot(` + expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { Sequence { Fetch(service: "S1") { @@ -1584,10 +1632,16 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); - expect(hasMutated).toBeTruthy(); - expect(response.data).toMatchInlineSnapshot(` + expect(hasMutated).toBeTruthy(); + expect(response.data).toMatchInlineSnapshot(` Object { "mutateSomething": Object { "one": 1, @@ -1595,7 +1649,7 @@ describe('executeQueryPlan', () => { }, } `); - }) + }); it('can mutate other subgraphs when the Mutation type is the type of a field', async () => { const s1 = gql` @@ -1618,9 +1672,9 @@ describe('executeQueryPlan', () => { } `; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ { name: 'S1', typeDefs: s1 }, - { name: 'S2', typeDefs: s2 } + { name: 'S2', typeDefs: s2 }, ]); let mutateOneCalled = false; @@ -1630,21 +1684,21 @@ describe('executeQueryPlan', () => { Query: { getA() { return { - getA: {} + getA: {}, }; }, }, A: { m() { return Object.create(null); - } + }, }, Mutation: { mutateOne() { mutateOneCalled = true; return 1; - } - } + }, + }, }); addResolversToSchema(serviceMap['S2'].schema, { @@ -1656,7 +1710,8 @@ describe('executeQueryPlan', () => { }, }); - const operation = parseOp(` + const operation = parseOp( + ` query { getA { m { @@ -1665,7 +1720,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); @@ -1695,7 +1752,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(mutateOneCalled).toBeTruthy(); expect(mutateTwoCalled).toBeTruthy(); expect(response.data).toMatchInlineSnapshot(` @@ -1708,57 +1771,58 @@ describe('executeQueryPlan', () => { }, } `); - }) + }); it('can mutate other subgraphs when the Mutation type is the type of a field after a mutation', async () => { - const s1 = gql` - type Query { - one: Int - } + const s1 = gql` + type Query { + one: Int + } - type Mutation { - mutateSomething: Mutation - } - `; + type Mutation { + mutateSomething: Mutation + } + `; - const s2 = gql` - type Mutation { - mutateTwo: Int - } - `; + const s2 = gql` + type Mutation { + mutateTwo: Int + } + `; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ - { name: 'S1', typeDefs: s1 }, - { name: 'S2', typeDefs: s2 } - ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + { name: 'S1', typeDefs: s1 }, + { name: 'S2', typeDefs: s2 }, + ]); - let somethingMutationCount = 0; - let hasMutatedTwo = false; + let somethingMutationCount = 0; + let hasMutatedTwo = false; - addResolversToSchema(serviceMap['S1'].schema, { - Query: { - one() { - return 1; - }, + addResolversToSchema(serviceMap['S1'].schema, { + Query: { + one() { + return 1; }, - Mutation: { - mutateSomething() { - ++somethingMutationCount; - return {}; - }, - } - }); + }, + Mutation: { + mutateSomething() { + ++somethingMutationCount; + return {}; + }, + }, + }); - addResolversToSchema(serviceMap['S2'].schema, { - Mutation: { - mutateTwo() { - hasMutatedTwo = true; - return 2; - }, + addResolversToSchema(serviceMap['S2'].schema, { + Mutation: { + mutateTwo() { + hasMutatedTwo = true; + return 2; }, - }); + }, + }); - const operation = parseOp(` + const operation = parseOp( + ` mutation { mutateSomething { mutateSomething { @@ -1766,11 +1830,13 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); - const queryPlan = buildPlan(operation, queryPlanner); + const queryPlan = buildPlan(operation, queryPlanner); - expect(queryPlan).toMatchInlineSnapshot(` + expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { Sequence { Fetch(service: "S1") { @@ -1795,11 +1861,17 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); - expect(somethingMutationCount).toBe(2); - expect(hasMutatedTwo).toBeTruthy(); - expect(response.data).toMatchInlineSnapshot(` + expect(somethingMutationCount).toBe(2); + expect(hasMutatedTwo).toBeTruthy(); + expect(response.data).toMatchInlineSnapshot(` Object { "mutateSomething": Object { "mutateSomething": Object { @@ -1808,7 +1880,7 @@ describe('executeQueryPlan', () => { }, } `); - }) + }); }); describe('interfaces on interfaces', () => { @@ -1847,7 +1919,8 @@ describe('executeQueryPlan', () => { c: String } - type T4 implements SubInterface1 & SubInterface2 & TopInterface @key(fields: "a") { + type T4 implements SubInterface1 & SubInterface2 & TopInterface + @key(fields: "a") { a: Int b: String @external c: String @external @@ -1867,24 +1940,34 @@ describe('executeQueryPlan', () => { } `; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ { name: 'S1', typeDefs: s1 }, - { name: 'S2', typeDefs: s2 } + { name: 'S2', typeDefs: s2 }, ]); - const t1s_s1: any[] = [{ __typename: 'T1', a: 1, b: 'T1_v1'}, {__typename: 'T1', a: 2, b: 'T1_v2'}]; - const t2s_s1: any[] = [{__typename: 'T2', b: 'k1'}, {__typename: 'T2', b: 'k2'}]; - const t3s_s1: any[] = [{__typename: 'T3', a: 42, c: 'T3_v1'}]; - const t4s_s1: any[] = [{__typename: 'T4', a: 0}, {__typename: 'T4', a: 10}, {__typename: 'T4', a: 20}]; + const t1s_s1: any[] = [ + { __typename: 'T1', a: 1, b: 'T1_v1' }, + { __typename: 'T1', a: 2, b: 'T1_v2' }, + ]; + const t2s_s1: any[] = [ + { __typename: 'T2', b: 'k1' }, + { __typename: 'T2', b: 'k2' }, + ]; + const t3s_s1: any[] = [{ __typename: 'T3', a: 42, c: 'T3_v1' }]; + const t4s_s1: any[] = [ + { __typename: 'T4', a: 0 }, + { __typename: 'T4', a: 10 }, + { __typename: 'T4', a: 20 }, + ]; - const t2s_s2 = new Map(); - t2s_s2.set('k1', {a: 12 , b: 'k1'}); - t2s_s2.set('k2', {a: 24 , b: 'k2'}); + const t2s_s2 = new Map(); + t2s_s2.set('k1', { a: 12, b: 'k1' }); + t2s_s2.set('k2', { a: 24, b: 'k2' }); - const t4s_s2 = new Map(); - t4s_s2.set(0, {a: 0, b: 'b_0', c: 'c_0'}); - t4s_s2.set(10, {a: 10, b: 'b_10', c: 'c_10'}); - t4s_s2.set(20, {a: 20, b: 'b_20', c: 'c_20'}); + const t4s_s2 = new Map(); + t4s_s2.set(0, { a: 0, b: 'b_0', c: 'c_0' }); + t4s_s2.set(10, { a: 10, b: 'b_10', c: 'c_10' }); + t4s_s2.set(20, { a: 20, b: 'b_20', c: 'c_20' }); addResolversToSchema(serviceMap['S1'].schema, { Query: { @@ -1898,16 +1981,17 @@ describe('executeQueryPlan', () => { T2: { __resolveReference(ref) { return t2s_s2.get(ref.b); - } + }, }, T4: { __resolveReference(ref) { return t4s_s2.get(ref.a); - } + }, }, }); - const operation = parseOp(` + const operation = parseOp( + ` query { allValues { a @@ -1919,7 +2003,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); @@ -1976,7 +2062,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { @@ -2058,7 +2150,8 @@ describe('executeQueryPlan', () => { c: String } - type T4 implements SubInterface1 & SubInterface2 & TopInterface @key(fields: "a") { + type T4 implements SubInterface1 & SubInterface2 & TopInterface + @key(fields: "a") { a: Int b: String @external c: String @external @@ -2078,24 +2171,34 @@ describe('executeQueryPlan', () => { } `; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ { name: 'S1', typeDefs: s1 }, - { name: 'S2', typeDefs: s2 } + { name: 'S2', typeDefs: s2 }, ]); - const t1s_s1: any[] = [{ __typename: 'T1', a: 1, b: 'T1_v1'}, {__typename: 'T1', a: 2, b: 'T1_v2'}]; - const t2s_s1: any[] = [{__typename: 'T2', a: 12}, {__typename: 'T2', a: 24}]; - const t3s_s1: any[] = [{__typename: 'T3', a: 42, c: 'T3_v1'}]; - const t4s_s1: any[] = [{__typename: 'T4', a: 0}, {__typename: 'T4', a: 10}, {__typename: 'T4', a: 20}]; + const t1s_s1: any[] = [ + { __typename: 'T1', a: 1, b: 'T1_v1' }, + { __typename: 'T1', a: 2, b: 'T1_v2' }, + ]; + const t2s_s1: any[] = [ + { __typename: 'T2', a: 12 }, + { __typename: 'T2', a: 24 }, + ]; + const t3s_s1: any[] = [{ __typename: 'T3', a: 42, c: 'T3_v1' }]; + const t4s_s1: any[] = [ + { __typename: 'T4', a: 0 }, + { __typename: 'T4', a: 10 }, + { __typename: 'T4', a: 20 }, + ]; - const t2s_s2 = new Map(); - t2s_s2.set(12, {a: 12 , b: 'k1'}); - t2s_s2.set(24, {a: 24 , b: 'k2'}); + const t2s_s2 = new Map(); + t2s_s2.set(12, { a: 12, b: 'k1' }); + t2s_s2.set(24, { a: 24, b: 'k2' }); - const t4s_s2 = new Map(); - t4s_s2.set(0, {a: 0, b: 'b_0', c: 'c_0'}); - t4s_s2.set(10, {a: 10, b: 'b_10', c: 'c_10'}); - t4s_s2.set(20, {a: 20, b: 'b_20', c: 'c_20'}); + const t4s_s2 = new Map(); + t4s_s2.set(0, { a: 0, b: 'b_0', c: 'c_0' }); + t4s_s2.set(10, { a: 10, b: 'b_10', c: 'c_10' }); + t4s_s2.set(20, { a: 20, b: 'b_20', c: 'c_20' }); addResolversToSchema(serviceMap['S1'].schema, { Query: { @@ -2109,22 +2212,25 @@ describe('executeQueryPlan', () => { T2: { __resolveReference(ref) { return t2s_s2.get(ref.b); - } + }, }, T4: { __resolveReference(ref) { return t4s_s2.get(ref.a); - } + }, }, }); - let operation = parseOp(` + let operation = parseOp( + ` query { allValues { a } } - `, schema); + `, + schema, + ); let queryPlan = buildPlan(operation, queryPlanner); @@ -2141,7 +2247,13 @@ describe('executeQueryPlan', () => { } `); - let response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + let response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "allValues": Array [ @@ -2173,7 +2285,8 @@ describe('executeQueryPlan', () => { } `); - operation = parseOp(` + operation = parseOp( + ` query { allValues { ... on SubInterface1 { @@ -2181,7 +2294,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); queryPlan = buildPlan(operation, queryPlanner); @@ -2209,7 +2324,13 @@ describe('executeQueryPlan', () => { } `); - response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "allValues": Array [ @@ -2261,8 +2382,8 @@ describe('executeQueryPlan', () => { type MyTypeB implements MyInterface { name: String } - ` - } + `, + }; const s2 = { name: 'S2', @@ -2274,15 +2395,18 @@ describe('executeQueryPlan', () => { type MyTypeC implements MyInterface { name: String } - ` - } + `, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); addResolversToSchema(serviceMap['S1'].schema, { Query: { myField() { - return { __typename: 'MyTypeA', name: "foo" }; + return { __typename: 'MyTypeA', name: 'foo' }; }, }, }); @@ -2290,13 +2414,16 @@ describe('executeQueryPlan', () => { // First, we just query the field without conditions. // Note that there is no reason to type-explode this: clearly, `myField` will never return a `MyTypeC` since // it's resolved by S1 which doesn't know that type, but that doesn't impact the plan. - let operation = parseOp(` + let operation = parseOp( + ` query { myField { name } } - `, schema); + `, + schema, + ); let queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -2310,7 +2437,13 @@ describe('executeQueryPlan', () => { }, } `); - let response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + let response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "myField": Object { @@ -2321,7 +2454,8 @@ describe('executeQueryPlan', () => { // Now forcing the query planning to notice that `MyTypeC` can never happen and making // sure it doesn't ask it from S1, which doesn't know it. - operation = parseOp(` + operation = parseOp( + ` query { myField { ... on MyTypeA { @@ -2332,7 +2466,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -2349,7 +2485,13 @@ describe('executeQueryPlan', () => { } `); - response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "myField": Object { @@ -2358,10 +2500,10 @@ describe('executeQueryPlan', () => { } `); - // Testing only getting name for `MyTypeB`, which is known by S1, but not returned // by `myField` in practice (so the result is "empty"). - operation = parseOp(` + operation = parseOp( + ` query { myField { ... on MyTypeB { @@ -2369,7 +2511,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` @@ -2386,14 +2530,21 @@ describe('executeQueryPlan', () => { }, } `); - response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "myField": Object {}, } `); - operation = parseOp(` + operation = parseOp( + ` query { myField { ... on MyTypeC { @@ -2401,7 +2552,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); // Lastly, same with only getting name for `MyTypeC`. It isn't known by S1 so the condition should not // be included in the query, but we should still query `myField` to know if it resolve to "something" @@ -2421,7 +2574,13 @@ describe('executeQueryPlan', () => { } `); - response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "myField": Object {}, @@ -2432,9 +2591,9 @@ describe('executeQueryPlan', () => { describe('@requires', () => { test('handles null in required field correctly (with nullable fields)', async () => { const s1_data = [ - { id: 0, f1: "foo" }, + { id: 0, f1: 'foo' }, { id: 1, f1: null }, - { id: 2, f1: "bar" }, + { id: 2, f1: 'bar' }, ]; const s1 = { @@ -2447,12 +2606,12 @@ describe('executeQueryPlan', () => { `, resolvers: { T1: { - __resolveReference(ref: {id: number}) { + __resolveReference(ref: { id: number }) { return s1_data[ref.id]; }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -2474,7 +2633,7 @@ describe('executeQueryPlan', () => { resolvers: { Query: { getT1s() { - return [{id: 0}, {id: 1}, {id: 2}]; + return [{ id: 0 }, { id: 1 }, { id: 2 }]; }, }, T1: { @@ -2484,14 +2643,18 @@ describe('executeQueryPlan', () => { }, f2(o: { f1: string }) { return o.f1 === null ? null : { a: `t1:${o.f1}` }; - } - } - } - } + }, + }, + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` query { getT1s { id @@ -2501,7 +2664,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -2550,7 +2715,13 @@ describe('executeQueryPlan', () => { }, } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "getT1s": Array [ @@ -2581,9 +2752,9 @@ describe('executeQueryPlan', () => { test('handles null in required field correctly (with @require field non-nullable)', async () => { const s1_data = [ - { id: 0, f1: "foo" }, + { id: 0, f1: 'foo' }, { id: 1, f1: null }, - { id: 2, f1: "bar" }, + { id: 2, f1: 'bar' }, ]; const s1 = { @@ -2600,8 +2771,8 @@ describe('executeQueryPlan', () => { return s1_data[ref.id]; }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -2623,7 +2794,7 @@ describe('executeQueryPlan', () => { resolvers: { Query: { getT1s() { - return [{id: 0}, {id: 1}, {id: 2}]; + return [{ id: 0 }, { id: 1 }, { id: 2 }]; }, }, T1: { @@ -2633,14 +2804,18 @@ describe('executeQueryPlan', () => { }, f2(o: { f1: string }) { return o.f1 === null ? null : { a: `t1:${o.f1}` }; - } - } - } - } + }, + }, + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` query { getT1s { id @@ -2650,7 +2825,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -2700,7 +2877,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); // `null` should bubble up since `f2` is now non-nullable. But we should still get the `id: 0` response. expect(response.data).toMatchInlineSnapshot(` Object { @@ -2725,7 +2908,9 @@ describe('executeQueryPlan', () => { `); // We returning `null` for f2 which isn't nullable, so it bubbled up and we should have an error - expect(response.errors?.map((e) => e.message)).toStrictEqual(['Cannot return null for non-nullable field T1.f2.']); + expect(response.errors?.map((e) => e.message)).toStrictEqual([ + 'Cannot return null for non-nullable field T1.f2.', + ]); }); test('handles null in required field correctly (with non-nullable required field)', async () => { @@ -2739,12 +2924,12 @@ describe('executeQueryPlan', () => { `, resolvers: { T1: { - __resolveReference(ref: { id: number}) { + __resolveReference(ref: { id: number }) { return s1_data[ref.id]; }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -2766,7 +2951,7 @@ describe('executeQueryPlan', () => { resolvers: { Query: { getT1s() { - return [{id: 0}, {id: 1}, {id: 2}]; + return [{ id: 0 }, { id: 1 }, { id: 2 }]; }, }, T1: { @@ -2776,20 +2961,24 @@ describe('executeQueryPlan', () => { }, f2(o: { f1: string }) { return o.f1 === null ? null : { a: `t1:${o.f1}` }; - } - } - } - } + }, + }, + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); const s1_data = [ - { id: 0, f1: "foo" }, + { id: 0, f1: 'foo' }, { id: 1, f1: null }, - { id: 2, f1: "bar" }, + { id: 2, f1: 'bar' }, ]; - const operation = parseOp(` + const operation = parseOp( + ` query { getT1s { id @@ -2799,7 +2988,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -2849,7 +3040,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); // `null` should bubble up since `f2` is now non-nullable. But we should still get the `id: 0` response. expect(response.data).toMatchInlineSnapshot(` Object { @@ -2872,7 +3069,9 @@ describe('executeQueryPlan', () => { ], } `); - expect(response.errors?.map((e) => e.message)).toStrictEqual(['Cannot return null for non-nullable field T1.f1.']); + expect(response.errors?.map((e) => e.message)).toStrictEqual([ + 'Cannot return null for non-nullable field T1.f1.', + ]); }); test('handles errors in required field correctly (with nullable fields)', async () => { @@ -2891,15 +3090,19 @@ describe('executeQueryPlan', () => { }, f1(o: { id: number }) { switch (o.id) { - case 0: return "foo"; - case 1: return [ "invalid" ]; // This will effectively throw - case 2: return "bar"; - default: throw new Error('Not handled'); + case 0: + return 'foo'; + case 1: + return ['invalid']; // This will effectively throw + case 2: + return 'bar'; + default: + throw new Error('Not handled'); } - } + }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -2921,7 +3124,7 @@ describe('executeQueryPlan', () => { resolvers: { Query: { getT1s() { - return [{id: 0}, {id: 1}, {id: 2}]; + return [{ id: 0 }, { id: 1 }, { id: 2 }]; }, }, T1: { @@ -2931,14 +3134,18 @@ describe('executeQueryPlan', () => { }, f2(o: { f1: string }) { return o.f1 === null ? null : { a: `t1:${o.f1}` }; - } - } - } - } + }, + }, + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` query { getT1s { id @@ -2948,7 +3155,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -2997,7 +3206,13 @@ describe('executeQueryPlan', () => { }, } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "getT1s": Array [ @@ -3023,7 +3238,9 @@ describe('executeQueryPlan', () => { ], } `); - expect(response.errors?.map((e) => e.message)).toStrictEqual(['String cannot represent value: ["invalid"]']); + expect(response.errors?.map((e) => e.message)).toStrictEqual([ + 'String cannot represent value: ["invalid"]', + ]); }); test('ensures type condition on inaccessible type in @require works correctly', async () => { @@ -3031,7 +3248,10 @@ describe('executeQueryPlan', () => { name: 'data', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"]) + @link( + url: "https://specs.apollo.dev/federation/v2.0" + import: ["@key", "@shareable"] + ) type Entity @key(fields: "id") { id: ID! @@ -3053,29 +3273,28 @@ describe('executeQueryPlan', () => { } `, resolvers: { - Query: { - dummy() { - return {}; - }, + Query: { + dummy() { + return {}; }, - Entity: { - __resolveReference() { - return {}; - }, - id() { - return "id"; - }, - data() { - return { - __typename: "Data", - foo: "foo", - bar: "bar", - }; - }, + }, + Entity: { + __resolveReference() { + return {}; }, - - } - } + id() { + return 'id'; + }, + data() { + return { + __typename: 'Data', + foo: 'foo', + bar: 'bar', + }; + }, + }, + }, + }; let requirerRepresentation: any = undefined; @@ -3083,10 +3302,16 @@ describe('executeQueryPlan', () => { name: 'requirer', typeDefs: gql` extend schema - @link( - url: "https://specs.apollo.dev/federation/v2.0", - import: ["@key", "@shareable", "@external", "@requires", "@inaccessible"] - ) + @link( + url: "https://specs.apollo.dev/federation/v2.0" + import: [ + "@key" + "@shareable" + "@external" + "@requires" + "@inaccessible" + ] + ) type Query { dummy: Entity @@ -3095,7 +3320,8 @@ describe('executeQueryPlan', () => { type Entity @key(fields: "id") { id: ID! data: Foo @external - requirer: String! @requires(fields: "data { foo ... on Bar { bar } }") + requirer: String! + @requires(fields: "data { foo ... on Bar { bar } }") } interface Foo { @@ -3124,24 +3350,30 @@ describe('executeQueryPlan', () => { return {}; }, id() { - return "id"; + return 'id'; }, requirer() { - return "requirer"; + return 'requirer'; }, }, - } - } + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` query { dummy { requirer } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` @@ -3202,7 +3434,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "dummy": Object { @@ -3259,14 +3497,16 @@ describe('executeQueryPlan', () => { }, One: { __resolveReference(ref: { id: string }) { - return ref.id === entityOne.id ? { ...entityOne, ...ref } : undefined; + return ref.id === entityOne.id + ? { ...entityOne, ...ref } + : undefined; }, computed(parent: any) { return `computed value: ${parent.two.external}`; }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -3282,18 +3522,24 @@ describe('executeQueryPlan', () => { return ref.id === entityTwo.id ? entityTwo : undefined; }, }, - } - } + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` { one { computed } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -3346,7 +3592,13 @@ describe('executeQueryPlan', () => { }, } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -3406,40 +3658,43 @@ describe('executeQueryPlan', () => { defaultValue: 24, valuePassed: 42, }, - ])('requires on field with argument: $name', async ({ - argNullable, - defaultValue, - valuePassed, - }: { - argNullable: boolean, - defaultValue?: number, - valuePassed?: number, - }) => { - - const argType = `Int${argNullable ? '' : '!'}${defaultValue ? ` = ${defaultValue}` : ''}`; - const s1 = { - name: 'S1', - typeDefs: gql` + ])( + 'requires on field with argument: $name', + async ({ + argNullable, + defaultValue, + valuePassed, + }: { + argNullable: boolean; + defaultValue?: number; + valuePassed?: number; + }) => { + const argType = `Int${argNullable ? '' : '!'}${ + defaultValue ? ` = ${defaultValue}` : '' + }`; + const s1 = { + name: 'S1', + typeDefs: gql` type T @key(fields: "id") { id: Int! x(opt: ${argType}): String! } `, - resolvers: { - T: { - __resolveReference(ref: { id: number}) { - return ref; + resolvers: { + T: { + __resolveReference(ref: { id: number }) { + return ref; + }, + x(_: any, args: any) { + return `args: ${JSON.stringify(args)}`; + }, }, - x(_: any, args: any) { - return `args: ${JSON.stringify(args)}`; - } }, - } - } + }; - const s2 = { - name: 'S2', - typeDefs: gql` + const s2 = { + name: 'S2', + typeDefs: gql` type Query { t: T } @@ -3447,49 +3702,57 @@ describe('executeQueryPlan', () => { type T @key(fields: "id") { id: Int! x(opt: ${argType}): String! @external - y: String @requires(fields: "x${valuePassed ? `(opt: ${valuePassed})` : ''}") + y: String @requires(fields: "x${ + valuePassed ? `(opt: ${valuePassed})` : '' + }") } `, - resolvers: { - Query: { - t() { - return {id: 0}; + resolvers: { + Query: { + t() { + return { id: 0 }; + }, }, - }, - T: { - __resolveReference(ref: { id: number }) { - // the ref has already the id and f1 is a require is triggered, and we resolve f2 below - return ref; + T: { + __resolveReference(ref: { id: number }) { + // the ref has already the id and f1 is a require is triggered, and we resolve f2 below + return ref; + }, + y(parent: any) { + return `x: ${parent.x}`; + }, }, - y(parent: any) { - return `x: ${parent.x}`; - } - } - } - } + }, + }; - if (!argNullable && !defaultValue && !valuePassed) { - // We test all combination of `argNullable`, `defaultValue` set/unset and `valuePassed` set/unset, and all should be allowed - // except if the value is non-nullable and has neither a default nor a value passed. In that case, just ensure the error message - // is meaningful. - expect(() => getFederatedTestingSchema([ s1, s2 ])).toThrowError( - '[S2] On field "T.y", for @requires(fields: "x"): Missing mandatory value for argument "opt" of field "T.x" in selection "x"' - ); - return; - } + if (!argNullable && !defaultValue && !valuePassed) { + // We test all combination of `argNullable`, `defaultValue` set/unset and `valuePassed` set/unset, and all should be allowed + // except if the value is non-nullable and has neither a default nor a value passed. In that case, just ensure the error message + // is meaningful. + expect(() => getFederatedTestingSchema([s1, s2])).toThrowError( + '[S2] On field "T.y", for @requires(fields: "x"): Missing mandatory value for argument "opt" of field "T.x" in selection "x"', + ); + return; + } - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` { t { y } } - `, schema); - const queryPlan = buildPlan(operation, queryPlanner); - expect(queryPlan).toMatchInlineSnapshot(` + `, + schema, + ); + const queryPlan = buildPlan(operation, queryPlanner); + expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { Sequence { Fetch(service: "S2") { @@ -3535,28 +3798,41 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); - expect(response.errors).toBeUndefined(); - expect(response.data).toMatchInlineSnapshot(` + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); + expect(response.errors).toBeUndefined(); + expect(response.data).toMatchInlineSnapshot(` Object { "t": Object { - "y": "x: args: {${valuePassed ? `\\"opt\\":${valuePassed}` : (defaultValue ? `\\"opt\\":${defaultValue}` : '')}}", + "y": "x: args: {${ + valuePassed + ? `\\"opt\\":${valuePassed}` + : defaultValue + ? `\\"opt\\":${defaultValue}` + : '' + }}", }, } `); - }); + }, + ); test('requires with nested field example from #2683 is fixed', async () => { const parentItems = [ - { "__typename": "ParentItem", "id": '1', "name": "Parent Item #1" }, - { "__typename": "ParentItem", "id": '2', "name": "Parent Item #2" }, - { "__typename": "ParentItem", "id": '3', "name": "Parent Item #3" }, + { __typename: 'ParentItem', id: '1', name: 'Parent Item #1' }, + { __typename: 'ParentItem', id: '2', name: 'Parent Item #2' }, + { __typename: 'ParentItem', id: '3', name: 'Parent Item #3' }, ]; const childItems = [ - { "__typename": "ChildItem", "id": '1', "name": "Child Item #1" }, - { "__typename": "ChildItem", "id": '2', "name": "Child Item #2" }, - { "__typename": "ChildItem", "id": '3', "name": "Child Item #3" }, + { __typename: 'ChildItem', id: '1', name: 'Child Item #1' }, + { __typename: 'ChildItem', id: '2', name: 'Child Item #2' }, + { __typename: 'ChildItem', id: '3', name: 'Child Item #3' }, ]; const s1 = { @@ -3579,11 +3855,11 @@ describe('executeQueryPlan', () => { }, ParentItem: { __resolveReference(ref: { id: string }) { - return parentItems.find(({id}) => ref.id === id); + return parentItems.find(({ id }) => ref.id === id); }, - } - } - } + }, + }, + }; const s2 = { name: 'S2', @@ -3602,22 +3878,22 @@ describe('executeQueryPlan', () => { resolvers: { ChildItem: { __resolveReference(ref: { id: string }) { - return childItems.find(({id}) => ref.id === id); + return childItems.find(({ id }) => ref.id === id); }, parentItem(ref: { id: string }) { - return parentItems.find(({id}) => ref.id === id); - } + return parentItems.find(({ id }) => ref.id === id); + }, }, ParentItem: { __resolveReference(ref: { id: string }) { - return parentItems.find(({id}) => ref.id === id); + return parentItems.find(({ id }) => ref.id === id); }, childItems(ref: { id: string }) { - return [childItems.find(({id}) => ref.id === id)]; - } + return [childItems.find(({ id }) => ref.id === id)]; + }, }, - } - } + }, + }; const s3 = { name: 'S3', @@ -3635,18 +3911,27 @@ describe('executeQueryPlan', () => { `, resolvers: { ChildItem: { - __resolveReference(ref: { id: string, name: string, parentItem: { name: string }}) { + __resolveReference(ref: { + id: string; + name: string; + parentItem: { name: string }; + }) { return { ...ref, message: `${ref.parentItem.name} | ${ref.name}`, }; - } - } - } - } + }, + }, + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2, s3 ]); - let operation = parseOp(` + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + s3, + ]); + let operation = parseOp( + ` query { parentItems { childItems { @@ -3654,7 +3939,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); let queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` @@ -3728,7 +4015,13 @@ describe('executeQueryPlan', () => { }, } `); - let response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + let response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -3758,7 +4051,8 @@ describe('executeQueryPlan', () => { } `); - operation = parseOp(` + operation = parseOp( + ` query { parentItems { childItems { @@ -3770,7 +4064,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` @@ -3867,7 +4163,13 @@ describe('executeQueryPlan', () => { }, } `); - response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -3914,9 +4216,9 @@ describe('executeQueryPlan', () => { describe('@key', () => { test('Works on a list of scalar', async () => { const s1_data = [ - { id: [0, 1], f1: "foo" }, - { id: [2, 3], f1: "bar" }, - { id: [4, 5], f1: "baz" }, + { id: [0, 1], f1: 'foo' }, + { id: [2, 3], f1: 'bar' }, + { id: [4, 5], f1: 'baz' }, ]; const s1 = { @@ -3933,8 +4235,8 @@ describe('executeQueryPlan', () => { return s1_data.find((e) => arrayEquals(e.id, ref.id)); }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -3950,22 +4252,28 @@ describe('executeQueryPlan', () => { resolvers: { Query: { getT1s() { - return [{id: [2, 3]}, {id: [4, 5]}]; + return [{ id: [2, 3] }, { id: [4, 5] }]; }, }, - } - } + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` query { getT1s { id f1 } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -3997,7 +4305,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "getT1s": Array [ @@ -4022,9 +4336,21 @@ describe('executeQueryPlan', () => { test('Works on a list of objects', async () => { const s1_data = [ - { o: [{a: 0, b: "b0", c: "zero"}, {a: 1, b: "b1", c: "one"}], f1: "foo" }, - { o: [{a: 2, b: "b2", c: "two"}], f1: "bar" }, - { o: [{a: 3, b: "b3", c: "three"}, {a: 4, b: "b4", c: "four"}], f1: "baz" }, + { + o: [ + { a: 0, b: 'b0', c: 'zero' }, + { a: 1, b: 'b1', c: 'one' }, + ], + f1: 'foo', + }, + { o: [{ a: 2, b: 'b2', c: 'two' }], f1: 'bar' }, + { + o: [ + { a: 3, b: 'b3', c: 'three' }, + { a: 4, b: 'b4', c: 'four' }, + ], + f1: 'baz', + }, ]; const s1 = { @@ -4043,12 +4369,14 @@ describe('executeQueryPlan', () => { `, resolvers: { T1: { - __resolveReference(ref: { o: {a : number, c: string}[] }) { - return s1_data.find((e) => arrayEquals(e.o, ref.o, (x, y) => x.a === y.a && x.c === y.c)); + __resolveReference(ref: { o: { a: number; c: string }[] }) { + return s1_data.find((e) => + arrayEquals(e.o, ref.o, (x, y) => x.a === y.a && x.c === y.c), + ); }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -4070,15 +4398,27 @@ describe('executeQueryPlan', () => { resolvers: { Query: { getT1s() { - return [{o: [{a: 2, b: "b2", c: "two"}]}, {o: [{a: 3, b: "b3", c: "three"}, {a: 4, b: "b4", c: "four"}]}]; + return [ + { o: [{ a: 2, b: 'b2', c: 'two' }] }, + { + o: [ + { a: 3, b: 'b3', c: 'three' }, + { a: 4, b: 'b4', c: 'four' }, + ], + }, + ]; }, }, - } - } + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` query { getT1s { o { @@ -4089,7 +4429,9 @@ describe('executeQueryPlan', () => { f1 } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -4128,7 +4470,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); // `null` should bubble up since `f2` is now non-nullable. But we should still get the `id: 0` response. expect(response.data).toMatchInlineSnapshot(` Object { @@ -4170,39 +4518,42 @@ describe('executeQueryPlan', () => { }: { s1?: { // additional resolvers for interface I - iResolversExtra?: any, + iResolversExtra?: any; // provide a default __resolveReference for the interface - hasIResolveReference?: boolean, + hasIResolveReference?: boolean; // turn an id into extra data returned by __resolveReference (if hasIResolveReference is true) - iResolveReferenceExtra?: (id: string) => { [k: string]: any }, + iResolveReferenceExtra?: (id: string) => { [k: string]: any }; // additional resolvers for type A - aResolversExtra?: any, + aResolversExtra?: any; // additional resolvers for type B - bResolversExtra?: any, - } + bResolversExtra?: any; + }; }) => { - // The example uses 2 entities: // - one of type A with id='idA' (x=1, y=2, z=3) // - one of type B with id='idB' (x=10, y=20, w=30) - const s1IBaseResolvers = (s1?.hasIResolveReference ?? true) - ? { - __resolveReference(ref: { id: string }) { - const extraFct = s1?.iResolveReferenceExtra; - const extraData = extraFct ? extraFct(ref.id) : {}; - return ref.id === 'idA' - ? { id: ref.id, x: 1, z: 3, ...extraData } - : { id: ref.id, x: 10, w: 30, ...extraData }; - } - } - : {}; + const s1IBaseResolvers = + s1?.hasIResolveReference ?? true + ? { + __resolveReference(ref: { id: string }) { + const extraFct = s1?.iResolveReferenceExtra; + const extraData = extraFct ? extraFct(ref.id) : {}; + return ref.id === 'idA' + ? { id: ref.id, x: 1, z: 3, ...extraData } + : { id: ref.id, x: 10, w: 30, ...extraData }; + }, + } + : {}; const subgraph1 = { name: 'S1', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key"] + ) type Query { iFromS1: I @@ -4229,7 +4580,7 @@ describe('executeQueryPlan', () => { Query: { iFromS1() { return { __typename: 'A', id: 'idA' }; - } + }, }, I: { ...s1IBaseResolvers, @@ -4241,14 +4592,17 @@ describe('executeQueryPlan', () => { B: { ...(s1?.bResolversExtra ?? {}), }, - } - } + }, + }; const subgraph2 = { name: 'S2', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@interfaceObject"]) + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@interfaceObject"] + ) type Query { iFromS2: I @@ -4267,28 +4621,38 @@ describe('executeQueryPlan', () => { id: 'idB', y: 20, }; - } + }, }, I: { __resolveReference(ref: { id: string }) { return { id: ref.id, y: ref.id === 'idA' ? 2 : 20, - } + }; }, }, - } - } + }, + }; - const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ subgraph1, subgraph2 ]); - return async (op: string): Promise<{ plan: QueryPlan, response: GatewayExecutionResult }> => { + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + subgraph1, + subgraph2, + ]); + return async ( + op: string, + ): Promise<{ plan: QueryPlan; response: GatewayExecutionResult }> => { const operation = parseOp(op, schema); const plan = buildPlan(operation, queryPlanner); - const response = await executePlan(plan, operation, undefined, schema, serviceMap); + const response = await executePlan( + plan, + operation, + undefined, + schema, + serviceMap, + ); return { plan, response }; }; - } - + }; test('handles __typename rewriting when using @key to @interfaceObject', async () => { // We don't need extra resolving from S1 in this case. @@ -4448,135 +4812,144 @@ describe('executeQueryPlan', () => { `); }); - test.each([{ - name: 'with manual __typename', - s1: { - iResolveReferenceExtra: (id: string) => ({ __typename: id === 'idA' ? 'A' : 'B' }), - }, - }, { - name: 'with __resolveType', - s1: { - iResolversExtra: { - __resolveType(ref: { id: string }) { - return ref.id === 'idA' ? 'A' : 'B'; - } + test.each([ + { + name: 'with manual __typename', + s1: { + iResolveReferenceExtra: (id: string) => ({ + __typename: id === 'idA' ? 'A' : 'B', + }), }, }, - }, { - name: 'with isTypeOf', - s1: { - aResolversExtra: { - __isTypeOf(ref: { id: string }) { - return ref.id === 'idA'; - } - }, - bResolversExtra: { - __isTypeOf(ref: { id: string }) { - // Same remark as above. - return ref.id === 'idB'; - } + { + name: 'with __resolveType', + s1: { + iResolversExtra: { + __resolveType(ref: { id: string }) { + return ref.id === 'idA' ? 'A' : 'B'; + }, + }, }, }, - }, { - name: 'with only a __resolveType on the interface but per-runtime-types __resolveReference', - s1: { - hasIResolveReference: false, - iResolversExtra: { - __resolveType(ref: { id: string }) { - return ref.id === 'idA' ? 'A' : 'B'; - } - }, - aResolversExtra: { - __resolveReference(ref: { id: string }) { - return ref.id === 'idA' - ? { id: ref.id, x: 1, z: 3 } - : undefined; - } - }, - bResolversExtra: { - __resolveReference(ref: { id: string }) { - return ref.id === 'idB' - ? { id: ref.id, x: 10, w: 30 } - : undefined; - } + { + name: 'with isTypeOf', + s1: { + aResolversExtra: { + __isTypeOf(ref: { id: string }) { + return ref.id === 'idA'; + }, + }, + bResolversExtra: { + __isTypeOf(ref: { id: string }) { + // Same remark as above. + return ref.id === 'idB'; + }, + }, }, }, - }, { - name: 'errors when nothing provides the runtime type', - expectedErrors: [ - 'Abstract type "I" `__resolveReference` method must resolve to an Object type at runtime. ' - + 'Either the object returned by "I.__resolveReference" must include a valid `__typename` field, ' - + 'or the "I" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.' - ], - }, { - name: 'with an async __resolveReference and a non-default __resolveType', - s1: { - iResolversExtra: { - async __resolveReference(ref: { id: string }) { - return ref.id === 'idA' - ? { id: ref.id, x: 1, z: 3 } - : { id: ref.id, x: 10, w: 30 }; - }, - __resolveType(ref: { id: string }) { - switch (ref.id) { - case 'idA': - return 'A'; - case 'idB': - return 'B'; - default: - throw new Error('Unknown type: ' + ref.id); - } + { + name: 'with only a __resolveType on the interface but per-runtime-types __resolveReference', + s1: { + hasIResolveReference: false, + iResolversExtra: { + __resolveType(ref: { id: string }) { + return ref.id === 'idA' ? 'A' : 'B'; + }, + }, + aResolversExtra: { + __resolveReference(ref: { id: string }) { + return ref.id === 'idA' ? { id: ref.id, x: 1, z: 3 } : undefined; + }, + }, + bResolversExtra: { + __resolveReference(ref: { id: string }) { + return ref.id === 'idB' + ? { id: ref.id, x: 10, w: 30 } + : undefined; + }, }, }, }, - }, { - name: 'with an async __resolveReference and a non-default async __resolveType', - s1: { - iResolversExtra: { - async __resolveReference(ref: { id: string }) { - return ref.id === 'idA' - ? { id: ref.id, x: 1, z: 3 } - : { id: ref.id, x: 10, w: 30 }; - }, - async __resolveType(ref: { id: string }) { - switch (ref.id) { - case 'idA': - return 'A'; - case 'idB': - return 'B'; - default: - throw new Error('Unknown type: ' + ref.id); - } + { + name: 'errors when nothing provides the runtime type', + expectedErrors: [ + 'Abstract type "I" `__resolveReference` method must resolve to an Object type at runtime. ' + + 'Either the object returned by "I.__resolveReference" must include a valid `__typename` field, ' + + 'or the "I" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.', + ], + }, + { + name: 'with an async __resolveReference and a non-default __resolveType', + s1: { + iResolversExtra: { + async __resolveReference(ref: { id: string }) { + return ref.id === 'idA' + ? { id: ref.id, x: 1, z: 3 } + : { id: ref.id, x: 10, w: 30 }; + }, + __resolveType(ref: { id: string }) { + switch (ref.id) { + case 'idA': + return 'A'; + case 'idB': + return 'B'; + default: + throw new Error('Unknown type: ' + ref.id); + } + }, }, }, }, - }, { - name: 'with an async __resolveReference and async __isTypeOf on implementations', - s1: { - iResolversExtra: { - async __resolveReference(ref: { - id: string; - // I don't understand the TypeScript error that occurs when the - // return type is removed here (like all the others); it surfaces - // because `aResolversExtra` is defined, which I can't explain. - }): Promise> { - return ref.id === 'idA' - ? { id: ref.id, x: 1, z: 3 } - : { id: ref.id, x: 10, w: 30 }; + { + name: 'with an async __resolveReference and a non-default async __resolveType', + s1: { + iResolversExtra: { + async __resolveReference(ref: { id: string }) { + return ref.id === 'idA' + ? { id: ref.id, x: 1, z: 3 } + : { id: ref.id, x: 10, w: 30 }; + }, + async __resolveType(ref: { id: string }) { + switch (ref.id) { + case 'idA': + return 'A'; + case 'idB': + return 'B'; + default: + throw new Error('Unknown type: ' + ref.id); + } + }, }, }, - aResolversExtra: { - async __isTypeOf(ref: { id: string }) { - return ref.id === 'idA'; + }, + { + name: 'with an async __resolveReference and async __isTypeOf on implementations', + s1: { + iResolversExtra: { + async __resolveReference(ref: { + id: string; + // I don't understand the TypeScript error that occurs when the + // return type is removed here (like all the others); it surfaces + // because `aResolversExtra` is defined, which I can't explain. + }): Promise> { + return ref.id === 'idA' + ? { id: ref.id, x: 1, z: 3 } + : { id: ref.id, x: 10, w: 30 }; + }, }, - }, - bResolversExtra: { - async __isTypeOf(ref: { id: string }) { - return ref.id === 'idB'; + aResolversExtra: { + async __isTypeOf(ref: { id: string }) { + return ref.id === 'idA'; + }, + }, + bResolversExtra: { + async __isTypeOf(ref: { id: string }) { + return ref.id === 'idB'; + }, }, }, }, - }])('resolving an interface @key $name', async ({ s1, expectedErrors }) => { + ])('resolving an interface @key $name', async ({ s1, expectedErrors }) => { const tester = defineSchema({ s1 }); const { plan, response } = await tester(` @@ -4692,7 +5065,11 @@ describe('executeQueryPlan', () => { test('handles __typename rewriting after forced resolution of implementation type', async () => { const tester = defineSchema({ - s1: { iResolveReferenceExtra: (id: string) => ({ __typename: id === 'idA' ? 'A' : 'B' }), }, + s1: { + iResolveReferenceExtra: (id: string) => ({ + __typename: id === 'idA' ? 'A' : 'B', + }), + }, }); const { plan, response } = await tester(` @@ -4763,17 +5140,17 @@ describe('executeQueryPlan', () => { test('handles querying fields of an implementation type coming from an @interfaceObject subgraph', async () => { const products = [ { - id: "1", - title: "Jane Eyre", + id: '1', + title: 'Jane Eyre', price: 12.99, - author: "Charlotte Bronte", - ISBN: "9780743273565", + author: 'Charlotte Bronte', + ISBN: '9780743273565', }, { - id: "2", - title: "Good Will Hunting", + id: '2', + title: 'Good Will Hunting', price: 14.99, - director: "Gus Van Sant", + director: 'Gus Van Sant', duration: 126, }, ]; @@ -4782,7 +5159,10 @@ describe('executeQueryPlan', () => { name: 'products', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key"] + ) type Query { products: [Product!]! @@ -4813,9 +5193,9 @@ describe('executeQueryPlan', () => { Product: { __resolveType(product: any) { if (product.author) { - return "Book"; + return 'Book'; } else if (product.director) { - return "Movie"; + return 'Movie'; } else { return null; } @@ -4824,14 +5204,17 @@ describe('executeQueryPlan', () => { return products.find((obj) => obj.id === reference.id); }, }, - } - } + }, + }; const s2 = { name: 'reviews', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@interfaceObject"]) + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@interfaceObject"] + ) type Query { allReviewedProducts: [Product!]! @@ -4851,13 +5234,17 @@ describe('executeQueryPlan', () => { resolvers: { Query: { allReviewedProducts: () => products, - } - } - } + }, + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - let operation = parseOp(` + let operation = parseOp( + ` { allReviewedProducts { ... on Book { @@ -4865,7 +5252,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); let queryPlan = buildPlan(operation, queryPlanner); // We're going check again with almost the query but requesting the `id` field. And the @@ -4904,7 +5293,13 @@ describe('executeQueryPlan', () => { `; expect(queryPlan).toMatchInlineSnapshot(expectedPlan); - let response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + let response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); // Note that the 2nd product is a Movie, so we should get an empty object expect(response.data).toMatchInlineSnapshot(` Object { @@ -4917,7 +5312,8 @@ describe('executeQueryPlan', () => { } `); - operation = parseOp(` + operation = parseOp( + ` { allReviewedProducts { ... on Book { @@ -4926,13 +5322,21 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); queryPlan = buildPlan(operation, queryPlanner); // As said above, we should get the same plan as the previous time. expect(queryPlan).toMatchInlineSnapshot(expectedPlan); - response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); // But now we should have the "id" of the book (and still nothing for the movie). expect(response.data).toMatchInlineSnapshot(` Object { @@ -4947,7 +5351,8 @@ describe('executeQueryPlan', () => { `); // Now with __typename just for the book - operation = parseOp(` + operation = parseOp( + ` { allReviewedProducts { ... on Book { @@ -4956,7 +5361,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); queryPlan = buildPlan(operation, queryPlanner); // The plan is almost the exact same as the previous one, but in this case we do end up asking for __typename @@ -4996,7 +5403,13 @@ describe('executeQueryPlan', () => { } `); - response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "allReviewedProducts": Array [ @@ -5010,7 +5423,8 @@ describe('executeQueryPlan', () => { `); // And lastly with __typename but for all products - operation = parseOp(` + operation = parseOp( + ` { allReviewedProducts { __typename @@ -5019,13 +5433,21 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); queryPlan = buildPlan(operation, queryPlanner); // As said above, we should get the same plan as the previous time. expect(queryPlan).toMatchInlineSnapshot(expectedPlan); - response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "allReviewedProducts": Array [ @@ -5046,7 +5468,10 @@ describe('executeQueryPlan', () => { name: 's1', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key"]) + @link( + url: "https://specs.apollo.dev/federation/v2.4" + import: ["@key"] + ) type Query { ts: [T!]! @@ -5062,16 +5487,19 @@ describe('executeQueryPlan', () => { `, resolvers: { Query: { - ts: () => [ { id: '2' }, { id: '4' }, { id: '1' } ] + ts: () => [{ id: '2' }, { id: '4' }, { id: '1' }], }, - } - } + }, + }; const s2 = { name: 's2', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key", "@interfaceObject", "@external", "@requires"]) + @link( + url: "https://specs.apollo.dev/federation/v2.4" + import: ["@key", "@interfaceObject", "@external", "@requires"] + ) type I @key(fields: "id") @interfaceObject { id: ID! @@ -5083,22 +5511,28 @@ describe('executeQueryPlan', () => { __resolveReference(ref: any) { return { ...ref, - v: `id=${ref.id}` + v: `id=${ref.id}`, }; }, - } - } - } + }, + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` { ts { v } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); const expectedPlan = ` @@ -5132,7 +5566,13 @@ describe('executeQueryPlan', () => { `; expect(queryPlan).toMatchInlineSnapshot(expectedPlan); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "ts": Array [ @@ -5155,7 +5595,10 @@ describe('executeQueryPlan', () => { name: 's1', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key"]) + @link( + url: "https://specs.apollo.dev/federation/v2.4" + import: ["@key"] + ) type Query { ts: [T!]! @@ -5167,16 +5610,19 @@ describe('executeQueryPlan', () => { `, resolvers: { Query: { - ts: () => [ { id: '2' }, { id: '4' }, { id: '1' } ] + ts: () => [{ id: '2' }, { id: '4' }, { id: '1' }], }, - } - } + }, + }; const s2 = { name: 's2', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key", "@interfaceObject"]) + @link( + url: "https://specs.apollo.dev/federation/v2.4" + import: ["@key", "@interfaceObject"] + ) interface I @key(fields: "id") { id: ID! @@ -5187,27 +5633,29 @@ describe('executeQueryPlan', () => { id: ID! required: String } - `, resolvers: { I: { __resolveReference(ref: any) { return [ - { id: '1', __typename: "T", required: "r1" }, - { id: '2', __typename: "T", required: "r2" }, - { id: '3', __typename: "T", required: "r3" }, - { id: '4', __typename: "T", required: "r4" }, - ].find(({id}) => id === ref.id); + { id: '1', __typename: 'T', required: 'r1' }, + { id: '2', __typename: 'T', required: 'r2' }, + { id: '3', __typename: 'T', required: 'r3' }, + { id: '4', __typename: 'T', required: 'r4' }, + ].find(({ id }) => id === ref.id); }, }, - } - } + }, + }; const s3 = { name: 's3', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key", "@interfaceObject", "@external", "@requires"]) + @link( + url: "https://specs.apollo.dev/federation/v2.4" + import: ["@key", "@interfaceObject", "@external", "@requires"] + ) type I @key(fields: "id") @interfaceObject { id: ID! @@ -5220,22 +5668,29 @@ describe('executeQueryPlan', () => { __resolveReference(ref: any) { return { ...ref, - v: `req=${ref.required}` + v: `req=${ref.required}`, }; }, - } - } - } + }, + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2, s3 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + s3, + ]); - const operation = parseOp(` + const operation = parseOp( + ` { ts { v } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); const expectedPlan = ` @@ -5286,7 +5741,13 @@ describe('executeQueryPlan', () => { `; expect(queryPlan).toMatchInlineSnapshot(expectedPlan); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "ts": Array [ @@ -5312,7 +5773,10 @@ describe('executeQueryPlan', () => { name: 's1', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key"]) + @link( + url: "https://specs.apollo.dev/federation/v2.4" + import: ["@key"] + ) type Query { is: [I!]! @@ -5343,19 +5807,22 @@ describe('executeQueryPlan', () => { resolvers: { Query: { is: () => [ - { __typename: 'T1', id: '2', name: 'e2', req: { id: 'r1'} }, - { __typename: 'T2', id: '4', name: 'e4', req: { id: 'r2'} }, - { __typename: 'T1', id: '1', name: 'e1', req: { id: 'r3'} } - ] + { __typename: 'T1', id: '2', name: 'e2', req: { id: 'r1' } }, + { __typename: 'T2', id: '4', name: 'e4', req: { id: 'r2' } }, + { __typename: 'T1', id: '1', name: 'e1', req: { id: 'r3' } }, + ], }, - } - } + }, + }; const s2 = { name: 's2', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key", "@interfaceObject", "@external", "@requires"]) + @link( + url: "https://specs.apollo.dev/federation/v2.4" + import: ["@key", "@interfaceObject", "@external", "@requires"] + ) type I @key(fields: "id") @interfaceObject { id: ID! @@ -5372,16 +5839,20 @@ describe('executeQueryPlan', () => { __resolveReference(ref: any) { return { ...ref, - v: `req=${ref.req.id}` + v: `req=${ref.req.id}`, }; }, - } - } - } + }, + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - let operation = parseOp(` + let operation = parseOp( + ` { is { ... on T1 { @@ -5392,7 +5863,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); let queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` @@ -5440,7 +5913,13 @@ describe('executeQueryPlan', () => { } `); - let response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + let response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -5459,7 +5938,8 @@ describe('executeQueryPlan', () => { `); // Sanity checking that if we ask for `v` (the field with @requires), then everything still works. - operation = parseOp(` + operation = parseOp( + ` { is { ... on T1 { @@ -5471,7 +5951,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` @@ -5528,7 +6010,13 @@ describe('executeQueryPlan', () => { } `); - response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -5552,7 +6040,7 @@ describe('executeQueryPlan', () => { const iList = [ { id: '1', - tField: 'field on a T' + tField: 'field on a T', }, ]; const S1 = { @@ -5640,7 +6128,9 @@ describe('executeQueryPlan', () => { }; const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ - S1, S2, S3, + S1, + S2, + S3, ]); const operation = parseOp( `#graphql @@ -5928,10 +6418,10 @@ describe('executeQueryPlan', () => { { __typename: 'A', id: 'keyA', g: 1 }, { __typename: 'B', id: 'keyB', g: 'foo' }, ]; - } + }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -5950,21 +6440,33 @@ describe('executeQueryPlan', () => { `, resolvers: { A: { - __resolveReference(ref: { id: string, g: any }) { - return { __typename: 'A', id: ref.id, f: `g is type ${typeof ref.g}` }; + __resolveReference(ref: { id: string; g: any }) { + return { + __typename: 'A', + id: ref.id, + f: `g is type ${typeof ref.g}`, + }; }, }, B: { - __resolveReference(ref: { id: string, g: any }) { - return { __typename: 'B', id: ref.id, f: `g is type ${typeof ref.g}` }; + __resolveReference(ref: { id: string; g: any }) { + return { + __typename: 'B', + id: ref.id, + f: `g is type ${typeof ref.g}`, + }; }, }, - } - } + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` query { us { ... on A { @@ -5975,7 +6477,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); // In the initial fetch, it's important that one of the `g` is aliased, since it's queried twice at the same level // but with different types. @@ -6027,7 +6531,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -6075,10 +6585,10 @@ describe('executeQueryPlan', () => { { __typename: 'A', id: 'keyA', g: 1 }, { __typename: 'B', id: 'keyB', g: 'foo' }, ]; - } + }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -6097,27 +6607,41 @@ describe('executeQueryPlan', () => { `, resolvers: { A: { - __resolveReference(ref: { id: string, g: any }) { - return { __typename: 'A', id: ref.id, f: `g is type ${typeof ref.g}` }; + __resolveReference(ref: { id: string; g: any }) { + return { + __typename: 'A', + id: ref.id, + f: `g is type ${typeof ref.g}`, + }; }, }, B: { - __resolveReference(ref: { id: string, g: any }) { - return { __typename: 'B', id: ref.id, f: `g is type ${typeof ref.g}` }; + __resolveReference(ref: { id: string; g: any }) { + return { + __typename: 'B', + id: ref.id, + f: `g is type ${typeof ref.g}`, + }; }, }, - } - } + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` query { us { f } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); // In the initial fetch, it's important that one of the `g` is aliased, since it's queried twice at the same level // but with different types. @@ -6169,7 +6693,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -6214,10 +6744,10 @@ describe('executeQueryPlan', () => { { __typename: 'A', g: 'foo' }, { __typename: 'B', g: 1 }, ]; - } + }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -6235,26 +6765,40 @@ describe('executeQueryPlan', () => { resolvers: { A: { __resolveReference(ref: { g: string }) { - return { __typename: 'A', g: ref.g, f: ref.g == 'foo' ? 'fA' : '' }; + return { + __typename: 'A', + g: ref.g, + f: ref.g == 'foo' ? 'fA' : '', + }; }, }, B: { __resolveReference(ref: { g: number }) { - return { __typename: 'B', g: ref.g, f: ref.g === 1 ? 'fB' : '' }; + return { + __typename: 'B', + g: ref.g, + f: ref.g === 1 ? 'fB' : '', + }; }, }, - } - } + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` query { us { f } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); // In the initial fetch, it's important that one of the `g` is aliased, since it's queried twice at the same level // but with different types. @@ -6302,7 +6846,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -6343,13 +6893,13 @@ describe('executeQueryPlan', () => { Query: { us() { return [ - { __typename: 'A', id: 'keyA', f: 'fA'}, + { __typename: 'A', id: 'keyA', f: 'fA' }, { __typename: 'B', f: 'fB' }, ]; - } + }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -6365,18 +6915,24 @@ describe('executeQueryPlan', () => { return { __typename: 'A', id: ref.id, f: 'fA' }; }, }, - } - } + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` query { us { f } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); // Here, the presence of the @provides "forces" the query planner to check type-explosion, and as type-exploding // is the most efficient solution, it is chosen. But as this result in `f` being queried twice at the same level @@ -6400,7 +6956,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -6449,10 +7011,10 @@ describe('executeQueryPlan', () => { { __typename: 'A', g: 'foo' }, { __typename: 'B', g: 1 }, ]; - } + }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -6478,25 +7040,37 @@ describe('executeQueryPlan', () => { resolvers: { Query: { t() { - return ({ id: 0 }); - } + return { id: 0 }; + }, }, A: { __resolveReference(ref: { g: string }) { - return { __typename: 'A', g: ref.g, f: ref.g == 'foo' ? 'fA' : '' }; + return { + __typename: 'A', + g: ref.g, + f: ref.g == 'foo' ? 'fA' : '', + }; }, }, B: { __resolveReference(ref: { g: number }) { - return { __typename: 'B', g: ref.g, f: ref.g === 1 ? 'fB' : '' }; + return { + __typename: 'B', + g: ref.g, + f: ref.g === 1 ? 'fB' : '', + }; }, }, - } - } + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` query { t { us { @@ -6504,7 +7078,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); // In the 2nd fetch, it's important that one of the `g` is aliased, since it's queried twice at the same level // but with different types. @@ -6570,7 +7146,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -6626,10 +7208,10 @@ describe('executeQueryPlan', () => { { __typename: 'A', id: 'keyA', g: 1, x: 'xA', y: 'yA' }, { __typename: 'B', id: 'keyB', g: 'foo', x: 'xB', y: 'yB' }, ]; - } + }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -6648,24 +7230,36 @@ describe('executeQueryPlan', () => { `, resolvers: { A: { - __resolveReference(ref: { id: string, g: any }) { - return { __typename: 'A', id: ref.id, f: `g is type ${typeof ref.g}` }; + __resolveReference(ref: { id: string; g: any }) { + return { + __typename: 'A', + id: ref.id, + f: `g is type ${typeof ref.g}`, + }; }, }, B: { - __resolveReference(ref: { id: string, g: any }) { - return { __typename: 'B', id: ref.id, f: `g is type ${typeof ref.g}` }; + __resolveReference(ref: { id: string; g: any }) { + return { + __typename: 'B', + id: ref.id, + f: `g is type ${typeof ref.g}`, + }; }, }, - } - } + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); // We known that `g` will need to be aliased in the 2nd occurrence on B, and by default it would be aliased // as `g__alias_0`. So we query something with that exact alias to check that we avoid the conflict. We // also use alias `g__alias_1` to further ensure multiple possible conflict are handled. - const operation = parseOp(` + const operation = parseOp( + ` query { us { g__alias_0: x @@ -6673,7 +7267,9 @@ describe('executeQueryPlan', () => { g__alias_1: y } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -6725,7 +7321,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); // We double-check that the final aliases are the one from the query expect(response.data).toMatchInlineSnapshot(` @@ -6773,11 +7375,11 @@ describe('executeQueryPlan', () => { resolvers: { Query: { t() { - return { "__typename": "T", "id": 0 }; - } - } - } - } + return { __typename: 'T', id: 0 }; + }, + }, + }, + }; const s2 = { name: 'S2', @@ -6791,20 +7393,26 @@ describe('executeQueryPlan', () => { T: { __resolveReference() { return null; - } - } - } - } + }, + }, + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); - const operation = parseOp(` + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); + const operation = parseOp( + ` { t { id x } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` @@ -6836,7 +7444,13 @@ describe('executeQueryPlan', () => { }, } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "t": null, @@ -6891,19 +7505,22 @@ describe('executeQueryPlan', () => { Node: { __resolveType(obj: any) { if (obj['foo']) { - return 'Foo' + return 'Foo'; } if (obj['bar']) { - return 'Bar' + return 'Bar'; } return undefined; - } - } - } - } + }, + }, + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1 ]); - const operation = parseOp(` + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + ]); + const operation = parseOp( + ` query { tests { node { @@ -6912,7 +7529,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` @@ -6934,7 +7553,13 @@ describe('executeQueryPlan', () => { }, } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.extensions).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` @@ -6946,32 +7571,32 @@ describe('executeQueryPlan', () => { ], } `); - }) + }); it(`handles duplicate aliased fields on entities correctly`, async () => { const s1Data = [ { id: 't1', type: 'T1' }, { id: 't2', type: 'T2' }, - ] + ]; const s1 = { name: 'S1', typeDefs: gql` - type Query { - testQuery(id: String!): I - } - - interface I { - id: String! - } - - type T1 implements I @key(fields: "id", resolvable: false) { - id: String! - } - - type T2 implements I @key(fields: "id", resolvable: false) { - id: String! - } - `, + type Query { + testQuery(id: String!): I + } + + interface I { + id: String! + } + + type T1 implements I @key(fields: "id", resolvable: false) { + id: String! + } + + type T2 implements I @key(fields: "id", resolvable: false) { + id: String! + } + `, resolvers: { Query: { testQuery(_: any, args: any) { @@ -6980,8 +7605,8 @@ describe('executeQueryPlan', () => { }, I: { __resolveType(obj: any) { - return obj.type - } + return obj.type; + }, }, }, }; @@ -6989,42 +7614,40 @@ describe('executeQueryPlan', () => { const t1 = { id: 't1', foo: { - field: "field1", + field: 'field1', }, }; const t2 = { id: 't2', bar: { - field: "field2", + field: 'field2', }, }; const s2 = { name: 'S2', typeDefs: gql` - interface I { - id: String! - } - - type Test { - field: String! - } - - type T1 implements I @key(fields: "id") { - id: String! - foo: Test - } - - type T2 implements I @key(fields: "id") { - id: String! - bar: Test - } - `, + interface I { + id: String! + } + + type Test { + field: String! + } + + type T1 implements I @key(fields: "id") { + id: String! + foo: Test + } + + type T2 implements I @key(fields: "id") { + id: String! + bar: Test + } + `, resolvers: { - Query: { - }, - Test: { - }, + Query: {}, + Test: {}, T1: { __resolveReference() { return t1; @@ -7044,7 +7667,7 @@ describe('executeQueryPlan', () => { ]); const operation = parseOp( - ` + ` query AliasedQuery($tId: String!) { testQuery(id: $tId) { ... on T1 { @@ -7060,46 +7683,42 @@ describe('executeQueryPlan', () => { } } `, - schema, + schema, ); const queryPlan = buildPlan(operation, queryPlanner); const requestContext = buildRequestContext(); - requestContext.request.variables = { tId: "t1" }; + requestContext.request.variables = { tId: 't1' }; let response = await executePlan( - queryPlan, - operation, - requestContext, - schema, - serviceMap, + queryPlan, + operation, + requestContext, + schema, + serviceMap, ); - expect(response.data).toMatchObject( - { - "testQuery": { - "foo": { - "field": "field1" - } - } - } - ) + expect(response.data).toMatchObject({ + testQuery: { + foo: { + field: 'field1', + }, + }, + }); - requestContext.request.variables = { tId: "t2" }; + requestContext.request.variables = { tId: 't2' }; response = await executePlan( - queryPlan, - operation, - requestContext, - schema, - serviceMap, + queryPlan, + operation, + requestContext, + schema, + serviceMap, ); - expect(response.data).toMatchObject( - { - "testQuery": { - "foo": { - "field": "field2" - } - } - } - ) + expect(response.data).toMatchObject({ + testQuery: { + foo: { + field: 'field2', + }, + }, + }); }); }); diff --git a/gateway-js/src/__tests__/gateway/endToEnd.test.ts b/gateway-js/src/__tests__/gateway/endToEnd.test.ts index dbd359898..254471a94 100644 --- a/gateway-js/src/__tests__/gateway/endToEnd.test.ts +++ b/gateway-js/src/__tests__/gateway/endToEnd.test.ts @@ -2,7 +2,7 @@ import { fixtures } from 'apollo-federation-integration-testsuite'; import { buildSchema, ObjectType } from '@apollo/federation-internals'; import gql from 'graphql-tag'; import { printSchema } from 'graphql'; -import { startSubgraphsAndGateway, Services } from './testUtils' +import { startSubgraphsAndGateway, Services } from './testUtils'; import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache'; import { QueryPlan } from '@apollo/query-planner'; import { createHash } from '@apollo/utils.createhash'; @@ -20,11 +20,15 @@ afterEach(async () => { } }); - describe('caching', () => { - const cache = new InMemoryLRUCache({maxSize: Math.pow(2, 20) * (30), sizeCalculation: approximateObjectSize}); + const cache = new InMemoryLRUCache({ + maxSize: Math.pow(2, 20) * 30, + sizeCalculation: approximateObjectSize, + }); beforeEach(async () => { - services = await startSubgraphsAndGateway(fixtures, { gatewayConfig: { queryPlannerConfig: { cache } } }); + services = await startSubgraphsAndGateway(fixtures, { + gatewayConfig: { queryPlannerConfig: { cache } }, + }); }); it(`cached query plan`, async () => { @@ -43,7 +47,7 @@ describe('caching', () => { `; await services.queryGateway(query); - const queryHash:string = createHash('sha256').update(query).digest('hex'); + const queryHash: string = createHash('sha256').update(query).digest('hex'); expect(await cache.get(queryHash)).toBeTruthy(); }); diff --git a/gateway-js/src/__tests__/gateway/extensions.test.ts b/gateway-js/src/__tests__/gateway/extensions.test.ts index c1a630a0f..e862c3b70 100644 --- a/gateway-js/src/__tests__/gateway/extensions.test.ts +++ b/gateway-js/src/__tests__/gateway/extensions.test.ts @@ -19,7 +19,9 @@ describe('addExtensions', () => { it('has extensions on loaded schemas', async () => { const { schema } = await gateway.load(); - expect(schema.extensions).toEqual({ apollo: { gateway: { version: version } } }); + expect(schema.extensions).toEqual({ + apollo: { gateway: { version: version } }, + }); }); it('has extensions on schema updates', async () => { @@ -32,6 +34,8 @@ describe('addExtensions', () => { gateway.load(); const { apiSchema } = await schemaChangeBlocker; - expect(apiSchema.extensions).toEqual({ apollo: { gateway: { version: version } } }); + expect(apiSchema.extensions).toEqual({ + apollo: { gateway: { version: version } }, + }); }); }); diff --git a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts index 9b5bf0cb2..9ffa0790f 100644 --- a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts +++ b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts @@ -138,7 +138,9 @@ describe('lifecycle hooks', () => { const [firstCall, secondCall] = mockDidUpdate.mock.calls; // Note that we've composing our usual test fixtures here - const expectedFirstId = createHash('sha256').update(getTestingSupergraphSdl()).digest('hex'); + const expectedFirstId = createHash('sha256') + .update(getTestingSupergraphSdl()) + .digest('hex'); expect(firstCall[0]!.compositionId).toEqual(expectedFirstId); // first call should have no second "previous" argument expect(firstCall[1]).toBeUndefined(); diff --git a/gateway-js/src/__tests__/gateway/opentelemetry.test.ts b/gateway-js/src/__tests__/gateway/opentelemetry.test.ts index 133788d36..1079f70e4 100644 --- a/gateway-js/src/__tests__/gateway/opentelemetry.test.ts +++ b/gateway-js/src/__tests__/gateway/opentelemetry.test.ts @@ -1,8 +1,14 @@ import gql from 'graphql-tag'; -import {ApolloGateway, LocalGraphQLDataSource} from '../../'; -import {fixtures, spanSerializer} from 'apollo-federation-integration-testsuite'; -import {InMemorySpanExporter, SimpleSpanProcessor} from '@opentelemetry/tracing' -import {NodeTracerProvider} from '@opentelemetry/node'; +import { ApolloGateway, LocalGraphQLDataSource } from '../../'; +import { + fixtures, + spanSerializer, +} from 'apollo-federation-integration-testsuite'; +import { + InMemorySpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/tracing'; +import { NodeTracerProvider } from '@opentelemetry/node'; import { buildSubgraphSchema } from '@apollo/subgraph'; expect.addSnapshotSerializer(spanSerializer); @@ -17,7 +23,12 @@ beforeEach(() => { }); describe('opentelemetry', () => { - async function execute(executor: any, source: string, variables: any, operationName: string) { + async function execute( + executor: any, + source: string, + variables: any, + operationName: string, + ) { await executor({ source, document: gql(source), @@ -31,8 +42,7 @@ describe('opentelemetry', () => { }); } - describe('with local data', () => - { + describe('with local data', () => { async function gateway() { const localDataSources = Object.fromEntries( fixtures.map((f) => [ @@ -47,7 +57,7 @@ describe('opentelemetry', () => { }, }); - const {executor} = await gateway.load(); + const { executor } = await gateway.load(); return executor; } @@ -62,7 +72,7 @@ describe('opentelemetry', () => { } `; - await execute(executor, source, {upc: '1'}, 'GetProduct'); + await execute(executor, source, { upc: '1' }, 'GetProduct'); expect(inMemorySpans.getFinishedSpans()).toMatchSnapshot(); }); @@ -91,14 +101,12 @@ describe('opentelemetry', () => { `; try { - await execute(executor, source, {upc: '1'}, 'GetProduct'); - } - catch(err) {} + await execute(executor, source, { upc: '1' }, 'GetProduct'); + } catch (err) {} expect(inMemorySpans.getFinishedSpans()).toMatchSnapshot(); }); }); - it('receives spans on fetch failure', async () => { const gateway = new ApolloGateway({ localServiceList: fixtures, @@ -117,7 +125,7 @@ describe('opentelemetry', () => { } `; - await execute(executor, source, {upc: '1'}, 'GetProduct'); + await execute(executor, source, { upc: '1' }, 'GetProduct'); expect(inMemorySpans.getFinishedSpans()).toMatchSnapshot(); }); }); diff --git a/gateway-js/src/__tests__/gateway/queryPlanCache.test.ts b/gateway-js/src/__tests__/gateway/queryPlanCache.test.ts index 851ae5f05..914b5cdeb 100644 --- a/gateway-js/src/__tests__/gateway/queryPlanCache.test.ts +++ b/gateway-js/src/__tests__/gateway/queryPlanCache.test.ts @@ -9,7 +9,10 @@ import { QueryPlanner } from '@apollo/query-planner'; import { unwrapSingleResultKind } from '../gateway/testUtils'; it('caches the query plan for a request', async () => { - const buildQueryPlanSpy = jest.spyOn(QueryPlanner.prototype, 'buildQueryPlan'); + const buildQueryPlanSpy = jest.spyOn( + QueryPlanner.prototype, + 'buildQueryPlan', + ); const localDataSources = Object.fromEntries( fixtures.map((f) => [ diff --git a/gateway-js/src/__tests__/gateway/queryPlannerConfig.test.ts b/gateway-js/src/__tests__/gateway/queryPlannerConfig.test.ts index 8df2f5425..a7ad72ded 100644 --- a/gateway-js/src/__tests__/gateway/queryPlannerConfig.test.ts +++ b/gateway-js/src/__tests__/gateway/queryPlannerConfig.test.ts @@ -1,5 +1,5 @@ import gql from 'graphql-tag'; -import { startSubgraphsAndGateway, Services } from './testUtils' +import { startSubgraphsAndGateway, Services } from './testUtils'; let services: Services; @@ -33,10 +33,10 @@ describe('`debug.bypassPlannerForSingleSubgraph` config', () => { b: { x: 1, y: 'foo', - } + }, }), - } - } + }, + }, }; const query = ` @@ -72,22 +72,21 @@ describe('`debug.bypassPlannerForSingleSubgraph` config', () => { const queryPlanner = services.gateway.__testing().queryPlanner!; // If the query planner is genuinely used, we shoud have evaluated 1 plan. - expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe(1); + expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe( + 1, + ); }); it('works when enabled', async () => { - services = await startSubgraphsAndGateway( - [subgraph], - { - gatewayConfig: { - queryPlannerConfig: { - debug: { - bypassPlannerForSingleSubgraph: true, - } - } - } - } - ); + services = await startSubgraphsAndGateway([subgraph], { + gatewayConfig: { + queryPlannerConfig: { + debug: { + bypassPlannerForSingleSubgraph: true, + }, + }, + }, + }); const response = await services.queryGateway(query); const result = await response.json(); @@ -95,7 +94,8 @@ describe('`debug.bypassPlannerForSingleSubgraph` config', () => { const queryPlanner = services.gateway.__testing().queryPlanner!; // The `bypassPlannerForSingleSubgraph` doesn't evaluate anything. It's use is the only case where `evaluatedPlanCount` can be 0. - expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe(0); + expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe( + 0, + ); }); }); - diff --git a/gateway-js/src/__tests__/gateway/reporting.test.ts b/gateway-js/src/__tests__/gateway/reporting.test.ts index 3be495036..c2f278469 100644 --- a/gateway-js/src/__tests__/gateway/reporting.test.ts +++ b/gateway-js/src/__tests__/gateway/reporting.test.ts @@ -92,22 +92,19 @@ describe('reporting', () => { return 'ok'; }); - services = await startSubgraphsAndGateway( - fixtures, - { - gatewayServerConfig: { - apollo: { - key: 'service:foo:bar', - graphRef: 'foo@current', - }, - plugins: [ - ApolloServerPluginUsageReporting({ - sendReportsImmediately: true, - }), - ], + services = await startSubgraphsAndGateway(fixtures, { + gatewayServerConfig: { + apollo: { + key: 'service:foo:bar', + graphRef: 'foo@current', }, - } - ); + plugins: [ + ApolloServerPluginUsageReporting({ + sendReportsImmediately: true, + }), + ], + }, + }); }); afterEach(async () => { @@ -134,9 +131,12 @@ describe('reporting', () => { `; const result = await toPromise( - execute(createHttpLink({ uri: services.gatewayUrl, fetch: fetch as any }), { - query, - }), + execute( + createHttpLink({ uri: services.gatewayUrl, fetch: fetch as any }), + { + query, + }, + ), ); expect(result).toMatchInlineSnapshot(` Object { diff --git a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts index 1854d0bdf..4e441fdab 100644 --- a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts +++ b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts @@ -4,7 +4,10 @@ import { SubgraphHealthCheckFunction, SupergraphSdlUpdateFunction, } from '@apollo/gateway'; -import { accounts, fixturesWithUpdate } from 'apollo-federation-integration-testsuite'; +import { + accounts, + fixturesWithUpdate, +} from 'apollo-federation-integration-testsuite'; import { createHash } from '@apollo/utils.createhash'; import { ApolloServer } from '@apollo/server'; import { startStandaloneServer } from '@apollo/server/standalone'; @@ -54,7 +57,9 @@ afterEach(async () => { } }); -const testingFixturesDefaultCompositionId = createHash('sha256').update(getTestingSupergraphSdl()).digest('hex'); +const testingFixturesDefaultCompositionId = createHash('sha256') + .update(getTestingSupergraphSdl()) + .digest('hex'); describe('Using supergraphSdl static configuration', () => { it('successfully starts and serves requests to the proper services', async () => { @@ -64,7 +69,6 @@ describe('Using supergraphSdl static configuration', () => { .post('/', { query: '{me{username}}', variables: {} }) .reply(200, { data: { me: { username: '@apollo-user' } } }); - const result = await server.executeOperation({ query: '{ me { username } }', }); @@ -185,9 +189,7 @@ describe('Using supergraphSdl dynamic configuration', () => { await gateway.load(); const { state, compositionId } = gateway.__testing(); expect(state.phase).toEqual('loaded'); - expect(compositionId).toEqual( - testingFixturesDefaultCompositionId, - ); + expect(compositionId).toEqual(testingFixturesDefaultCompositionId); await gateway.stop(); expect(cleanup).toHaveBeenCalledTimes(1); @@ -210,9 +212,7 @@ describe('Using supergraphSdl dynamic configuration', () => { await gateway.load(); const { state, compositionId } = gateway.__testing(); expect(state.phase).toEqual('loaded'); - expect(compositionId).toEqual( - testingFixturesDefaultCompositionId, - ); + expect(compositionId).toEqual(testingFixturesDefaultCompositionId); await expect(healthCheckCallback!(supergraphSdl)).resolves.toBeUndefined(); }); @@ -292,9 +292,7 @@ describe('Using supergraphSdl dynamic configuration', () => { await gateway.load(); const { state, compositionId } = gateway.__testing(); expect(state.phase).toEqual('loaded'); - expect(compositionId).toEqual( - testingFixturesDefaultCompositionId - ); + expect(compositionId).toEqual(testingFixturesDefaultCompositionId); await expect(healthCheckCallback!(supergraphSdl)).rejects.toThrowError( /The gateway subgraphs health check failed\. Updating to the provided `supergraphSdl` will likely result in future request failures to subgraphs\. The following error occurred during the health check/, diff --git a/gateway-js/src/__tests__/httpSpec.test.ts b/gateway-js/src/__tests__/httpSpec.test.ts index 5dfa09fc3..0955b2c1d 100644 --- a/gateway-js/src/__tests__/httpSpec.test.ts +++ b/gateway-js/src/__tests__/httpSpec.test.ts @@ -12,7 +12,7 @@ describe('httpSpecTests.ts', () => { beforeAll(async () => { gatewayServer = new ApolloServer({ gateway: new ApolloGateway({ - supergraphSdl: getTestingSupergraphSdl() + supergraphSdl: getTestingSupergraphSdl(), }), // The test doesn't know we should send apollo-require-preflight along // with GETs. We could override `fetchFn` to add it but this seems simple enough. diff --git a/gateway-js/src/__tests__/integration/abstract-types.test.ts b/gateway-js/src/__tests__/integration/abstract-types.test.ts index 9decdf99d..8cc4ed0e3 100644 --- a/gateway-js/src/__tests__/integration/abstract-types.test.ts +++ b/gateway-js/src/__tests__/integration/abstract-types.test.ts @@ -1,6 +1,10 @@ import { execute } from '../execution-utils'; -import { astSerializer, fed2gql as gql, queryPlanSerializer } from 'apollo-federation-integration-testsuite'; +import { + astSerializer, + fed2gql as gql, + queryPlanSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(astSerializer); expect.addSnapshotSerializer(queryPlanSerializer); @@ -823,7 +827,7 @@ describe('unions', () => { }); describe("doesn't result in duplicate fetches", () => { - it("when exploding types", async () => { + it('when exploding types', async () => { const query = `#graphql query { topProducts { @@ -1045,7 +1049,7 @@ describe("doesn't result in duplicate fetches", () => { `); }); -it("when including the same nested fields under different type conditions", async () => { + it('when including the same nested fields under different type conditions', async () => { const query = `#graphql query { topProducts { @@ -1245,8 +1249,8 @@ it("when including the same nested fields under different type conditions", asyn `); }); -it('when including multiple nested fields to the same service under different type conditions', async () => { - const query = `#graphql + it('when including multiple nested fields to the same service under different type conditions', async () => { + const query = `#graphql query { topProducts { ... on Book { @@ -1479,10 +1483,10 @@ it('when including multiple nested fields to the same service under different ty }, } `); -}); + }); -it('when exploding types through multiple levels', async () => { - const query = `#graphql + it('when exploding types through multiple levels', async () => { + const query = `#graphql query { productsByCategory { name @@ -1506,90 +1510,90 @@ it('when exploding types through multiple levels', async () => { } `; - const { queryPlan, errors } = await execute({ query }, [ - { - name: 'accounts', - typeDefs: gql` - type User @key(fields: "id") { - id: ID! - name: String - username: String @shareable - } - `, - }, - { - name: 'products', - typeDefs: gql` - type Book implements Product @key(fields: "isbn") { - isbn: String! - title: String - year: Int - name: String - price: Int - } + const { queryPlan, errors } = await execute({ query }, [ + { + name: 'accounts', + typeDefs: gql` + type User @key(fields: "id") { + id: ID! + name: String + username: String @shareable + } + `, + }, + { + name: 'products', + typeDefs: gql` + type Book implements Product @key(fields: "isbn") { + isbn: String! + title: String + year: Int + name: String + price: Int + } - type Furniture implements Product @key(fields: "sku") { - sku: String! - name: String - price: Int - weight: Int - } + type Furniture implements Product @key(fields: "sku") { + sku: String! + name: String + price: Int + weight: Int + } - interface Product { - name: String - price: Int - } + interface Product { + name: String + price: Int + } - extend type Query { - productsByCategory: [ProductCategory] - } + extend type Query { + productsByCategory: [ProductCategory] + } - interface ProductCategory { - name: String! - } + interface ProductCategory { + name: String! + } - type BookCategory implements ProductCategory { - name: String! - items: [Book] - } + type BookCategory implements ProductCategory { + name: String! + items: [Book] + } - type FurnitureCategory implements ProductCategory { - name: String! - items: [Furniture] - } - `, - }, - { - name: 'reviews', - typeDefs: gql` - extend type Book implements Product @key(fields: "isbn") { - isbn: String! @external - reviews: [Review] - } - extend type Furniture implements Product @key(fields: "sku") { - sku: String! @external - reviews: [Review] - } - extend interface Product { - reviews: [Review] - } - type Review @key(fields: "id") { - id: ID! - body: String - author: User @provides(fields: "username") - product: Product - } - extend type User @key(fields: "id") { - id: ID! @external - username: String @external - reviews: [Review] - } - `, - }, - ]); + type FurnitureCategory implements ProductCategory { + name: String! + items: [Furniture] + } + `, + }, + { + name: 'reviews', + typeDefs: gql` + extend type Book implements Product @key(fields: "isbn") { + isbn: String! @external + reviews: [Review] + } + extend type Furniture implements Product @key(fields: "sku") { + sku: String! @external + reviews: [Review] + } + extend interface Product { + reviews: [Review] + } + type Review @key(fields: "id") { + id: ID! + body: String + author: User @provides(fields: "username") + product: Product + } + extend type User @key(fields: "id") { + id: ID! @external + username: String @external + reviews: [Review] + } + `, + }, + ]); - expect(errors).toBeUndefined(); - expect(queryPlan).toMatchInlineSnapshot(` + expect(errors).toBeUndefined(); + expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { Sequence { Fetch(service: "products") { @@ -1643,9 +1647,9 @@ it('when exploding types through multiple levels', async () => { }, } `); -}); + }); -it("when including the same nested fields under different type conditions that are split between services", async () => { + it('when including the same nested fields under different type conditions that are split between services', async () => { const query = `#graphql query { topProducts { diff --git a/gateway-js/src/__tests__/integration/boolean.test.ts b/gateway-js/src/__tests__/integration/boolean.test.ts index 2f82477d3..1ede900e2 100644 --- a/gateway-js/src/__tests__/integration/boolean.test.ts +++ b/gateway-js/src/__tests__/integration/boolean.test.ts @@ -1,5 +1,8 @@ import { execute } from '../execution-utils'; -import { astSerializer, queryPlanSerializer } from 'apollo-federation-integration-testsuite'; +import { + astSerializer, + queryPlanSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(astSerializer); expect.addSnapshotSerializer(queryPlanSerializer); @@ -91,11 +94,26 @@ describe('@skip', () => { expect(data).toEqual({ topReviews: [ - { body: 'Love it!', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Too expensive.', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Could be better.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Prefer something else.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Wish I had read this before.', author: { name: { first: 'Alan', last: 'Turing' } } }, + { + body: 'Love it!', + author: { name: { first: 'Ada', last: 'Lovelace' } }, + }, + { + body: 'Too expensive.', + author: { name: { first: 'Ada', last: 'Lovelace' } }, + }, + { + body: 'Could be better.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, + { + body: 'Prefer something else.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, + { + body: 'Wish I had read this before.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, ], }); @@ -127,15 +145,33 @@ describe('@skip', () => { expect(data).toEqual({ topReviews: [ - { body: 'Love it!', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Too expensive.', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Could be better.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Prefer something else.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Wish I had read this before.', author: { name: { first: 'Alan', last: 'Turing' } } }, + { + body: 'Love it!', + author: { name: { first: 'Ada', last: 'Lovelace' } }, + }, + { + body: 'Too expensive.', + author: { name: { first: 'Ada', last: 'Lovelace' } }, + }, + { + body: 'Could be better.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, + { + body: 'Prefer something else.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, + { + body: 'Wish I had read this before.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, ], }); - const variables = { definitions: operation.variableDefinitions, values: {skip} }; + const variables = { + definitions: operation.variableDefinitions, + values: { skip }, + }; expect(queryPlan).toCallService('reviews', variables); expect(queryPlan).toCallService('accounts', variables); }); @@ -227,11 +263,26 @@ describe('@include', () => { expect(data).toEqual({ topReviews: [ - { body: 'Love it!', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Too expensive.', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Could be better.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Prefer something else.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Wish I had read this before.', author: { name: { first: 'Alan', last: 'Turing' } } }, + { + body: 'Love it!', + author: { name: { first: 'Ada', last: 'Lovelace' } }, + }, + { + body: 'Too expensive.', + author: { name: { first: 'Ada', last: 'Lovelace' } }, + }, + { + body: 'Could be better.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, + { + body: 'Prefer something else.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, + { + body: 'Wish I had read this before.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, ], }); @@ -264,15 +315,33 @@ describe('@include', () => { expect(data).toEqual({ topReviews: [ - { body: 'Love it!', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Too expensive.', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Could be better.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Prefer something else.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Wish I had read this before.', author: { name: { first: 'Alan', last: 'Turing' } } }, + { + body: 'Love it!', + author: { name: { first: 'Ada', last: 'Lovelace' } }, + }, + { + body: 'Too expensive.', + author: { name: { first: 'Ada', last: 'Lovelace' } }, + }, + { + body: 'Could be better.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, + { + body: 'Prefer something else.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, + { + body: 'Wish I had read this before.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, ], }); - const variables = { definitions: operation.variableDefinitions, values: {include} }; + const variables = { + definitions: operation.variableDefinitions, + values: { include }, + }; expect(queryPlan).toCallService('accounts', variables); expect(queryPlan).toCallService('reviews', variables); }); diff --git a/gateway-js/src/__tests__/integration/complex-key.test.ts b/gateway-js/src/__tests__/integration/complex-key.test.ts index 4ded8ba97..999b0b98f 100644 --- a/gateway-js/src/__tests__/integration/complex-key.test.ts +++ b/gateway-js/src/__tests__/integration/complex-key.test.ts @@ -1,6 +1,9 @@ import gql from 'graphql-tag'; import { execute, ServiceDefinitionModule } from '../execution-utils'; -import { astSerializer, queryPlanSerializer } from 'apollo-federation-integration-testsuite'; +import { + astSerializer, + queryPlanSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(astSerializer); expect.addSnapshotSerializer(queryPlanSerializer); @@ -85,13 +88,13 @@ const userService: ServiceDefinitionModule = { User: { __resolveReference(reference) { return users.find( - user => + (user) => user.id === reference.id && user.organizationId === reference.organization.id, ); }, organization(user) { - return organizations.find(org => org.id === user.organizationId); + return organizations.find((org) => org.id === user.organizationId); }, }, }, diff --git a/gateway-js/src/__tests__/integration/configuration.test.ts b/gateway-js/src/__tests__/integration/configuration.test.ts index 5af71bec1..cbe5e7114 100644 --- a/gateway-js/src/__tests__/integration/configuration.test.ts +++ b/gateway-js/src/__tests__/integration/configuration.test.ts @@ -201,9 +201,9 @@ describe('gateway startup errors', () => { } const expected = - "A valid schema couldn't be composed. The following composition errors were found:\n" - + ' [accounts] On type "User", for @key(fields: "id"): Cannot query field "id" on type "User" (the field should either be added to this subgraph or, if it should not be resolved by this subgraph, you need to add it to this subgraph with @external).\n' - + ' [accounts] On type "Account", for @key(fields: "id"): Cannot query field "id" on type "Account" (the field should either be added to this subgraph or, if it should not be resolved by this subgraph, you need to add it to this subgraph with @external).' + "A valid schema couldn't be composed. The following composition errors were found:\n" + + ' [accounts] On type "User", for @key(fields: "id"): Cannot query field "id" on type "User" (the field should either be added to this subgraph or, if it should not be resolved by this subgraph, you need to add it to this subgraph with @external).\n' + + ' [accounts] On type "Account", for @key(fields: "id"): Cannot query field "id" on type "Account" (the field should either be added to this subgraph or, if it should not be resolved by this subgraph, you need to add it to this subgraph with @external).'; expect(err.message).toBe(expected); }); }); @@ -352,7 +352,7 @@ describe('deprecation warnings', () => { try { await gateway.load(); - // gateway will throw since we're not providing an actual service list, disregard + // gateway will throw since we're not providing an actual service list, disregard } catch {} expect(logger.warn).toHaveBeenCalledWith( diff --git a/gateway-js/src/__tests__/integration/custom-directives.test.ts b/gateway-js/src/__tests__/integration/custom-directives.test.ts index f6915fd9a..22caea0bb 100644 --- a/gateway-js/src/__tests__/integration/custom-directives.test.ts +++ b/gateway-js/src/__tests__/integration/custom-directives.test.ts @@ -98,7 +98,7 @@ describe('custom executable directives', () => { }); // With relaxed composition, instead of erroring out if a directive is not declared everywhere, we compose but don't - // include the directive in the supergraph and generate a hint. So the following test will complain that @stream + // include the directive in the supergraph and generate a hint. So the following test will complain that @stream // is unknown in the query. Not that the hints tests do test we properly raise an hint in that case. it.skip("returns validation errors when directives aren't present across all services", async () => { const invalidService = { diff --git a/gateway-js/src/__tests__/integration/fragments.test.ts b/gateway-js/src/__tests__/integration/fragments.test.ts index 939cac59c..9c08c1b1e 100644 --- a/gateway-js/src/__tests__/integration/fragments.test.ts +++ b/gateway-js/src/__tests__/integration/fragments.test.ts @@ -131,7 +131,7 @@ it('supports multiple named fragments (one level, mixed ordering)', async () => name: { first: 'Ada', last: 'Lovelace', - } + }, }, }); diff --git a/gateway-js/src/__tests__/integration/list-key.test.ts b/gateway-js/src/__tests__/integration/list-key.test.ts index cb7e84b90..5f51baa4e 100644 --- a/gateway-js/src/__tests__/integration/list-key.test.ts +++ b/gateway-js/src/__tests__/integration/list-key.test.ts @@ -1,6 +1,9 @@ import gql from 'graphql-tag'; import { execute, ServiceDefinitionModule } from '../execution-utils'; -import { astSerializer, queryPlanSerializer } from 'apollo-federation-integration-testsuite'; +import { + astSerializer, + queryPlanSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(astSerializer); expect.addSnapshotSerializer(queryPlanSerializer); @@ -62,7 +65,7 @@ const userService: ServiceDefinitionModule = { resolvers: { User: { __resolveReference(reference) { - return users.find(user => listsAreEqual(user.id, reference.id)); + return users.find((user) => listsAreEqual(user.id, reference.id)); }, }, }, diff --git a/gateway-js/src/__tests__/integration/logger.test.ts b/gateway-js/src/__tests__/integration/logger.test.ts index c323b9c7a..d990673a9 100644 --- a/gateway-js/src/__tests__/integration/logger.test.ts +++ b/gateway-js/src/__tests__/integration/logger.test.ts @@ -1,16 +1,17 @@ import { ApolloGateway } from '../..'; import type { Logger } from '@apollo/utils.logger'; -import { PassThrough } from "stream"; +import { PassThrough } from 'stream'; -import * as winston from "winston"; +import * as winston from 'winston'; import WinstonTransport from 'winston-transport'; -import * as bunyan from "bunyan"; -import * as loglevel from "loglevel"; -import * as log4js from "log4js"; +import * as bunyan from 'bunyan'; +import * as loglevel from 'loglevel'; +import * as log4js from 'log4js'; -const LOWEST_LOG_LEVEL = "debug"; +const LOWEST_LOG_LEVEL = 'debug'; -const KNOWN_DEBUG_MESSAGE = "Gateway successfully initialized (but not yet loaded)"; +const KNOWN_DEBUG_MESSAGE = + 'Gateway successfully initialized (but not yet loaded)'; async function triggerKnownDebugMessage(logger: Logger) { // Trigger a known error. @@ -18,13 +19,13 @@ async function triggerKnownDebugMessage(logger: Logger) { // message outside of the constructor, but it seemed worth testing // the compatibility with `ApolloGateway` itself rather than generically. // The error does not matter, so it is caught and ignored. - await new ApolloGateway({ logger }).load().catch(_e => undefined); + await new ApolloGateway({ logger }).load().catch((_e) => undefined); } -describe("logger", () => { +describe('logger', () => { it("works with 'winston'", async () => { const sink = jest.fn(); - const transport = new class extends WinstonTransport { + const transport = new (class extends WinstonTransport { constructor() { super({ format: winston.format.json(), @@ -34,16 +35,18 @@ describe("logger", () => { log(info: any) { sink(info); } - }; + })(); const logger = winston.createLogger({ level: 'debug' }).add(transport); await triggerKnownDebugMessage(logger); - expect(sink).toHaveBeenCalledWith(expect.objectContaining({ - level: LOWEST_LOG_LEVEL, - message: KNOWN_DEBUG_MESSAGE, - })); + expect(sink).toHaveBeenCalledWith( + expect.objectContaining({ + level: LOWEST_LOG_LEVEL, + message: KNOWN_DEBUG_MESSAGE, + }), + ); }); it("works with 'bunyan'", async () => { @@ -51,30 +54,36 @@ describe("logger", () => { // Bunyan uses streams for its logging implementations. const writable = new PassThrough(); - writable.on("data", data => sink(JSON.parse(data.toString()))); + writable.on('data', (data) => sink(JSON.parse(data.toString()))); const logger = bunyan.createLogger({ - name: "test-logger-bunyan", - streams: [{ - level: LOWEST_LOG_LEVEL, - stream: writable, - }] + name: 'test-logger-bunyan', + streams: [ + { + level: LOWEST_LOG_LEVEL, + stream: writable, + }, + ], }); await triggerKnownDebugMessage(logger); - expect(sink).toHaveBeenCalledWith(expect.objectContaining({ - level: bunyan.DEBUG, - msg: KNOWN_DEBUG_MESSAGE, - })); + expect(sink).toHaveBeenCalledWith( + expect.objectContaining({ + level: bunyan.DEBUG, + msg: KNOWN_DEBUG_MESSAGE, + }), + ); }); it("works with 'loglevel'", async () => { const sink = jest.fn(); - const logger = loglevel.getLogger("test-logger-loglevel") - logger.methodFactory = (_methodName, level): loglevel.LoggingMethod => - (message) => sink({ level, message }); + const logger = loglevel.getLogger('test-logger-loglevel'); + logger.methodFactory = + (_methodName, level): loglevel.LoggingMethod => + (message) => + sink({ level, message }); // The `setLevel` method must be called after overwriting `methodFactory`. // This is an intentional API design pattern of the loglevel package: @@ -96,17 +105,17 @@ describe("logger", () => { appenders: { custom: { type: { - configure: () => - (loggingEvent: log4js.LoggingEvent) => sink(loggingEvent) - } - } + configure: () => (loggingEvent: log4js.LoggingEvent) => + sink(loggingEvent), + }, + }, }, categories: { default: { appenders: ['custom'], level: LOWEST_LOG_LEVEL, - } - } + }, + }, }); const logger = log4js.getLogger(); @@ -114,9 +123,11 @@ describe("logger", () => { await triggerKnownDebugMessage(logger); - expect(sink).toHaveBeenCalledWith(expect.objectContaining({ - level: log4js.levels.DEBUG, - data: [KNOWN_DEBUG_MESSAGE], - })); + expect(sink).toHaveBeenCalledWith( + expect.objectContaining({ + level: log4js.levels.DEBUG, + data: [KNOWN_DEBUG_MESSAGE], + }), + ); }); }); diff --git a/gateway-js/src/__tests__/integration/managed.test.ts b/gateway-js/src/__tests__/integration/managed.test.ts index 07d5b2b3b..af48a3dc0 100644 --- a/gateway-js/src/__tests__/integration/managed.test.ts +++ b/gateway-js/src/__tests__/integration/managed.test.ts @@ -262,7 +262,7 @@ describe('Managed gateway with explicit UplinkSupergraphManager', () => { gateway = new ApolloGateway({ logger, - supergraphSdl: uplinkManager + supergraphSdl: uplinkManager, }); await expect(gateway.load()).resolves.not.toThrow(); diff --git a/gateway-js/src/__tests__/integration/multiple-key.test.ts b/gateway-js/src/__tests__/integration/multiple-key.test.ts index 772dbced1..e4100bddc 100644 --- a/gateway-js/src/__tests__/integration/multiple-key.test.ts +++ b/gateway-js/src/__tests__/integration/multiple-key.test.ts @@ -1,6 +1,9 @@ import gql from 'graphql-tag'; import { execute, ServiceDefinitionModule } from '../execution-utils'; -import { astSerializer, queryPlanSerializer } from 'apollo-federation-integration-testsuite'; +import { + astSerializer, + queryPlanSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(astSerializer); expect.addSnapshotSerializer(queryPlanSerializer); @@ -45,7 +48,7 @@ const reviewService: ServiceDefinitionModule = { }, User: { reviews(user) { - return reviews.filter(review => review.authorId === user.id); + return reviews.filter((review) => review.authorId === user.id); }, }, Review: { @@ -107,8 +110,8 @@ const userService: ServiceDefinitionModule = { group: () => ({ id: 1, name: 'Apollo GraphQL' }), __resolveReference(reference) { if (reference.ssn) - return users.find(user => user.ssn === reference.ssn); - else return users.find(user => user.id === reference.id); + return users.find((user) => user.ssn === reference.ssn); + else return users.find((user) => user.id === reference.id); }, }, }, @@ -234,7 +237,7 @@ it('fetches keys as needed to reduce round trip queries', async () => { { query, }, - [userService, reviewService, actuaryService] + [userService, reviewService, actuaryService], ); expect(errors).toBeFalsy(); diff --git a/gateway-js/src/__tests__/integration/networkRequests.test.ts b/gateway-js/src/__tests__/integration/networkRequests.test.ts index bd3826e6f..0dd055cbf 100644 --- a/gateway-js/src/__tests__/integration/networkRequests.test.ts +++ b/gateway-js/src/__tests__/integration/networkRequests.test.ts @@ -455,9 +455,9 @@ describe('Downstream service health checks', () => { // Update mockSupergraphSdlRequestSuccessIfAfter( - 'originalId-1234', - 'updatedId-5678', - getTestingSupergraphSdl(fixturesWithUpdate), + 'originalId-1234', + 'updatedId-5678', + getTestingSupergraphSdl(fixturesWithUpdate), ); mockAllServicesHealthCheckSuccess(); @@ -499,8 +499,8 @@ describe('Downstream service health checks', () => { // Update (with one health check failure) mockSupergraphSdlRequestSuccessIfAfter( - 'originalId-1234', - 'updatedId-5678', + 'originalId-1234', + 'updatedId-5678', getTestingSupergraphSdl(fixturesWithUpdate), ); mockServiceHealthCheck(accounts).reply(500); diff --git a/gateway-js/src/__tests__/integration/provides.test.ts b/gateway-js/src/__tests__/integration/provides.test.ts index 14c925139..f1473d1d9 100644 --- a/gateway-js/src/__tests__/integration/provides.test.ts +++ b/gateway-js/src/__tests__/integration/provides.test.ts @@ -12,7 +12,7 @@ it('does not have to go to another service when field is given', async () => { } `; - const { data, queryPlan } = await execute( { + const { data, queryPlan } = await execute({ query, }); @@ -63,11 +63,30 @@ it('does not load fields provided even when going to other service', async () => expect(data).toEqual({ topReviews: [ - { author: { username: '@ada', name: { first: 'Ada', last: 'Lovelace' } } }, - { author: { username: '@ada', name: { first: 'Ada', last: 'Lovelace' } } }, - { author: { username: '@complete', name: { first: 'Alan', last: 'Turing' } } }, - { author: { username: '@complete', name: { first: 'Alan', last: 'Turing' } } }, - { author: { username: '@complete', name: { first: 'Alan', last: 'Turing' } } }, + { + author: { username: '@ada', name: { first: 'Ada', last: 'Lovelace' } }, + }, + { + author: { username: '@ada', name: { first: 'Ada', last: 'Lovelace' } }, + }, + { + author: { + username: '@complete', + name: { first: 'Alan', last: 'Turing' }, + }, + }, + { + author: { + username: '@complete', + name: { first: 'Alan', last: 'Turing' }, + }, + }, + { + author: { + username: '@complete', + name: { first: 'Alan', last: 'Turing' }, + }, + }, ], }); diff --git a/gateway-js/src/__tests__/integration/requires.test.ts b/gateway-js/src/__tests__/integration/requires.test.ts index 3e24d1b6f..e9bc39c5e 100644 --- a/gateway-js/src/__tests__/integration/requires.test.ts +++ b/gateway-js/src/__tests__/integration/requires.test.ts @@ -1,6 +1,9 @@ import gql from 'graphql-tag'; import { execute } from '../execution-utils'; -import { astSerializer, queryPlanSerializer } from 'apollo-federation-integration-testsuite'; +import { + astSerializer, + queryPlanSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(astSerializer); expect.addSnapshotSerializer(queryPlanSerializer); diff --git a/gateway-js/src/__tests__/integration/scope.test.ts b/gateway-js/src/__tests__/integration/scope.test.ts index 9bcad9569..e730c9856 100644 --- a/gateway-js/src/__tests__/integration/scope.test.ts +++ b/gateway-js/src/__tests__/integration/scope.test.ts @@ -343,7 +343,7 @@ describe('scope', () => { `); }); - it("merges nested conditions when possible", async () => { + it('merges nested conditions when possible', async () => { const query = `#graphql query GetProducts { topProducts { diff --git a/gateway-js/src/__tests__/integration/unions.test.ts b/gateway-js/src/__tests__/integration/unions.test.ts index bc8bd7fda..145325af4 100644 --- a/gateway-js/src/__tests__/integration/unions.test.ts +++ b/gateway-js/src/__tests__/integration/unions.test.ts @@ -1,5 +1,8 @@ import gql from 'graphql-tag'; -import { astSerializer, queryPlanSerializer } from 'apollo-federation-integration-testsuite'; +import { + astSerializer, + queryPlanSerializer, +} from 'apollo-federation-integration-testsuite'; import { execute } from '../execution-utils'; expect.addSnapshotSerializer(astSerializer); @@ -24,35 +27,32 @@ it('handles multiple union type conditions that share a response name (media)', } `; - const { queryPlan, errors } = await execute( - { query }, - [ - { - name: 'contentService', - typeDefs: gql` - extend type Query { - content: Content - } - union Content = Audio | Video - type Audio { - media: AudioURL - } - type AudioURL { - url: String - } - type Video { - media: VideoAspectRatio - } - type VideoAspectRatio { - aspectRatio: String - } - `, - resolvers: { - Query: {}, - }, + const { queryPlan, errors } = await execute({ query }, [ + { + name: 'contentService', + typeDefs: gql` + extend type Query { + content: Content + } + union Content = Audio | Video + type Audio { + media: AudioURL + } + type AudioURL { + url: String + } + type Video { + media: VideoAspectRatio + } + type VideoAspectRatio { + aspectRatio: String + } + `, + resolvers: { + Query: {}, }, - ], - ); + }, + ]); expect(errors).toBeUndefined(); expect(queryPlan).toMatchInlineSnapshot(` diff --git a/gateway-js/src/__tests__/integration/variables.test.ts b/gateway-js/src/__tests__/integration/variables.test.ts index 15209f44f..ed70a7006 100644 --- a/gateway-js/src/__tests__/integration/variables.test.ts +++ b/gateway-js/src/__tests__/integration/variables.test.ts @@ -109,7 +109,7 @@ it('works with default variables in the schema', async () => { name: { first: 'Ada', last: 'Lovelace', - } + }, }, }, }); diff --git a/gateway-js/src/__tests__/queryPlanCucumber.test.ts b/gateway-js/src/__tests__/queryPlanCucumber.test.ts index 9b4648e43..8715db16e 100644 --- a/gateway-js/src/__tests__/queryPlanCucumber.test.ts +++ b/gateway-js/src/__tests__/queryPlanCucumber.test.ts @@ -7,13 +7,10 @@ import { getFederatedTestingSchema } from './execution-utils'; import { operationFromDocument } from '@apollo/federation-internals'; const buildQueryPlanFeature = loadFeature( - './gateway-js/src/__tests__/build-query-plan.feature' + './gateway-js/src/__tests__/build-query-plan.feature', ); - -const features = [ - buildQueryPlanFeature -]; +const features = [buildQueryPlanFeature]; features.forEach((feature) => { defineFeature(feature, (test) => { @@ -28,26 +25,31 @@ features.forEach((feature) => { const givenQuery = () => { given(/^query$/im, (operation: string) => { operationDocument = gql(operation); - }) - } + }); + }; const thenQueryPlanShouldBe = () => { then(/^query plan$/i, (expectedQueryPlan: string) => { - queryPlan = queryPlanner.buildQueryPlan(operationFromDocument(schema, operationDocument)); + queryPlan = queryPlanner.buildQueryPlan( + operationFromDocument(schema, operationDocument), + ); const parsedExpectedPlan = JSON.parse(expectedQueryPlan); expect(queryPlan).toEqual(parsedExpectedPlan); - }) - } + }); + }; // step over each defined step in the .feature and execute the correct // matching step fn defined above scenario.steps.forEach(({ stepText }) => { const title = stepText.toLocaleLowerCase(); - if (title === "query") givenQuery(); - else if (title === "query plan") thenQueryPlanShouldBe(); - else throw new Error(`Unrecognized steps used in "build-query-plan.feature"`); + if (title === 'query') givenQuery(); + else if (title === 'query plan') thenQueryPlanShouldBe(); + else + throw new Error( + `Unrecognized steps used in "build-query-plan.feature"`, + ); }); }); }); diff --git a/gateway-js/src/__tests__/resultShaping.test.ts b/gateway-js/src/__tests__/resultShaping.test.ts index 47a88d66c..a33602964 100644 --- a/gateway-js/src/__tests__/resultShaping.test.ts +++ b/gateway-js/src/__tests__/resultShaping.test.ts @@ -1,6 +1,9 @@ -import { buildSchemaFromAST, parseOperation } from "@apollo/federation-internals" -import { gql } from "apollo-federation-integration-testsuite"; -import { computeResponse } from "../resultShaping"; +import { + buildSchemaFromAST, + parseOperation, +} from '@apollo/federation-internals'; +import { gql } from 'apollo-federation-integration-testsuite'; +import { computeResponse } from '../resultShaping'; const introspectionHandling = () => null; @@ -39,27 +42,32 @@ describe('gateway post-processing', () => { `); const input = { - "t": { - "a": 0, - "b": 'testData', - "c": [{ - __typename: 'P1', - id: 'foo', - x: 1, - y: 2, - }, { - __typename: 'P2', - x: 10, - y: 20, - w: 30, - z: 40, - }], - "d": 1, + t: { + a: 0, + b: 'testData', + c: [ + { + __typename: 'P1', + id: 'foo', + x: 1, + y: 2, + }, + { + __typename: 'P2', + x: 10, + y: 20, + w: 30, + z: 40, + }, + ], + d: 1, }, - "v": 42 - } + v: 42, + }; - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t { a @@ -75,13 +83,16 @@ describe('gateway post-processing', () => { } } } - `); + `, + ); - expect(computeResponse({ - operation, - input, - introspectionHandling, - })).toMatchInlineSnapshot(` + expect( + computeResponse({ + operation, + input, + introspectionHandling, + }), + ).toMatchInlineSnapshot(` Object { "data": Object { "t": Object { @@ -121,33 +132,38 @@ describe('gateway post-processing', () => { `); const tObj = { - "a": null, - "b": null, - "c": [24, null, 42, null], - "d": [24, null, 42, null], - "e": [24, null, 42, null], - "f": [24, null, 42, null], - } + a: null, + b: null, + c: [24, null, 42, null], + d: [24, null, 42, null], + e: [24, null, 42, null], + f: [24, null, 42, null], + }; const input = { - "tNullable": tObj, - "tNonNullable": tObj, - } + tNullable: tObj, + tNonNullable: tObj, + }; test('no propagation on nullable (non-list) type', () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { tNonNullable { a } } - `); - - expect(computeResponse({ - operation, - input, - introspectionHandling, - })).toMatchInlineSnapshot(` + `, + ); + + expect( + computeResponse({ + operation, + input, + introspectionHandling, + }), + ).toMatchInlineSnapshot(` Object { "data": Object { "tNonNullable": Object { @@ -160,13 +176,16 @@ describe('gateway post-processing', () => { }); test('propagation on non-nullable (non-list) type', () => { - const operationNonNullable = parseOperation(schema, ` + const operationNonNullable = parseOperation( + schema, + ` { tNonNullable { b } } - `); + `, + ); let res = computeResponse({ operation: operationNonNullable, @@ -183,14 +202,16 @@ describe('gateway post-processing', () => { `); expect(res.errors[0].path).toStrictEqual(['tNonNullable', 'b']); - - const operationNullable = parseOperation(schema, ` + const operationNullable = parseOperation( + schema, + ` { tNullable { b } } - `); + `, + ); res = computeResponse({ operation: operationNullable, @@ -211,19 +232,24 @@ describe('gateway post-processing', () => { }); test('no propagation on nullable list type', () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { tNonNullable { c } } - `); - - expect(computeResponse({ - operation, - input, - introspectionHandling, - })).toMatchInlineSnapshot(` + `, + ); + + expect( + computeResponse({ + operation, + input, + introspectionHandling, + }), + ).toMatchInlineSnapshot(` Object { "data": Object { "tNonNullable": Object { @@ -241,13 +267,16 @@ describe('gateway post-processing', () => { }); test('no propagation on null elements of non-nullable list type with nullable inner element type', () => { - const operationNonNullable = parseOperation(schema, ` + const operationNonNullable = parseOperation( + schema, + ` { tNonNullable { d } } - `); + `, + ); const res = computeResponse({ operation: operationNonNullable, @@ -272,13 +301,16 @@ describe('gateway post-processing', () => { }); test('propagation on null elements of list type with non-nullable inner element type', () => { - const operationNonNullable = parseOperation(schema, ` + const operationNonNullable = parseOperation( + schema, + ` { tNonNullable { e } } - `); + `, + ); const res = computeResponse({ operation: operationNonNullable, @@ -303,13 +335,16 @@ describe('gateway post-processing', () => { }); test('propagation on null elements of non-nullable list type with non-nullable inner element type', () => { - const operationNonNullable = parseOperation(schema, ` + const operationNonNullable = parseOperation( + schema, + ` { tNonNullable { f } } - `); + `, + ); let res = computeResponse({ operation: operationNonNullable, @@ -328,13 +363,16 @@ describe('gateway post-processing', () => { expect(res.errors[0].path).toStrictEqual(['tNonNullable', 'f', 1]); expect(res.errors[1].path).toStrictEqual(['tNonNullable', 'f', 3]); - const operationNullable = parseOperation(schema, ` + const operationNullable = parseOperation( + schema, + ` { tNullable { f } } - `); + `, + ); res = computeResponse({ operation: operationNullable, @@ -365,14 +403,17 @@ describe('gateway post-processing', () => { `); const input = { - "x": 'foo', - } + x: 'foo', + }; - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { x } - `); + `, + ); const res = computeResponse({ operation, @@ -403,21 +444,23 @@ describe('gateway post-processing', () => { `); const input = { - "t": { - "a": 42, - "q": { - "t": { - "q": { - "t": { - "a": 24 + t: { + a: 42, + q: { + t: { + q: { + t: { + a: 24, }, }, }, }, }, - } + }; - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { __typename t { @@ -435,13 +478,16 @@ describe('gateway post-processing', () => { } } } - `); + `, + ); - expect(computeResponse({ - operation, - input, - introspectionHandling, - })).toMatchInlineSnapshot(` + expect( + computeResponse({ + operation, + input, + introspectionHandling, + }), + ).toMatchInlineSnapshot(` Object { "data": Object { "__typename": "Query", @@ -485,19 +531,21 @@ describe('gateway post-processing', () => { `); const input = { - "i": [ + i: [ { - "__typename": "A", - "x": 24, + __typename: 'A', + x: 24, }, { - "__typename": "B", - "x": 42, + __typename: 'B', + x: 42, }, - ] - } + ], + }; - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { i { ... on I { @@ -506,13 +554,16 @@ describe('gateway post-processing', () => { } } } - `); + `, + ); - expect(computeResponse({ - operation, - input, - introspectionHandling, - })).toMatchInlineSnapshot(` + expect( + computeResponse({ + operation, + input, + introspectionHandling, + }), + ).toMatchInlineSnapshot(` Object { "data": Object { "i": Array [ @@ -543,12 +594,15 @@ describe('gateway post-processing', () => { included: 'world', }; - const operation = parseOperation(schema, `#graphql + const operation = parseOperation( + schema, + `#graphql query DefaultedIfCondition($if: Boolean = true) { skipped: hello @skip(if: $if) included: hello @include(if: $if) } - `); + `, + ); expect( computeResponse({ @@ -578,13 +632,16 @@ describe('gateway post-processing', () => { included: 'world', }; - const operation = parseOperation(schema, `#graphql + const operation = parseOperation( + schema, + `#graphql # note that the default conditional is inverted from the previous test query DefaultedIfCondition($if: Boolean = false) { skipped: hello @skip(if: $if) included: hello @include(if: $if) } - `); + `, + ); expect( computeResponse({ @@ -602,4 +659,4 @@ describe('gateway post-processing', () => { } `); }); -}) +}); diff --git a/gateway-js/src/datasources/__tests__/LocalGraphQLDataSource.test.ts b/gateway-js/src/datasources/__tests__/LocalGraphQLDataSource.test.ts index 2ffeb89fb..64c645dde 100644 --- a/gateway-js/src/datasources/__tests__/LocalGraphQLDataSource.test.ts +++ b/gateway-js/src/datasources/__tests__/LocalGraphQLDataSource.test.ts @@ -42,7 +42,7 @@ describe('constructing requests', () => { }, incomingRequestContext: { context: { userId: 2 }, - } as GatewayGraphQLRequestContext<{userId: number}>, + } as GatewayGraphQLRequestContext<{ userId: number }>, context: { userId: 2 }, }); diff --git a/gateway-js/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts b/gateway-js/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts index 537dc798e..74482c057 100644 --- a/gateway-js/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts +++ b/gateway-js/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts @@ -231,7 +231,11 @@ describe('constructing requests', () => { nock('https://api.example.com') .post('/foo', { query: '{ me { name } }' }) - .reply(200, { data: { me: 'james' } }, {'content-type': 'application/graphql-response+json'}); + .reply( + 200, + { data: { me: 'james' } }, + { 'content-type': 'application/graphql-response+json' }, + ); const { data } = await DataSource.process({ ...defaultProcessOptions, diff --git a/gateway-js/src/schema-helper/__tests__/addExtensions.test.ts b/gateway-js/src/schema-helper/__tests__/addExtensions.test.ts index 7d3f6fbc9..53b2553de 100644 --- a/gateway-js/src/schema-helper/__tests__/addExtensions.test.ts +++ b/gateway-js/src/schema-helper/__tests__/addExtensions.test.ts @@ -4,12 +4,14 @@ import { ApolloGraphQLSchemaExtensions } from '../../typings/graphql'; const { version } = require('../../../package.json'); describe('addExtensions', () => { - it('adds gateway extensions to a schema', async () => { const schema = buildSchema('type Query { hello: String }'); expect(schema.extensions).toEqual({}); - const actualExtensions: ApolloGraphQLSchemaExtensions = addExtensions(schema).extensions; - expect(actualExtensions).toEqual({ apollo: { gateway: { version: version } } }); + const actualExtensions: ApolloGraphQLSchemaExtensions = + addExtensions(schema).extensions; + expect(actualExtensions).toEqual({ + apollo: { gateway: { version: version } }, + }); }); it('does not delete existing extensions', async () => { @@ -19,18 +21,19 @@ describe('addExtensions', () => { foo: 'bar', apollo: { gateway: { - version: 'hello' - } - } + version: 'hello', + }, + }, }; - const actualExtensions: ApolloGraphQLSchemaExtensions = addExtensions(schema).extensions; + const actualExtensions: ApolloGraphQLSchemaExtensions = + addExtensions(schema).extensions; expect(actualExtensions).toEqual({ foo: 'bar', apollo: { gateway: { - version: version - } - } + version: version, + }, + }, }); }); @@ -38,15 +41,16 @@ describe('addExtensions', () => { const schema = buildSchema('type Query { hello: String }'); expect(schema.extensions).toEqual({}); schema.extensions = { - apollo: undefined + apollo: undefined, }; - const actualExtensions: ApolloGraphQLSchemaExtensions = addExtensions(schema).extensions; + const actualExtensions: ApolloGraphQLSchemaExtensions = + addExtensions(schema).extensions; expect(actualExtensions).toEqual({ apollo: { gateway: { - version: version - } - } + version: version, + }, + }, }); }); @@ -55,16 +59,17 @@ describe('addExtensions', () => { expect(schema.extensions).toEqual({}); schema.extensions = { apollo: { - gateway: undefined - } + gateway: undefined, + }, }; - const actualExtensions: ApolloGraphQLSchemaExtensions = addExtensions(schema).extensions; + const actualExtensions: ApolloGraphQLSchemaExtensions = + addExtensions(schema).extensions; expect(actualExtensions).toEqual({ apollo: { gateway: { - version: version - } - } + version: version, + }, + }, }); }); }); diff --git a/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts b/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts index b13b3a3d5..5873afd81 100644 --- a/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts +++ b/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts @@ -3,11 +3,17 @@ import { fixtures, fixturesWithUpdate, } from 'apollo-federation-integration-testsuite'; -import { nockBeforeEach, nockAfterEach } from '../../../__tests__/nockAssertions'; +import { + nockBeforeEach, + nockAfterEach, +} from '../../../__tests__/nockAssertions'; import { RemoteGraphQLDataSource, ServiceEndpointDefinition } from '../../..'; import { IntrospectAndCompose } from '..'; import { mockAllServicesSdlQuerySuccess } from '../../../__tests__/integration/nockMocks'; -import { getTestingSupergraphSdl, wait } from '../../../__tests__/execution-utils'; +import { + getTestingSupergraphSdl, + wait, +} from '../../../__tests__/execution-utils'; import resolvable from '@josephg/resolvable'; import type { Logger } from '@apollo/utils.logger'; @@ -182,10 +188,7 @@ describe('IntrospectAndCompose', () => { }, }); - await Promise.all([ - healthCheckPromiseOnLoad, - healthCheckPromiseOnUpdate, - ]); + await Promise.all([healthCheckPromiseOnLoad, healthCheckPromiseOnUpdate]); expect(healthCheckSpy).toHaveBeenNthCalledWith( 1, diff --git a/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts b/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts index c13a5d131..3fb686d73 100644 --- a/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts +++ b/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts @@ -10,7 +10,7 @@ describe('loadServicesFromRemoteEndpoint', () => { loadServicesFromRemoteEndpoint({ serviceList, serviceSdlCache, - getServiceIntrospectionHeaders: async () => ({}) + getServiceIntrospectionHeaders: async () => ({}), }), ).rejects.toThrowError( "Tried to load schema for 'test' but no 'url' was specified.", diff --git a/gateway-js/src/supergraphManagers/UplinkSupergraphManager/__tests__/loadSupergraphSdlFromStorage.test.ts b/gateway-js/src/supergraphManagers/UplinkSupergraphManager/__tests__/loadSupergraphSdlFromStorage.test.ts index 720676ba6..90672ed87 100644 --- a/gateway-js/src/supergraphManagers/UplinkSupergraphManager/__tests__/loadSupergraphSdlFromStorage.test.ts +++ b/gateway-js/src/supergraphManagers/UplinkSupergraphManager/__tests__/loadSupergraphSdlFromStorage.test.ts @@ -93,7 +93,9 @@ describe('loadSupergraphSdlFromStorage', () => { }); it('Queries alternate Uplink URL if first one times out', async () => { - mockSupergraphSdlRequest('originalId-1234', mockCloudConfigUrl1).delay(120_000).reply(500); + mockSupergraphSdlRequest('originalId-1234', mockCloudConfigUrl1) + .delay(120_000) + .reply(500); mockSupergraphSdlRequestIfAfter( 'originalId-1234', mockCloudConfigUrl2, diff --git a/query-planner-js/src/__tests__/allFeatures.test.ts b/query-planner-js/src/__tests__/allFeatures.test.ts index 4cfc265b2..698a21092 100644 --- a/query-planner-js/src/__tests__/allFeatures.test.ts +++ b/query-planner-js/src/__tests__/allFeatures.test.ts @@ -1,9 +1,7 @@ import fs from 'fs'; import { defineFeature, loadFeatures } from 'jest-cucumber'; import path from 'path'; -import { - QueryPlan, QueryPlanner -} from '..'; +import { QueryPlan, QueryPlanner } from '..'; import { Operation, Schema, @@ -12,7 +10,6 @@ import { } from '@apollo/federation-internals'; import { parse, validate } from 'graphql'; - // This test looks over all directories under tests/features and finds "supergraphSdl.graphql" in // each of those directories. It runs all of the .feature cases in that directory against that schema. // To add test cases against new schemas, create a sub directory under "features" with the new schema @@ -42,8 +39,12 @@ for (const directory of directories) { const supergraphSdl = fs.readFileSync(schemaPath, 'utf8'); const supergraph = Supergraph.build(supergraphSdl); schema = supergraph.schema; - const exposeDocumentNodeInFetchNode = feature.title.endsWith("(with ExposeDocumentNodeInFetchNode)"); - queryPlanner = new QueryPlanner(supergraph, { exposeDocumentNodeInFetchNode }); + const exposeDocumentNodeInFetchNode = feature.title.endsWith( + '(with ExposeDocumentNodeInFetchNode)', + ); + queryPlanner = new QueryPlanner(supergraph, { + exposeDocumentNodeInFetchNode, + }); }); feature.scenarios.forEach((scenario) => { @@ -54,7 +55,9 @@ for (const directory of directories) { const givenQuery = () => { given(/^query$/im, (operationString: string) => { operation = parseOperation(schema, operationString); - expect(validate(schema.toGraphQLJSSchema(), parse(operationString))).toStrictEqual([]); + expect( + validate(schema.toGraphQLJSSchema(), parse(operationString)), + ).toStrictEqual([]); }); }; diff --git a/query-planner-js/src/__tests__/buildPlan.defer.test.ts b/query-planner-js/src/__tests__/buildPlan.defer.test.ts index e55da6bcb..bca4a418e 100644 --- a/query-planner-js/src/__tests__/buildPlan.defer.test.ts +++ b/query-planner-js/src/__tests__/buildPlan.defer.test.ts @@ -1,10 +1,21 @@ -import { operationFromDocument, Schema, ServiceDefinition } from '@apollo/federation-internals'; +import { + operationFromDocument, + Schema, + ServiceDefinition, +} from '@apollo/federation-internals'; import gql from 'graphql-tag'; import { QueryPlanner } from '@apollo/query-planner'; -import { composeAndCreatePlanner, composeAndCreatePlannerWithOptions } from "./testHelper"; - -function composeAndCreatePlannerWithDefer(...services: ServiceDefinition[]): [Schema, QueryPlanner] { - return composeAndCreatePlannerWithOptions(services, { incrementalDelivery: { enableDefer : true }}); +import { + composeAndCreatePlanner, + composeAndCreatePlannerWithOptions, +} from './testHelper'; + +function composeAndCreatePlannerWithDefer( + ...services: ServiceDefinition[] +): [Schema, QueryPlanner] { + return composeAndCreatePlannerWithOptions(services, { + incrementalDelivery: { enableDefer: true }, + }); } describe('handles simple @defer', () => { @@ -12,14 +23,14 @@ describe('handles simple @defer', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T + t: T } type T @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -29,21 +40,24 @@ describe('handles simple @defer', () => { v1: Int v2: Int } - ` - } + `, + }; test('without defer-support enabled', () => { const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - v1 - ... @defer { - v2 + const operation = operationFromDocument( + api, + gql` + { + t { + v1 + ... @defer { + v2 + } } } - } - `); + `, + ); // Without defer-support enabled, we should get the same plan than if `@defer` wasn't there. const plan = queryPlanner.buildQueryPlan(operation); @@ -80,17 +94,23 @@ describe('handles simple @defer', () => { }); test('with defer-support enabled', () => { - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - v1 - ... @defer { - v2 + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v1 + ... @defer { + v2 + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -161,14 +181,14 @@ describe('non-router-based-defer', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T + t: T } type T @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -182,22 +202,28 @@ describe('non-router-based-defer', () => { a: Int b: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - v { - a - ... @defer { - b + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v { + a + ... @defer { + b + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // We cannot handle a @defer on value type at the query planning level, so we expect nothing to be @@ -259,15 +285,15 @@ describe('non-router-based-defer', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T + t: T } type T @key(fields: "id", resolvable: false) { id: ID! v1: String } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -276,20 +302,26 @@ describe('non-router-based-defer', () => { id: ID! v2: String } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - ... @defer { - v1 + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + ... @defer { + v1 + } + v2 } - v2 } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // While the @defer in the operation is on an entity, the @key in the first subgraph @@ -348,7 +380,7 @@ describe('non-router-based-defer', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T + t: T } type T @key(fields: "id") { @@ -359,8 +391,8 @@ describe('non-router-based-defer', () => { id: ID! x: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -378,24 +410,30 @@ describe('non-router-based-defer', () => { type U @key(fields: "id") { id: ID! } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - v { - a - ... @defer { - u { - x + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v { + a + ... @defer { + u { + x + } } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // While we cannot defer the initial resolving of `u`, we can defer the fetch of it's `x` field, @@ -478,7 +516,7 @@ test('@defer resuming in same subgraph', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T + t: T } type T @key(fields: "id") { @@ -486,20 +524,23 @@ test('@defer resuming in same subgraph', () => { v0: String v1: String } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1); - const operation = operationFromDocument(api, gql` - { - t { - v0 - ... @defer { - v1 + const operation = operationFromDocument( + api, + gql` + { + t { + v0 + ... @defer { + v1 + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -552,7 +593,7 @@ test('@defer multiple fields in different subgraphs', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T + t: T } type T @key(fields: "id") { @@ -560,8 +601,8 @@ test('@defer multiple fields in different subgraphs', () => { v0: String v1: String } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -570,8 +611,8 @@ test('@defer multiple fields in different subgraphs', () => { id: ID! v2: String } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -580,22 +621,29 @@ test('@defer multiple fields in different subgraphs', () => { id: ID! v3: String } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` - { - t { - v0 - ... @defer { - v1 - v2 - v3 + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v0 + ... @defer { + v1 + v2 + v3 + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -682,7 +730,7 @@ test('multiple (non-nested) @defer + label handling', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T + t: T } type T @key(fields: "id") { @@ -690,8 +738,8 @@ test('multiple (non-nested) @defer + label handling', () => { v0: String v1: String } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -705,8 +753,8 @@ test('multiple (non-nested) @defer + label handling', () => { type U @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -716,29 +764,36 @@ test('multiple (non-nested) @defer + label handling', () => { x: Int y: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` - { - t { - v0 - ... @defer(label: "defer_v1") { - v1 - } - ... @defer { - v2 - } - v3 { - x - ... @defer(label: "defer_in_v3") { - y + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v0 + ... @defer(label: "defer_v1") { + v1 + } + ... @defer { + v2 + } + v3 { + x + ... @defer(label: "defer_in_v3") { + y + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -870,15 +925,15 @@ describe('nested @defer', () => { name: 'Subgraph1', typeDefs: gql` type Query { - me : User + me: User } type User @key(fields: "id") { id: ID! name: String } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -893,30 +948,36 @@ describe('nested @defer', () => { body: String author: User } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - me { - name - ... on User @defer { - messages { - body - author { - name - ... @defer { - messages { - body + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + me { + name + ... on User @defer { + messages { + body + author { + name + ... @defer { + messages { + body + } } } } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -1028,15 +1089,15 @@ describe('nested @defer', () => { name: 'Subgraph1', typeDefs: gql` type Query { - me : User + me: User } type User @key(fields: "id") { id: ID! name: String } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -1055,25 +1116,31 @@ describe('nested @defer', () => { paragraphs: [String] lines: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - me { - ... @defer { - messages { - ... @defer { - body { - lines + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + me { + ... @defer { + messages { + ... @defer { + body { + lines + } } } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -1135,15 +1202,15 @@ describe('nested @defer', () => { name: 'Subgraph1', typeDefs: gql` type Query { - me : User + me: User } type User @key(fields: "id") { id: ID! name: String } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -1153,23 +1220,29 @@ describe('nested @defer', () => { age: Int address: String } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - me { - name - ... @defer { - age + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + me { + name ... @defer { - address + age + ... @defer { + address + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -1247,32 +1320,35 @@ describe('nested @defer', () => { name: 'Subgraph1', typeDefs: gql` type Query { - me : User + me: User } - type User { + type User { id: ID! name: String age: Int address: String } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1); - const operation = operationFromDocument(api, gql` - { - me { - name - ... @defer { - age + const operation = operationFromDocument( + api, + gql` + { + me { + name ... @defer { - address + age + ... @defer { + address + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -1320,7 +1396,7 @@ describe('nested @defer', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T + t: T } type T { @@ -1328,8 +1404,8 @@ describe('nested @defer', () => { a: Int b: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -1337,22 +1413,28 @@ describe('nested @defer', () => { type T @key(fields: "id") { id: ID! } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - ... @defer { - a + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + t { ... @defer { - b + a + ... @defer { + b + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Note that nothing can effectively be deferred, so everything is fetched in the very first fetch, but @@ -1400,7 +1482,7 @@ describe('@defer on mutation', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T + t: T } type Mutation { @@ -1413,9 +1495,8 @@ describe('@defer on mutation', () => { v0: String v1: String } - ` - - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -1424,27 +1505,33 @@ describe('@defer on mutation', () => { id: ID! v2: String } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - mutation mut { - update1 { - v0 - ... @defer { - v1 - } - } - update2 { - v1 - ... @defer { + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + mutation mut { + update1 { v0 - v2 + ... @defer { + v1 + } + } + update2 { + v1 + ... @defer { + v0 + v2 + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // What matters here is that the updates (that go to different fields) are correctly done in sequence, @@ -1545,7 +1632,7 @@ describe('@defer on mutation', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T + t: T } type Mutation { @@ -1557,8 +1644,8 @@ describe('@defer on mutation', () => { v0: String v1: String } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -1571,27 +1658,33 @@ describe('@defer on mutation', () => { id: ID! v2: String } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - mutation mut { - update1 { - v0 - ... @defer { - v1 - } - } - update2 { - v1 - ... @defer { + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + mutation mut { + update1 { v0 - v2 + ... @defer { + v1 + } + } + update2 { + v1 + ... @defer { + v0 + v2 + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // What matters here is that the updates (that go to different fields) are correctly done in sequence, @@ -1720,8 +1813,8 @@ test('multi-dependency deferred section', () => { id0: ID! v1: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -1731,8 +1824,8 @@ test('multi-dependency deferred section', () => { id1: ID! v2: Int } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -1742,8 +1835,8 @@ test('multi-dependency deferred section', () => { id2: ID! v3: Int } - ` - } + `, + }; const subgraph4 = { name: 'Subgraph4', @@ -1753,22 +1846,30 @@ test('multi-dependency deferred section', () => { id2: ID! v4: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2, subgraph3, subgraph4); - let operation = operationFromDocument(api, gql` - { - t { - v1 - v2 - v3 - ... @defer { - v4 + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + subgraph3, + subgraph4, + ); + let operation = operationFromDocument( + api, + gql` + { + t { + v1 + v2 + v3 + ... @defer { + v4 + } } } - } - `); + `, + ); let plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -1854,16 +1955,19 @@ test('multi-dependency deferred section', () => { } `); - operation = operationFromDocument(api, gql` - { - t { - v1 - ... @defer { - v4 + operation = operationFromDocument( + api, + gql` + { + t { + v1 + ... @defer { + v4 + } } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); // TODO: the following plan is admittedly not as effecient as it could be, as the 2 queries to @@ -1871,7 +1975,7 @@ test('multi-dependency deferred section', () => { // key dependencies for the deferred block, so it would make more sense to defer those fetches // as well. It is however tricky to both improve this here _and_ maintain the plan generate // just above (which is admittedly optimial). More precisely, what the code currently does is - // that when it gets to a defer, then it defers the fetch that gets the deferred fields (the + // that when it gets to a defer, then it defers the fetch that gets the deferred fields (the // fetch to subgraph 4 here), but it puts the "condition" resolution for the key of that fetch // in the non-deferred section. Here, resolving that fetch conditions is what creates the // dependency on the the fetches to subgraph 2 and 3, and so those get non-deferred. @@ -1981,8 +2085,8 @@ describe('@require', () => { id: ID! v1: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -1992,8 +2096,8 @@ describe('@require', () => { v2: Int @requires(fields: "v3") v3: Int @external } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -2002,20 +2106,27 @@ describe('@require', () => { id: ID! v3: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` - { - t { - v1 - ... @defer { - v2 + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v1 + ... @defer { + v2 + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2097,7 +2208,7 @@ describe('@provides', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T @provides(fields: "v2") + t: T @provides(fields: "v2") } type T @key(fields: "id") { @@ -2105,8 +2216,8 @@ describe('@provides', () => { v1: Int v2: Int @external } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -2115,20 +2226,26 @@ describe('@provides', () => { id: ID! v2: Int @shareable } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - v1 - ... @defer { - v2 + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v1 + ... @defer { + v2 + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2182,7 +2299,7 @@ test('@defer on query root type', () => { name: 'Subgraph1', typeDefs: gql` type Query { - op1 : Int + op1: Int op2: A } @@ -2191,8 +2308,8 @@ test('@defer on query root type', () => { y: Int next: Query } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -2201,25 +2318,31 @@ test('@defer on query root type', () => { op3: Int op4: Int } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - op2 { - x - y - next { - op3 - ... @defer { - op1 - op4 + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + op2 { + x + y + next { + op3 + ... @defer { + op1 + op4 + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2302,8 +2425,8 @@ test('@defer on everything queried', () => { id: ID! x: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -2312,20 +2435,26 @@ test('@defer on everything queried', () => { id: ID! y: Int } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - ... @defer { - t { - x - y + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + ... @defer { + t { + x + y + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2390,8 +2519,8 @@ test('@defer everything within entity', () => { id: ID! x: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -2400,20 +2529,26 @@ test('@defer everything within entity', () => { id: ID! y: Int } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - ... @defer { - x - y + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + ... @defer { + x + y + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2475,13 +2610,16 @@ test('@defer everything within entity', () => { }); describe('defer with conditions', () => { - test.each([{ - name: 'without explicit label', - label: undefined, - }, { - name: 'with explicit label', - label: 'testLabel', - }])('simple @defer with condition $name', ({label}) => { + test.each([ + { + name: 'without explicit label', + label: undefined, + }, + { + name: 'with explicit label', + label: 'testLabel', + }, + ])('simple @defer with condition $name', ({ label }) => { const subgraph1 = { name: 'Subgraph1', typeDefs: gql` @@ -2493,8 +2631,8 @@ describe('defer with conditions', () => { id: ID! x: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -2503,11 +2641,16 @@ describe('defer with conditions', () => { id: ID! y: Int } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` query($cond: Boolean) { t { x @@ -2516,7 +2659,8 @@ describe('defer with conditions', () => { } } } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2540,7 +2684,9 @@ describe('defer with conditions', () => { } } }, [ - Deferred(depends: [0], path: "t"${label ? `, label: "${label}"` : ''}) { + Deferred(depends: [0], path: "t"${ + label ? `, label: "${label}"` : '' + }) { { y }: @@ -2612,20 +2758,23 @@ describe('defer with conditions', () => { x: Int y: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1); - const operation = operationFromDocument(api, gql` - query($cond: Boolean) { - t { - x - ... @defer(if: $cond) { - y + const operation = operationFromDocument( + api, + gql` + query ($cond: Boolean) { + t { + x + ... @defer(if: $cond) { + y + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2704,8 +2853,8 @@ describe('defer with conditions', () => { id: ID! a: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -2714,8 +2863,8 @@ describe('defer with conditions', () => { id: ID! y: Int } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -2724,28 +2873,35 @@ describe('defer with conditions', () => { id: ID! b: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` - query($cond1: Boolean, $cond2: Boolean) { - t { - x - ... @defer(if: $cond1, label: "foo") { - y - } - ... @defer(if: $cond2, label: "bar") { - u { - a - ... @defer(if: $cond1) { - b + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` + query ($cond1: Boolean, $cond2: Boolean) { + t { + x + ... @defer(if: $cond1, label: "foo") { + y + } + ... @defer(if: $cond2, label: "bar") { + u { + a + ... @defer(if: $cond1) { + b + } } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -3080,8 +3236,8 @@ test('defer when some interface has different definitions in different subgraphs a: Int c: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -3095,21 +3251,27 @@ test('defer when some interface has different definitions in different subgraphs a: Int @external b: Int @requires(fields: "a") } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - query Dimensions { - i { - a - b - ... @defer { - c + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + query Dimensions { + i { + a + b + ... @defer { + c + } } } - } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` @@ -3180,8 +3342,8 @@ describe('named fragments', () => { type T @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -3191,22 +3353,28 @@ describe('named fragments', () => { x: Int y: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - ...TestFragment @defer + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + ...TestFragment @defer + } } - } - fragment TestFragment on T { - x - y - } - `); + fragment TestFragment on T { + x + y + } + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` @@ -3264,8 +3432,8 @@ describe('named fragments', () => { type T @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -3276,28 +3444,34 @@ describe('named fragments', () => { y: Int z: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - ...Fragment1 - ...Fragment2 @defer + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + ...Fragment1 + ...Fragment2 @defer + } } - } - fragment Fragment1 on T { - x - y - } + fragment Fragment1 on T { + x + y + } - fragment Fragment2 on T { - y - z - } - `); + fragment Fragment2 on T { + y + z + } + `, + ); // Field 'y' is queried twice, both in the deferred and non-deferred section. The spec says that // means the field is requested twice, so ensures that's what we do. @@ -3384,8 +3558,8 @@ describe('named fragments', () => { id: ID! x: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -3394,23 +3568,29 @@ describe('named fragments', () => { id: ID! y: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - ...OnT @defer - x + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + ...OnT @defer + x + } } - } - fragment OnT on T { - y - __typename - } - `); + fragment OnT on T { + y + __typename + } + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` @@ -3476,8 +3656,8 @@ test('do not merge query branches with @defer', () => { a: Int b: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -3486,24 +3666,30 @@ test('do not merge query branches with @defer', () => { id: ID! c: Int } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); // We have 2 separate @defer, so we should 2 deferred parts, not 1 defer parts with parallel fetches. - const operation = operationFromDocument(api, gql` - { - t { - a - ... @defer { - b - } - ... @defer { - c + const operation = operationFromDocument( + api, + gql` + { + t { + a + ... @defer { + b + } + ... @defer { + c + } } } - } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` @@ -3576,27 +3762,30 @@ test('@defer only the key of an entity', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T + t: T } type T @key(fields: "id") { id: ID! v0: String } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1); - const operation = operationFromDocument(api, gql` - { - t { - v0 - ... @defer { - id + const operation = operationFromDocument( + api, + gql` + { + t { + v0 + ... @defer { + id + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Making sure that the deferred part has no fetches since we only defer the key @@ -3635,7 +3824,7 @@ test('the path in @defer includes traversed fragments', () => { name: 'Subgraph1', typeDefs: gql` type Query { - i : I + i: I } interface I { @@ -3652,24 +3841,27 @@ test('the path in @defer includes traversed fragments', () => { v1: String v2: String } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1); - const operation = operationFromDocument(api, gql` - { - i { - ... on A { - t { - v1 - ... @defer { - v2 + const operation = operationFromDocument( + api, + gql` + { + i { + ... on A { + t { + v1 + ... @defer { + v2 + } } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` diff --git a/query-planner-js/src/__tests__/buildPlan.interfaceObject.test.ts b/query-planner-js/src/__tests__/buildPlan.interfaceObject.test.ts index 27335501e..34847a1b5 100644 --- a/query-planner-js/src/__tests__/buildPlan.interfaceObject.test.ts +++ b/query-planner-js/src/__tests__/buildPlan.interfaceObject.test.ts @@ -1,7 +1,7 @@ import { assert, operationFromDocument } from '@apollo/federation-internals'; import gql from 'graphql-tag'; import { isPlanNode } from '../QueryPlan'; -import { composeAndCreatePlanner, findFetchNodes } from "./testHelper"; +import { composeAndCreatePlanner, findFetchNodes } from './testHelper'; describe('basic @key on interface/@interfaceObject handling', () => { const subgraph1 = { @@ -27,8 +27,8 @@ describe('basic @key on interface/@interfaceObject handling', () => { x: Int w: Int } - ` - } + `, + }; const subgraph2 = { name: 'S2', @@ -41,21 +41,24 @@ describe('basic @key on interface/@interfaceObject handling', () => { id: ID! y: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); test('can use a @key on an @interfaceObject type', () => { // Start by ensuring we can use the key on an @interfaceObject type - const operation = operationFromDocument(api, gql` - { - iFromS1 { - x - y + const operation = operationFromDocument( + api, + gql` + { + iFromS1 { + x + y + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -88,17 +91,20 @@ describe('basic @key on interface/@interfaceObject handling', () => { }, } `); - }); + }); test('can use a @key on an interface "from" an @interfaceObject type', () => { - const operation = operationFromDocument(api, gql` - { - iFromS2 { - x - y + const operation = operationFromDocument( + api, + gql` + { + iFromS2 { + x + y + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -135,13 +141,16 @@ describe('basic @key on interface/@interfaceObject handling', () => { }); test('only uses an @interfaceObject if it can', () => { - const operation = operationFromDocument(api, gql` - { - iFromS2 { - y + const operation = operationFromDocument( + api, + gql` + { + iFromS2 { + y + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -158,14 +167,17 @@ describe('basic @key on interface/@interfaceObject handling', () => { }); test('does not rely on an @interfaceObject directly for `__typename`', () => { - const operation = operationFromDocument(api, gql` - { - iFromS2 { - __typename - y + const operation = operationFromDocument( + api, + gql` + { + iFromS2 { + __typename + y + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -205,15 +217,18 @@ describe('basic @key on interface/@interfaceObject handling', () => { // fact that we "filter" a single implementation should act as if `__typename` was queried // (effectively, the gateway/router need that `__typename` to decide if the returned data // should be included or not. - const operation = operationFromDocument(api, gql` - { - iFromS2 { - ... on A { - y + const operation = operationFromDocument( + api, + gql` + { + iFromS2 { + ... on A { + y + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -263,15 +278,18 @@ describe('basic @key on interface/@interfaceObject handling', () => { }); test('can use a @key on an @interfaceObject type even for a concrete implementation', () => { - const operation = operationFromDocument(api, gql` - { - iFromS1 { - ... on A { - y + const operation = operationFromDocument( + api, + gql` + { + iFromS1 { + ... on A { + y + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -319,15 +337,18 @@ describe('basic @key on interface/@interfaceObject handling', () => { test('handles query of an interface field (that is not on the `@interfaceObject`) for a specific implementation when query starts on the @interfaceObject', () => { // Here, we start on S2, but `x` is only in S1. Further, while `x` is on the `I` interface, we only query it for `A`. - const operation = operationFromDocument(api, gql` - { - iFromS2 { - ... on A { - x + const operation = operationFromDocument( + api, + gql` + { + iFromS2 { + ... on A { + x + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -377,8 +398,8 @@ it('avoids buffering @interfaceObject results that may have to filtered with lis id: ID! expansiveField: String } - ` - } + `, + }; const subgraph2 = { name: 'S2', @@ -396,21 +417,24 @@ it('avoids buffering @interfaceObject results that may have to filtered with lis id: ID! b: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - everything { - ... on A { - a - expansiveField + const operation = operationFromDocument( + api, + gql` + { + everything { + ... on A { + a + expansiveField + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -470,8 +494,8 @@ it('handles @requires on concrete type of field provided by interface object', ( id: ID! x: Int @shareable } - ` - } + `, + }; const subgraph2 = { name: 'S2', @@ -495,20 +519,23 @@ it('handles @requires on concrete type of field provided by interface object', ( id: ID! x: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - i { - ... on A { - y + const operation = operationFromDocument( + api, + gql` + { + i { + ... on A { + y + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -573,8 +600,8 @@ it('handles @interfaceObject in nested entity', () => { type T { relatedIs: [I] } - ` - } + `, + }; const subgraph2 = { name: 'S2', @@ -597,22 +624,25 @@ it('handles @interfaceObject in nested entity', () => { id: ID! a: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - i { - t { - relatedIs { - a + const operation = operationFromDocument( + api, + gql` + { + i { + t { + relatedIs { + a + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` diff --git a/query-planner-js/src/__tests__/buildPlan.subscription.test.ts b/query-planner-js/src/__tests__/buildPlan.subscription.test.ts index 3f0e6c12a..33e103bd4 100644 --- a/query-planner-js/src/__tests__/buildPlan.subscription.test.ts +++ b/query-planner-js/src/__tests__/buildPlan.subscription.test.ts @@ -1,6 +1,9 @@ import { operationFromDocument } from '@apollo/federation-internals'; import gql from 'graphql-tag'; -import { composeAndCreatePlanner, composeAndCreatePlannerWithOptions } from './testHelper'; +import { + composeAndCreatePlanner, + composeAndCreatePlannerWithOptions, +} from './testHelper'; describe('subscription query plan tests', () => { it('basic subscription query plan', () => { @@ -187,7 +190,10 @@ describe('subscription query plan tests', () => { `, }; - const [api, queryPlanner] = composeAndCreatePlannerWithOptions([subgraphA, subgraphB], { incrementalDelivery: { enableDefer: true } }); + const [api, queryPlanner] = composeAndCreatePlannerWithOptions( + [subgraphA, subgraphB], + { incrementalDelivery: { enableDefer: true } }, + ); const operation = operationFromDocument( api, gql` @@ -202,6 +208,8 @@ describe('subscription query plan tests', () => { } `, ); - expect(() => queryPlanner.buildQueryPlan(operation)).toThrow('@defer is not supported on subscriptions'); + expect(() => queryPlanner.buildQueryPlan(operation)).toThrow( + '@defer is not supported on subscriptions', + ); }); }); diff --git a/query-planner-js/src/__tests__/buildPlan.test.ts b/query-planner-js/src/__tests__/buildPlan.test.ts index f2a1e0d10..d3cba514b 100644 --- a/query-planner-js/src/__tests__/buildPlan.test.ts +++ b/query-planner-js/src/__tests__/buildPlan.test.ts @@ -1,9 +1,17 @@ import { QueryPlanner } from '@apollo/query-planner'; -import { assert, operationFromDocument, ServiceDefinition, Supergraph } from '@apollo/federation-internals'; +import { + assert, + operationFromDocument, + ServiceDefinition, + Supergraph, +} from '@apollo/federation-internals'; import gql from 'graphql-tag'; import { FetchNode, FlattenNode, SequenceNode } from '../QueryPlan'; import { FieldNode, OperationDefinitionNode, parse } from 'graphql'; -import { composeAndCreatePlanner, composeAndCreatePlannerWithOptions } from './testHelper'; +import { + composeAndCreatePlanner, + composeAndCreatePlannerWithOptions, +} from './testHelper'; import { enforceQueryPlannerConfigDefaults } from '../config'; describe('shareable root fields', () => { @@ -19,8 +27,8 @@ describe('shareable root fields', () => { id: ID! prop1: String } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -33,18 +41,21 @@ describe('shareable root fields', () => { id: ID! prop2: String } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - me { - prop1 - prop2 + const operation = operationFromDocument( + api, + gql` + { + me { + prop1 + prop2 + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Note that even though we have keys, it is faster to query both @@ -82,8 +93,8 @@ describe('shareable root fields', () => { id: ID! ${fields.map((f) => `${f}: Int\n`)} } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -95,8 +106,8 @@ describe('shareable root fields', () => { type User @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -108,17 +119,24 @@ describe('shareable root fields', () => { type User @key(fields: "id") { id: ID! } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` { me { ${fields.map((f) => `${f}\n`)} } } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -172,8 +190,8 @@ test('pick keys that minimize fetches', () => { type Country @key(fields: "iso") { iso: String! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -193,26 +211,29 @@ test('pick keys that minimize fetches', () => { name: String! sign: String! } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - transfers { - from { - currency { - name + const operation = operationFromDocument( + api, + gql` + { + transfers { + from { + currency { + name + } } - } - to { - currency { - sign + to { + currency { + sign + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // We want to make sure we use the key on Transfer just once, not 2 fetches using the keys @@ -274,7 +295,10 @@ describe('@provides', () => { typeDefs: gql` type Query { doSomething: Response - doSomethingWithProvides: Response @provides(fields: "responseValue { subResponseValue { subSubResponseValue } }") + doSomethingWithProvides: Response + @provides( + fields: "responseValue { subResponseValue { subSubResponseValue } }" + ) } type Response { @@ -289,8 +313,8 @@ describe('@provides', () => { id: ID! subSubResponseValue: Int @external } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -299,21 +323,24 @@ describe('@provides', () => { id: ID! subSubResponseValue: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - let operation = operationFromDocument(api, gql` - { - doSomething { - responseValue { - subResponseValue { - subSubResponseValue + let operation = operationFromDocument( + api, + gql` + { + doSomething { + responseValue { + subResponseValue { + subSubResponseValue + } } } } - } - `); + `, + ); let plan = queryPlanner.buildQueryPlan(operation); // This is our sanity check: we first query _without_ the provides to make sure we _do_ need to @@ -353,17 +380,20 @@ describe('@provides', () => { `); // And now make sure with the provides we do only get a fetch to subgraph1 - operation = operationFromDocument(api, gql` - { - doSomethingWithProvides { - responseValue { - subResponseValue { - subSubResponseValue + operation = operationFromDocument( + api, + gql` + { + doSomethingWithProvides { + responseValue { + subResponseValue { + subSubResponseValue + } } } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -409,8 +439,8 @@ describe('@provides', () => { id: ID! v: Value @external } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -429,19 +459,22 @@ describe('@provides', () => { id: ID! v: Value @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - let operation = operationFromDocument(api, gql` - { - noProvides { - v { - a + let operation = operationFromDocument( + api, + gql` + { + noProvides { + v { + a + } } } - } - `); + `, + ); let plan = queryPlanner.buildQueryPlan(operation); // This is our sanity check: we first query _without_ the provides to make sure we _do_ need to @@ -495,15 +528,18 @@ describe('@provides', () => { `); // Ensuring that querying only `a` can be done with subgraph1 only. - operation = operationFromDocument(api, gql` - { - withProvides { - v { - a + operation = operationFromDocument( + api, + gql` + { + withProvides { + v { + a + } } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -522,16 +558,19 @@ describe('@provides', () => { `); // Sanity check that if we query `b` however we have to got to subgraph2. - operation = operationFromDocument(api, gql` - { - withProvides { - v { - a - b + operation = operationFromDocument( + api, + gql` + { + withProvides { + v { + a + b + } } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -593,7 +632,8 @@ describe('@provides', () => { type Query { noProvides: U withProvidesForT1: U @provides(fields: "... on T1 { a }") - withProvidesForBoth: U @provides(fields: "... on T1 { a } ... on T2 {b}") + withProvidesForBoth: U + @provides(fields: "... on T1 { a } ... on T2 {b}") } union U = T1 | T2 @@ -608,8 +648,8 @@ describe('@provides', () => { a: Int b: Int @external } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -623,23 +663,26 @@ describe('@provides', () => { id: ID! b: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - let operation = operationFromDocument(api, gql` - { - noProvides { - ... on T1 { - a - } - ... on T2 { - a - b + let operation = operationFromDocument( + api, + gql` + { + noProvides { + ... on T1 { + a + } + ... on T2 { + a + b + } } } - } - `); + `, + ); let plan = queryPlanner.buildQueryPlan(operation); // This is our sanity check: we first query _without_ the provides to make sure we _do_ need to @@ -690,18 +733,21 @@ describe('@provides', () => { `); // Ensuring that querying only `a` can be done with subgraph1 only when provided. - operation = operationFromDocument(api, gql` - { - withProvidesForT1 { - ... on T1 { - a - } - ... on T2 { - a + operation = operationFromDocument( + api, + gql` + { + withProvidesForT1 { + ... on T1 { + a + } + ... on T2 { + a + } } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -723,19 +769,22 @@ describe('@provides', () => { `); // But ensure that querying `b` still goes to subgraph2 if only a is provided. - operation = operationFromDocument(api, gql` - { - withProvidesForT1 { - ... on T1 { - a - } - ... on T2 { - a - b + operation = operationFromDocument( + api, + gql` + { + withProvidesForT1 { + ... on T1 { + a + } + ... on T2 { + a + b + } } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -776,19 +825,22 @@ describe('@provides', () => { `); // Lastly, if both are provided, ensures we only hit subgraph1. - operation = operationFromDocument(api, gql` - { - withProvidesForBoth { - ... on T1 { - a - } - ... on T2 { - a - b + operation = operationFromDocument( + api, + gql` + { + withProvidesForBoth { + ... on T1 { + a + } + ... on T2 { + a + b + } } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -837,8 +889,8 @@ describe('@provides', () => { a: Int @external b: Int @external } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -853,18 +905,21 @@ describe('@provides', () => { a: Int @shareable b: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - let operation = operationFromDocument(api, gql` - { - noProvides { - a - b + let operation = operationFromDocument( + api, + gql` + { + noProvides { + a + b + } } - } - `); + `, + ); let plan = queryPlanner.buildQueryPlan(operation); // This is our sanity check: we first query _without_ the provides to make sure we _do_ need to @@ -916,13 +971,16 @@ describe('@provides', () => { `); // Ensuring that querying only `a` can be done with subgraph1 only. - operation = operationFromDocument(api, gql` - { - withProvidesOnA { - a + operation = operationFromDocument( + api, + gql` + { + withProvidesOnA { + a + } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -944,13 +1002,16 @@ describe('@provides', () => { `); // Ensuring that for `b`, only the T2 value is provided by subgraph1. - operation = operationFromDocument(api, gql` - { - withProvidesOnB { - b + operation = operationFromDocument( + api, + gql` + { + withProvidesOnB { + b + } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -990,15 +1051,18 @@ describe('@provides', () => { `); // But if we only query for T2, then no reason to go to subgraph2. - operation = operationFromDocument(api, gql` - { - withProvidesOnB { - ... on T2 { - b + operation = operationFromDocument( + api, + gql` + { + withProvidesOnB { + ... on T2 { + b + } } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -1045,8 +1109,8 @@ describe('@provides', () => { id: ID! a: Int @external } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -1071,25 +1135,28 @@ describe('@provides', () => { a: Int @shareable c: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - let operation = operationFromDocument(api, gql` - { - noProvides { - i { - a - ... on T1 { - b - } - ... on T2 { - c + let operation = operationFromDocument( + api, + gql` + { + noProvides { + i { + a + ... on T1 { + b + } + ... on T2 { + c + } } } } - } - `); + `, + ); let plan = queryPlanner.buildQueryPlan(operation); // This is our sanity check: we first query _without_ the provides to make sure we _do_ need to @@ -1134,21 +1201,24 @@ describe('@provides', () => { `); // But the same operation with the provides allow to get what is provided from the first subgraph. - operation = operationFromDocument(api, gql` - { - withProvides { - i { - a - ... on T1 { - b - } - ... on T2 { - c + operation = operationFromDocument( + api, + gql` + { + withProvides { + i { + a + ... on T1 { + b + } + ... on T2 { + c + } } } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -1224,8 +1294,8 @@ describe('@requires', () => { f: Int g: Int @external } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -1241,17 +1311,20 @@ describe('@requires', () => { f: Int @external g: Int @requires(fields: "f") } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - is { - g + const operation = operationFromDocument( + api, + gql` + { + is { + g + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // The main goal of this test is to show that the 2 @requires for `f` gets handled seemlessly @@ -1327,8 +1400,8 @@ describe('@requires', () => { id: ID! value: String } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -1345,21 +1418,24 @@ describe('@requires', () => { value: String @external computed: String @requires(fields: "value") } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - list { - computed - computed2 - user { + const operation = operationFromDocument( + api, + gql` + { + list { computed + computed2 + user { + computed + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // The main goal of this test is to show that the 2 @requires for `f` gets handled seemlessly @@ -1421,7 +1497,7 @@ describe('@requires', () => { }, } `); - }) + }); it('handles simple require chain (require that depends on another require)', () => { const subgraph1 = { @@ -1435,19 +1511,19 @@ describe('@requires', () => { id: ID! v: Int! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', typeDefs: gql` type T @key(fields: "id") { - id: ID! - v: Int! @external - inner: Int! @requires(fields: "v") + id: ID! + v: Int! @external + inner: Int! @requires(fields: "v") } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -1457,18 +1533,25 @@ describe('@requires', () => { inner: Int! @external outer: Int! @requires(fields: "inner") } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3); + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + ); // Ensures that if we only ask `outer`, we get everything needed in between. - let operation = operationFromDocument(api, gql` - { - t { - outer + let operation = operationFromDocument( + api, + gql` + { + t { + outer + } } - } - `); + `, + ); let plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -1523,15 +1606,18 @@ describe('@requires', () => { // (note: technically it happens to switch the order of fields in the inputs of "Subgraph2" // so the plans are not 100% the same "string", which is why we inline it in both cases, // but that's still the same plan and a perfectly valid output). - operation = operationFromDocument(api, gql` - { - t { - v - inner - outer + operation = operationFromDocument( + api, + gql` + { + t { + v + inner + outer + } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -1596,19 +1682,19 @@ describe('@requires', () => { type T @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', typeDefs: gql` type T @key(fields: "id") { - id: ID! - v: Int! @external - inner: Int! @requires(fields: "v") + id: ID! + v: Int! @external + inner: Int! @requires(fields: "v") } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -1618,8 +1704,8 @@ describe('@requires', () => { inner: Int! @external outer: Int! @requires(fields: "inner") } - ` - } + `, + }; const subgraph4 = { name: 'Subgraph4', @@ -1628,18 +1714,26 @@ describe('@requires', () => { id: ID! v: Int! } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3, subgraph4); + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + subgraph4, + ); // Ensures that if we only ask `outer`, we get everything needed in between. - let operation = operationFromDocument(api, gql` - { - t { - outer - } - } - `); + let operation = operationFromDocument( + api, + gql` + { + t { + outer + } + } + `, + ); let plan = queryPlanner.buildQueryPlan(operation); const expectedPlan = ` @@ -1706,15 +1800,18 @@ describe('@requires', () => { expect(plan).toMatchInlineSnapshot(expectedPlan); // Ensures that manually asking for the required dependencies doesn't change anything. - operation = operationFromDocument(api, gql` - { - t { - v - inner - outer + operation = operationFromDocument( + api, + gql` + { + t { + v + inner + outer + } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(expectedPlan); @@ -1732,8 +1829,8 @@ describe('@requires', () => { id: ID! v1: Int! } - ` - } + `, + }; const totalRequires = 10; const subgraphs: ServiceDefinition[] = [subgraph1]; @@ -1743,22 +1840,25 @@ describe('@requires', () => { typeDefs: gql` type T @key(fields: "id") { id: ID! - v${i-1}: Int! @external - v${i}: Int! @requires(fields: "v${i-1}") + v${i - 1}: Int! @external + v${i}: Int! @requires(fields: "v${i - 1}") } - ` + `, }); } const [api, queryPlanner] = composeAndCreatePlanner(...subgraphs); // Ensures that if we only ask `outer`, we get everything needed in between. - const operation = operationFromDocument(api, gql` + const operation = operationFromDocument( + api, + gql` { t { v${totalRequires} } } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); const dependentFetches: string[] = []; @@ -1768,7 +1868,7 @@ describe('@requires', () => { { ... on T { __typename - v${i-1} + v${i - 1} id } } => @@ -1778,8 +1878,7 @@ describe('@requires', () => { } } }, - },` - ); + },`); } const expectedPlan = ` QueryPlan { @@ -1813,37 +1912,37 @@ describe('@requires', () => { type T @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', typeDefs: gql` type T @key(fields: "id") { - id: ID! - inner1: Int! - inner2_required: Int! + id: ID! + inner1: Int! + inner2_required: Int! } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', typeDefs: gql` type T @key(fields: "id") { - id: ID! - inner2_required: Int! @external - inner2: Int! @requires(fields: "inner2_required") + id: ID! + inner2_required: Int! @external + inner2: Int! @requires(fields: "inner2_required") } - ` - } + `, + }; const subgraph4 = { name: 'Subgraph4', typeDefs: gql` type T @key(fields: "id") { - id: ID! - inner3: Inner3Type! + id: ID! + inner3: Inner3Type! } type Inner3Type @key(fields: "k3") { @@ -1854,8 +1953,8 @@ describe('@requires', () => { k4: ID! inner4_required: Int! } - ` - } + `, + }; const subgraph5 = { name: 'Subgraph5', @@ -1867,54 +1966,68 @@ describe('@requires', () => { inner3: Inner3Type! @external inner4: Inner4Type! @external inner5: Int! @external - outer: Int! @requires(fields: "inner1 inner2 inner3 { inner3_nested } inner4 { inner4_nested } inner5") + outer: Int! + @requires( + fields: "inner1 inner2 inner3 { inner3_nested } inner4 { inner4_nested } inner5" + ) } - type Inner3Type @key(fields: "k3") { - k3: ID! - inner3_nested: Int! - } + type Inner3Type @key(fields: "k3") { + k3: ID! + inner3_nested: Int! + } - type Inner4Type @key(fields: "k4") { - k4: ID! - inner4_nested: Int! @requires(fields: "inner4_required") - inner4_required: Int! @external - } - ` - } + type Inner4Type @key(fields: "k4") { + k4: ID! + inner4_nested: Int! @requires(fields: "inner4_required") + inner4_required: Int! @external + } + `, + }; const subgraph6 = { name: 'Subgraph6', typeDefs: gql` type T @key(fields: "id") { - id: ID! - inner4: Inner4Type! + id: ID! + inner4: Inner4Type! } type Inner4Type @key(fields: "k4") { k4: ID! } - ` - } + `, + }; const subgraph7 = { name: 'Subgraph7', typeDefs: gql` type T @key(fields: "id") { - id: ID! - inner5: Int! - } - ` - } - - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3, subgraph4, subgraph5, subgraph6, subgraph7); - const operation = operationFromDocument(api, gql` - { - t { - outer + id: ID! + inner5: Int! + } + `, + }; + + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + subgraph4, + subgraph5, + subgraph6, + subgraph7, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + outer + } } - } - `); + `, + ); // This is a big plan, but afaict, this is optimal. That is, there is 3 main steps: // 1. it get the `id` for `T`, which is needed for anything else. @@ -2122,8 +2235,8 @@ describe('@requires', () => { v1: String v2: String } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -2139,26 +2252,32 @@ describe('@requires', () => { v1: String v2: String } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const op1 = operationFromDocument(api, gql` - { - entity { - f2 - f3 + const op1 = operationFromDocument( + api, + gql` + { + entity { + f2 + f3 + } } - } - `); + `, + ); - const op2 = operationFromDocument(api, gql` - { - entity { - f3 + const op2 = operationFromDocument( + api, + gql` + { + entity { + f3 + } } - } - `); + `, + ); const plan1 = queryPlanner.buildQueryPlan(op2); const expectedPlan = ` @@ -2253,14 +2372,14 @@ describe('@requires', () => { name: 'A', typeDefs: gql` type Query { - t : T + t: T } type T @key(fields: "id1") { id1: ID! } - ` - } + `, + }; const subgraph2 = { name: 'B', @@ -2271,8 +2390,8 @@ describe('@requires', () => { v1: Int v2: Int } - ` - } + `, + }; const subgraph3 = { name: 'C', @@ -2281,8 +2400,8 @@ describe('@requires', () => { id1: ID! v3: Int } - ` - } + `, + }; const subgraph4 = { name: 'D', @@ -2292,20 +2411,28 @@ describe('@requires', () => { v3: Int @external v4: Int @requires(fields: "v3") } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3, subgraph4); - const operation = operationFromDocument(api, gql` - { - t { - v1 - v2 - v3 - v4 + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + subgraph4, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v1 + v2 + v3 + v4 + } } - } - `); + `, + ); // The optimal plan should: // 1. fetch id1 from A @@ -2395,8 +2522,8 @@ describe('@requires', () => { id: ID! a: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -2406,17 +2533,20 @@ describe('@requires', () => { a: Int @external b: Int @requires(fields: "a") } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - query foo($test: Boolean!){ - t @include(if: $test) { - b + const operation = operationFromDocument( + api, + gql` + query foo($test: Boolean!) { + t @include(if: $test) { + b + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2466,8 +2596,8 @@ describe('@requires', () => { id: ID! a: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -2477,17 +2607,20 @@ describe('@requires', () => { a: Int @external b: Int @requires(fields: "a") } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - query foo($test: Boolean!){ - t { - b @include(if: $test) + const operation = operationFromDocument( + api, + gql` + query foo($test: Boolean!) { + t { + b @include(if: $test) + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2538,8 +2671,8 @@ describe('@requires', () => { type A @key(fields: "idA") { idA: ID! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -2553,8 +2686,8 @@ describe('@requires', () => { idB: ID! required: Int } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -2564,19 +2697,26 @@ describe('@requires', () => { c: Int @requires(fields: "required") required: Int @external } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` - query foo($test1: Boolean!, $test2: Boolean!){ - a @include(if: $test1) { - b @include(if: $test2) { - c + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` + query foo($test1: Boolean!, $test2: Boolean!) { + a @include(if: $test1) { + b @include(if: $test2) { + c + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2655,8 +2795,8 @@ describe('@requires', () => { a: String @inaccessible onlyIn1: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -2668,20 +2808,23 @@ describe('@requires', () => { type One @key(fields: "id") { id: ID! a: String @external - b: String @requires(fields: "a" ) + b: String @requires(fields: "a") onlyIn2: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - one { - b + const operation = operationFromDocument( + api, + gql` + { + one { + b + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2728,15 +2871,15 @@ describe('@requires', () => { name: 'A', typeDefs: gql` type Query { - t : T + t: T } type T @key(fields: "id1") @key(fields: "req1") { id1: ID! req1: Int } - ` - } + `, + }; const subgraph2 = { name: 'B', @@ -2747,8 +2890,8 @@ describe('@requires', () => { req2: Int @external v: Int @requires(fields: "req1 req2") } - ` - } + `, + }; const subgraph3 = { name: 'C', @@ -2757,17 +2900,24 @@ describe('@requires', () => { req1: Int req2: Int } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` - { - t { - v + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2832,8 +2982,8 @@ describe('fetch operation names', () => { type T @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph2 = { name: 'non-graphql-name', @@ -2842,17 +2992,20 @@ describe('fetch operation names', () => { id: ID! x: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - query myOp { - t { - x + const operation = operationFromDocument( + api, + gql` + query myOp { + t { + x + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2884,7 +3037,8 @@ describe('fetch operation names', () => { }, } `); - const fetch = ((plan.node as SequenceNode).nodes[1] as FlattenNode).node as FetchNode; + const fetch = ((plan.node as SequenceNode).nodes[1] as FlattenNode) + .node as FetchNode; expect(fetch.operation).toMatch(/^query myOp__non_graphql_name__1.*/i); }); @@ -2899,8 +3053,8 @@ describe('fetch operation names', () => { type T @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph2 = { name: 'a-na&me-with-plen&ty-replace*ments', @@ -2909,17 +3063,20 @@ describe('fetch operation names', () => { id: ID! x: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - query myOp { - t { - x + const operation = operationFromDocument( + api, + gql` + query myOp { + t { + x + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2951,8 +3108,11 @@ describe('fetch operation names', () => { }, } `); - const fetch = ((plan.node as SequenceNode).nodes[1] as FlattenNode).node as FetchNode; - expect(fetch.operation).toMatch(/^query myOp__a_name_with_plenty_replacements__1.*/i); + const fetch = ((plan.node as SequenceNode).nodes[1] as FlattenNode) + .node as FetchNode; + expect(fetch.operation).toMatch( + /^query myOp__a_name_with_plenty_replacements__1.*/i, + ); }); test('handle very non-graph subgraph name', () => { @@ -2966,8 +3126,8 @@ describe('fetch operation names', () => { type T @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph2 = { name: '42!', @@ -2976,17 +3136,20 @@ describe('fetch operation names', () => { id: ID! x: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - query myOp { - t { - x + const operation = operationFromDocument( + api, + gql` + query myOp { + t { + x + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -3018,7 +3181,8 @@ describe('fetch operation names', () => { }, } `); - const fetch = ((plan.node as SequenceNode).nodes[1] as FlattenNode).node as FetchNode; + const fetch = ((plan.node as SequenceNode).nodes[1] as FlattenNode) + .node as FetchNode; expect(fetch.operation).toMatch(/^query myOp___42__1.*/i); }); @@ -3032,7 +3196,8 @@ test('Correctly handle case where there is too many plans to consider', () => { // gets very large very quickly). Obviously, there is no reason to do this in practice. // Each leaf field is reachable from 2 subgraphs, so doubles the number of plans. - const defaultMaxComputedPlans = enforceQueryPlannerConfigDefaults().debug.maxEvaluatedPlans!; + const defaultMaxComputedPlans = + enforceQueryPlannerConfigDefaults().debug.maxEvaluatedPlans!; const fieldCount = Math.ceil(Math.log2(defaultMaxComputedPlans)) + 1; const fields = [...Array(fieldCount).keys()].map((i) => `f${i}`); @@ -3046,14 +3211,20 @@ test('Correctly handle case where there is too many plans to consider', () => { } `; - const [api, queryPlanner] = composeAndCreatePlanner({ name: 'S1', typeDefs }, { name: 'S2', typeDefs }); - const operation = operationFromDocument(api, gql` + const [api, queryPlanner] = composeAndCreatePlanner( + { name: 'S1', typeDefs }, + { name: 'S2', typeDefs }, + ); + const operation = operationFromDocument( + api, + gql` { t { ${fields.map((f) => `${f}\n`)} } } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Note: The way the code that handle multiple plans currently work, it mess up the order of fields a bit. It's not a @@ -3073,9 +3244,11 @@ test('Correctly handle case where there is too many plans to consider', () => { // ... all fields // } // } - const mainSelection = (fetchOp.definitions[0] as OperationDefinitionNode).selectionSet; + const mainSelection = (fetchOp.definitions[0] as OperationDefinitionNode) + .selectionSet; const subSelection = (mainSelection.selections[0] as FieldNode).selectionSet; - const queriedFields = subSelection?.selections.map((s) => (s as FieldNode).name.value) ?? []; + const queriedFields = + subSelection?.selections.map((s) => (s as FieldNode).name.value) ?? []; fields.sort(); // Note that alphabetical order is not numerical order, hence this queriedFields.sort(); expect(queriedFields).toStrictEqual(fields); @@ -3128,19 +3301,22 @@ describe('Field covariance and type-explosion', () => { const api = supergraph.apiSchema(); const queryPlanner = new QueryPlanner(supergraph); - const operation = operationFromDocument(api, gql` - { - dummy { - field { - ... on Object { - field { - __typename + const operation = operationFromDocument( + api, + gql` + { + dummy { + field { + ... on Object { + field { + __typename + } } } } } - } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -3180,8 +3356,8 @@ describe('Field covariance and type-explosion', () => { field: Object @provides(fields: "x") x: Int @external } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -3190,23 +3366,26 @@ describe('Field covariance and type-explosion', () => { id: ID! x: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - dummy { - field { - ... on Object { - field { - __typename + const operation = operationFromDocument( + api, + gql` + { + dummy { + field { + ... on Object { + field { + __typename + } } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -3229,7 +3408,7 @@ describe('Field covariance and type-explosion', () => { } `); }); -}) +}); describe('handles non-intersecting fragment conditions', () => { test('with federation 1 supergraphs', () => { @@ -3275,30 +3454,33 @@ describe('handles non-intersecting fragment conditions', () => { enum join__Graph { S1 @join__graph(name: "S1" url: "") } - ` + `; const supergraph = Supergraph.build(supergraphSdl); const api = supergraph.apiSchema(); const queryPlanner = new QueryPlanner(supergraph); - const operation = operationFromDocument(api, gql` - fragment OrangeYouGladIDidntSayBanana on Fruit { - ... on Banana { - inBunch - } - ... on Apple { - hasStem + const operation = operationFromDocument( + api, + gql` + fragment OrangeYouGladIDidntSayBanana on Fruit { + ... on Banana { + inBunch + } + ... on Apple { + hasStem + } } - } - query Fruitiness { - fruit { - ... on Apple { - ...OrangeYouGladIDidntSayBanana + query Fruitiness { + fruit { + ... on Apple { + ...OrangeYouGladIDidntSayBanana + } } } - } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -3337,28 +3519,31 @@ describe('handles non-intersecting fragment conditions', () => { type Query { fruit: Fruit! } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1); - const operation = operationFromDocument(api, gql` - fragment OrangeYouGladIDidntSayBanana on Fruit { - ... on Banana { - inBunch - } - ... on Apple { - hasStem + const operation = operationFromDocument( + api, + gql` + fragment OrangeYouGladIDidntSayBanana on Fruit { + ... on Banana { + inBunch + } + ... on Apple { + hasStem + } } - } - query Fruitiness { - fruit { - ... on Apple { - ...OrangeYouGladIDidntSayBanana + query Fruitiness { + fruit { + ... on Apple { + ...OrangeYouGladIDidntSayBanana + } } } - } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -3402,8 +3587,8 @@ test('avoids unnecessary fetches', () => { type A @key(fields: "idA2") { idA2: ID! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -3413,12 +3598,11 @@ test('avoids unnecessary fetches', () => { u: U } - type U @key(fields: "idU") { idU: ID! } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -3426,8 +3610,8 @@ test('avoids unnecessary fetches', () => { type A @key(fields: "idA1") { idA1: ID! } - ` - } + `, + }; const subgraph4 = { name: 'Subgraph4', @@ -3436,8 +3620,8 @@ test('avoids unnecessary fetches', () => { idA1: ID! idA2: ID! } - ` - } + `, + }; const subgraph5 = { name: 'Subgraph5', @@ -3446,22 +3630,31 @@ test('avoids unnecessary fetches', () => { idU: ID! v: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3, subgraph4, subgraph5); - const operation = operationFromDocument(api, gql` - { - t { - u { - v - } - a { - idA1 + `, + }; + + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + subgraph4, + subgraph5, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + u { + v + } + a { + idA1 + } } } - } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -3585,15 +3778,18 @@ describe('Fed1 supergraph handling', () => { const api = supergraph.apiSchema(); const queryPlanner = new QueryPlanner(supergraph); - const operation = operationFromDocument(api, gql` - { - t { - nodes { - id + const operation = operationFromDocument( + api, + gql` + { + t { + nodes { + id + } } } - } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` @@ -3647,48 +3843,51 @@ describe('Named fragments preservation', () => { child: Foo child2: Foo } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1); - const operation = operationFromDocument(api, gql` - query { - a { - ...on A1 { - ...FooSelect - } - ...on A2 { - ...FooSelect - } - ...on A3 { - ...FooSelect + const operation = operationFromDocument( + api, + gql` + query { + a { + ... on A1 { + ...FooSelect + } + ... on A2 { + ...FooSelect + } + ... on A3 { + ...FooSelect + } } } - } - fragment FooSelect on Foo { - __typename - foo - child { - ...FooChildSelect - } - child2 { - ...FooChildSelect + fragment FooSelect on Foo { + __typename + foo + child { + ...FooChildSelect + } + child2 { + ...FooChildSelect + } } - } - fragment FooChildSelect on Foo { - __typename - foo - child { + fragment FooChildSelect on Foo { + __typename + foo child { child { - foo + child { + foo + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -3757,8 +3956,8 @@ describe('Named fragments preservation', () => { b: Int c: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -3774,31 +3973,34 @@ describe('Named fragments preservation', () => { b: Int c: Int } - ` - } + `, + }; // We use a fragment which does save some on the original query, but as each // field gets to a different subgraph, the fragment would only be used one // on each sub-fetch and we make sure the fragment is not used in that case. const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - let operation = operationFromDocument(api, gql` - query { - t { - v1 { - ...OnV - } - v2 { - ...OnV + let operation = operationFromDocument( + api, + gql` + query { + t { + v1 { + ...OnV + } + v2 { + ...OnV + } } } - } - fragment OnV on V { - a - b - c - } - `); + fragment OnV on V { + a + b + c + } + `, + ); let plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -3842,24 +4044,27 @@ describe('Named fragments preservation', () => { // But double-check that if we query 2 fields from the same subgraph, then // the fragment gets used now. - operation = operationFromDocument(api, gql` - query { - t { - v2 { - ...OnV - } - v3 { - ...OnV + operation = operationFromDocument( + api, + gql` + query { + t { + v2 { + ...OnV + } + v3 { + ...OnV + } } } - } - fragment OnV on V { - a - b - c - } - `); + fragment OnV on V { + a + b + c + } + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -3904,47 +4109,55 @@ describe('Named fragments preservation', () => { `); }); - it.each([ true, false ])('respects query planner option "reuseQueryFragments=%p"', (reuseQueryFragments: boolean) => { - const subgraph1 = { - name: 'Subgraph1', - typeDefs: gql` - type Query { - t: T - } - - type T { - a1: A - a2: A - } + it.each([true, false])( + 'respects query planner option "reuseQueryFragments=%p"', + (reuseQueryFragments: boolean) => { + const subgraph1 = { + name: 'Subgraph1', + typeDefs: gql` + type Query { + t: T + } - type A { - x: Int - y: Int - } - ` - } + type T { + a1: A + a2: A + } - const [api, queryPlanner] = composeAndCreatePlannerWithOptions([subgraph1], { reuseQueryFragments }); - const operation = operationFromDocument(api, gql` - query { - t { - a1 { - ...Selection + type A { + x: Int + y: Int } - a2 { - ...Selection + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithOptions( + [subgraph1], + { reuseQueryFragments }, + ); + const operation = operationFromDocument( + api, + gql` + query { + t { + a1 { + ...Selection + } + a2 { + ...Selection + } + } } - } - } - fragment Selection on A { - x - y - } - `); + fragment Selection on A { + x + y + } + `, + ); - const plan = queryPlanner.buildQueryPlan(operation); - const withReuse = ` + const plan = queryPlanner.buildQueryPlan(operation); + const withReuse = ` QueryPlan { Fetch(service: "Subgraph1") { { @@ -3965,7 +4178,7 @@ describe('Named fragments preservation', () => { }, } `; - const withoutReuse = ` + const withoutReuse = ` QueryPlan { Fetch(service: "Subgraph1") { { @@ -3982,10 +4195,13 @@ describe('Named fragments preservation', () => { } }, } - ` + `; - expect(plan).toMatchInlineSnapshot(reuseQueryFragments ? withReuse : withoutReuse); - }); + expect(plan).toMatchInlineSnapshot( + reuseQueryFragments ? withReuse : withoutReuse, + ); + }, + ); it('works with nested fragments when only the nested fragment gets preserved', () => { const subgraph1 = { @@ -3995,7 +4211,7 @@ describe('Named fragments preservation', () => { t: T } - type T @key(fields : "id") { + type T @key(fields: "id") { id: ID! a: V b: V @@ -4005,32 +4221,34 @@ describe('Named fragments preservation', () => { v1: Int v2: Int } - - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1); - const operation = operationFromDocument(api, gql` - { - t { - ...OnT + const operation = operationFromDocument( + api, + gql` + { + t { + ...OnT + } } - } - fragment OnT on T { - a { - ...OnV - } - b { - ...OnV + fragment OnT on T { + a { + ...OnV + } + b { + ...OnV + } } - } - fragment OnV on V { - v1 - v2 - } - `); + fragment OnV on V { + v1 + v2 + } + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -4064,28 +4282,31 @@ describe('Named fragments preservation', () => { t: T } - type T @key(fields : "id") { + type T @key(fields: "id") { id: ID! a: Int b: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1); - const operation = operationFromDocument(api, gql` - query test($if: Boolean) { - t { - id - ...OnT @include(if: $if) + const operation = operationFromDocument( + api, + gql` + query test($if: Boolean) { + t { + id + ...OnT @include(if: $if) + } } - } - fragment OnT on T { - a - b - } - `); + fragment OnT on T { + a + b + } + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -4113,29 +4334,32 @@ describe('Named fragments preservation', () => { t: T } - type T @key(fields : "id") { + type T @key(fields: "id") { id: ID! a: Int b: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1); - const operation = operationFromDocument(api, gql` - query test($test1: Boolean, $test2: Boolean) { - t { - id - ...OnT @include(if: $test1) - ...OnT @include(if: $test2) + const operation = operationFromDocument( + api, + gql` + query test($test1: Boolean, $test2: Boolean) { + t { + id + ...OnT @include(if: $test1) + ...OnT @include(if: $test2) + } } - } - fragment OnT on T { - a - b - } - `); + fragment OnT on T { + a + b + } + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -4184,8 +4408,8 @@ describe('Named fragments preservation', () => { a: Int b: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -4194,28 +4418,31 @@ describe('Named fragments preservation', () => { a: Int b: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - query { - i1 { - ... on T { - ...Frag + const operation = operationFromDocument( + api, + gql` + query { + i1 { + ... on T { + ...Frag + } } - } - i2 { - ... on T { - ...Frag + i2 { + ... on T { + ...Frag + } } } - } - fragment Frag on I { - b - } - `); + fragment Frag on I { + b + } + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -4257,7 +4484,7 @@ describe('Named fragments preservation', () => { name: 'Subgraph1', typeDefs: gql` type V @shareable { - x: Int + x: Int } interface I { @@ -4268,8 +4495,8 @@ describe('Named fragments preservation', () => { id: ID! v: V } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -4280,7 +4507,7 @@ describe('Named fragments preservation', () => { } type V @shareable { - x: Int + x: Int } interface I { @@ -4298,25 +4525,36 @@ describe('Named fragments preservation', () => { inner: Inner w: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - let operation = operationFromDocument(api, gql` - query { - outer1 { ...OuterFrag } - outer2 { ...OuterFrag } - } + let operation = operationFromDocument( + api, + gql` + query { + outer1 { + ...OuterFrag + } + outer2 { + ...OuterFrag + } + } - fragment OuterFrag on Outer { - ...IFrag - inner { ...IFrag } - } + fragment OuterFrag on Outer { + ...IFrag + inner { + ...IFrag + } + } - fragment IFrag on I { - v { x } - } - `); + fragment IFrag on I { + v { + x + } + } + `, + ); const expectedPlan = ` QueryPlan { @@ -4382,54 +4620,80 @@ describe('Named fragments preservation', () => { }, } `; - expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot(expectedPlan); + expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot( + expectedPlan, + ); // We very slighly modify the operation to add an artificial indirection within `IFrag`. // This does not really change the query, and should result in the same plan, but // ensure the code handle correctly such indirection. - operation = operationFromDocument(api, gql` - query { - outer1 { ...OuterFrag } - outer2 { ...OuterFrag } - } + operation = operationFromDocument( + api, + gql` + query { + outer1 { + ...OuterFrag + } + outer2 { + ...OuterFrag + } + } - fragment OuterFrag on Outer { - ...IFrag - inner { ...IFrag } - } + fragment OuterFrag on Outer { + ...IFrag + inner { + ...IFrag + } + } - fragment IFrag on I { - ...IFragDelegate - } + fragment IFrag on I { + ...IFragDelegate + } - fragment IFragDelegate on I { - v { x } - } - `); + fragment IFragDelegate on I { + v { + x + } + } + `, + ); - expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot(expectedPlan); + expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot( + expectedPlan, + ); - // The previous cases tests the cases where nothing in the `...IFrag` spread at the + // The previous cases tests the cases where nothing in the `...IFrag` spread at the // top-level of `OuterFrag` applied at all: it all gets eliminated in the plan. But // in the schema of `Subgraph2`, while `Outer` does not implement `I` (and does not // have `v` in particular), it does contains field `w` that `I` also have, so we // add that field to `IFrag` and make sure we still correctly query that field. - operation = operationFromDocument(api, gql` - query { - outer1 { ...OuterFrag } - outer2 { ...OuterFrag } - } + operation = operationFromDocument( + api, + gql` + query { + outer1 { + ...OuterFrag + } + outer2 { + ...OuterFrag + } + } - fragment OuterFrag on Outer { - ...IFrag - inner { ...IFrag } - } + fragment OuterFrag on Outer { + ...IFrag + inner { + ...IFrag + } + } - fragment IFrag on I { - v { x } - w - } - `); + fragment IFrag on I { + v { + x + } + w + } + `, + ); expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot(` QueryPlan { @@ -4506,7 +4770,7 @@ describe('Named fragments preservation', () => { name: 'Subgraph1', typeDefs: gql` type V @shareable { - x: Int + x: Int } union U = Outer @@ -4515,8 +4779,8 @@ describe('Named fragments preservation', () => { id: ID! v: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -4538,30 +4802,39 @@ describe('Named fragments preservation', () => { inner: Inner w: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - let operation = operationFromDocument(api, gql` - query { - outer1 { ...OuterFrag } - outer2 { ...OuterFrag } - } - - fragment OuterFrag on Outer { - ...UFrag - inner { ...UFrag } - } + let operation = operationFromDocument( + api, + gql` + query { + outer1 { + ...OuterFrag + } + outer2 { + ...OuterFrag + } + } - fragment UFrag on U { - ... on Outer { - v + fragment OuterFrag on Outer { + ...UFrag + inner { + ...UFrag + } } - ... on Inner { - v + + fragment UFrag on U { + ... on Outer { + v + } + ... on Inner { + v + } } - } - `); + `, + ); const expectedPlan = ` QueryPlan { @@ -4621,64 +4894,86 @@ describe('Named fragments preservation', () => { }, } `; - expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot(expectedPlan); + expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot( + expectedPlan, + ); // We very slighly modify the operation to add an artificial indirection within `IFrag`. // This does not really change the query, and should result in the same plan, but // ensure the code handle correctly such indirection. - operation = operationFromDocument(api, gql` - query { - outer1 { ...OuterFrag } - outer2 { ...OuterFrag } - } - - fragment OuterFrag on Outer { - ...UFrag - inner { ...UFrag } - } + operation = operationFromDocument( + api, + gql` + query { + outer1 { + ...OuterFrag + } + outer2 { + ...OuterFrag + } + } - fragment UFrag on U { - ...UFragDelegate - } + fragment OuterFrag on Outer { + ...UFrag + inner { + ...UFrag + } + } - fragment UFragDelegate on U { - ... on Outer { - v + fragment UFrag on U { + ...UFragDelegate } - ... on Inner { - v + + fragment UFragDelegate on U { + ... on Outer { + v + } + ... on Inner { + v + } } - } - `); + `, + ); - expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot(expectedPlan); + expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot( + expectedPlan, + ); - // The previous cases tests the cases where nothing in the `...IFrag` spread at the + // The previous cases tests the cases where nothing in the `...IFrag` spread at the // top-level of `OuterFrag` applied at all: it all gets eliminated in the plan. But // in the schema of `Subgraph2`, while `Outer` does not implement `I` (and does not // have `v` in particular), it does contains field `w` that `I` also have, so we // add that field to `IFrag` and make sure we still correctly query that field. - operation = operationFromDocument(api, gql` - query { - outer1 { ...OuterFrag } - outer2 { ...OuterFrag } - } - - fragment OuterFrag on Outer { - ...UFrag - inner { ...UFrag } - } + operation = operationFromDocument( + api, + gql` + query { + outer1 { + ...OuterFrag + } + outer2 { + ...OuterFrag + } + } - fragment UFrag on U { - ... on Outer { - v - w + fragment OuterFrag on Outer { + ...UFrag + inner { + ...UFrag + } } - ... on Inner { - v + + fragment UFrag on U { + ... on Outer { + v + w + } + ... on Inner { + v + } } - } - `); + `, + ); expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot(` QueryPlan { @@ -4753,18 +5048,18 @@ test('works with key chains', () => { type T @key(fields: "id1") { id1: ID! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', typeDefs: gql` - type T @key(fields: "id1") @key(fields: "id2") { + type T @key(fields: "id1") @key(fields: "id2") { id1: ID! id2: ID! } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -4774,22 +5069,29 @@ test('works with key chains', () => { x: Int y: Int } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3); + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + ); // Note: querying `id2` is only purpose, because there is 2 choice to get `id2` (either // from then 2nd or 3rd subgraph), and that create some choice in the query planning algorithm, // so excercices additional paths. - const operation = operationFromDocument(api, gql` - { - t { - id2 - x - y + const operation = operationFromDocument( + api, + gql` + { + t { + id2 + x + y + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -4852,18 +5154,21 @@ describe('__typename handling', () => { id: ID! x: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1); - let operation = operationFromDocument(api, gql` - query { - t { - foo: __typename - x + let operation = operationFromDocument( + api, + gql` + query { + t { + foo: __typename + x + } } - } - `); + `, + ); let plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -4879,15 +5184,18 @@ describe('__typename handling', () => { } `); - operation = operationFromDocument(api, gql` - query { - t { - foo: __typename - x - __typename + operation = operationFromDocument( + api, + gql` + query { + t { + foo: __typename + x + __typename + } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -4916,8 +5224,8 @@ describe('__typename handling', () => { type S @key(fields: "id") { id: ID } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -4930,8 +5238,8 @@ describe('__typename handling', () => { type T { x: Int } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -4945,28 +5253,37 @@ describe('__typename handling', () => { id: ID! y: Int } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3); + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + ); // This tests the patch from https://github.com/apollographql/federation/pull/2137. // Namely, the schema is such that `x` can only be fetched from one subgraph, but // technically __typename can be fetched from 2 subgraphs. However, the optimization // we test for is that we actually don't consider both choices for __typename and // instead only evaluate a single query plan (the assertion on `evaluatePlanCount`) - let operation = operationFromDocument(api, gql` - query { - s { - t { - __typename - x + let operation = operationFromDocument( + api, + gql` + query { + s { + t { + __typename + x + } } } - } - `); + `, + ); let plan = queryPlanner.buildQueryPlan(operation); - expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe(1); + expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe( + 1, + ); expect(plan).toMatchInlineSnapshot(` QueryPlan { Sequence { @@ -5007,22 +5324,27 @@ describe('__typename handling', () => { // in the implementation made this example forgo the optimization of the // __typename within `t`. We make sure this is not case (that we still only // consider a single choice of plan). - operation = operationFromDocument(api, gql` - query { - s { - __typename - ... on S { - t { - __typename - x + operation = operationFromDocument( + api, + gql` + query { + s { + __typename + ... on S { + t { + __typename + x + } } } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); - expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe(1); + expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe( + 1, + ); expect(plan).toMatchInlineSnapshot(` QueryPlan { Sequence { @@ -5071,8 +5393,8 @@ describe('mutations', () => { type Mutation { m1: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5080,16 +5402,19 @@ describe('mutations', () => { type Mutation { m2: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - mutation { - m2 - m1 - } - `); + const operation = operationFromDocument( + api, + gql` + mutation { + m2 + m1 + } + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -5132,8 +5457,8 @@ describe('interface type-explosion', () => { type S @shareable { x: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5147,19 +5472,22 @@ describe('interface type-explosion', () => { x: Int y: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - i { - s { - y + const operation = operationFromDocument( + api, + gql` + { + i { + s { + y + } } } - } - `); + `, + ); // The schema is constructed in such a way that we *need* to type-explode interface `I` // to be able to find field `y`. Make sure that happens. @@ -5221,8 +5549,8 @@ describe('interface type-explosion', () => { x: Int y: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5236,19 +5564,22 @@ describe('interface type-explosion', () => { x: Int y: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - i { - s { - y + const operation = operationFromDocument( + api, + gql` + { + i { + s { + y + } } } - } - `); + `, + ); // This test is a small variation on the previous test ('handles non-matching ...'), we // we _can_ use the interface field directly and don't need to type-explode. So we @@ -5271,7 +5602,9 @@ describe('interface type-explosion', () => { }, } `); - expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe(1); + expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe( + 1, + ); }); }); @@ -5307,8 +5640,8 @@ describe('merged abstract types handling', () => { type C implements I { v: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5320,19 +5653,22 @@ describe('merged abstract types handling', () => { type A implements I { v: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - u { - ... on I { - v + const operation = operationFromDocument( + api, + gql` + { + u { + ... on I { + v + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Type `A` can be returned by `u` and is a `I` *in the supergraph* but not in `Subgraph1`, so need to @@ -5384,8 +5720,8 @@ describe('merged abstract types handling', () => { type C implements I { v: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5395,19 +5731,22 @@ describe('merged abstract types handling', () => { type A { v: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - u { - ... on I { - v + const operation = operationFromDocument( + api, + gql` + { + u { + ... on I { + v + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // While `A` is a `U` in the supergraph while not in `Subgraph1`, since the `u` @@ -5455,8 +5794,8 @@ describe('merged abstract types handling', () => { type C implements I { v: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5466,21 +5805,24 @@ describe('merged abstract types handling', () => { type A { v: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - i { - ... on U { - ... on A { - v + const operation = operationFromDocument( + api, + gql` + { + i { + ... on U { + ... on A { + v + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Type `A` can be returned by `i` and is a `U` *in the supergraph* but not in `Subgraph1`, so need to @@ -5526,8 +5868,8 @@ describe('merged abstract types handling', () => { type C implements I { v: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5539,21 +5881,24 @@ describe('merged abstract types handling', () => { type A implements I { v: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - i { - ... on U { - ... on A { - v + const operation = operationFromDocument( + api, + gql` + { + i { + ... on U { + ... on A { + v + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Here, `A` is a `I` in the supergraph while not in `Subgraph1`, and since the `i` operation is resolved by @@ -5598,8 +5943,8 @@ describe('merged abstract types handling', () => { type C implements I1 & I2 { v: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5611,19 +5956,22 @@ describe('merged abstract types handling', () => { type A implements I2 { v: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - i1 { - ... on I2 { - v + const operation = operationFromDocument( + api, + gql` + { + i1 { + ... on I2 { + v + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Type `A` can be returned by `i1` and is a `I2` *in the supergraph* but not in `Subgraph1`, so need to @@ -5677,8 +6025,8 @@ describe('merged abstract types handling', () => { type C implements I1 & I2 { v: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5690,19 +6038,22 @@ describe('merged abstract types handling', () => { type A implements I1 { v: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - i1 { - ... on I2 { - v + const operation = operationFromDocument( + api, + gql` + { + i1 { + ... on I2 { + v + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // While `A` is a `I1` in the supergraph while not in `Subgraph1`, since the `i1` @@ -5748,8 +6099,8 @@ describe('merged abstract types handling', () => { type C { v: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5759,21 +6110,24 @@ describe('merged abstract types handling', () => { type A { v: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - u1 { - ... on U2 { - ... on A { - v + const operation = operationFromDocument( + api, + gql` + { + u1 { + ... on U2 { + ... on A { + v + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Type `A` can be returned by `u1` and is a `U2` *in the supergraph* but not in `Subgraph1`, so need to @@ -5816,8 +6170,8 @@ describe('merged abstract types handling', () => { type C { v: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5827,21 +6181,24 @@ describe('merged abstract types handling', () => { type A { v: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - u1 { - ... on U2 { - ... on A { - v + const operation = operationFromDocument( + api, + gql` + { + u1 { + ... on U2 { + ... on A { + v + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Similar case than in the `interface/union` case: the whole `... on U2` sub-selection happens to be @@ -5880,12 +6237,12 @@ test('handles spread unions correctly', () => { b: Int } - type C @key(fields: "id") { + type C @key(fields: "id") { id: ID! c1: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5905,19 +6262,22 @@ test('handles spread unions correctly', () => { id: ID! c2: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - u { - ... on C { - c1 + const operation = operationFromDocument( + api, + gql` + { + u { + ... on C { + c1 + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Note: it's important that the query below DO NOT include the `... on C` part. Because in @@ -5934,7 +6294,7 @@ test('handles spread unions correctly', () => { }, } `); -}) +}); test('handles case of key chains in parallel requires', () => { const subgraph1 = { @@ -5954,18 +6314,18 @@ test('handles case of key chains in parallel requires', () => { id: ID! y: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', typeDefs: gql` - type T1 @key(fields: "id1") @key(fields: "id2") { + type T1 @key(fields: "id1") @key(fields: "id2") { id1: ID! id2: ID! } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -5980,22 +6340,29 @@ test('handles case of key chains in parallel requires', () => { y: Int @external z: Int @requires(fields: "y") } - ` - } - - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` - { - t { - ... on T1 { - x - } - ... on T2 { - z + `, + }; + + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + ... on T1 { + x + } + ... on T2 { + z + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -6101,8 +6468,8 @@ test('handles types with no common supertype at the same "mergeAt"', () => { id: ID! x: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -6116,26 +6483,29 @@ test('handles types with no common supertype at the same "mergeAt"', () => { id: ID! y: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - ... on T1 { - sub { - y + const operation = operationFromDocument( + api, + gql` + { + t { + ... on T1 { + sub { + y + } } - } - ... on T2 { - sub { - y + ... on T2 { + sub { + y + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -6213,37 +6583,40 @@ test('does not error out handling fragments when interface subtyping is involved v1: Int! v2: Int! } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1); - const operation = operationFromDocument(api, gql` - { - a { - ...F1 - ...F2 - ...F3 + const operation = operationFromDocument( + api, + gql` + { + a { + ...F1 + ...F2 + ...F3 + } } - } - fragment F1 on A { - b { - v2 + fragment F1 on A { + b { + v2 + } } - } - fragment F2 on IA { - b { - v1 + fragment F2 on IA { + b { + v1 + } } - } - fragment F3 on IA { - b { - __typename + fragment F3 on IA { + b { + __typename + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -6263,7 +6636,7 @@ test('does not error out handling fragments when interface subtyping is involved `); }); -describe("named fragments", () => { +describe('named fragments', () => { test('handles mix of fragments indirection and unions', () => { const subgraph1 = { name: 'Subgraph1', @@ -6285,38 +6658,41 @@ describe("named fragments", () => { type Cat { name: String } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1); - const operation = operationFromDocument(api, gql` - query { - parent { - ...F_indirection1_parent + const operation = operationFromDocument( + api, + gql` + query { + parent { + ...F_indirection1_parent + } } - } - - fragment F_indirection1_parent on Parent { - ...F_indirection2_catOrPerson - } - fragment F_indirection2_catOrPerson on CatOrPerson { - ...F_catOrPerson - } + fragment F_indirection1_parent on Parent { + ...F_indirection2_catOrPerson + } - fragment F_catOrPerson on CatOrPerson { - __typename - ... on Cat { - name + fragment F_indirection2_catOrPerson on CatOrPerson { + ...F_catOrPerson } - ... on Parent { - childs { - __typename - id + + fragment F_catOrPerson on CatOrPerson { + __typename + ... on Cat { + name + } + ... on Parent { + childs { + __typename + id + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -6374,47 +6750,50 @@ describe("named fragments", () => { id1: ID! id2: ID! } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1); - let operation = operationFromDocument(api, gql` - { - owner { - u { - ... on I { - id1 - id2 + let operation = operationFromDocument( + api, + gql` + { + owner { + u { + ... on I { + id1 + id2 + } + ...Fragment1 + ...Fragment2 } - ...Fragment1 - ...Fragment2 } } - } - fragment Fragment1 on T1 { - owner { - ... on Owner { - ...Fragment3 + fragment Fragment1 on T1 { + owner { + ... on Owner { + ...Fragment3 + } } } - } - fragment Fragment2 on T2 { - ...Fragment4 - id1 - } + fragment Fragment2 on T2 { + ...Fragment4 + id1 + } - fragment Fragment3 on OItf { - v0 - } + fragment Fragment3 on OItf { + v0 + } - fragment Fragment4 on I { - id1 - id2 - __typename - } - `); + fragment Fragment4 on I { + id1 + id2 + __typename + } + `, + ); let plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -6446,42 +6825,45 @@ describe("named fragments", () => { } `); - operation = operationFromDocument(api, gql` - { - owner { - u { - ... on I { - id1 - id2 + operation = operationFromDocument( + api, + gql` + { + owner { + u { + ... on I { + id1 + id2 + } + ...Fragment1 + ...Fragment2 } - ...Fragment1 - ...Fragment2 } } - } - fragment Fragment1 on T1 { - owner { - ... on Owner { - ...Fragment3 + fragment Fragment1 on T1 { + owner { + ... on Owner { + ...Fragment3 + } } } - } - fragment Fragment2 on T2 { - ...Fragment4 - id1 - } + fragment Fragment2 on T2 { + ...Fragment4 + id1 + } - fragment Fragment3 on OItf { - v0 - } + fragment Fragment3 on OItf { + v0 + } - fragment Fragment4 on I { - id1 - id2 - } - `); + fragment Fragment4 on I { + id1 + id2 + } + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -6534,34 +6916,35 @@ describe("named fragments", () => { other: T1! } - - type T2 implements I - { + type T2 implements I { id: ID! other: T2! } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1); - const operation = operationFromDocument(api, gql` - { - t1 { - ...Fragment1 + const operation = operationFromDocument( + api, + gql` + { + t1 { + ...Fragment1 + } } - } - fragment Fragment1 on I { - other { - ... on T1 { - id - } - ... on T2 { - id + fragment Fragment1 on I { + other { + ... on T1 { + id + } + ... on T2 { + id + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -6595,8 +6978,8 @@ describe("named fragments", () => { v1: Int v2: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -6605,27 +6988,30 @@ describe("named fragments", () => { id: ID! v3: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t1 { - ...allTFields - } - t2 { - ...allTFields + const operation = operationFromDocument( + api, + gql` + { + t1 { + ...allTFields + } + t2 { + ...allTFields + } } - } - fragment allTFields on T { - v0 - v1 - v2 - v3 - } - `); + fragment allTFields on T { + v0 + v1 + v2 + v3 + } + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -6699,8 +7085,8 @@ describe("named fragments", () => { type T @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -6709,7 +7095,6 @@ describe("named fragments", () => { id: ID! u1: U u2: U - } type U @key(fields: "id") { @@ -6717,8 +7102,8 @@ describe("named fragments", () => { v0: Int v1: Int } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -6728,29 +7113,36 @@ describe("named fragments", () => { v2: Int v3: Int } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` - { - t { - u1 { - ...allUFields - } - u2 { - ...allUFields + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + u1 { + ...allUFields + } + u2 { + ...allUFields + } } } - } - fragment allUFields on U { - v0 - v1 - v2 - v3 - } - `); + fragment allUFields on U { + v0 + v1 + v2 + v3 + } + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -6831,7 +7223,7 @@ describe("named fragments", () => { } `); }); -}) +}); describe('`debug.maxEvaluatedPlans` configuration', () => { // Simple schema, created to force the query planner to have multiple choice. We'll build @@ -6859,10 +7251,12 @@ describe('`debug.maxEvaluatedPlans` configuration', () => { const subgraphs = [ { name: 'Subgraph1', - typeDefs - }, { + typeDefs, + }, + { name: 'Subgraph2', - typeDefs } + typeDefs, + }, ]; test('works when unset', () => { @@ -6873,18 +7267,24 @@ describe('`debug.maxEvaluatedPlans` configuration', () => { // plans are considered) and we'll have to adapt the example (find a better way to force // choices). - const config = { debug : { maxEvaluatedPlans : undefined } }; - const [api, queryPlanner] = composeAndCreatePlannerWithOptions(subgraphs, config); - const operation = operationFromDocument(api, gql` - { - t { - v1 - v2 - v3 - v4 + const config = { debug: { maxEvaluatedPlans: undefined } }; + const [api, queryPlanner] = composeAndCreatePlannerWithOptions( + subgraphs, + config, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v1 + v2 + v3 + v4 + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -6907,18 +7307,24 @@ describe('`debug.maxEvaluatedPlans` configuration', () => { }); test('allows setting down to 1', () => { - const config = { debug : { maxEvaluatedPlans : 1 } }; - const [api, queryPlanner] = composeAndCreatePlannerWithOptions(subgraphs, config); - const operation = operationFromDocument(api, gql` - { - t { - v1 - v2 - v3 - v4 + const config = { debug: { maxEvaluatedPlans: 1 } }; + const [api, queryPlanner] = composeAndCreatePlannerWithOptions( + subgraphs, + config, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v1 + v2 + v3 + v4 + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Note that in theory, the planner would be excused if it wasn't generated this @@ -6949,18 +7355,24 @@ describe('`debug.maxEvaluatedPlans` configuration', () => { }); test('can be set to an arbitrary number', () => { - const config = { debug : { maxEvaluatedPlans : 10 } }; - const [api, queryPlanner] = composeAndCreatePlannerWithOptions(subgraphs, config); - const operation = operationFromDocument(api, gql` - { - t { - v1 - v2 - v3 - v4 + const config = { debug: { maxEvaluatedPlans: 10 } }; + const [api, queryPlanner] = composeAndCreatePlannerWithOptions( + subgraphs, + config, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v1 + v2 + v3 + v4 + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -6986,14 +7398,14 @@ describe('`debug.maxEvaluatedPlans` configuration', () => { }); test('cannot be set to 0 or a negative number', () => { - let config = { debug : { maxEvaluatedPlans : 0 } }; + let config = { debug: { maxEvaluatedPlans: 0 } }; expect(() => composeAndCreatePlannerWithOptions(subgraphs, config)).toThrow( - 'Invalid value for query planning configuration "debug.maxEvaluatedPlans"; expected a number >= 1 but got 0' + 'Invalid value for query planning configuration "debug.maxEvaluatedPlans"; expected a number >= 1 but got 0', ); - config = { debug : { maxEvaluatedPlans : -1 } }; + config = { debug: { maxEvaluatedPlans: -1 } }; expect(() => composeAndCreatePlannerWithOptions(subgraphs, config)).toThrow( - 'Invalid value for query planning configuration "debug.maxEvaluatedPlans"; expected a number >= 1 but got -1' + 'Invalid value for query planning configuration "debug.maxEvaluatedPlans"; expected a number >= 1 but got -1', ); }); }); @@ -7023,8 +7435,8 @@ test('correctly generate plan built from some non-individually optimal branch op type T { x: Int @shareable } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -7036,8 +7448,8 @@ test('correctly generate plan built from some non-individually optimal branch op type T @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -7047,18 +7459,25 @@ test('correctly generate plan built from some non-individually optimal branch op x: Int @shareable y: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` - { - t { - x - y + `, + }; + + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + x + y + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -7107,8 +7526,8 @@ test('does not error on some complex fetch group dependencies', () => { type User { id: ID! @shareable } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -7125,8 +7544,8 @@ test('does not error on some complex fetch group dependencies', () => { type Props { id: ID! @shareable } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -7160,27 +7579,34 @@ test('does not error on some complex fetch group dependencies', () => { type V { x: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` - { - me { - p { - v0 - t { - v1 { - x - } - v2 { - x + `, + }; + + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` + { + me { + p { + v0 + t { + v1 { + x + } + v2 { + x + } } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -7235,8 +7661,8 @@ test('does not evaluate plans relying on a key field to fetch that same field', type T @key(fields: "otherId") { otherId: ID! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -7245,8 +7671,8 @@ test('does not evaluate plans relying on a key field to fetch that same field', id: ID! otherId: ID! } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -7254,17 +7680,24 @@ test('does not evaluate plans relying on a key field to fetch that same field', type T @key(fields: "id") { id: ID! } - ` - } - - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` - { - t { - id + `, + }; + + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + id + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -7307,7 +7740,9 @@ test('does not evaluate plans relying on a key field to fetch that same field', // this test ensure this is not considered anymore (considering that later plan // was not incorrect, but it was adding to the options to evaluate which in some // cases could impact query planning performance quite a bit). - expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe(1); + expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe( + 1, + ); }); test('avoid considering indirect paths from the root when a more direct one exists', () => { @@ -7322,8 +7757,8 @@ test('avoid considering indirect paths from the root when a more direct one exis id: ID! v0: Int @shareable } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -7337,24 +7772,30 @@ test('avoid considering indirect paths from the root when a more direct one exis v0: Int @shareable v1: Int } - ` - } + `, + }; // Each of id/v0 can have 2 options each, so that's 4 combinations. If we were to consider 2 options for each // v1 value however, that would multiple it by 2 each times, so it would 32 possibilities. We limit the number of // evaluated plans just above our expected number of 4 so that if we exceed it, the generated plan will be sub-optimal. - const [api, queryPlanner] = composeAndCreatePlannerWithOptions([subgraph1, subgraph2], { debug: { maxEvaluatedPlans: 6 } }); - const operation = operationFromDocument(api, gql` - { - t { - id - v0 - a0: v1 - a1: v1 - a2: v1 + const [api, queryPlanner] = composeAndCreatePlannerWithOptions( + [subgraph1, subgraph2], + { debug: { maxEvaluatedPlans: 6 } }, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + id + v0 + a0: v1 + a1: v1 + a2: v1 + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -7376,5 +7817,7 @@ test('avoid considering indirect paths from the root when a more direct one exis // As said above, we legit have 2 options for `id` and `v0`, and we cannot know which are best before we evaluate the // plans completely. But for the multiple `v1`, we should recognize that going through the 1st subgraph (and taking a // key) is never exactly a good idea. - expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe(4); + expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe( + 4, + ); }); diff --git a/query-planner-js/src/__tests__/generateAllPlans.test.ts b/query-planner-js/src/__tests__/generateAllPlans.test.ts index b95f50d34..5215b2f9a 100644 --- a/query-planner-js/src/__tests__/generateAllPlans.test.ts +++ b/query-planner-js/src/__tests__/generateAllPlans.test.ts @@ -1,7 +1,10 @@ import { assert } from '@apollo/federation-internals'; import { generateAllPlansAndFindBest } from '../generateAllPlans'; -function generateTestPlans(initial: string[], choices: string[][]): { best: string[], generated: string[][], evaluated: string[][] } { +function generateTestPlans( + initial: string[], + choices: string[][], +): { best: string[]; generated: string[][]; evaluated: string[][] } { const generated: string[][] = []; const evaluated: string[][] = []; const { best } = generateAllPlansAndFindBest({ @@ -28,38 +31,47 @@ function expectSamePlans(expected: string[][], actual: string[][]) { const expectedSet = new Set(expected.map((e) => normalize(e))); for (const value of actual) { const normalized = normalize(value); - assert(expectedSet.has(normalized), `Unexpected plan [${value.join(', ')}] is not in [\n${expected.map((e) => `[ ${e.join(', ')} ]`).join('\n')}\n]`); + assert( + expectedSet.has(normalized), + `Unexpected plan [${value.join(', ')}] is not in [\n${expected + .map((e) => `[ ${e.join(', ')} ]`) + .join('\n')}\n]`, + ); } const actualSet = new Set(actual.map((e) => normalize(e))); for (const value of expected) { const normalized = normalize(value); - assert(actualSet.has(normalized), `Expected plan [${value.join(', ')}] not found in [\n${actual.map((e) => `[ ${e.join(', ')} ]`).join('\n')}\n]`); + assert( + actualSet.has(normalized), + `Expected plan [${value.join(', ')}] not found in [\n${actual + .map((e) => `[ ${e.join(', ')} ]`) + .join('\n')}\n]`, + ); } } - test('Pick elements at same index first', () => { const { best, generated } = generateTestPlans( - ['I'], + ['I'], [ - [ 'A1', 'B1'], - [ 'A2', 'B2'], - [ 'A3', 'B3'], + ['A1', 'B1'], + ['A2', 'B2'], + ['A3', 'B3'], ], ); expect(best).toEqual(['I', 'A1', 'A2', 'A3']); expect(generated[0]).toEqual(['I', 'A1', 'A2', 'A3']); expect(generated[1]).toEqual(['I', 'B1', 'B2', 'B3']); -}) +}); test('Bail early for more costly elements', () => { const { best, generated } = generateTestPlans( - ['I'], + ['I'], [ - [ 'A1', 'B1VeryCostly'], - [ 'A2', 'B2Co'], - [ 'A3', 'B3'], + ['A1', 'B1VeryCostly'], + ['A2', 'B2Co'], + ['A3', 'B3'], ], ); @@ -68,29 +80,27 @@ test('Bail early for more costly elements', () => { expect(generated).toHaveLength(2); expect(generated[0]).toEqual(['I', 'A1', 'A2', 'A3']); expect(generated[1]).toEqual(['I', 'A1', 'A2', 'B3']); -}) +}); test('Handles branches of various sizes', () => { const { best, generated } = generateTestPlans( - ['I'], - [ - [ 'A1x', 'B1'], - [ 'A2', 'B2Costly', 'C2'], - [ 'A3'], - [ 'A4', 'B4' ], - ], + ['I'], + [['A1x', 'B1'], ['A2', 'B2Costly', 'C2'], ['A3'], ['A4', 'B4']], ); expect(best).toEqual(['I', 'B1', 'A2', 'A3', 'A4']); // We should generate every option, except those including `B2Costly` - expectSamePlans([ - [ 'I', 'A1x', 'A2', 'A3', 'A4' ], - [ 'I', 'A1x', 'A2', 'A3', 'B4' ], - [ 'I', 'A1x', 'C2', 'A3', 'A4' ], - [ 'I', 'A1x', 'C2', 'A3', 'B4' ], - [ 'I', 'B1', 'A2', 'A3', 'A4' ], - [ 'I', 'B1', 'A2', 'A3', 'B4' ], - [ 'I', 'B1', 'C2', 'A3', 'A4' ], - [ 'I', 'B1', 'C2', 'A3', 'B4' ], - ], generated); -}) + expectSamePlans( + [ + ['I', 'A1x', 'A2', 'A3', 'A4'], + ['I', 'A1x', 'A2', 'A3', 'B4'], + ['I', 'A1x', 'C2', 'A3', 'A4'], + ['I', 'A1x', 'C2', 'A3', 'B4'], + ['I', 'B1', 'A2', 'A3', 'A4'], + ['I', 'B1', 'A2', 'A3', 'B4'], + ['I', 'B1', 'C2', 'A3', 'A4'], + ['I', 'B1', 'C2', 'A3', 'B4'], + ], + generated, + ); +}); diff --git a/query-planner-js/src/__tests__/supergraphBackwardCompatibility.test.ts b/query-planner-js/src/__tests__/supergraphBackwardCompatibility.test.ts index 00b83a728..d9d48cf64 100644 --- a/query-planner-js/src/__tests__/supergraphBackwardCompatibility.test.ts +++ b/query-planner-js/src/__tests__/supergraphBackwardCompatibility.test.ts @@ -1,5 +1,12 @@ import { composeServices } from '@apollo/composition'; -import { asFed2SubgraphDocument, assert, isDefined, operationFromDocument, Schema, Supergraph } from '@apollo/federation-internals'; +import { + asFed2SubgraphDocument, + assert, + isDefined, + operationFromDocument, + Schema, + Supergraph, +} from '@apollo/federation-internals'; import fs from 'fs'; import gql from 'graphql-tag'; import path from 'path'; @@ -16,7 +23,6 @@ import { astSerializer, queryPlanSerializer, QueryPlanner } from '..'; * */ - const accounts = { name: 'accounts', typeDefs: gql` @@ -32,8 +38,8 @@ const accounts = { password: String nickname: String @override(from: "reviews") } - ` -} + `, +}; const products = { name: 'products', @@ -59,7 +65,7 @@ const products = { rating: Int @external } - type Movie implements Product @key(fields: "id") { + type Movie implements Product @key(fields: "id") { id: ID! price: Price title: String @@ -92,8 +98,8 @@ const products = { USD EUR } - ` -} + `, +}; const reviews = { name: 'reviews', @@ -130,8 +136,8 @@ const reviews = { id: ID! reviews: [Review] } - ` -} + `, +}; const testSupergraphDir = path.join(__dirname, 'testSupergraphs'); @@ -140,33 +146,47 @@ function testSupergraphPath(v: string): fs.PathLike { } export function generateTestSupergraph(v: string) { - const services = [ accounts, products, reviews ]; - const res = composeServices( - services.map((s) => ({ ...s, typeDefs: asFed2SubgraphDocument(s.typeDefs) })) + const services = [accounts, products, reviews]; + const res = composeServices( + services.map((s) => ({ + ...s, + typeDefs: asFed2SubgraphDocument(s.typeDefs), + })), + ); + assert( + !res.errors, + `Expected to compose but got errors:\n${res.errors?.join('\n\n')}`, ); - assert(!res.errors, `Expected to compose but got errors:\n${res.errors?.join('\n\n')}`); fs.writeFileSync(testSupergraphPath(v), res.supergraphSdl); } type TestSupergraph = { - version: string, - supergraph: Supergraph, - api: Schema, -} + version: string; + supergraph: Supergraph; + api: Schema; +}; function listTestSupergraphs(): TestSupergraph[] { - return fs.readdirSync(testSupergraphDir).map((file) => { - if (!file.startsWith('testSupergraph_')) { - return undefined; - } - const version = file.slice('testSupergraph_'.length, file.length - '.graphql'.length); - const supergraph = Supergraph.build(fs.readFileSync(path.join(testSupergraphDir, file), 'utf8')); - return { - version, - supergraph, - api: supergraph.apiSchema(), - }; - }).filter(isDefined); + return fs + .readdirSync(testSupergraphDir) + .map((file) => { + if (!file.startsWith('testSupergraph_')) { + return undefined; + } + const version = file.slice( + 'testSupergraph_'.length, + file.length - '.graphql'.length, + ); + const supergraph = Supergraph.build( + fs.readFileSync(path.join(testSupergraphDir, file), 'utf8'), + ); + return { + version, + supergraph, + api: supergraph.apiSchema(), + }; + }) + .filter(isDefined); } // This file is loaded by the `genTestSupergraph.ts` script to generate the test supergraph for the current version. @@ -180,27 +200,32 @@ if (typeof describe !== 'undefined') { const toTest = listTestSupergraphs(); - describe.each(toTest)(`handles supergraphs from $version`, ({supergraph, api}) => { - let qp: QueryPlanner; - - test('can extract subgraphs and build the query planner', () => { - qp = new QueryPlanner(supergraph); - }); - - test('can execute "me" query', () => { - const operation = operationFromDocument(api, gql` - { - me { - nickname - reviews { - rating + describe.each(toTest)( + `handles supergraphs from $version`, + ({ supergraph, api }) => { + let qp: QueryPlanner; + + test('can extract subgraphs and build the query planner', () => { + qp = new QueryPlanner(supergraph); + }); + + test('can execute "me" query', () => { + const operation = operationFromDocument( + api, + gql` + { + me { + nickname + reviews { + rating + } + } } - } - } - `); + `, + ); - const plan = qp.buildQueryPlan(operation); - expect(plan).toMatchInlineSnapshot(` + const plan = qp.buildQueryPlan(operation); + expect(plan).toMatchInlineSnapshot(` QueryPlan { Parallel { Fetch(service: "accounts") { @@ -222,34 +247,37 @@ if (typeof describe !== 'undefined') { }, } `); - }); - - test('can execute "bestRatedProducts" query', () => { - const operation = operationFromDocument(api, gql` - { - bestRatedProducts(limit: 10) { - reviews { - author { - nickname + }); + + test('can execute "bestRatedProducts" query', () => { + const operation = operationFromDocument( + api, + gql` + { + bestRatedProducts(limit: 10) { + reviews { + author { + nickname + } + rating + } + price { + value + currency + } + ... on Movie { + length_minutes + } + ... on Book { + avg_rating + } } - rating - } - price { - value - currency - } - ... on Movie { - length_minutes } - ... on Book { - avg_rating - } - } - } - `); + `, + ); - const plan = qp.buildQueryPlan(operation); - expect(plan).toMatchInlineSnapshot(` + const plan = qp.buildQueryPlan(operation); + expect(plan).toMatchInlineSnapshot(` QueryPlan { Sequence { Fetch(service: "reviews") { @@ -330,7 +358,8 @@ if (typeof describe !== 'undefined') { }, } `); - }); - }); + }); + }, + ); }); } diff --git a/subgraph-js/src/__tests__/buildSubgraphSchema.test.ts b/subgraph-js/src/__tests__/buildSubgraphSchema.test.ts index be3e7b732..81ee9cf3c 100644 --- a/subgraph-js/src/__tests__/buildSubgraphSchema.test.ts +++ b/subgraph-js/src/__tests__/buildSubgraphSchema.test.ts @@ -862,7 +862,8 @@ describe('buildSubgraphSchema', () => { directiveDefinitions: string, typeDefinitions: string, ) => { - const schema = buildSubgraphSchema(gql`${header} + const schema = buildSubgraphSchema(gql` + ${header} type User @key(fields: "email") @tag(name: "tagOnType") { email: String @tag(name: "tagOnField") } @@ -881,8 +882,8 @@ describe('buildSubgraphSchema', () => { ? '' : ` ${header.trim()} - `) - + ` + `) + + ` ${directiveDefinitions.trim()} type User @@ -903,13 +904,15 @@ describe('buildSubgraphSchema', () => { = User ${typeDefinitions.trim()} - `); + `, + ); }; - it.each([{ - name: 'fed1', - header: '', - directiveDefinitions: ` + it.each([ + { + name: 'fed1', + header: '', + directiveDefinitions: ` directive @key(fields: _FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE directive @requires(fields: _FieldSet!) on FIELD_DEFINITION @@ -922,7 +925,7 @@ describe('buildSubgraphSchema', () => { directive @extends on OBJECT | INTERFACE `, - typesDefinitions: ` + typesDefinitions: ` scalar _FieldSet scalar _Any @@ -938,14 +941,15 @@ describe('buildSubgraphSchema', () => { _service: _Service! } `, - }, { - name: 'fed2', - header: ` + }, + { + name: 'fed2', + header: ` extend schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@tag"]) `, - directiveDefinitions: ` + directiveDefinitions: ` directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA directive @key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE @@ -966,7 +970,7 @@ describe('buildSubgraphSchema', () => { directive @federation__override(from: String!) on FIELD_DEFINITION `, - typesDefinitions: ` + typesDefinitions: ` enum link__Purpose { """ \`SECURITY\` features provide metadata necessary to securely resolve fields. @@ -996,9 +1000,13 @@ describe('buildSubgraphSchema', () => { _service: _Service! } `, - }])('adds it for $name schema', async ({header, directiveDefinitions, typesDefinitions}) => { - await validateTag(header, directiveDefinitions, typesDefinitions); - }); + }, + ])( + 'adds it for $name schema', + async ({ header, directiveDefinitions, typesDefinitions }) => { + await validateTag(header, directiveDefinitions, typesDefinitions); + }, + ); }); it(`fails on bad linking`, () => { @@ -1006,8 +1014,10 @@ describe('buildSubgraphSchema', () => { buildSubgraphSchema(gql` extend schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/federation/v2.0", - import: [ { name: "@key", as: "@primaryKey" } ]) + @link( + url: "https://specs.apollo.dev/federation/v2.0" + import: [{ name: "@key", as: "@primaryKey" }] + ) type Query { t: T @@ -1016,10 +1026,10 @@ describe('buildSubgraphSchema', () => { type T @key(fields: "id") { id: ID! } - `); + `); } catch (e) { expect(errorCauses(e)?.map((e) => e.message)).toStrictEqual([ - 'Unknown directive "@key". If you meant the "@key" federation directive, you should use "@primaryKey" as it is imported under that name in the @link to the federation specification of this schema.' + 'Unknown directive "@key". If you meant the "@key" federation directive, you should use "@primaryKey" as it is imported under that name in the @link to the federation specification of this schema.', ]); } }); @@ -1047,9 +1057,12 @@ describe('buildSubgraphSchema', () => { { kind: Kind.OPERATION_TYPE_DEFINITION, operation: OperationTypeNode.QUERY, - type: { kind: Kind.NAMED_TYPE, name: { kind: Kind.NAME, value: 'Query' } } + type: { + kind: Kind.NAMED_TYPE, + name: { kind: Kind.NAME, value: 'Query' }, + }, }, - ] + ], }, { kind: Kind.OBJECT_TYPE_DEFINITION, @@ -1060,20 +1073,19 @@ describe('buildSubgraphSchema', () => { name: { kind: Kind.NAME, value: 'test' }, type: { kind: Kind.NAMED_TYPE, - name: { kind: Kind.NAME, value: 'String' } + name: { kind: Kind.NAME, value: 'String' }, }, }, - ] + ], }, ], }; - expect(() => buildSubgraphSchema(doc)).not.toThrow(); }); it('correctly attaches the provided subscribe function to the schema object', () => { - async function* subscribeFn () { + async function* subscribeFn() { for await (const word of ['Hello', 'Bonjour', 'Ciao']) { yield word; } @@ -1123,18 +1135,20 @@ describe('buildSubgraphSchema', () => { type Query { x: Int } - `) + `); const { data, errors } = await graphql({ schema, source: query }); expect(errors).toBeUndefined(); expect((data?._service as any).sdl).toMatchString(expectedOutput); - } + }; it('expands federation 2.0 correctly', async () => { // For 2.0, we expect in particular that: // - the @composeDirective directive is *not* present // - the @shareable directive is *not* repeatable - await testVersion('2.0', ` + await testVersion( + '2.0', + ` schema @link(url: \"https://specs.apollo.dev/link/v1.0\") { @@ -1190,14 +1204,17 @@ describe('buildSubgraphSchema', () => { type _Service { sdl: String } - `); + `, + ); }); it('expands federation 2.1 correctly', async () => { // For 2.1, we expect in particular that: // - the @composeDirective directive to exists // - the @shareable directive is *not* repeatable - await testVersion('2.1', ` + await testVersion( + '2.1', + ` schema @link(url: \"https://specs.apollo.dev/link/v1.0\") { @@ -1255,13 +1272,16 @@ describe('buildSubgraphSchema', () => { type _Service { sdl: String } - `); + `, + ); }); it('expands federation 2.2 correctly', async () => { // For 2.2, we expect everything from 2.1 plus: // - the @shareable directive to be repeatable - await testVersion('2.2', ` + await testVersion( + '2.2', + ` schema @link(url: \"https://specs.apollo.dev/link/v1.0\") { @@ -1319,14 +1339,17 @@ describe('buildSubgraphSchema', () => { type _Service { sdl: String } - `); + `, + ); }); it('expands federation 2.3 correctly', async () => { // For 2.3, we expect in everything from 2.2 plus: // - the @interfaceObject directive // - the @tag directive to additionally have the SCHEMA location - await testVersion('2.3', ` + await testVersion( + '2.3', + ` schema @link(url: \"https://specs.apollo.dev/link/v1.0\") { @@ -1386,7 +1409,8 @@ describe('buildSubgraphSchema', () => { type _Service { sdl: String } - `); + `, + ); }); }); }); From bf55417fad686ecd40962edd1e401e91ae3a5727 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 22 Sep 2023 11:17:16 -0700 Subject: [PATCH 81/95] add commit to ignore from git blame --- .git-blame-ignore-revs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 064966d92..4d841d837 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,2 +1,4 @@ # Run prettier -f2f1d6041cd67c67fdacabe0570fe2ce642c052c \ No newline at end of file +f2f1d6041cd67c67fdacabe0570fe2ce642c052c +# Enforce prettier on all test files https://github.com/apollographql/federation/pull/2785 +dca3d464c8a699deaa1109ae69f376750b48c249 \ No newline at end of file From dd35060076a4879494850676608d27a66a510287 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 23 Sep 2023 09:11:58 +0000 Subject: [PATCH 82/95] chore(deps): update all non-major dependencies (#2789) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 28 +++++++++++++++------------- package.json | 4 ++-- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index ad90fe107..b841aac82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,9 +28,9 @@ "@josephg/resolvable": "1.0.1", "@opentelemetry/node": "0.24.0", "@svitejs/changesets-changelog-github-compact": "1.1.0", - "@types/async-retry": "1.4.5", + "@types/async-retry": "1.4.6", "@types/bunyan": "1.8.9", - "@types/deep-equal": "1.0.1", + "@types/deep-equal": "1.0.2", "@types/jest": "29.5.5", "@types/js-levenshtein": "1.1.1", "@types/loglevel": "1.5.4", @@ -2285,9 +2285,9 @@ "peer": true }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.21.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", - "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "version": "13.22.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", + "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", "dev": true, "peer": true, "dependencies": { @@ -3808,9 +3808,10 @@ "license": "MIT" }, "node_modules/@types/async-retry": { - "version": "1.4.5", + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@types/async-retry/-/async-retry-1.4.6.tgz", + "integrity": "sha512-or8JPgYUtyPpO0ddHImwUWmSjVE/UalxgMm2d0r3698QhjzlM7eke0PT60bOxs1NG7HxU232RQ1vy1iQKGGRTw==", "dev": true, - "license": "MIT", "dependencies": { "@types/retry": "*" } @@ -3879,9 +3880,10 @@ } }, "node_modules/@types/deep-equal": { - "version": "1.0.1", - "dev": true, - "license": "MIT" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-equal/-/deep-equal-1.0.2.tgz", + "integrity": "sha512-pjMMQWhqEKL/rxzUWQKpnbM2oFhRIx4kJ/7mH7MtbJWOA0yrZK0h4WA+RgKa70IHS9amWT7+jvmVmEcL2nXm3A==", + "dev": true }, "node_modules/@types/express": { "version": "4.17.17", @@ -7396,9 +7398,9 @@ } }, "node_modules/eslint/node_modules/globals": { - "version": "13.21.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", - "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "version": "13.22.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", + "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", "dev": true, "peer": true, "dependencies": { diff --git a/package.json b/package.json index 8db1b4a95..b802471ba 100644 --- a/package.json +++ b/package.json @@ -60,9 +60,9 @@ "@josephg/resolvable": "1.0.1", "@opentelemetry/node": "0.24.0", "@svitejs/changesets-changelog-github-compact": "1.1.0", - "@types/async-retry": "1.4.5", + "@types/async-retry": "1.4.6", "@types/bunyan": "1.8.9", - "@types/deep-equal": "1.0.1", + "@types/deep-equal": "1.0.2", "@types/jest": "29.5.5", "@types/js-levenshtein": "1.1.1", "@types/loglevel": "1.5.4", From d43035ff987a1346230b5a6e332c5ad629794237 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 29 Sep 2023 10:30:48 +0200 Subject: [PATCH 83/95] Note how s are and aren't like PKs --- docs/source/entities.mdx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/source/entities.mdx b/docs/source/entities.mdx index 1e70d07ed..dd40d072c 100644 --- a/docs/source/entities.mdx +++ b/docs/source/entities.mdx @@ -53,10 +53,12 @@ type Product @key(fields: "id") { } ``` -The `@key` directive defines the entity's **primary key**, which consists of one or more of the type's `fields`. In the example above, the `Product` entity's primary key is its `id` field. **Every instance of an entity must be uniquely identifiable by its `@key` fields.** This is what enables your router to associate field data from _different_ subgraphs with the _same_ entity instance. +The `@key` directive defines the entity's **unique key**, which consists of one or more of the type's `fields`. In the example above, the `Product` entity's unique key is its `id` field. **Every instance of an entity must be uniquely identifiable by its `@key` fields.** This is what enables your router to associate field data from _different_ subgraphs with the _same_ entity instance. In most cases, the `@key` field(s) for the same entity will be the same across subgraphs. For example, if one subgraph uses `id` as the `@key` field for the `Product` entity, other subgraphs should do the same. However, this [isn't strictly required](./entities-advanced#differing-keys-across-subgraphs). +> If coming from a database context, it can be helpful to think of a `@key` as an entity's [**primary key**](https://en.wikipedia.org/wiki/Primary_key). The term isn't completely accurate for entities since a single entity can have [multiple `@key`s](./entities-advanced#multiple-keys). The field(s) you select for an entity's `@key` must, however, uniquely identify the entity. + ```graphql title="Products subgraph" @@ -87,7 +89,7 @@ For more information on advanced key options, like how to define [multiple keys] ### 2. Define a reference resolver -The `@key` directive effectively tells the router, "This subgraph can resolve an instance of this entity if you provide its primary key." In order for this to be true, the subgraph needs to define a **reference resolver** for the entity. +The `@key` directive effectively tells the router, "This subgraph can resolve an instance of this entity if you provide its unique key." In order for this to be true, the subgraph needs to define a **reference resolver** for the entity. > ⚠️ **This section describes how to create reference resolvers in Apollo Server.** If you're using another [subgraph-compatible library](./building-supergraphs/supported-subgraphs/), see its documentation for creating reference resolvers (or the equivalent functionality). From a8bda67178c26b947101d16003dd2b402b8fb69a Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 29 Sep 2023 10:31:08 +0200 Subject: [PATCH 84/95] Replace pk with unique key --- docs/source/entities-advanced.mdx | 2 +- docs/source/federated-types/federated-directives.mdx | 2 +- docs/source/migrating-from-stitching.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/entities-advanced.mdx b/docs/source/entities-advanced.mdx index 2dbf14cfe..af4616f37 100644 --- a/docs/source/entities-advanced.mdx +++ b/docs/source/entities-advanced.mdx @@ -21,7 +21,7 @@ type User @key(fields: "username domain") { #### Nested fields in compound `@key`s -Compound keys can also include _nested_ fields. In the following example, the `User` entity's primary key consists of both a user's `id` _and_ the `id` of that user's associated `Organization`: +Compound keys can also include _nested_ fields. In the following example, the `User` entity's unique key consists of both a user's `id` _and_ the `id` of that user's associated `Organization`: ```graphql {1} title="Users subgraph" type User @key(fields: "id organization { id }") { diff --git a/docs/source/federated-types/federated-directives.mdx b/docs/source/federated-types/federated-directives.mdx index 0ac36afe9..fc115cf7d 100644 --- a/docs/source/federated-types/federated-directives.mdx +++ b/docs/source/federated-types/federated-directives.mdx @@ -134,7 +134,7 @@ To check whether your subgraph library supports repeatable directives, see the ` -**Required.** A GraphQL selection set (provided as a string) of fields and subfields that contribute to the entity's primary key. +**Required.** A GraphQL selection set (provided as a string) of fields and subfields that contribute to the entity's unique key. Examples: diff --git a/docs/source/migrating-from-stitching.md b/docs/source/migrating-from-stitching.md index ac289b7b4..4f392dfa5 100644 --- a/docs/source/migrating-from-stitching.md +++ b/docs/source/migrating-from-stitching.md @@ -122,7 +122,7 @@ resolvers: { This resolver calls `Query.user` on the `userSchema` to look up a `User`. It adds that user to the `Reservation.user` field that was previously defined at the gateway. This code can all remain. You don't need to remove it from the stitched gateway. In fact, if you did that, the stitched gateway would break. -On the other hand, a _federated_ architecture defines its resolvers at the subgraph level. These resolvers rely on **entities**, which are identified by a primary key. For example, the Reservation subgraph must define the `Reservation` type as an entity to allow other subgraphs to extend it. These other subgraphs use the `Reservation`'s `@key` fields to uniquely identify a given instance: +On the other hand, a _federated_ architecture defines its resolvers at the subgraph level. These resolvers rely on **entities**, which are identified by a unique key. For example, the Reservation subgraph must define the `Reservation` type as an entity to allow other subgraphs to extend it. These other subgraphs use the `Reservation`'s `@key` fields to uniquely identify a given instance: ```graphql type Reservation @key(fields: "id") { From 50a0d87ae36ec03f5c47690433401766838dfcfa Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 29 Sep 2023 10:38:20 +0200 Subject: [PATCH 85/95] Replace renaming directives example --- docs/source/federated-types/federated-directives.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/federated-types/federated-directives.mdx b/docs/source/federated-types/federated-directives.mdx index fc115cf7d..6b6d6378a 100644 --- a/docs/source/federated-types/federated-directives.mdx +++ b/docs/source/federated-types/federated-directives.mdx @@ -27,10 +27,10 @@ If an imported directive's default name matches one of your own custom directive ```graphql extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", - import: [{ name: "@key", as: "@primaryKey"}, "@shareable"]) + import: [{ name: "@key", as: "@uniqueKey"}, "@shareable"]) ``` -This example subgraph schema uses `@primaryKey` for the federated directive usually named [`@key`](#key). +This example subgraph schema uses `@uniqueKey` for the federated directive usually named [`@key`](#key). ### Namespaced directives From 8b01d2db9071c1c39722213038488682dba0a6f3 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 29 Sep 2023 13:12:05 +0200 Subject: [PATCH 86/95] Copyedit --- docs/source/entities-advanced.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/entities-advanced.mdx b/docs/source/entities-advanced.mdx index af4616f37..7debc595e 100644 --- a/docs/source/entities-advanced.mdx +++ b/docs/source/entities-advanced.mdx @@ -21,7 +21,7 @@ type User @key(fields: "username domain") { #### Nested fields in compound `@key`s -Compound keys can also include _nested_ fields. In the following example, the `User` entity's unique key consists of both a user's `id` _and_ the `id` of that user's associated `Organization`: +Compound keys can also include _nested_ fields. In the following example, the `User` entity's `@key` consists of both a user's `id` _and_ the `id` of that user's associated `Organization`: ```graphql {1} title="Users subgraph" type User @key(fields: "id organization { id }") { From da64ed8dcd56781d8081d8028f2c0556a1c742d0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 10:44:38 -0700 Subject: [PATCH 87/95] chore(deps): update dependency npm to v10 (#2765) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 2 +- package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b841aac82..2fd37cf39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,7 +66,7 @@ }, "engines": { "node": ">=14.15.0", - "npm": "<10" + "npm": "<11" } }, "composition-js": { diff --git a/package.json b/package.json index b802471ba..f1263ff25 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ }, "engines": { "node": ">=14.15.0", - "npm": "<10" + "npm": "<11" }, "workspaces": [ "internals-js", @@ -108,6 +108,6 @@ }, "volta": { "node": "18.18.0", - "npm": "9.8.1" + "npm": "10.1.0" } } From 378f1240475a08bf8527c2943dac77e789f9f0bd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 10:46:40 -0700 Subject: [PATCH 88/95] chore(deps): update all non-major dependencies (#2790) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 22 +++++++++++----------- package.json | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2fd37cf39..4d557756f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,8 +36,8 @@ "@types/loglevel": "1.5.4", "@types/make-fetch-happen": "10.0.1", "@types/nock": "10.0.3", - "@types/node": "14.18.62", - "@types/node-fetch": "2.6.5", + "@types/node": "14.18.63", + "@types/node-fetch": "2.6.6", "@types/uuid": "9.0.4", "@typescript-eslint/eslint-plugin": "5.62.0", "bunyan": "1.8.15", @@ -4026,14 +4026,14 @@ } }, "node_modules/@types/node": { - "version": "14.18.62", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.62.tgz", - "integrity": "sha512-53Fhb08qfKwSNCIUtysIqw0ye+v1d5QCdL2kl8liKQFlOZTAo+nEYr/FztzMaHBFwB5H0ugF0PF0gmtojaNNiQ==" + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" }, "node_modules/@types/node-fetch": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.5.tgz", - "integrity": "sha512-OZsUlr2nxvkqUFLSaY2ZbA+P1q22q+KrlxWOn/38RX+u5kTkYL2mTujEpzUhGkS+K/QCYp9oagfXG39XOzyySg==", + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.6.tgz", + "integrity": "sha512-95X8guJYhfqiuVVhRFxVQcf4hW/2bCuoPwDasMf/531STFoNoWTT7YDnWdXHEZKqAGUigmpG31r2FE70LwnzJw==", "dependencies": { "@types/node": "*", "form-data": "^4.0.0" @@ -4505,9 +4505,9 @@ } }, "node_modules/@whatwg-node/fetch/node_modules/@types/node": { - "version": "18.17.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.18.tgz", - "integrity": "sha512-/4QOuy3ZpV7Ya1GTRz5CYSz3DgkKpyUptXuQ5PPce7uuyJAOR7r9FhkmxJfvcNUXyklbC63a+YvB3jxy7s9ngw==", + "version": "18.17.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.19.tgz", + "integrity": "sha512-+pMhShR3Or5GR0/sp4Da7FnhVmTalWm81M6MkEldbwjETSaPalw138Z4KdpQaistvqQxLB7Cy4xwYdxpbSOs9Q==", "dev": true, "peer": true }, diff --git a/package.json b/package.json index f1263ff25..184a105a4 100644 --- a/package.json +++ b/package.json @@ -68,8 +68,8 @@ "@types/loglevel": "1.5.4", "@types/make-fetch-happen": "10.0.1", "@types/nock": "10.0.3", - "@types/node": "14.18.62", - "@types/node-fetch": "2.6.5", + "@types/node": "14.18.63", + "@types/node-fetch": "2.6.6", "@types/uuid": "9.0.4", "@typescript-eslint/eslint-plugin": "5.62.0", "bunyan": "1.8.15", From 429f9c827c632697c4b9eb97ba0ae118b83b497f Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 29 Sep 2023 11:39:11 -0700 Subject: [PATCH 89/95] update renovate config --- renovate.json5 | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/renovate.json5 b/renovate.json5 index 7ee3b9d96..2c1a62562 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -151,5 +151,35 @@ "matchPackageNames": ["make-fetch-happen"], "allowedVersions": "11.x", } + // make-fetch-happen@11 drops support for node 12 + { + "matchBaseBranches": ["version-0.x"], + "matchPackageNames": ["make-fetch-happen"], + "allowedVersions": "10.x", + }, + // cspell@7 drops support for node 14 + { + "matchPackageNames": ["cspell"], + "allowedVersions": "6.x", + }, + // @apollo/cache-control-types@1.0.3 accidentally broke support for node 12 + { + "matchBaseBranches": ["version-0.x"], + "matchPackageNames": ["@apollo/cache-control-types"], + "allowedVersions": "1.0.2", + }, + // typescript@5.1 drops support for node 12 + { + "matchBaseBranches": ["version-0.x"], + "matchPackageNames": ["typescript"], + "allowedVersions": "5.0.x", + }, + // lru-cache@7.13.1 is the last patch which includes an important AbortSignal type as part of its polyfill + // this was a breaking change for node 12 support + { + "matchBaseBranches": ["version-0.x"], + "matchPackageNames": ["lru-cache"], + "allowedVersions": "7.13.1", + }, ] } From 51b5c602723c93f999c6aecf226a5935fbdd90c8 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 29 Sep 2023 11:40:36 -0700 Subject: [PATCH 90/95] add missing comma to renovate config --- renovate.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renovate.json5 b/renovate.json5 index 2c1a62562..9c06d2f1d 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -150,7 +150,7 @@ { "matchPackageNames": ["make-fetch-happen"], "allowedVersions": "11.x", - } + }, // make-fetch-happen@11 drops support for node 12 { "matchBaseBranches": ["version-0.x"], From c0afc5abd4e3ad10c2a9290546d86616d42c83e6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 11:35:15 -0700 Subject: [PATCH 91/95] chore(deps): update dependency @types/make-fetch-happen to v10.0.2 (#2798) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 40 ++++++++++++++++++++++------------------ package.json | 2 +- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4d557756f..40fbc1ce4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "@types/jest": "29.5.5", "@types/js-levenshtein": "1.1.1", "@types/loglevel": "1.5.4", - "@types/make-fetch-happen": "10.0.1", + "@types/make-fetch-happen": "10.0.2", "@types/nock": "10.0.3", "@types/node": "14.18.63", "@types/node-fetch": "2.6.6", @@ -3993,9 +3993,10 @@ "license": "MIT" }, "node_modules/@types/make-fetch-happen": { - "version": "10.0.1", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@types/make-fetch-happen/-/make-fetch-happen-10.0.2.tgz", + "integrity": "sha512-NSMHhLp2dpXGqN0aolc0SygCrmck6HYTZjlRd1ys51yCVLhoFwV/5xwSDe4XDkNmWpeqKIqpSrN+w/xXU3XPEw==", "dev": true, - "license": "MIT", "dependencies": { "@types/node-fetch": "*", "@types/retry": "*", @@ -4129,9 +4130,9 @@ } }, "node_modules/@types/yargs": { - "version": "17.0.24", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", - "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "version": "17.0.26", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.26.tgz", + "integrity": "sha512-Y3vDy2X6zw/ZCumcwLpdhM5L7jmyGpmBCTYMHDLqT2IKVMYRRLdv6ZakA+wxhra6Z/3bwhNbNl9bDGXaFU+6rw==", "dev": true, "dependencies": { "@types/yargs-parser": "*" @@ -4505,9 +4506,9 @@ } }, "node_modules/@whatwg-node/fetch/node_modules/@types/node": { - "version": "18.17.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.19.tgz", - "integrity": "sha512-+pMhShR3Or5GR0/sp4Da7FnhVmTalWm81M6MkEldbwjETSaPalw138Z4KdpQaistvqQxLB7Cy4xwYdxpbSOs9Q==", + "version": "18.18.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.1.tgz", + "integrity": "sha512-3G42sxmm0fF2+Vtb9TJQpnjmP+uKlWvFa8KoEGquh4gqRmoUG/N0ufuhikw6HEsdG2G2oIKhog1GCTfz9v5NdQ==", "dev": true, "peer": true }, @@ -8198,9 +8199,9 @@ } }, "node_modules/fs-minipass/node_modules/minipass": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", - "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", "engines": { "node": ">=16 || 14 >=14.17" } @@ -10164,9 +10165,10 @@ "license": "MIT" }, "node_modules/jest-cucumber/node_modules/@types/yargs": { - "version": "15.0.15", + "version": "15.0.16", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.16.tgz", + "integrity": "sha512-2FeD5qezW3FvLpZ0JpfuaEWepgNLl9b2gQYiz/ce0NhoB1W/D+VZu98phITXkADYerfr/jb7JcDcVhITsc9bwg==", "dev": true, - "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } @@ -11052,9 +11054,10 @@ } }, "node_modules/jest-environment-jsdom/node_modules/@types/yargs": { - "version": "15.0.15", + "version": "15.0.16", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.16.tgz", + "integrity": "sha512-2FeD5qezW3FvLpZ0JpfuaEWepgNLl9b2gQYiz/ce0NhoB1W/D+VZu98phITXkADYerfr/jb7JcDcVhITsc9bwg==", "dev": true, - "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } @@ -11393,9 +11396,10 @@ } }, "node_modules/jest-jasmine2/node_modules/@types/yargs": { - "version": "15.0.15", + "version": "15.0.16", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.16.tgz", + "integrity": "sha512-2FeD5qezW3FvLpZ0JpfuaEWepgNLl9b2gQYiz/ce0NhoB1W/D+VZu98phITXkADYerfr/jb7JcDcVhITsc9bwg==", "dev": true, - "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } diff --git a/package.json b/package.json index 184a105a4..bf890a2b0 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@types/jest": "29.5.5", "@types/js-levenshtein": "1.1.1", "@types/loglevel": "1.5.4", - "@types/make-fetch-happen": "10.0.1", + "@types/make-fetch-happen": "10.0.2", "@types/nock": "10.0.3", "@types/node": "14.18.63", "@types/node-fetch": "2.6.6", From 5e763597f7d6f333a0828175a602382cdd58da66 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 2 Oct 2023 17:22:30 -0600 Subject: [PATCH 92/95] Update docs/source/entities.mdx Co-authored-by: Trevor Scheer --- docs/source/entities.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/entities.mdx b/docs/source/entities.mdx index dd40d072c..2236180d8 100644 --- a/docs/source/entities.mdx +++ b/docs/source/entities.mdx @@ -53,7 +53,7 @@ type Product @key(fields: "id") { } ``` -The `@key` directive defines the entity's **unique key**, which consists of one or more of the type's `fields`. In the example above, the `Product` entity's unique key is its `id` field. **Every instance of an entity must be uniquely identifiable by its `@key` fields.** This is what enables your router to associate field data from _different_ subgraphs with the _same_ entity instance. +The `@key` directive defines an entity's **unique key**, which consists of one or more of the type's `fields`. In the example above, the `Product` entity's unique key is its `id` field. **Every instance of an entity must be uniquely identifiable by its `@key` fields.** This is what enables your router to associate field data from _different_ subgraphs with the _same_ entity instance. In most cases, the `@key` field(s) for the same entity will be the same across subgraphs. For example, if one subgraph uses `id` as the `@key` field for the `Product` entity, other subgraphs should do the same. However, this [isn't strictly required](./entities-advanced#differing-keys-across-subgraphs). From 17b0ba8c8ab7b7073bc1332faf5456d5206e1517 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 2 Oct 2023 17:31:04 -0600 Subject: [PATCH 93/95] Update docs/source/entities.mdx --- docs/source/entities.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/entities.mdx b/docs/source/entities.mdx index 2236180d8..b129a71de 100644 --- a/docs/source/entities.mdx +++ b/docs/source/entities.mdx @@ -57,7 +57,7 @@ The `@key` directive defines an entity's **unique key**, which consists of one o In most cases, the `@key` field(s) for the same entity will be the same across subgraphs. For example, if one subgraph uses `id` as the `@key` field for the `Product` entity, other subgraphs should do the same. However, this [isn't strictly required](./entities-advanced#differing-keys-across-subgraphs). -> If coming from a database context, it can be helpful to think of a `@key` as an entity's [**primary key**](https://en.wikipedia.org/wiki/Primary_key). The term isn't completely accurate for entities since a single entity can have [multiple `@key`s](./entities-advanced#multiple-keys). The field(s) you select for an entity's `@key` must, however, uniquely identify the entity. +> If coming from a database context, it can be helpful to think of a `@key` as an entity's [**primary key**](https://en.wikipedia.org/wiki/Primary_key). This term isn't completely accurate for entities since a single entity can have [multiple `@key`s](./entities-advanced#multiple-keys). The field(s) you select for an entity's `@key` must, however, uniquely identify the entity. In that way, `@key`s are similar to [candidate keys](https://en.wikipedia.org/wiki/Candidate_key). From 4339fd53028c535d91de3dd6464b1e44fa57a685 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 2 Oct 2023 17:55:02 -0600 Subject: [PATCH 94/95] Remove bolding --- docs/source/entities.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/entities.mdx b/docs/source/entities.mdx index b129a71de..700ab5fc2 100644 --- a/docs/source/entities.mdx +++ b/docs/source/entities.mdx @@ -57,7 +57,7 @@ The `@key` directive defines an entity's **unique key**, which consists of one o In most cases, the `@key` field(s) for the same entity will be the same across subgraphs. For example, if one subgraph uses `id` as the `@key` field for the `Product` entity, other subgraphs should do the same. However, this [isn't strictly required](./entities-advanced#differing-keys-across-subgraphs). -> If coming from a database context, it can be helpful to think of a `@key` as an entity's [**primary key**](https://en.wikipedia.org/wiki/Primary_key). This term isn't completely accurate for entities since a single entity can have [multiple `@key`s](./entities-advanced#multiple-keys). The field(s) you select for an entity's `@key` must, however, uniquely identify the entity. In that way, `@key`s are similar to [candidate keys](https://en.wikipedia.org/wiki/Candidate_key). +> If coming from a database context, it can be helpful to think of a `@key` as an entity's [primary key](https://en.wikipedia.org/wiki/Primary_key). This term isn't completely accurate for entities since a single entity can have [multiple `@key`s](./entities-advanced#multiple-keys). The field(s) you select for an entity's `@key` must, however, uniquely identify the entity. In that way, `@key`s are similar to [candidate keys](https://en.wikipedia.org/wiki/Candidate_key). From 81cefa9a031c88757726b9c28daba9bfcbd9626f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 21:48:57 +0000 Subject: [PATCH 95/95] chore(deps): update dependency @apollo/server to v4.9.4 (#2808) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 39 +++++++++++++++++++++++---------------- package.json | 2 +- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 40fbc1ce4..f6ab49f31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "devDependencies": { "@apollo/cache-control-types": "1.0.3", "@apollo/client": "3.8.4", - "@apollo/server": "4.9.3", + "@apollo/server": "4.9.4", "@apollo/utils.fetcher": "2.0.1", "@changesets/changelog-github": "0.4.8", "@changesets/cli": "2.26.2", @@ -283,9 +283,9 @@ "link": true }, "node_modules/@apollo/server": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/@apollo/server/-/server-4.9.3.tgz", - "integrity": "sha512-U56Sx/UmzR3Es344hQ/Ptf2EJrH+kV4ZPoLmgGjWoiwf2wYQ/pRSvkSXgjOvoyE34wSa8Gh7f92ljfLfY+6q1w==", + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/@apollo/server/-/server-4.9.4.tgz", + "integrity": "sha512-lopNDM3sZerTcYH/P85QX5HqSNV4HoVbtX3zOrf0ak7eplhPDiGVyF0jQWRbL64znG6KXW+nMuLDTyFTMQnvgA==", "dev": true, "dependencies": { "@apollo/cache-control-types": "^1.0.3", @@ -3210,9 +3210,9 @@ } }, "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.0.tgz", - "integrity": "sha512-x58orMzEVfzPUKqlbLd1hXCnySCxKdDKa6Rjg97CwuLLRI4g3FHTdnExu1OqffVFay6zeMW+T6/DowFLndWnIw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", + "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==", "dev": true, "dependencies": { "@babel/core": "^7.12.3", @@ -4506,9 +4506,9 @@ } }, "node_modules/@whatwg-node/fetch/node_modules/@types/node": { - "version": "18.18.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.1.tgz", - "integrity": "sha512-3G42sxmm0fF2+Vtb9TJQpnjmP+uKlWvFa8KoEGquh4gqRmoUG/N0ufuhikw6HEsdG2G2oIKhog1GCTfz9v5NdQ==", + "version": "18.18.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.3.tgz", + "integrity": "sha512-0OVfGupTl3NBFr8+iXpfZ8NR7jfFO+P1Q+IO/q0wbo02wYkP5gy36phojeYWpLQ6WAMjl+VfmqUk2YbUfp0irA==", "dev": true, "peer": true }, @@ -5618,7 +5618,9 @@ } }, "node_modules/ci-info": { - "version": "3.8.0", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "dev": true, "funding": [ { @@ -5626,7 +5628,6 @@ "url": "https://github.com/sponsors/sibiraj-s" } ], - "license": "MIT", "engines": { "node": ">=8" } @@ -17309,19 +17310,25 @@ "license": "MIT" }, "node_modules/v8-to-istanbul": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", - "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.2.tgz", + "integrity": "sha512-ZGBe7VAivuuoQXTeckpbYKTdtjXGcm3ZUHXC0PAk0CzFyuYvwi73a58iEKI3GkGD1c3EHc+EgfR1w5pgbfzJlQ==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" + "convert-source-map": "^2.0.0" }, "engines": { "node": ">=10.12.0" } }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "dev": true, diff --git a/package.json b/package.json index bf890a2b0..cdfcce130 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "devDependencies": { "@apollo/cache-control-types": "1.0.3", "@apollo/client": "3.8.4", - "@apollo/server": "4.9.3", + "@apollo/server": "4.9.4", "@apollo/utils.fetcher": "2.0.1", "@changesets/changelog-github": "0.4.8", "@changesets/cli": "2.26.2",